Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix prefer web first assertion rule #287

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
267 changes: 267 additions & 0 deletions src/rules/prefer-web-first-assertions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,90 @@
],
output: test('await expect(page.locator(".tweet")).toBeHidden()'),
},
{
code: test(`
const unrelatedAssignment = 'unrelated'
const isTweetVisible = await page.locator(".tweet").isVisible()
expect(isTweetVisible).toBe(true)
`),
output: test(`

Check warning on line 42 in src/rules/prefer-web-first-assertions.test.ts

View workflow job for this annotation

GitHub Actions / test (16.x)

Object properties should be sorted alphabetically

Check warning on line 42 in src/rules/prefer-web-first-assertions.test.ts

View workflow job for this annotation

GitHub Actions / test (18.x)

Object properties should be sorted alphabetically

Check warning on line 42 in src/rules/prefer-web-first-assertions.test.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

Object properties should be sorted alphabetically
const unrelatedAssignment = 'unrelated'
const isTweetVisible = page.locator(".tweet")
await expect(isTweetVisible).toBeVisible()
`),
errors: [
{
column: 9,
data: { matcher: 'toBeVisible', method: 'isVisible' },
endColumn: 31,
line: 4,
messageId: 'useWebFirstAssertion',
},
],
},
{
code: test(`
const unrelatedAssignment = 'unrelated'
const isTweetVisible = await page.locator(".tweet").isVisible()
expect(isTweetVisible).toBe(false)
`),
output: test(`

Check warning on line 63 in src/rules/prefer-web-first-assertions.test.ts

View workflow job for this annotation

GitHub Actions / test (16.x)

Object properties should be sorted alphabetically

Check warning on line 63 in src/rules/prefer-web-first-assertions.test.ts

View workflow job for this annotation

GitHub Actions / test (18.x)

Object properties should be sorted alphabetically

Check warning on line 63 in src/rules/prefer-web-first-assertions.test.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

Object properties should be sorted alphabetically
const unrelatedAssignment = 'unrelated'
const isTweetVisible = page.locator(".tweet")
await expect(isTweetVisible).toBeHidden()
`),
errors: [
{
column: 9,
data: { matcher: 'toBeHidden', method: 'isVisible' },
endColumn: 31,
line: 4,
messageId: 'useWebFirstAssertion',
},
],
},
{
code: test(`
const locatorFoo = page.locator(".foo")
const isBarVisible = await locatorFoo.locator(".bar").isVisible()
expect(isBarVisible).toBe(false)
`),
output: test(`

Check warning on line 84 in src/rules/prefer-web-first-assertions.test.ts

View workflow job for this annotation

GitHub Actions / test (16.x)

Object properties should be sorted alphabetically

Check warning on line 84 in src/rules/prefer-web-first-assertions.test.ts

View workflow job for this annotation

GitHub Actions / test (18.x)

Object properties should be sorted alphabetically

Check warning on line 84 in src/rules/prefer-web-first-assertions.test.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

Object properties should be sorted alphabetically
const locatorFoo = page.locator(".foo")
const isBarVisible = locatorFoo.locator(".bar")
await expect(isBarVisible).toBeHidden()
`),
errors: [
{
column: 9,
data: { matcher: 'toBeHidden', method: 'isVisible' },
endColumn: 29,
line: 4,
messageId: 'useWebFirstAssertion',
},
],
},
{
code: test(`
const locatorFoo = page.locator(".foo")
const isBarVisible = await locatorFoo.locator(".bar").isVisible()
expect(isBarVisible).toBe(true)
`),
output: test(`

Check warning on line 105 in src/rules/prefer-web-first-assertions.test.ts

View workflow job for this annotation

GitHub Actions / test (16.x)

Object properties should be sorted alphabetically

Check warning on line 105 in src/rules/prefer-web-first-assertions.test.ts

View workflow job for this annotation

GitHub Actions / test (18.x)

Object properties should be sorted alphabetically

Check warning on line 105 in src/rules/prefer-web-first-assertions.test.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

Object properties should be sorted alphabetically
const locatorFoo = page.locator(".foo")
const isBarVisible = locatorFoo.locator(".bar")
await expect(isBarVisible).toBeVisible()
`),
errors: [
{
column: 9,
data: { matcher: 'toBeVisible', method: 'isVisible' },
endColumn: 29,
line: 4,
messageId: 'useWebFirstAssertion',
},
],
},
{
code: test(
'expect(await page.locator(".tweet").isVisible()).toEqual(true)',
Expand Down Expand Up @@ -301,6 +385,150 @@
],
output: test('await expect(foo).not.toHaveText("bar")'),
},
{
code: test(`
const fooLocator = page.locator('.fooClass');
const fooLocatorText = await fooLocator.textContent();
expect(fooLocatorText).toEqual('foo');
`),
output: test(`

Check warning on line 394 in src/rules/prefer-web-first-assertions.test.ts

View workflow job for this annotation

GitHub Actions / test (16.x)

Object properties should be sorted alphabetically

Check warning on line 394 in src/rules/prefer-web-first-assertions.test.ts

View workflow job for this annotation

GitHub Actions / test (18.x)

Object properties should be sorted alphabetically

Check warning on line 394 in src/rules/prefer-web-first-assertions.test.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

Object properties should be sorted alphabetically
const fooLocator = page.locator('.fooClass');
const fooLocatorText = fooLocator;
await expect(fooLocatorText).toHaveText('foo');
`),
errors: [
{
column: 9,
data: { matcher: 'toHaveText', method: 'textContent' },
endColumn: 31,
line: 4,
messageId: 'useWebFirstAssertion',
},
],
},
{
code: test(`
const fooLocator = page.locator('.fooClass');
let fooLocatorText = await fooLocator.textContent();
expect(fooLocatorText).toEqual('foo');
fooLocatorText = 'foo';
expect(fooLocatorText).toEqual('foo');
`),
output: test(`

Check warning on line 417 in src/rules/prefer-web-first-assertions.test.ts

View workflow job for this annotation

GitHub Actions / test (16.x)

Object properties should be sorted alphabetically

Check warning on line 417 in src/rules/prefer-web-first-assertions.test.ts

View workflow job for this annotation

GitHub Actions / test (18.x)

Object properties should be sorted alphabetically

Check warning on line 417 in src/rules/prefer-web-first-assertions.test.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

Object properties should be sorted alphabetically
const fooLocator = page.locator('.fooClass');
let fooLocatorText = fooLocator;
await expect(fooLocatorText).toHaveText('foo');
fooLocatorText = 'foo';
expect(fooLocatorText).toEqual('foo');
`),
errors: [
{
column: 9,
data: { matcher: 'toHaveText', method: 'textContent' },
endColumn: 31,
line: 4,
messageId: 'useWebFirstAssertion',
},
],
},
{
code: test(`
let fooLocatorText;
const fooLocator = page.locator('.fooClass');
fooLocatorText = 'Unrelated';
fooLocatorText = await fooLocator.textContent();
expect(fooLocatorText).toEqual('foo');
`),
output: test(`

Check warning on line 442 in src/rules/prefer-web-first-assertions.test.ts

View workflow job for this annotation

GitHub Actions / test (16.x)

Object properties should be sorted alphabetically

Check warning on line 442 in src/rules/prefer-web-first-assertions.test.ts

View workflow job for this annotation

GitHub Actions / test (18.x)

Object properties should be sorted alphabetically

Check warning on line 442 in src/rules/prefer-web-first-assertions.test.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

Object properties should be sorted alphabetically
let fooLocatorText;
const fooLocator = page.locator('.fooClass');
fooLocatorText = 'Unrelated';
fooLocatorText = fooLocator;
await expect(fooLocatorText).toHaveText('foo');
`),
errors: [
{
column: 9,
data: { matcher: 'toHaveText', method: 'textContent' },
endColumn: 31,
line: 6,
messageId: 'useWebFirstAssertion',
},
],
},
{
code: test(`
let fooLocatorText;
let fooLocatorText2;
const fooLocator = page.locator('.fooClass');
fooLocatorText = await fooLocator.textContent();
fooLocatorText2 = await fooLocator.textContent();
expect(fooLocatorText).toEqual('foo');
`),
output: test(`

Check warning on line 468 in src/rules/prefer-web-first-assertions.test.ts

View workflow job for this annotation

GitHub Actions / test (16.x)

Object properties should be sorted alphabetically

Check warning on line 468 in src/rules/prefer-web-first-assertions.test.ts

View workflow job for this annotation

GitHub Actions / test (18.x)

Object properties should be sorted alphabetically

Check warning on line 468 in src/rules/prefer-web-first-assertions.test.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

Object properties should be sorted alphabetically
let fooLocatorText;
let fooLocatorText2;
const fooLocator = page.locator('.fooClass');
fooLocatorText = fooLocator;
fooLocatorText2 = await fooLocator.textContent();
await expect(fooLocatorText).toHaveText('foo');
`),
errors: [
{
column: 9,
data: { matcher: 'toHaveText', method: 'textContent' },
endColumn: 31,
line: 7,
messageId: 'useWebFirstAssertion',
},
],
},
{
code: test(`
let fooLocatorText;
fooLocatorText = 'foo';
expect(fooLocatorText).toEqual('foo');
fooLocatorText = await page.locator('.fooClass').textContent();
expect(fooLocatorText).toEqual('foo');
`),
output: test(`

Check warning on line 494 in src/rules/prefer-web-first-assertions.test.ts

View workflow job for this annotation

GitHub Actions / test (16.x)

Object properties should be sorted alphabetically

Check warning on line 494 in src/rules/prefer-web-first-assertions.test.ts

View workflow job for this annotation

GitHub Actions / test (18.x)

Object properties should be sorted alphabetically

Check warning on line 494 in src/rules/prefer-web-first-assertions.test.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

Object properties should be sorted alphabetically
let fooLocatorText;
fooLocatorText = 'foo';
expect(fooLocatorText).toEqual('foo');
fooLocatorText = page.locator('.fooClass');
await expect(fooLocatorText).toHaveText('foo');
`),
errors: [
{
column: 9,
data: { matcher: 'toHaveText', method: 'textContent' },
endColumn: 31,
line: 6,
messageId: 'useWebFirstAssertion',
},
],
},
{
code: test(`
const unrelatedAssignment = "unrelated";
const fooLocatorText = await page.locator('.foo').textContent();
expect(fooLocatorText).toEqual('foo');
`),
output: test(`

Check warning on line 517 in src/rules/prefer-web-first-assertions.test.ts

View workflow job for this annotation

GitHub Actions / test (16.x)

Object properties should be sorted alphabetically

Check warning on line 517 in src/rules/prefer-web-first-assertions.test.ts

View workflow job for this annotation

GitHub Actions / test (18.x)

Object properties should be sorted alphabetically

Check warning on line 517 in src/rules/prefer-web-first-assertions.test.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

Object properties should be sorted alphabetically
const unrelatedAssignment = "unrelated";
const fooLocatorText = page.locator('.foo');
await expect(fooLocatorText).toHaveText('foo');
`),
errors: [
{
column: 9,
data: { matcher: 'toHaveText', method: 'textContent' },
endColumn: 31,
line: 4,
messageId: 'useWebFirstAssertion',
},
],
},

