Skip to content
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 @@ -370,7 +370,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 @@ -383,7 +385,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 @@ -399,9 +403,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 @@ -637,6 +707,183 @@ runRuleTester('prefer-web-first-assertions', rule, {
),
},

// allTextContents
{
code: test('expect(await foo.allTextContents()).toBe("bar")'),
errors: [{ messageId: 'useWebFirstAssertion' }],
},
{
code: test(`
const myText = page.locator('foo li').allTextContents();
expect(myText).toEqual(['Alpha', 'Beta', 'Gamma'])`),
errors: [{ messageId: 'useWebFirstAssertion' }],
},
{
code: test('expect(await foo.allTextContents()).not.toBe("bar")'),
errors: [{ messageId: 'useWebFirstAssertion' }],
},
{
code: test('expect(await foo.allTextContents()).toEqual("bar")'),
errors: [{ messageId: 'useWebFirstAssertion' }],
},
{
code: test('expect.soft(await foo.allTextContents()).toBe("bar")'),
errors: [{ messageId: 'useWebFirstAssertion' }],
},
{
code: test(
'expect["soft"](await foo.allTextContents()).not.toEqual("bar")',
),
errors: [{ messageId: 'useWebFirstAssertion' }],
},
{
code: test(`
const fooLocator = page.locator('.fooClass');
const fooLocatorText = await fooLocator.allTextContents();
expect(fooLocatorText).toEqual('foo');
`),
errors: [{ messageId: 'useWebFirstAssertion' }],
},
{
code: test(`
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 @@ -1097,6 +1344,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: dedent`
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 (24.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

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
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