Repository: playwright-community/eslint-plugin-playwright
Pull request #347 (2025-01-23 15:25:41)

Playwright 1.50
https://github.com/microsoft/playwright/releases/tag/v1.50.0


Patch:
diff --git a/src/rules/missing-playwright-await.ts b/src/rules/missing-playwright-await.ts
index 0095812..ba6d4ce 100644
--- a/src/rules/missing-playwright-await.ts
+++ b/src/rules/missing-playwright-await.ts
@@ -31,6 +31,7 @@ const expectPlaywrightMatchers = [
 ]
 
 const playwrightTestMatchers = [
+  'toBeAttached',
   'toBeChecked',
   'toBeDisabled',
   'toBeEditable',
@@ -38,23 +39,23 @@ const playwrightTestMatchers = [
   'toBeEnabled',
   'toBeFocused',
   'toBeHidden',
+  'toBeInViewport',
+  'toBeOK',
   'toBeVisible',
   'toContainText',
+  'toHaveAccessibleErrorMessage',
   'toHaveAttribute',
+  'toHaveCSS',
   'toHaveClass',
   'toHaveCount',
-  'toHaveCSS',
   'toHaveId',
   'toHaveJSProperty',
-  'toBeOK',
   'toHaveScreenshot',
   'toHaveText',
   'toHaveTitle',
   'toHaveURL',
   'toHaveValue',
   'toHaveValues',
-  'toBeAttached',
-  'toBeInViewport',
 ]
 
 function getReportNode(node: ESTree.Node) {
diff --git a/src/rules/no-nested-step.ts b/src/rules/no-nested-step.ts
index b764e0a..18c41e8 100644
--- a/src/rules/no-nested-step.ts
+++ b/src/rules/no-nested-step.ts
@@ -1,24 +1,16 @@
 import { Rule } from 'eslint'
-import ESTree from 'estree'
-import { isPropertyAccessor } from '../utils/ast.js'
 import { createRule } from '../utils/createRule.js'
-
-function isStepCall(node: ESTree.Node): boolean {
-  const inner = node.type === 'CallExpression' ? node.callee : node
-
-  if (inner.type !== 'MemberExpression') {
-    return false
-  }
-
-  return isPropertyAccessor(inner, 'step')
-}
+import { isTypeOfFnCall } from '../utils/parseFnCall.js'
 
 export default createRule({
   create(context) {
     const stack: number[] = []
 
     function pushStepCallback(node: Rule.Node) {
-      if (node.parent.type !== 'CallExpression' || !isStepCall(node.parent)) {
+      if (
+        node.parent.type !== 'CallExpression' ||
+        !isTypeOfFnCall(context, node.parent, ['step'])
+      ) {
         return
       }
 
@@ -35,7 +27,10 @@ export default createRule({
     function popStepCallback(node: Rule.Node) {
       const { parent } = node
 
-      if (parent.type === 'CallExpression' && isStepCall(parent)) {
+      if (
+        parent.type === 'CallExpression' &&
+        isTypeOfFnCall(context, parent, ['step'])
+      ) {
         stack.pop()
       }
     }
diff --git a/src/utils/parseFnCall.ts b/src/utils/parseFnCall.ts
index 8539d5a..91f78b3 100644
--- a/src/utils/parseFnCall.ts
+++ b/src/utils/parseFnCall.ts
@@ -63,6 +63,7 @@ const VALID_CHAINS = new Set([
   'test.only',
   'test.skip',
   'test.step',
+  'test.step.skip',
   'test.slow',
   'test.use',
 ])


Test patch:
diff --git a/src/rules/expect-expect.test.ts b/src/rules/expect-expect.test.ts
index 54c8249..725288d 100644
--- a/src/rules/expect-expect.test.ts
+++ b/src/rules/expect-expect.test.ts
@@ -83,6 +83,15 @@ runRuleTester('expect-expect', rule, {
         });
       `,
     },
+    {
+      code: javascript`
+        test.only('steps', async ({ page }) => {
+          await test.step.skip('first tab', async () => {
+            await expect(page.getByText('Hello')).toBeVisible();
+          });
+        });
+      `,
+    },
     {
       code: javascript`
         test('should fail', async ({ page }) => {
diff --git a/src/rules/no-conditional-in-test.test.ts b/src/rules/no-conditional-in-test.test.ts
index d77fa9f..c0d232b 100644
--- a/src/rules/no-conditional-in-test.test.ts
+++ b/src/rules/no-conditional-in-test.test.ts
@@ -181,6 +181,16 @@ runRuleTester('no-conditional-in-test', rule, {
       `,
       errors: [{ column: 5, endColumn: 17, endLine: 3, line: 3, messageId }],
     },
+    {
+      code: javascript`
+        test('test', async ({ page }) => {
+          await test.step.skip('step', async () => {
+            if (true) {}
+          });
+        });
+      `,
+      errors: [{ column: 5, endColumn: 17, endLine: 3, line: 3, messageId }],
+    },
     {
       code: 'it("foo", () => { if (true) { expect(1).toBe(1); } });',
       errors: [{ column: 19, endColumn: 51, endLine: 1, line: 1, messageId }],
diff --git a/src/rules/no-nested-step.test.ts b/src/rules/no-nested-step.test.ts
index c571975..26e63d1 100644
--- a/src/rules/no-nested-step.test.ts
+++ b/src/rules/no-nested-step.test.ts
@@ -35,6 +35,24 @@ runRuleTester('max-nested-step', rule, {
         { column: 11, endColumn: 20, endLine: 6, line: 6, messageId },
       ],
     },
+    {
+      code: javascript`
+        test('foo', async () => {
+          await test.step("step1", async () => {
+            await test.step("nested step1", async () => {
+              await expect(true).toBe(true);
+            });
+            await test.step.skip("nested step2", async () => {
+              await expect(true).toBe(true);
+            });
+          });
+        });
+      `,
+      errors: [
+        { column: 11, endColumn: 20, endLine: 3, line: 3, messageId },
+        { column: 11, endColumn: 25, endLine: 6, line: 6, messageId },
+      ],
+    },
     // Global aliases
     {
       code: javascript`
@@ -57,6 +75,7 @@ runRuleTester('max-nested-step', rule, {
   valid: [
     'await test.step("step1", () => {});',
     'await test.step("step1", async () => {});',
+    'await test.step.skip("step1", async () => {});',
     {
       code: javascript`
         test('foo', async () => {
@@ -85,6 +104,18 @@ runRuleTester('max-nested-step', rule, {
         });
       `,
     },
+    {
+      code: javascript`
+        test('foo', async () => {
+          await test.step("step1", async () => {
+            await expect(true).toBe(true);
+          });
+          await test.step.skip("step2", async () => {
+            await expect(true).toBe(true);
+          });
+        });
+      `,
+    },
     // Global aliases
     {
       code: 'await it.step("step1", () => {});',
diff --git a/src/rules/no-skipped-test.test.ts b/src/rules/no-skipped-test.test.ts
index 2846ac9..aaff974 100644
--- a/src/rules/no-skipped-test.test.ts
+++ b/src/rules/no-skipped-test.test.ts
@@ -247,6 +247,62 @@ runRuleTester('no-skipped-test', rule, {
         },
       },
     },
+    {
+      code: 'test.step.skip("a step", async () => {});',
+      errors: [
+        {
+          column: 11,
+          endColumn: 15,
+          line: 1,
+          messageId: 'noSkippedTest',
+          suggestions: [
+            {
+              messageId,
+              output: 'test.step("a step", async () => {});',
+            },
+          ],
+        },
+      ],
+    },
+    {
+      code: 'test.step.skip("a step", async () => {}, { timeout: 1000 });',
+      errors: [
+        {
+          column: 11,
+          endColumn: 15,
+          line: 1,
+          messageId: 'noSkippedTest',
+          suggestions: [
+            {
+              messageId,
+              output: 'test.step("a step", async () => {}, { timeout: 1000 });',
+            },
+          ],
+        },
+      ],
+    },
+    {
+      code: 'it.step.skip("a step", async () => {});',
+      errors: [
+        {
+          column: 9,
+          endColumn: 13,
+          line: 1,
+          messageId: 'noSkippedTest',
+          suggestions: [
+            {
+              messageId,
+              output: 'it.step("a step", async () => {});',
+            },
+          ],
+        },
+      ],
+      settings: {
+        playwright: {
+          globalAliases: { test: ['it'] },
+        },
+      },
+    },
   ],
   valid: [
     'test("a test", () => {});',
diff --git a/src/rules/no-skipped-test.ts b/src/rules/no-skipped-test.ts
index ea88700..2e32ad9 100644
--- a/src/rules/no-skipped-test.ts
+++ b/src/rules/no-skipped-test.ts
@@ -10,7 +10,11 @@ export default createRule({
         const allowConditional = !!options.allowConditional
 
         const call = parseFnCall(context, node)
-        if (call?.group !== 'test' && call?.group !== 'describe') {
+        if (
+          call?.group !== 'test' &&
+          call?.group !== 'describe' &&
+          call?.group !== 'step'
+        ) {
           return
         }
 
diff --git a/src/rules/valid-title.test.ts b/src/rules/valid-title.test.ts
index 5224b23..52702b3 100644
--- a/src/rules/valid-title.test.ts
+++ b/src/rules/valid-title.test.ts
@@ -75,6 +75,18 @@ runRuleTester('valid-title', rule, {
       ],
       options: [{ disallowedWords: ['properly'] }],
     },
+    {
+      code: 'test.step.skip(`that the value is set properly`, function () {})',
+      errors: [
+        {
+          column: 16,
+          data: { word: 'properly' },
+          line: 1,
+          messageId: 'disallowedWord',
+        },
+      ],
+      options: [{ disallowedWords: ['properly'] }],
+    },
     // Global aliases
     {
       code: 'it("the correct way to properly handle all things", () => {});',
diff --git a/src/utils/parseFnCall.test.ts b/src/utils/parseFnCall.test.ts
index e71fb8b..fee4e20 100644
--- a/src/utils/parseFnCall.test.ts
+++ b/src/utils/parseFnCall.test.ts
@@ -511,6 +511,48 @@ runRuleTester('test', rule, {
         },
       ],
     },
+    {
+      code: 'test.step.skip("a step", () => {});',
+      errors: [
+        {
+          column: 1,
+          data: expectedParsedFnCallResultData({
+            group: 'step',
+            head: {
+              local: 'test',
+              node: 'test',
+              original: null,
+            },
+            members: ['step', 'skip'],
+            name: 'step',
+            type: 'step',
+          }),
+          line: 1,
+          messageId: 'details',
+        },
+      ],
+    },
+    {
+      code: 'test.step("a step", () => {}, { timeout: 1000 });',
+      errors: [
+        {
+          column: 1,
+          data: expectedParsedFnCallResultData({
+            group: 'step',
+            head: {
+              local: 'test',
+              node: 'test',
+              original: null,
+            },
+            members: ['step'],
+            name: 'step',
+            type: 'step',
+          }),
+          line: 1,
+          messageId: 'details',
+        },
+      ],
+    },
     {
       code: 'test.only("a test", () => {});',
       errors: [