diff --git a/src/rules/prefer-web-first-assertions.test.ts b/src/rules/prefer-web-first-assertions.test.ts index a236b65a..a2dc2b65 100644 --- a/src/rules/prefer-web-first-assertions.test.ts +++ b/src/rules/prefer-web-first-assertions.test.ts @@ -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")'), @@ -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( @@ -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 { @@ -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)'), @@ -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'; diff --git a/src/rules/prefer-web-first-assertions.ts b/src/rules/prefer-web-first-assertions.ts index 3d6e0e72..91b4f1e4 100644 --- a/src/rules/prefer-web-first-assertions.ts +++ b/src/rules/prefer-web-first-assertions.ts @@ -11,16 +11,24 @@ import { parseFnCall } from '../utils/parseFnCall.js' type MethodConfig = { inverse?: string matcher: string + noFix?: boolean + options?: string prop?: string type: 'boolean' | 'string' } const methods: Record = { + 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', + options: '{ useInnerText: true }', + }, inputValue: { matcher: 'toHaveValue', type: 'string' }, isChecked: { matcher: 'toBeChecked', @@ -109,6 +117,17 @@ export default createRule({ (+!!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: { @@ -183,6 +202,39 @@ export default createRule({ ) } + // 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',