// isChecked
{
Expand Down Expand Up @@ -736,5 +964,44 @@
expect(myValue).toBeVisible();
`),
},
{
code: test(`
let fooLocatorText;
const fooLocator = page.locator('.fooClass');
fooLocatorText = await fooLocator.textContent();
fooLocatorText = 'foo';
expect(fooLocatorText).toEqual('foo');
`),
},
{
code: test(`
let fooLocatorText;
let fooLocatorText2;
const fooLocator = page.locator('.fooClass');
fooLocatorText = 'foo';
fooLocatorText2 = await fooLocator.textContent();
expect(fooLocatorText).toEqual('foo');
`),
},
{
code: test(`
let fooLocatorText;
fooLocatorText = 'foo';
expect(fooLocatorText).toEqual('foo')
const fooLocator = page.locator('.fooClass');
fooLocatorText = fooLocator;
expect(fooLocatorText).toHaveText('foo');
`),
},
{
code: test(`
const fooLocator = page.locator('.fooClass');
let fooLocatorText;
fooLocatorText = fooLocator;
expect(fooLocatorText).toHaveText('foo');
fooLocatorText = 'foo';
expect(fooLocatorText).toEqual('foo')
`),
},
],
})
69 changes: 60 additions & 9 deletions src/rules/prefer-web-first-assertions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Rule } from 'eslint'
import ESTree from 'estree'
import ESTree, { AssignmentExpression } from 'estree'
import {
findParent,
getRawValue,
Expand All @@ -8,6 +8,7 @@ import {
} from '../utils/ast'
import { createRule } from '../utils/createRule'
import { parseFnCall } from '../utils/parseFnCall'
import { TypedNodeWithParent } from '../utils/types'

type MethodConfig = {
inverse?: string
Expand Down Expand Up @@ -59,25 +60,75 @@ const supportedMatchers = new Set([
'toBeFalsy',
])

const isVariableDeclarator = (
node: ESTree.Node,
): node is TypedNodeWithParent<'VariableDeclarator'> =>
node.type === 'VariableDeclarator'

const isAssignmentExpression = (
node: ESTree.Node,
): node is TypedNodeWithParent<'AssignmentExpression'> =>
node.type === 'AssignmentExpression'

/**
* Given a Node and an assignment expression, finds out if the assignment
* expression happens before the node identifier (based on their range
* properties) and if the assignment expression left side is of the same name as
* the name of the given node.
*
* @param node The node we are comparing the assignment expression to.
* @param assignment The assignment that will be verified to see if its left
* operand is the same as the node.name and if it happens before it.
* @returns True if the assignment left hand operator belongs to the node and
* occurs before it, false otherwise. If either the node or the assignment
* expression doesn't contain a range array, this will also return false
* because their relative positions cannot be calculated.
*/
function isNodeLastAssignment(
node: ESTree.Identifier,
assignment: AssignmentExpression,
) {
if (node.range && assignment.range && node.range[0] < assignment.range[1]) {
return false
}

return (
assignment.left.type === 'Identifier' && assignment.left.name === node.name
)
}

/**
* If the expect call argument is a variable reference, finds the variable
* initializer.
* initializer or last variable assignment.
*
* If a variable is assigned after initialization we have to look for the last
* time it was assigned because it could have been changed multiple times. We
* then use its right hand assignment operator as the dereferenced node.
*/
function dereference(context: Rule.RuleContext, node: ESTree.Node | undefined) {
if (node?.type !== 'Identifier') {
return node
}

const scope = context.sourceCode.getScope(node)
const parents = scope.references
.map((ref) => ref.identifier as Rule.Node)
.map((ident) => ident.parent)

// Find the variable declaration and return the initializer
for (const ref of scope.references) {
const refParent = (ref.identifier as Rule.Node).parent
// Look for any variable declarators in the scope references that match the
// dereferenced node variable name
const decl = parents
.filter(isVariableDeclarator)
.find((p) => p.id.type === 'Identifier' && p.id.name === node.name)

if (refParent.type === 'VariableDeclarator') {
return refParent.init
}
}
// Look for any variable assignments in the scope references and pick the last
// one that matches the dereferenced node variable name
const expr = parents
.filter(isAssignmentExpression)
.reverse()
.find((assignment) => isNodeLastAssignment(node, assignment))

return expr?.right ?? decl?.init
}

export default createRule({
Expand Down