From 0579822707ebbeaba4201d045867c15ce6e2961b Mon Sep 17 00:00:00 2001 From: Mark Skelton Date: Mon, 19 Jun 2023 12:33:37 -0500 Subject: [PATCH] feat: Add `no-nth-methods` --- README.md | 1 + docs/rules/no-nth-methods.md | 14 ++++++ src/index.ts | 2 + src/rules/no-nth-methods.ts | 38 +++++++++++++++++ test/spec/no-nth-methods.spec.ts | 73 ++++++++++++++++++++++++++++++++ 5 files changed, 128 insertions(+) create mode 100644 docs/rules/no-nth-methods.md create mode 100644 src/rules/no-nth-methods.ts create mode 100644 test/spec/no-nth-methods.spec.ts diff --git a/README.md b/README.md index 2e0f606..9a0e65e 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ command line option.\ | ✔ | | | [no-force-option](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-force-option.md) | Disallow usage of the `{ force: true }` option | | ✔ | | | [no-nested-step](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-nested-step.md) | Disallow nested `test.step()` methods | | ✔ | | | [no-networkidle](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-networkidle.md) | Disallow usage of the `networkidle` option | +| | | | [no-nth-methods](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-nth-methods.md) | Disallow usage of `first()`, `last()`, and `nth()` methods | | ✔ | | | [no-page-pause](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-page-pause.md) | Disallow using `page.pause` | | ✔ | 🔧 | | [no-useless-await](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-useless-await.md) | Disallow unnecessary `await`s for Playwright methods | | | | | [no-restricted-matchers](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-restricted-matchers.md) | Disallow specific matchers & modifiers | diff --git a/docs/rules/no-nth-methods.md b/docs/rules/no-nth-methods.md new file mode 100644 index 0000000..8171c02 --- /dev/null +++ b/docs/rules/no-nth-methods.md @@ -0,0 +1,14 @@ +# Disallow usage of `nth` methods (`no-nth-methods`) + +This rule prevents the usage of `nth` methods (`first()`, `last()`, and +`nth()`). These methods can be prone to flakiness if the DOM structure changes. + +## Rule Details + +Examples of **incorrect** code for this rule: + +```javascript +page.locator('button').first(); +page.locator('button').last(); +page.locator('button').nth(3); +``` diff --git a/src/index.ts b/src/index.ts index 8e90274..073c3a6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ import noFocusedTest from './rules/no-focused-test'; import noForceOption from './rules/no-force-option'; import noNestedStep from './rules/no-nested-step'; import noNetworkidle from './rules/no-networkidle'; +import noNthMethods from './rules/no-nth-methods'; import noPagePause from './rules/no-page-pause'; import noRestrictedMatchers from './rules/no-restricted-matchers'; import noSkippedTest from './rules/no-skipped-test'; @@ -95,6 +96,7 @@ export = { 'no-force-option': noForceOption, 'no-nested-step': noNestedStep, 'no-networkidle': noNetworkidle, + 'no-nth-methods': noNthMethods, 'no-page-pause': noPagePause, 'no-restricted-matchers': noRestrictedMatchers, 'no-skipped-test': noSkippedTest, diff --git a/src/rules/no-nth-methods.ts b/src/rules/no-nth-methods.ts new file mode 100644 index 0000000..b773590 --- /dev/null +++ b/src/rules/no-nth-methods.ts @@ -0,0 +1,38 @@ +import { Rule } from 'eslint'; +import { getStringValue } from '../utils/ast'; + +const methods = new Set(['first', 'last', 'nth']); + +export default { + create(context) { + return { + CallExpression(node) { + if (node.callee.type !== 'MemberExpression') return; + + const method = getStringValue(node.callee.property); + if (!methods.has(method)) return; + + context.report({ + data: { method }, + loc: { + end: node.loc!.end, + start: node.callee.property.loc!.start, + }, + messageId: 'noNthMethod', + }); + }, + }; + }, + meta: { + docs: { + category: 'Best Practices', + description: 'Disallow usage of nth methods', + recommended: true, + url: 'https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-nth-methods.md', + }, + messages: { + noNthMethod: 'Unexpected use of {{method}}()', + }, + type: 'problem', + }, +} as Rule.RuleModule; diff --git a/test/spec/no-nth-methods.spec.ts b/test/spec/no-nth-methods.spec.ts new file mode 100644 index 0000000..57c52a3 --- /dev/null +++ b/test/spec/no-nth-methods.spec.ts @@ -0,0 +1,73 @@ +import rule from '../../src/rules/no-nth-methods'; +import { runRuleTester } from '../utils/rule-tester'; + +const messageId = 'noNthMethod'; + +runRuleTester('no-nth-methods', rule, { + invalid: [ + // First + { + code: 'page.locator("button").first()', + errors: [{ column: 24, endColumn: 31, line: 1, messageId }], + }, + { + code: 'frame.locator("button").first()', + errors: [{ column: 25, endColumn: 32, line: 1, messageId }], + }, + { + code: 'foo.locator("button").first()', + errors: [{ column: 23, endColumn: 30, line: 1, messageId }], + }, + { + code: 'foo.first()', + errors: [{ column: 5, endColumn: 12, line: 1, messageId }], + }, + + // Last + { + code: 'page.locator("button").last()', + errors: [{ column: 24, endColumn: 30, line: 1, messageId }], + }, + { + code: 'frame.locator("button").last()', + errors: [{ column: 25, endColumn: 31, line: 1, messageId }], + }, + { + code: 'foo.locator("button").last()', + errors: [{ column: 23, endColumn: 29, line: 1, messageId }], + }, + { + code: 'foo.last()', + errors: [{ column: 5, endColumn: 11, line: 1, messageId }], + }, + + // nth + { + code: 'page.locator("button").nth(3)', + errors: [{ column: 24, endColumn: 30, line: 1, messageId }], + }, + { + code: 'frame.locator("button").nth(3)', + errors: [{ column: 25, endColumn: 31, line: 1, messageId }], + }, + { + code: 'foo.locator("button").nth(3)', + errors: [{ column: 23, endColumn: 29, line: 1, messageId }], + }, + { + code: 'foo.nth(32)', + errors: [{ column: 5, endColumn: 12, line: 1, messageId }], + }, + ], + valid: [ + 'page', + 'page.locator("button")', + 'frame.locator("button")', + 'foo.locator("button")', + + 'page.locator("button").click()', + 'frame.locator("button").click()', + 'foo.locator("button").click()', + 'foo.click()', + ], +});