Skip to content

Commit 0579822

Browse files
committed
feat: Add no-nth-methods
1 parent 56eb171 commit 0579822

File tree

5 files changed

+128
-0
lines changed

5 files changed

+128
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ command line option.\
6666
|| | | [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 |
6767
|| | | [no-nested-step](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-nested-step.md) | Disallow nested `test.step()` methods |
6868
|| | | [no-networkidle](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-networkidle.md) | Disallow usage of the `networkidle` option |
69+
| | | | [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 |
6970
|| | | [no-page-pause](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-page-pause.md) | Disallow using `page.pause` |
7071
|| 🔧 | | [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 |
7172
| | | | [no-restricted-matchers](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-restricted-matchers.md) | Disallow specific matchers & modifiers |

docs/rules/no-nth-methods.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Disallow usage of `nth` methods (`no-nth-methods`)
2+
3+
This rule prevents the usage of `nth` methods (`first()`, `last()`, and
4+
`nth()`). These methods can be prone to flakiness if the DOM structure changes.
5+
6+
## Rule Details
7+
8+
Examples of **incorrect** code for this rule:
9+
10+
```javascript
11+
page.locator('button').first();
12+
page.locator('button').last();
13+
page.locator('button').nth(3);
14+
```

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import noFocusedTest from './rules/no-focused-test';
77
import noForceOption from './rules/no-force-option';
88
import noNestedStep from './rules/no-nested-step';
99
import noNetworkidle from './rules/no-networkidle';
10+
import noNthMethods from './rules/no-nth-methods';
1011
import noPagePause from './rules/no-page-pause';
1112
import noRestrictedMatchers from './rules/no-restricted-matchers';
1213
import noSkippedTest from './rules/no-skipped-test';
@@ -95,6 +96,7 @@ export = {
9596
'no-force-option': noForceOption,
9697
'no-nested-step': noNestedStep,
9798
'no-networkidle': noNetworkidle,
99+
'no-nth-methods': noNthMethods,
98100
'no-page-pause': noPagePause,
99101
'no-restricted-matchers': noRestrictedMatchers,
100102
'no-skipped-test': noSkippedTest,

src/rules/no-nth-methods.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Rule } from 'eslint';
2+
import { getStringValue } from '../utils/ast';
3+
4+
const methods = new Set(['first', 'last', 'nth']);
5+
6+
export default {
7+
create(context) {
8+
return {
9+
CallExpression(node) {
10+
if (node.callee.type !== 'MemberExpression') return;
11+
12+
const method = getStringValue(node.callee.property);
13+
if (!methods.has(method)) return;
14+
15+
context.report({
16+
data: { method },
17+
loc: {
18+
end: node.loc!.end,
19+
start: node.callee.property.loc!.start,
20+
},
21+
messageId: 'noNthMethod',
22+
});
23+
},
24+
};
25+
},
26+
meta: {
27+
docs: {
28+
category: 'Best Practices',
29+
description: 'Disallow usage of nth methods',
30+
recommended: true,
31+
url: 'https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-nth-methods.md',
32+
},
33+
messages: {
34+
noNthMethod: 'Unexpected use of {{method}}()',
35+
},
36+
type: 'problem',
37+
},
38+
} as Rule.RuleModule;

test/spec/no-nth-methods.spec.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import rule from '../../src/rules/no-nth-methods';
2+
import { runRuleTester } from '../utils/rule-tester';
3+
4+
const messageId = 'noNthMethod';
5+
6+
runRuleTester('no-nth-methods', rule, {
7+
invalid: [
8+
// First
9+
{
10+
code: 'page.locator("button").first()',
11+
errors: [{ column: 24, endColumn: 31, line: 1, messageId }],
12+
},
13+
{
14+
code: 'frame.locator("button").first()',
15+
errors: [{ column: 25, endColumn: 32, line: 1, messageId }],
16+
},
17+
{
18+
code: 'foo.locator("button").first()',
19+
errors: [{ column: 23, endColumn: 30, line: 1, messageId }],
20+
},
21+
{
22+
code: 'foo.first()',
23+
errors: [{ column: 5, endColumn: 12, line: 1, messageId }],
24+
},
25+
26+
// Last
27+
{
28+
code: 'page.locator("button").last()',
29+
errors: [{ column: 24, endColumn: 30, line: 1, messageId }],
30+
},
31+
{
32+
code: 'frame.locator("button").last()',
33+
errors: [{ column: 25, endColumn: 31, line: 1, messageId }],
34+
},
35+
{
36+
code: 'foo.locator("button").last()',
37+
errors: [{ column: 23, endColumn: 29, line: 1, messageId }],
38+
},
39+
{
40+
code: 'foo.last()',
41+
errors: [{ column: 5, endColumn: 11, line: 1, messageId }],
42+
},
43+
44+
// nth
45+
{
46+
code: 'page.locator("button").nth(3)',
47+
errors: [{ column: 24, endColumn: 30, line: 1, messageId }],
48+
},
49+
{
50+
code: 'frame.locator("button").nth(3)',
51+
errors: [{ column: 25, endColumn: 31, line: 1, messageId }],
52+
},
53+
{
54+
code: 'foo.locator("button").nth(3)',
55+
errors: [{ column: 23, endColumn: 29, line: 1, messageId }],
56+
},
57+
{
58+
code: 'foo.nth(32)',
59+
errors: [{ column: 5, endColumn: 12, line: 1, messageId }],
60+
},
61+
],
62+
valid: [
63+
'page',
64+
'page.locator("button")',
65+
'frame.locator("button")',
66+
'foo.locator("button")',
67+
68+
'page.locator("button").click()',
69+
'frame.locator("button").click()',
70+
'foo.locator("button").click()',
71+
'foo.click()',
72+
],
73+
});

0 commit comments

Comments
 (0)