Skip to content

#362 - Extend prefer-web-first-assertion #382

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

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
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
254 changes: 251 additions & 3 deletions src/rules/prefer-web-first-assertions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,9 @@ runRuleTester('prefer-web-first-assertions', rule, {
messageId: 'useWebFirstAssertion',
},
],
output: test('await expect.soft(foo).toHaveText("bar")'),
output: test(
'await expect.soft(foo).toHaveText("bar", { useInnerText: true })',
),
},
{
code: test('expect.soft(await foo.innerText()).not.toBe("bar")'),
Expand All @@ -382,7 +384,9 @@ runRuleTester('prefer-web-first-assertions', rule, {
messageId: 'useWebFirstAssertion',
},
],
output: test('await expect.soft(foo).not.toHaveText("bar")'),
output: test(
'await expect.soft(foo).not.toHaveText("bar", { useInnerText: true })',
),
},
{
code: test(
Expand All @@ -398,9 +402,75 @@ runRuleTester('prefer-web-first-assertions', rule, {
},
],
output: test(
'await expect(page.locator(".text")).toHaveText("Hello World")',
'await expect(page.locator(".text")).toHaveText("Hello World", { useInnerText: true })',
),
},
{
code: test('expect(await foo.innerText()).toBe("bar")'),
errors: [
{
column: 28,
data: { matcher: 'toHaveText', method: 'innerText' },
endColumn: 57,
line: 1,
messageId: 'useWebFirstAssertion',
},
],
output: test(
'await expect(foo).toHaveText("bar", { useInnerText: true })',
),
},
{
code: test('expect(await foo.innerText()).not.toBe("bar")'),
errors: [
{
column: 28,
data: { matcher: 'toHaveText', method: 'innerText' },
endColumn: 57,
line: 1,
messageId: 'useWebFirstAssertion',
},
],
output: test(
'await expect(foo).not.toHaveText("bar", { useInnerText: true })',
),
},
{
code: test('expect(await foo.innerText()).toEqual("bar")'),
errors: [
{
column: 28,
data: { matcher: 'toHaveText', method: 'innerText' },
endColumn: 57,
line: 1,
messageId: 'useWebFirstAssertion',
},
],
output: test(
'await expect(foo).toHaveText("bar", { useInnerText: true })',
),
},
{
code: test(`
const fooLocator = page.locator('.fooClass');
const fooLocatorText = await fooLocator.innerText();
expect(fooLocatorText).toEqual('foo');
`),
errors: [
{
column: 9,
data: { matcher: 'toHaveText', method: 'innerText' },
endColumn: 31,
line: 4,
messageId: 'useWebFirstAssertion',
},
],
output: test(`
const fooLocator = page.locator('.fooClass');
const fooLocatorText = fooLocator;
await expect(fooLocatorText).toHaveText('foo', { useInnerText: true });
`),
},

// inputValue
{
Expand Down Expand Up @@ -636,6 +706,183 @@ runRuleTester('prefer-web-first-assertions', rule, {
),
},

// allTextContents
{
code: javascript('expect(await foo.allTextContents()).toBe("bar")'),
errors: [{ messageId: 'useWebFirstAssertion' }],
},
{
code: javascript(`
const myText = page.locator('foo li').allTextContents();
expect(myText).toEqual(['Alpha', 'Beta', 'Gamma'])`),
errors: [{ messageId: 'useWebFirstAssertion' }],
},
{
code: javascript('expect(await foo.allTextContents()).not.toBe("bar")'),
errors: [{ messageId: 'useWebFirstAssertion' }],
},
{
code: javascript('expect(await foo.allTextContents()).toEqual("bar")'),
errors: [{ messageId: 'useWebFirstAssertion' }],
},
{
code: javascript('expect.soft(await foo.allTextContents()).toBe("bar")'),
errors: [{ messageId: 'useWebFirstAssertion' }],
},
{
code: javascript(
'expect["soft"](await foo.allTextContents()).not.toEqual("bar")',
),
errors: [{ messageId: 'useWebFirstAssertion' }],
},
{
code: javascript(`
const fooLocator = page.locator('.fooClass');
const fooLocatorText = await fooLocator.allTextContents();
expect(fooLocatorText).toEqual('foo');
`),
errors: [{ messageId: 'useWebFirstAssertion' }],
},
{
code: javascript(`
const fooLocator = page.locator('.fooClass');
let fooLocatorText = await fooLocator.allTextContents();
expect(fooLocatorText).toEqual('foo');
fooLocatorText = 'foo';
expect(fooLocatorText).toEqual('foo');
`),
errors: [{ messageId: 'useWebFirstAssertion' }],
},
{
code: test(`
let fooLocatorText;
const fooLocator = page.locator('.fooClass');
fooLocatorText = 'Unrelated';
fooLocatorText = await fooLocator.allTextContents();
expect(fooLocatorText).toEqual('foo');
`),
errors: [{ messageId: 'useWebFirstAssertion' }],
},
{
code: test(`
let fooLocatorText;
let fooLocatorText2;
const fooLocator = page.locator('.fooClass');
fooLocatorText = await fooLocator.allTextContents();
fooLocatorText2 = await fooLocator.allTextContents();
expect(fooLocatorText).toEqual('foo');
`),
errors: [{ messageId: 'useWebFirstAssertion' }],
},
{
code: test(`
let fooLocatorText;
fooLocatorText = 'foo';
expect(fooLocatorText).toEqual('foo');
fooLocatorText = await page.locator('.fooClass').allTextContents();
expect(fooLocatorText).toEqual('foo');
`),
errors: [{ messageId: 'useWebFirstAssertion' }],
},
{
code: test(`
const unrelatedAssignment = "unrelated";
const fooLocatorText = await page.locator('.foo').allTextContents();
expect(fooLocatorText).toEqual('foo');
`),
errors: [{ messageId: 'useWebFirstAssertion' }],
},
{
code: test(`
const locatorFoo = page.locator(".foo")
const isBarText = await locatorFoo.locator(".bar").allTextContents()
expect(isBarText).toBe("bar")
`),
errors: [{ messageId: 'useWebFirstAssertion' }],
},
{
code: test(`
const content = await foo.allTextContents();
expect(content).toBe("bar")
`),
errors: [{ messageId: 'useWebFirstAssertion' }],
},

// allInnerTexts
{
code: test('expect(await foo.allInnerTexts()).toBe("bar")'),
errors: [{ messageId: 'useWebFirstAssertion' }],
},
{
code: test('expect(await foo.allInnerTexts()).not.toBe("bar")'),
errors: [{ messageId: 'useWebFirstAssertion' }],
},
{
code: test('expect(await foo.allInnerTexts()).toEqual("bar")'),
errors: [{ messageId: 'useWebFirstAssertion' }],
},
{
code: test('expect.soft(await foo.allInnerTexts()).toBe("bar")'),
errors: [{ messageId: 'useWebFirstAssertion' }],
},
{
code: test(
'expect["soft"](await foo.allInnerTexts()).not.toEqual("bar")',
),
errors: [{ messageId: 'useWebFirstAssertion' }],
},
{
code: test(`
const fooLocator = page.locator('.fooClass');
const fooLocatorText = await fooLocator.allInnerTexts();
expect(fooLocatorText).toEqual('foo');
`),
errors: [{ messageId: 'useWebFirstAssertion' }],
},
{
code: test(`
let fooLocatorText;
const fooLocator = page.locator('.fooClass');
fooLocatorText = 'Unrelated';
fooLocatorText = await fooLocator.allInnerTexts();
expect(fooLocatorText).toEqual('foo');
`),
errors: [{ messageId: 'useWebFirstAssertion' }],
},
{
code: test(`
let fooLocatorText;
fooLocatorText = 'foo';
expect(fooLocatorText).toEqual('foo');
fooLocatorText = await page.locator('.fooClass').allInnerTexts();
expect(fooLocatorText).toEqual('foo');
`),
errors: [{ messageId: 'useWebFirstAssertion' }],
},
{
code: test(`
const unrelatedAssignment = "unrelated";
const fooLocatorText = await page.locator('.foo').allInnerTexts();
expect(fooLocatorText).toEqual('foo');
`),
errors: [{ messageId: 'useWebFirstAssertion' }],
},
{
code: test(`
const locatorFoo = page.locator(".foo")
const isBarText = await locatorFoo.locator(".bar").allInnerTexts()
expect(isBarText).toBe("bar")
`),
errors: [{ messageId: 'useWebFirstAssertion' }],
},
{
code: test(`
const content = await foo.allInnerTexts();
expect(content).toBe("bar")
`),
errors: [{ messageId: 'useWebFirstAssertion' }],
},

// isChecked
{
code: test('expect(await page.locator("howdy").isChecked()).toBe(true)'),
Expand Down Expand Up @@ -1096,6 +1343,7 @@ runRuleTester('prefer-web-first-assertions', rule, {
{ code: test('const value = await bar["inputValue"]()') },
{ code: test('const isEditable = await baz[`isEditable`]()') },
{ code: test('await expect(await locator.toString()).toBe("something")') },
{ code: test('const myText = page.locator("foo li").allTextContents()') },
{
code: javascript`
import { expect } from '@playwright/test';
Expand Down
54 changes: 53 additions & 1 deletion src/rules/prefer-web-first-assertions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,24 @@
type MethodConfig = {
inverse?: string
matcher: string
noFix?: boolean
options?: string
prop?: string
type: 'boolean' | 'string'
}

const methods: Record<string, MethodConfig> = {
allInnerTexts: { matcher: 'toHaveText', noFix: true, type: 'string' },
allTextContents: { matcher: 'toHaveText', noFix: true, type: 'string' },
getAttribute: {
matcher: 'toHaveAttribute',
type: 'string',
},
innerText: { matcher: 'toHaveText', type: 'string' },
innerText: {
matcher: 'toHaveText',
type: 'string',

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

View workflow job for this annotation

GitHub Actions / test (18.x)

Object properties should be sorted alphabetically

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

View workflow job for this annotation

GitHub Actions / test (20.x)

Object properties should be sorted alphabetically

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

View workflow job for this annotation

GitHub Actions / test (22.x)

Object properties should be sorted alphabetically
options: '{ useInnerText: true }',
},
inputValue: { matcher: 'toHaveValue', type: 'string' },
isChecked: {
matcher: 'toBeChecked',
Expand Down Expand Up @@ -109,6 +117,17 @@
(+!!notModifier ^ +isFalsy && methodConfig.inverse) ||
methodConfig.matcher

// We don't want to provide fix suggestion for some methods.
// In this case, we just report the error and let the user handle it.
if (methodConfig.noFix) {
context.report({
data: { matcher: methodConfig.matcher, method },
messageId: 'useWebFirstAssertion',
node: call.callee.property,
})
return
}

const { callee } = call
context.report({
data: {
Expand Down Expand Up @@ -183,6 +202,39 @@
)
}

// Add options if needed
if (methodConfig.options) {
const range = fnCall.matcher.range!

// Get the matcher argument (the text to match)
const [matcherArg] = fnCall.matcherArgs ?? []

if (matcherArg) {
// If there's a matcher argument, combine it with the options
const textValue = getRawValue(matcherArg)
const combinedArgs = `${textValue}, ${methodConfig.options}`

// Remove the original matcher argument
fixes.push(fixer.remove(matcherArg))

// Add the combined arguments
fixes.push(
fixer.insertTextAfterRange(
[range[0], range[1] + 1],
combinedArgs,
),
)
} else {
// No matcher argument, just add the options
fixes.push(
fixer.insertTextAfterRange(
[range[0], range[1] + 1],
methodConfig.options,
),
)
}
}

return fixes
},
messageId: 'useWebFirstAssertion',
Expand Down