From d7129cf0f6819b7db58ef06bd5f0fff71d0ea2a9 Mon Sep 17 00:00:00 2001 From: Michal Sroczynski <93290723+MSroczynski3@users.noreply.github.com> Date: Sun, 27 Jul 2025 23:19:33 +0200 Subject: [PATCH 1/8] Add allTextContets to the array of locators --- src/rules/prefer-web-first-assertions.test.ts | 251 ++++++++++++++++++ src/rules/prefer-web-first-assertions.ts | 1 + 2 files changed, 252 insertions(+) diff --git a/src/rules/prefer-web-first-assertions.test.ts b/src/rules/prefer-web-first-assertions.test.ts index ec9e96a..7c4f436 100644 --- a/src/rules/prefer-web-first-assertions.test.ts +++ b/src/rules/prefer-web-first-assertions.test.ts @@ -636,6 +636,257 @@ runRuleTester('prefer-web-first-assertions', rule, { ), }, + // allTextContents + { + code: test('expect(await foo.allTextContents()).toBe("bar")'), + errors: [ + { + column: 28, + data: { matcher: 'toHaveText', method: 'allTextContents' }, + endColumn: 63, + line: 1, + messageId: 'useWebFirstAssertion', + }, + ], + output: test('await expect(foo).toHaveText("bar")'), + }, + { + code: test('expect(await foo.allTextContents()).not.toBe("bar")'), + errors: [ + { + column: 28, + data: { matcher: 'toHaveText', method: 'allTextContents' }, + endColumn: 63, + line: 1, + messageId: 'useWebFirstAssertion', + }, + ], + output: test('await expect(foo).not.toHaveText("bar")'), + }, + { + code: test('expect(await foo.allTextContents()).toEqual("bar")'), + errors: [ + { + column: 28, + data: { matcher: 'toHaveText', method: 'allTextContents' }, + endColumn: 63, + line: 1, + messageId: 'useWebFirstAssertion', + }, + ], + output: test('await expect(foo).toHaveText("bar")'), + }, + { + code: test('expect.soft(await foo.allTextContents()).toBe("bar")'), + errors: [ + { + column: 28, + data: { matcher: 'toHaveText', method: 'allTextContents' }, + endColumn: 68, + line: 1, + messageId: 'useWebFirstAssertion', + }, + ], + output: test('await expect.soft(foo).toHaveText("bar")'), + }, + { + code: test('expect["soft"](await foo.allTextContents()).not.toEqual("bar")'), + errors: [ + { + column: 28, + data: { matcher: 'toHaveText', method: 'allTextContents' }, + endColumn: 71, + line: 1, + messageId: 'useWebFirstAssertion', + }, + ], + output: test('await expect["soft"](foo).not.toHaveText("bar")'), + }, + { + code: test(` + const fooLocator = page.locator('.fooClass'); + const fooLocatorText = await fooLocator.allTextContents(); + expect(fooLocatorText).toEqual('foo'); + `), + errors: [ + { + column: 9, + data: { matcher: 'toHaveText', method: 'allTextContents' }, + endColumn: 31, + line: 4, + messageId: 'useWebFirstAssertion', + }, + ], + output: test(` + const fooLocator = page.locator('.fooClass'); + const fooLocatorText = fooLocator; + await expect(fooLocatorText).toHaveText('foo'); + `), + }, + { + code: test(` + const fooLocator = page.locator('.fooClass'); + let fooLocatorText = await fooLocator.allTextContents(); + expect(fooLocatorText).toEqual('foo'); + fooLocatorText = 'foo'; + expect(fooLocatorText).toEqual('foo'); + `), + errors: [ + { + column: 9, + data: { matcher: 'toHaveText', method: 'allTextContents' }, + endColumn: 31, + line: 4, + messageId: 'useWebFirstAssertion', + }, + ], + output: test(` + const fooLocator = page.locator('.fooClass'); + let fooLocatorText = fooLocator; + await expect(fooLocatorText).toHaveText('foo'); + fooLocatorText = 'foo'; + expect(fooLocatorText).toEqual('foo'); + `), + }, + { + code: test(` + let fooLocatorText; + const fooLocator = page.locator('.fooClass'); + fooLocatorText = 'Unrelated'; + fooLocatorText = await fooLocator.allTextContents(); + expect(fooLocatorText).toEqual('foo'); + `), + errors: [ + { + column: 9, + data: { matcher: 'toHaveText', method: 'allTextContents' }, + endColumn: 31, + line: 6, + messageId: 'useWebFirstAssertion', + }, + ], + output: test(` + let fooLocatorText; + const fooLocator = page.locator('.fooClass'); + fooLocatorText = 'Unrelated'; + fooLocatorText = fooLocator; + await expect(fooLocatorText).toHaveText('foo'); + `), + }, + { + code: test(` + let fooLocatorText; + let fooLocatorText2; + const fooLocator = page.locator('.fooClass'); + fooLocatorText = await fooLocator.allTextContents(); + fooLocatorText2 = await fooLocator.allTextContents(); + expect(fooLocatorText).toEqual('foo'); + `), + errors: [ + { + column: 9, + data: { matcher: 'toHaveText', method: 'allTextContents' }, + endColumn: 31, + line: 7, + messageId: 'useWebFirstAssertion', + }, + ], + output: test(` + let fooLocatorText; + let fooLocatorText2; + const fooLocator = page.locator('.fooClass'); + fooLocatorText = fooLocator; + fooLocatorText2 = await fooLocator.allTextContents(); + await expect(fooLocatorText).toHaveText('foo'); + `), + }, + { + code: test(` + let fooLocatorText; + fooLocatorText = 'foo'; + expect(fooLocatorText).toEqual('foo'); + fooLocatorText = await page.locator('.fooClass').allTextContents(); + expect(fooLocatorText).toEqual('foo'); + `), + errors: [ + { + column: 9, + data: { matcher: 'toHaveText', method: 'allTextContents' }, + endColumn: 31, + line: 6, + messageId: 'useWebFirstAssertion', + }, + ], + output: test(` + let fooLocatorText; + fooLocatorText = 'foo'; + expect(fooLocatorText).toEqual('foo'); + fooLocatorText = page.locator('.fooClass'); + await expect(fooLocatorText).toHaveText('foo'); + `), + }, + { + code: test(` + const unrelatedAssignment = "unrelated"; + const fooLocatorText = await page.locator('.foo').allTextContents(); + expect(fooLocatorText).toEqual('foo'); + `), + errors: [ + { + column: 9, + data: { matcher: 'toHaveText', method: 'allTextContents' }, + endColumn: 31, + line: 4, + messageId: 'useWebFirstAssertion', + }, + ], + output: test(` + const unrelatedAssignment = "unrelated"; + const fooLocatorText = page.locator('.foo'); + await expect(fooLocatorText).toHaveText('foo'); + `), + }, + { + code: test(` + const locatorFoo = page.locator(".foo") + const isBarText = await locatorFoo.locator(".bar").allTextContents() + expect(isBarText).toBe("bar") + `), + errors: [ + { + column: 9, + data: { matcher: 'toHaveText', method: 'allTextContents' }, + endColumn: 26, + line: 4, + messageId: 'useWebFirstAssertion', + }, + ], + output: test(` + const locatorFoo = page.locator(".foo") + const isBarText = locatorFoo.locator(".bar") + await expect(isBarText).toHaveText("bar") + `), + }, + { + code: test(` + const content = await foo.allTextContents(); + expect(content).toBe("bar") + `), + errors: [ + { + column: 9, + data: { matcher: 'toHaveText', method: 'allTextContents' }, + endColumn: 24, + line: 3, + messageId: 'useWebFirstAssertion', + }, + ], + output: test(` + const content = foo; + await expect(content).toHaveText("bar") + `), + }, + // isChecked { code: test('expect(await page.locator("howdy").isChecked()).toBe(true)'), diff --git a/src/rules/prefer-web-first-assertions.ts b/src/rules/prefer-web-first-assertions.ts index 5a2ff0b..bbdde64 100644 --- a/src/rules/prefer-web-first-assertions.ts +++ b/src/rules/prefer-web-first-assertions.ts @@ -16,6 +16,7 @@ type MethodConfig = { } const methods: Record = { + allTextContents: { matcher: 'toHaveText', type: 'string' }, getAttribute: { matcher: 'toHaveAttribute', type: 'string', From 773a62a86cc2b75c6fb337eebcf42909878f4ee1 Mon Sep 17 00:00:00 2001 From: Michal Sroczynski <93290723+MSroczynski3@users.noreply.github.com> Date: Sun, 27 Jul 2025 23:25:03 +0200 Subject: [PATCH 2/8] Add innerTextContets to the array of locators --- src/rules/prefer-web-first-assertions.test.ts | 251 ++++++++++++++++++ src/rules/prefer-web-first-assertions.ts | 1 + 2 files changed, 252 insertions(+) diff --git a/src/rules/prefer-web-first-assertions.test.ts b/src/rules/prefer-web-first-assertions.test.ts index 7c4f436..31a8849 100644 --- a/src/rules/prefer-web-first-assertions.test.ts +++ b/src/rules/prefer-web-first-assertions.test.ts @@ -887,6 +887,257 @@ runRuleTester('prefer-web-first-assertions', rule, { `), }, + // allInnerTexts + { + code: test('expect(await foo.allInnerTexts()).toBe("bar")'), + errors: [ + { + column: 28, + data: { matcher: 'toHaveText', method: 'allInnerTexts' }, + endColumn: 61, + line: 1, + messageId: 'useWebFirstAssertion', + }, + ], + output: test('await expect(foo).toHaveText("bar")'), + }, + { + code: test('expect(await foo.allInnerTexts()).not.toBe("bar")'), + errors: [ + { + column: 28, + data: { matcher: 'toHaveText', method: 'allInnerTexts' }, + endColumn: 61, + line: 1, + messageId: 'useWebFirstAssertion', + }, + ], + output: test('await expect(foo).not.toHaveText("bar")'), + }, + { + code: test('expect(await foo.allInnerTexts()).toEqual("bar")'), + errors: [ + { + column: 28, + data: { matcher: 'toHaveText', method: 'allInnerTexts' }, + endColumn: 61, + line: 1, + messageId: 'useWebFirstAssertion', + }, + ], + output: test('await expect(foo).toHaveText("bar")'), + }, + { + code: test('expect.soft(await foo.allInnerTexts()).toBe("bar")'), + errors: [ + { + column: 28, + data: { matcher: 'toHaveText', method: 'allInnerTexts' }, + endColumn: 66, + line: 1, + messageId: 'useWebFirstAssertion', + }, + ], + output: test('await expect.soft(foo).toHaveText("bar")'), + }, + { + code: test('expect["soft"](await foo.allInnerTexts()).not.toEqual("bar")'), + errors: [ + { + column: 28, + data: { matcher: 'toHaveText', method: 'allInnerTexts' }, + endColumn: 69, + line: 1, + messageId: 'useWebFirstAssertion', + }, + ], + output: test('await expect["soft"](foo).not.toHaveText("bar")'), + }, + { + code: test(` + const fooLocator = page.locator('.fooClass'); + const fooLocatorText = await fooLocator.allInnerTexts(); + expect(fooLocatorText).toEqual('foo'); + `), + errors: [ + { + column: 9, + data: { matcher: 'toHaveText', method: 'allInnerTexts' }, + endColumn: 31, + line: 4, + messageId: 'useWebFirstAssertion', + }, + ], + output: test(` + const fooLocator = page.locator('.fooClass'); + const fooLocatorText = fooLocator; + await expect(fooLocatorText).toHaveText('foo'); + `), + }, + { + code: test(` + const fooLocator = page.locator('.fooClass'); + let fooLocatorText = await fooLocator.allInnerTexts(); + expect(fooLocatorText).toEqual('foo'); + fooLocatorText = 'foo'; + expect(fooLocatorText).toEqual('foo'); + `), + errors: [ + { + column: 9, + data: { matcher: 'toHaveText', method: 'allInnerTexts' }, + endColumn: 31, + line: 4, + messageId: 'useWebFirstAssertion', + }, + ], + output: test(` + const fooLocator = page.locator('.fooClass'); + let fooLocatorText = fooLocator; + await expect(fooLocatorText).toHaveText('foo'); + fooLocatorText = 'foo'; + expect(fooLocatorText).toEqual('foo'); + `), + }, + { + code: test(` + let fooLocatorText; + const fooLocator = page.locator('.fooClass'); + fooLocatorText = 'Unrelated'; + fooLocatorText = await fooLocator.allInnerTexts(); + expect(fooLocatorText).toEqual('foo'); + `), + errors: [ + { + column: 9, + data: { matcher: 'toHaveText', method: 'allInnerTexts' }, + endColumn: 31, + line: 6, + messageId: 'useWebFirstAssertion', + }, + ], + output: test(` + let fooLocatorText; + const fooLocator = page.locator('.fooClass'); + fooLocatorText = 'Unrelated'; + fooLocatorText = fooLocator; + await expect(fooLocatorText).toHaveText('foo'); + `), + }, + { + code: test(` + let fooLocatorText; + let fooLocatorText2; + const fooLocator = page.locator('.fooClass'); + fooLocatorText = await fooLocator.allInnerTexts(); + fooLocatorText2 = await fooLocator.allInnerTexts(); + expect(fooLocatorText).toEqual('foo'); + `), + errors: [ + { + column: 9, + data: { matcher: 'toHaveText', method: 'allInnerTexts' }, + endColumn: 31, + line: 7, + messageId: 'useWebFirstAssertion', + }, + ], + output: test(` + let fooLocatorText; + let fooLocatorText2; + const fooLocator = page.locator('.fooClass'); + fooLocatorText = fooLocator; + fooLocatorText2 = await fooLocator.allInnerTexts(); + await expect(fooLocatorText).toHaveText('foo'); + `), + }, + { + code: test(` + let fooLocatorText; + fooLocatorText = 'foo'; + expect(fooLocatorText).toEqual('foo'); + fooLocatorText = await page.locator('.fooClass').allInnerTexts(); + expect(fooLocatorText).toEqual('foo'); + `), + errors: [ + { + column: 9, + data: { matcher: 'toHaveText', method: 'allInnerTexts' }, + endColumn: 31, + line: 6, + messageId: 'useWebFirstAssertion', + }, + ], + output: test(` + let fooLocatorText; + fooLocatorText = 'foo'; + expect(fooLocatorText).toEqual('foo'); + fooLocatorText = page.locator('.fooClass'); + await expect(fooLocatorText).toHaveText('foo'); + `), + }, + { + code: test(` + const unrelatedAssignment = "unrelated"; + const fooLocatorText = await page.locator('.foo').allInnerTexts(); + expect(fooLocatorText).toEqual('foo'); + `), + errors: [ + { + column: 9, + data: { matcher: 'toHaveText', method: 'allInnerTexts' }, + endColumn: 31, + line: 4, + messageId: 'useWebFirstAssertion', + }, + ], + output: test(` + const unrelatedAssignment = "unrelated"; + const fooLocatorText = page.locator('.foo'); + await expect(fooLocatorText).toHaveText('foo'); + `), + }, + { + code: test(` + const locatorFoo = page.locator(".foo") + const isBarText = await locatorFoo.locator(".bar").allInnerTexts() + expect(isBarText).toBe("bar") + `), + errors: [ + { + column: 9, + data: { matcher: 'toHaveText', method: 'allInnerTexts' }, + endColumn: 26, + line: 4, + messageId: 'useWebFirstAssertion', + }, + ], + output: test(` + const locatorFoo = page.locator(".foo") + const isBarText = locatorFoo.locator(".bar") + await expect(isBarText).toHaveText("bar") + `), + }, + { + code: test(` + const content = await foo.allInnerTexts(); + expect(content).toBe("bar") + `), + errors: [ + { + column: 9, + data: { matcher: 'toHaveText', method: 'allInnerTexts' }, + endColumn: 24, + line: 3, + messageId: 'useWebFirstAssertion', + }, + ], + output: test(` + const content = foo; + await expect(content).toHaveText("bar") + `), + }, + // isChecked { code: test('expect(await page.locator("howdy").isChecked()).toBe(true)'), diff --git a/src/rules/prefer-web-first-assertions.ts b/src/rules/prefer-web-first-assertions.ts index bbdde64..07c50dc 100644 --- a/src/rules/prefer-web-first-assertions.ts +++ b/src/rules/prefer-web-first-assertions.ts @@ -16,6 +16,7 @@ type MethodConfig = { } const methods: Record = { + allInnerTexts: { matcher: 'toHaveText', type: 'string' }, allTextContents: { matcher: 'toHaveText', type: 'string' }, getAttribute: { matcher: 'toHaveAttribute', From 87619354ea905c95e125a5c6f9eccfa4d5247f6c Mon Sep 17 00:00:00 2001 From: Michal Sroczynski <93290723+MSroczynski3@users.noreply.github.com> Date: Mon, 28 Jul 2025 23:44:19 +0200 Subject: [PATCH 3/8] FMT --- src/rules/prefer-web-first-assertions.test.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/rules/prefer-web-first-assertions.test.ts b/src/rules/prefer-web-first-assertions.test.ts index 31a8849..0b826c6 100644 --- a/src/rules/prefer-web-first-assertions.test.ts +++ b/src/rules/prefer-web-first-assertions.test.ts @@ -690,7 +690,9 @@ runRuleTester('prefer-web-first-assertions', rule, { output: test('await expect.soft(foo).toHaveText("bar")'), }, { - code: test('expect["soft"](await foo.allTextContents()).not.toEqual("bar")'), + code: test( + 'expect["soft"](await foo.allTextContents()).not.toEqual("bar")', + ), errors: [ { column: 28, @@ -941,7 +943,9 @@ runRuleTester('prefer-web-first-assertions', rule, { output: test('await expect.soft(foo).toHaveText("bar")'), }, { - code: test('expect["soft"](await foo.allInnerTexts()).not.toEqual("bar")'), + code: test( + 'expect["soft"](await foo.allInnerTexts()).not.toEqual("bar")', + ), errors: [ { column: 28, From fb927de981bebb9a159a57f5e61985550ebf7264 Mon Sep 17 00:00:00 2001 From: Michal Sroczynski <93290723+MSroczynski3@users.noreply.github.com> Date: Tue, 29 Jul 2025 00:12:00 +0200 Subject: [PATCH 4/8] Simplified tests --- src/rules/prefer-web-first-assertions.test.ts | 52 ------------------- 1 file changed, 52 deletions(-) diff --git a/src/rules/prefer-web-first-assertions.test.ts b/src/rules/prefer-web-first-assertions.test.ts index 0b826c6..bdef89b 100644 --- a/src/rules/prefer-web-first-assertions.test.ts +++ b/src/rules/prefer-web-first-assertions.test.ts @@ -978,31 +978,6 @@ runRuleTester('prefer-web-first-assertions', rule, { await expect(fooLocatorText).toHaveText('foo'); `), }, - { - code: test(` - const fooLocator = page.locator('.fooClass'); - let fooLocatorText = await fooLocator.allInnerTexts(); - expect(fooLocatorText).toEqual('foo'); - fooLocatorText = 'foo'; - expect(fooLocatorText).toEqual('foo'); - `), - errors: [ - { - column: 9, - data: { matcher: 'toHaveText', method: 'allInnerTexts' }, - endColumn: 31, - line: 4, - messageId: 'useWebFirstAssertion', - }, - ], - output: test(` - const fooLocator = page.locator('.fooClass'); - let fooLocatorText = fooLocator; - await expect(fooLocatorText).toHaveText('foo'); - fooLocatorText = 'foo'; - expect(fooLocatorText).toEqual('foo'); - `), - }, { code: test(` let fooLocatorText; @@ -1028,33 +1003,6 @@ runRuleTester('prefer-web-first-assertions', rule, { await expect(fooLocatorText).toHaveText('foo'); `), }, - { - code: test(` - let fooLocatorText; - let fooLocatorText2; - const fooLocator = page.locator('.fooClass'); - fooLocatorText = await fooLocator.allInnerTexts(); - fooLocatorText2 = await fooLocator.allInnerTexts(); - expect(fooLocatorText).toEqual('foo'); - `), - errors: [ - { - column: 9, - data: { matcher: 'toHaveText', method: 'allInnerTexts' }, - endColumn: 31, - line: 7, - messageId: 'useWebFirstAssertion', - }, - ], - output: test(` - let fooLocatorText; - let fooLocatorText2; - const fooLocator = page.locator('.fooClass'); - fooLocatorText = fooLocator; - fooLocatorText2 = await fooLocator.allInnerTexts(); - await expect(fooLocatorText).toHaveText('foo'); - `), - }, { code: test(` let fooLocatorText; From 77b9b49487c6c08501bb8e6c300420b955ecab98 Mon Sep 17 00:00:00 2001 From: Michal Sroczynski <93290723+MSroczynski3@users.noreply.github.com> Date: Wed, 30 Jul 2025 20:13:20 +0200 Subject: [PATCH 5/8] Alternative approach --- src/rules/prefer-web-first-assertions.test.ts | 350 ++---------------- src/rules/prefer-web-first-assertions.ts | 16 +- 2 files changed, 50 insertions(+), 316 deletions(-) diff --git a/src/rules/prefer-web-first-assertions.test.ts b/src/rules/prefer-web-first-assertions.test.ts index bdef89b..d4642ef 100644 --- a/src/rules/prefer-web-first-assertions.test.ts +++ b/src/rules/prefer-web-first-assertions.test.ts @@ -638,117 +638,44 @@ runRuleTester('prefer-web-first-assertions', rule, { // allTextContents { - code: test('expect(await foo.allTextContents()).toBe("bar")'), - errors: [ - { - column: 28, - data: { matcher: 'toHaveText', method: 'allTextContents' }, - endColumn: 63, - line: 1, - messageId: 'useWebFirstAssertion', - }, - ], - output: test('await expect(foo).toHaveText("bar")'), + code: javascript('expect(await foo.allTextContents()).toBe("bar")'), + errors: [{ messageId: 'useWebFirstAssertion' }], }, { - code: test('expect(await foo.allTextContents()).not.toBe("bar")'), - errors: [ - { - column: 28, - data: { matcher: 'toHaveText', method: 'allTextContents' }, - endColumn: 63, - line: 1, - messageId: 'useWebFirstAssertion', - }, - ], - output: test('await expect(foo).not.toHaveText("bar")'), + code: javascript('expect(await foo.allTextContents()).not.toBe("bar")'), + errors: [{ messageId: 'useWebFirstAssertion' }], }, { - code: test('expect(await foo.allTextContents()).toEqual("bar")'), - errors: [ - { - column: 28, - data: { matcher: 'toHaveText', method: 'allTextContents' }, - endColumn: 63, - line: 1, - messageId: 'useWebFirstAssertion', - }, - ], - output: test('await expect(foo).toHaveText("bar")'), + code: javascript('expect(await foo.allTextContents()).toEqual("bar")'), + errors: [{ messageId: 'useWebFirstAssertion' }], }, { - code: test('expect.soft(await foo.allTextContents()).toBe("bar")'), - errors: [ - { - column: 28, - data: { matcher: 'toHaveText', method: 'allTextContents' }, - endColumn: 68, - line: 1, - messageId: 'useWebFirstAssertion', - }, - ], - output: test('await expect.soft(foo).toHaveText("bar")'), + code: javascript('expect.soft(await foo.allTextContents()).toBe("bar")'), + errors: [{ messageId: 'useWebFirstAssertion' }], }, { - code: test( + code: javascript( 'expect["soft"](await foo.allTextContents()).not.toEqual("bar")', ), - errors: [ - { - column: 28, - data: { matcher: 'toHaveText', method: 'allTextContents' }, - endColumn: 71, - line: 1, - messageId: 'useWebFirstAssertion', - }, - ], - output: test('await expect["soft"](foo).not.toHaveText("bar")'), + errors: [{ messageId: 'useWebFirstAssertion' }], }, { - code: test(` + code: javascript(` const fooLocator = page.locator('.fooClass'); const fooLocatorText = await fooLocator.allTextContents(); expect(fooLocatorText).toEqual('foo'); `), - errors: [ - { - column: 9, - data: { matcher: 'toHaveText', method: 'allTextContents' }, - endColumn: 31, - line: 4, - messageId: 'useWebFirstAssertion', - }, - ], - output: test(` - const fooLocator = page.locator('.fooClass'); - const fooLocatorText = fooLocator; - await expect(fooLocatorText).toHaveText('foo'); - `), + errors: [{ messageId: 'useWebFirstAssertion' }], }, { - code: test(` + code: javascript(` const fooLocator = page.locator('.fooClass'); let fooLocatorText = await fooLocator.allTextContents(); expect(fooLocatorText).toEqual('foo'); fooLocatorText = 'foo'; expect(fooLocatorText).toEqual('foo'); `), - errors: [ - { - column: 9, - data: { matcher: 'toHaveText', method: 'allTextContents' }, - endColumn: 31, - line: 4, - messageId: 'useWebFirstAssertion', - }, - ], - output: test(` - const fooLocator = page.locator('.fooClass'); - let fooLocatorText = fooLocator; - await expect(fooLocatorText).toHaveText('foo'); - fooLocatorText = 'foo'; - expect(fooLocatorText).toEqual('foo'); - `), + errors: [{ messageId: 'useWebFirstAssertion' }], }, { code: test(` @@ -758,22 +685,7 @@ runRuleTester('prefer-web-first-assertions', rule, { fooLocatorText = await fooLocator.allTextContents(); expect(fooLocatorText).toEqual('foo'); `), - errors: [ - { - column: 9, - data: { matcher: 'toHaveText', method: 'allTextContents' }, - endColumn: 31, - line: 6, - messageId: 'useWebFirstAssertion', - }, - ], - output: test(` - let fooLocatorText; - const fooLocator = page.locator('.fooClass'); - fooLocatorText = 'Unrelated'; - fooLocatorText = fooLocator; - await expect(fooLocatorText).toHaveText('foo'); - `), + errors: [{ messageId: 'useWebFirstAssertion' }], }, { code: test(` @@ -784,23 +696,7 @@ runRuleTester('prefer-web-first-assertions', rule, { fooLocatorText2 = await fooLocator.allTextContents(); expect(fooLocatorText).toEqual('foo'); `), - errors: [ - { - column: 9, - data: { matcher: 'toHaveText', method: 'allTextContents' }, - endColumn: 31, - line: 7, - messageId: 'useWebFirstAssertion', - }, - ], - output: test(` - let fooLocatorText; - let fooLocatorText2; - const fooLocator = page.locator('.fooClass'); - fooLocatorText = fooLocator; - fooLocatorText2 = await fooLocator.allTextContents(); - await expect(fooLocatorText).toHaveText('foo'); - `), + errors: [{ messageId: 'useWebFirstAssertion' }], }, { code: test(` @@ -810,22 +706,7 @@ runRuleTester('prefer-web-first-assertions', rule, { fooLocatorText = await page.locator('.fooClass').allTextContents(); expect(fooLocatorText).toEqual('foo'); `), - errors: [ - { - column: 9, - data: { matcher: 'toHaveText', method: 'allTextContents' }, - endColumn: 31, - line: 6, - messageId: 'useWebFirstAssertion', - }, - ], - output: test(` - let fooLocatorText; - fooLocatorText = 'foo'; - expect(fooLocatorText).toEqual('foo'); - fooLocatorText = page.locator('.fooClass'); - await expect(fooLocatorText).toHaveText('foo'); - `), + errors: [{ messageId: 'useWebFirstAssertion' }], }, { code: test(` @@ -833,20 +714,7 @@ runRuleTester('prefer-web-first-assertions', rule, { const fooLocatorText = await page.locator('.foo').allTextContents(); expect(fooLocatorText).toEqual('foo'); `), - errors: [ - { - column: 9, - data: { matcher: 'toHaveText', method: 'allTextContents' }, - endColumn: 31, - line: 4, - messageId: 'useWebFirstAssertion', - }, - ], - output: test(` - const unrelatedAssignment = "unrelated"; - const fooLocatorText = page.locator('.foo'); - await expect(fooLocatorText).toHaveText('foo'); - `), + errors: [{ messageId: 'useWebFirstAssertion' }], }, { code: test(` @@ -854,108 +722,38 @@ runRuleTester('prefer-web-first-assertions', rule, { const isBarText = await locatorFoo.locator(".bar").allTextContents() expect(isBarText).toBe("bar") `), - errors: [ - { - column: 9, - data: { matcher: 'toHaveText', method: 'allTextContents' }, - endColumn: 26, - line: 4, - messageId: 'useWebFirstAssertion', - }, - ], - output: test(` - const locatorFoo = page.locator(".foo") - const isBarText = locatorFoo.locator(".bar") - await expect(isBarText).toHaveText("bar") - `), + errors: [{ messageId: 'useWebFirstAssertion' }], }, { code: test(` const content = await foo.allTextContents(); expect(content).toBe("bar") `), - errors: [ - { - column: 9, - data: { matcher: 'toHaveText', method: 'allTextContents' }, - endColumn: 24, - line: 3, - messageId: 'useWebFirstAssertion', - }, - ], - output: test(` - const content = foo; - await expect(content).toHaveText("bar") - `), + errors: [{ messageId: 'useWebFirstAssertion' }], }, // allInnerTexts { code: test('expect(await foo.allInnerTexts()).toBe("bar")'), - errors: [ - { - column: 28, - data: { matcher: 'toHaveText', method: 'allInnerTexts' }, - endColumn: 61, - line: 1, - messageId: 'useWebFirstAssertion', - }, - ], - output: test('await expect(foo).toHaveText("bar")'), + errors: [{ messageId: 'useWebFirstAssertion' }], }, { code: test('expect(await foo.allInnerTexts()).not.toBe("bar")'), - errors: [ - { - column: 28, - data: { matcher: 'toHaveText', method: 'allInnerTexts' }, - endColumn: 61, - line: 1, - messageId: 'useWebFirstAssertion', - }, - ], - output: test('await expect(foo).not.toHaveText("bar")'), + errors: [{ messageId: 'useWebFirstAssertion' }], }, { code: test('expect(await foo.allInnerTexts()).toEqual("bar")'), - errors: [ - { - column: 28, - data: { matcher: 'toHaveText', method: 'allInnerTexts' }, - endColumn: 61, - line: 1, - messageId: 'useWebFirstAssertion', - }, - ], - output: test('await expect(foo).toHaveText("bar")'), + errors: [{ messageId: 'useWebFirstAssertion' }], }, { code: test('expect.soft(await foo.allInnerTexts()).toBe("bar")'), - errors: [ - { - column: 28, - data: { matcher: 'toHaveText', method: 'allInnerTexts' }, - endColumn: 66, - line: 1, - messageId: 'useWebFirstAssertion', - }, - ], - output: test('await expect.soft(foo).toHaveText("bar")'), + errors: [{ messageId: 'useWebFirstAssertion' }], }, { code: test( 'expect["soft"](await foo.allInnerTexts()).not.toEqual("bar")', ), - errors: [ - { - column: 28, - data: { matcher: 'toHaveText', method: 'allInnerTexts' }, - endColumn: 69, - line: 1, - messageId: 'useWebFirstAssertion', - }, - ], - output: test('await expect["soft"](foo).not.toHaveText("bar")'), + errors: [{ messageId: 'useWebFirstAssertion' }], }, { code: test(` @@ -963,20 +761,7 @@ runRuleTester('prefer-web-first-assertions', rule, { const fooLocatorText = await fooLocator.allInnerTexts(); expect(fooLocatorText).toEqual('foo'); `), - errors: [ - { - column: 9, - data: { matcher: 'toHaveText', method: 'allInnerTexts' }, - endColumn: 31, - line: 4, - messageId: 'useWebFirstAssertion', - }, - ], - output: test(` - const fooLocator = page.locator('.fooClass'); - const fooLocatorText = fooLocator; - await expect(fooLocatorText).toHaveText('foo'); - `), + errors: [{ messageId: 'useWebFirstAssertion' }], }, { code: test(` @@ -986,22 +771,7 @@ runRuleTester('prefer-web-first-assertions', rule, { fooLocatorText = await fooLocator.allInnerTexts(); expect(fooLocatorText).toEqual('foo'); `), - errors: [ - { - column: 9, - data: { matcher: 'toHaveText', method: 'allInnerTexts' }, - endColumn: 31, - line: 6, - messageId: 'useWebFirstAssertion', - }, - ], - output: test(` - let fooLocatorText; - const fooLocator = page.locator('.fooClass'); - fooLocatorText = 'Unrelated'; - fooLocatorText = fooLocator; - await expect(fooLocatorText).toHaveText('foo'); - `), + errors: [{ messageId: 'useWebFirstAssertion' }], }, { code: test(` @@ -1011,22 +781,7 @@ runRuleTester('prefer-web-first-assertions', rule, { fooLocatorText = await page.locator('.fooClass').allInnerTexts(); expect(fooLocatorText).toEqual('foo'); `), - errors: [ - { - column: 9, - data: { matcher: 'toHaveText', method: 'allInnerTexts' }, - endColumn: 31, - line: 6, - messageId: 'useWebFirstAssertion', - }, - ], - output: test(` - let fooLocatorText; - fooLocatorText = 'foo'; - expect(fooLocatorText).toEqual('foo'); - fooLocatorText = page.locator('.fooClass'); - await expect(fooLocatorText).toHaveText('foo'); - `), + errors: [{ messageId: 'useWebFirstAssertion' }], }, { code: test(` @@ -1034,20 +789,7 @@ runRuleTester('prefer-web-first-assertions', rule, { const fooLocatorText = await page.locator('.foo').allInnerTexts(); expect(fooLocatorText).toEqual('foo'); `), - errors: [ - { - column: 9, - data: { matcher: 'toHaveText', method: 'allInnerTexts' }, - endColumn: 31, - line: 4, - messageId: 'useWebFirstAssertion', - }, - ], - output: test(` - const unrelatedAssignment = "unrelated"; - const fooLocatorText = page.locator('.foo'); - await expect(fooLocatorText).toHaveText('foo'); - `), + errors: [{ messageId: 'useWebFirstAssertion' }], }, { code: test(` @@ -1055,39 +797,14 @@ runRuleTester('prefer-web-first-assertions', rule, { const isBarText = await locatorFoo.locator(".bar").allInnerTexts() expect(isBarText).toBe("bar") `), - errors: [ - { - column: 9, - data: { matcher: 'toHaveText', method: 'allInnerTexts' }, - endColumn: 26, - line: 4, - messageId: 'useWebFirstAssertion', - }, - ], - output: test(` - const locatorFoo = page.locator(".foo") - const isBarText = locatorFoo.locator(".bar") - await expect(isBarText).toHaveText("bar") - `), + errors: [{ messageId: 'useWebFirstAssertion' }], }, { code: test(` const content = await foo.allInnerTexts(); expect(content).toBe("bar") `), - errors: [ - { - column: 9, - data: { matcher: 'toHaveText', method: 'allInnerTexts' }, - endColumn: 24, - line: 3, - messageId: 'useWebFirstAssertion', - }, - ], - output: test(` - const content = foo; - await expect(content).toHaveText("bar") - `), + errors: [{ messageId: 'useWebFirstAssertion' }], }, // isChecked @@ -1615,5 +1332,10 @@ runRuleTester('prefer-web-first-assertions', rule, { expect(fooLocatorText).toEqual('foo') `), }, + { + code: test(` + const myText = page.locator('foo li').allTextContents(); + expect(myText).toEqual(['Alpha', 'Beta', 'Gamma'])`), + }, ], }) diff --git a/src/rules/prefer-web-first-assertions.ts b/src/rules/prefer-web-first-assertions.ts index 07c50dc..3fde2d8 100644 --- a/src/rules/prefer-web-first-assertions.ts +++ b/src/rules/prefer-web-first-assertions.ts @@ -12,12 +12,13 @@ type MethodConfig = { inverse?: string matcher: string prop?: string + noFix?: boolean type: 'boolean' | 'string' } const methods: Record = { - allInnerTexts: { matcher: 'toHaveText', type: 'string' }, - allTextContents: { matcher: 'toHaveText', type: 'string' }, + allInnerTexts: { matcher: 'toHaveText', noFix: true, type: 'string' }, + allTextContents: { matcher: 'toHaveText', noFix: true, type: 'string' }, getAttribute: { matcher: 'toHaveAttribute', type: 'string', @@ -111,6 +112,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: { From 1cd4c02c3c4d0bed0233336f00bf1267ca7b3d26 Mon Sep 17 00:00:00 2001 From: Michal Sroczynski <93290723+MSroczynski3@users.noreply.github.com> Date: Wed, 30 Jul 2025 20:56:52 +0200 Subject: [PATCH 6/8] Tests & FMT --- src/rules/prefer-web-first-assertions.test.ts | 12 +++++----- src/rules/prefer-web-first-assertions.ts | 22 +++++++++---------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/rules/prefer-web-first-assertions.test.ts b/src/rules/prefer-web-first-assertions.test.ts index d4642ef..6fab876 100644 --- a/src/rules/prefer-web-first-assertions.test.ts +++ b/src/rules/prefer-web-first-assertions.test.ts @@ -641,6 +641,12 @@ runRuleTester('prefer-web-first-assertions', rule, { 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' }], @@ -1266,6 +1272,7 @@ runRuleTester('prefer-web-first-assertions', rule, { { code: test('let visible = await foo.isVisible()') }, { code: test('const value = await bar["inputValue"]()') }, { code: test('const isEditable = await baz[`isEditable`]()') }, + { code: test('const myText = page.locator("foo li").allTextContents()') }, { code: javascript` import { expect } from '@playwright/test'; @@ -1332,10 +1339,5 @@ runRuleTester('prefer-web-first-assertions', rule, { expect(fooLocatorText).toEqual('foo') `), }, - { - code: test(` - const myText = page.locator('foo li').allTextContents(); - expect(myText).toEqual(['Alpha', 'Beta', 'Gamma'])`), - }, ], }) diff --git a/src/rules/prefer-web-first-assertions.ts b/src/rules/prefer-web-first-assertions.ts index 3fde2d8..9216dd4 100644 --- a/src/rules/prefer-web-first-assertions.ts +++ b/src/rules/prefer-web-first-assertions.ts @@ -112,17 +112,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 - } - + // 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: { From 55681a17ef1cac6cc1e9ed34a8a8bb55da385d75 Mon Sep 17 00:00:00 2001 From: Michal Sroczynski <93290723+MSroczynski3@users.noreply.github.com> Date: Wed, 30 Jul 2025 21:01:53 +0200 Subject: [PATCH 7/8] Lint --- src/rules/prefer-web-first-assertions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rules/prefer-web-first-assertions.ts b/src/rules/prefer-web-first-assertions.ts index 9216dd4..e213db8 100644 --- a/src/rules/prefer-web-first-assertions.ts +++ b/src/rules/prefer-web-first-assertions.ts @@ -11,8 +11,8 @@ import { parseFnCall } from '../utils/parseFnCall.js' type MethodConfig = { inverse?: string matcher: string - prop?: string noFix?: boolean + prop?: string type: 'boolean' | 'string' } From 8761304c61713ca52d735949ddde4229ae29ead6 Mon Sep 17 00:00:00 2001 From: Michal Sroczynski <93290723+MSroczynski3@users.noreply.github.com> Date: Sun, 3 Aug 2025 23:25:58 +0200 Subject: [PATCH 8/8] Handle method options --- src/rules/prefer-web-first-assertions.test.ts | 76 ++++++++++++++++++- src/rules/prefer-web-first-assertions.ts | 40 +++++++++- 2 files changed, 112 insertions(+), 4 deletions(-) diff --git a/src/rules/prefer-web-first-assertions.test.ts b/src/rules/prefer-web-first-assertions.test.ts index f55049f..a2dc2b6 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 { diff --git a/src/rules/prefer-web-first-assertions.ts b/src/rules/prefer-web-first-assertions.ts index 0fde77e..91b4f1e 100644 --- a/src/rules/prefer-web-first-assertions.ts +++ b/src/rules/prefer-web-first-assertions.ts @@ -12,6 +12,7 @@ type MethodConfig = { inverse?: string matcher: string noFix?: boolean + options?: string prop?: string type: 'boolean' | 'string' } @@ -23,7 +24,11 @@ const methods: Record = { 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', @@ -197,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',