Skip to content

Commit 731a4e1

Browse files
carlbrayCarl Bray
and
Carl Bray
authored
feat(prefer-locator): Add rule to suggest not using page methods (#315)
* Add prefer-page-locator-fill * Rename rule * Add set of methods to check against * Update docs too * Fix formatting * Change descriptons, add more tests and examples * Switch to CallExpression * Add examples without await * Use test from rule-tester --------- Co-authored-by: Carl Bray <[email protected]>
1 parent 5c20c52 commit 731a4e1

File tree

5 files changed

+200
-0
lines changed

5 files changed

+200
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ CLI option\
195195
| [prefer-hooks-on-top](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-hooks-on-top.md) | Suggest having hooks before any test cases | | | |
196196
| [prefer-lowercase-title](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-lowercase-title.md) | Enforce lowercase test names | | 🔧 | |
197197
| [prefer-native-locators](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-native-locators.md) | Suggest built-in locators over `page.locator()` | | 🔧 | |
198+
| [prefer-locator](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-locator.md) | Suggest locators over page methods | | | |
198199
| [prefer-strict-equal](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-strict-equal.md) | Suggest using `toStrictEqual()` | | | 💡 |
199200
| [prefer-to-be](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-to-be.md) | Suggest using `toBe()` | | 🔧 | |
200201
| [prefer-to-contain](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-to-contain.md) | Suggest using `toContain()` | | 🔧 | |

docs/rules/prefer-locator.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Suggest using `page.locator()` (`prefer-locator`)
2+
3+
Suggest using locators and their associated methods instead of page methods for
4+
performing actions.
5+
6+
## Rule details
7+
8+
This rule triggers a warning if page methods are used, instead of locators.
9+
10+
The following patterns are considered warnings:
11+
12+
```javascript
13+
page.click('css=button')
14+
await page.click('css=button')
15+
await page.dblclick('xpath=//button')
16+
await page.fill('input[type="password"]', 'password')
17+
18+
await page.frame('frame-name').click('css=button')
19+
```
20+
21+
The following pattern are **not** warnings:
22+
23+
```javascript
24+
const locator = page.locator('css=button')
25+
await page.getByRole('password').fill('password')
26+
await page.getByLabel('User Name').fill('John')
27+
await page.getByRole('button', { name: 'Sign in' }).click()
28+
await page.locator('input[type="password"]').fill('password')
29+
await page.locator('css=button').click()
30+
await page.locator('xpath=//button').dblclick()
31+
32+
await page.frameLocator('#my-iframe').getByText('Submit').click()
33+
```

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import preferComparisonMatcher from './rules/prefer-comparison-matcher'
3030
import preferEqualityMatcher from './rules/prefer-equality-matcher'
3131
import preferHooksInOrder from './rules/prefer-hooks-in-order'
3232
import preferHooksOnTop from './rules/prefer-hooks-on-top'
33+
import preferLocator from './rules/prefer-locator'
3334
import preferLowercaseTitle from './rules/prefer-lowercase-title'
3435
import preferNativeLocators from './rules/prefer-native-locators'
3536
import preferStrictEqual from './rules/prefer-strict-equal'
@@ -81,6 +82,7 @@ const index = {
8182
'prefer-equality-matcher': preferEqualityMatcher,
8283
'prefer-hooks-in-order': preferHooksInOrder,
8384
'prefer-hooks-on-top': preferHooksOnTop,
85+
'prefer-locator': preferLocator,
8486
'prefer-lowercase-title': preferLowercaseTitle,
8587
'prefer-native-locators': preferNativeLocators,
8688
'prefer-strict-equal': preferStrictEqual,

src/rules/prefer-locator.test.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { runRuleTester, test } from '../utils/rule-tester'
2+
import rule from './prefer-locator'
3+
4+
runRuleTester('prefer-locator', rule, {
5+
invalid: [
6+
{
7+
code: test(`await page.fill('input[type="password"]', 'password')`),
8+
errors: [
9+
{
10+
column: 34,
11+
endColumn: 81,
12+
endLine: 1,
13+
line: 1,
14+
messageId: 'preferLocator',
15+
},
16+
],
17+
output: null,
18+
},
19+
{
20+
code: test(`await page.dblclick('xpath=//button')`),
21+
errors: [
22+
{
23+
column: 34,
24+
endColumn: 65,
25+
endLine: 1,
26+
line: 1,
27+
messageId: 'preferLocator',
28+
},
29+
],
30+
output: null,
31+
},
32+
{
33+
code: `page.click('xpath=//button')`,
34+
errors: [
35+
{
36+
column: 1,
37+
endColumn: 29,
38+
endLine: 1,
39+
line: 1,
40+
messageId: 'preferLocator',
41+
},
42+
],
43+
output: null,
44+
},
45+
{
46+
code: test(`await page.frame('frame-name').click('css=button')`),
47+
errors: [
48+
{
49+
column: 34,
50+
endColumn: 78,
51+
endLine: 1,
52+
line: 1,
53+
messageId: 'preferLocator',
54+
},
55+
],
56+
output: null,
57+
},
58+
{
59+
code: `page.frame('frame-name').click('css=button')`,
60+
errors: [
61+
{
62+
column: 1,
63+
endColumn: 45,
64+
endLine: 1,
65+
line: 1,
66+
messageId: 'preferLocator',
67+
},
68+
],
69+
output: null,
70+
},
71+
],
72+
valid: [
73+
{
74+
code: `const locator = page.locator('input[type="password"]')`,
75+
},
76+
{
77+
code: test(
78+
`await page.locator('input[type="password"]').fill('password')`,
79+
),
80+
},
81+
{
82+
code: test(`await page.locator('xpath=//button').dblclick()`),
83+
},
84+
{
85+
code: `page.locator('xpath=//button').click()`,
86+
},
87+
{
88+
code: test(
89+
`await page.frameLocator('#my-iframe').locator('css=button').click()`,
90+
),
91+
},
92+
{
93+
code: test(`await page.evaluate('1 + 2')`),
94+
},
95+
{
96+
code: `page.frame('frame-name')`,
97+
},
98+
],
99+
})

src/rules/prefer-locator.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import ESTree from 'estree'
2+
import { getStringValue, isPageMethod } from '../utils/ast'
3+
import { createRule } from '../utils/createRule'
4+
5+
const pageMethods = new Set([
6+
'click',
7+
'dblclick',
8+
'dispatchEvent',
9+
'fill',
10+
'focus',
11+
'getAttribute',
12+
'hover',
13+
'innerHTML',
14+
'innerText',
15+
'inputValue',
16+
'isChecked',
17+
'isDisabled',
18+
'isEditable',
19+
'isEnabled',
20+
'isHidden',
21+
'isVisible',
22+
'press',
23+
'selectOption',
24+
'setChecked',
25+
'setInputFiles',
26+
'tap',
27+
'textContent',
28+
'uncheck',
29+
])
30+
31+
function isSupportedMethod(node: ESTree.CallExpression) {
32+
if (node.callee.type !== 'MemberExpression') return false
33+
34+
const name = getStringValue(node.callee.property)
35+
return pageMethods.has(name) && isPageMethod(node, name)
36+
}
37+
38+
export default createRule({
39+
create(context) {
40+
return {
41+
CallExpression(node) {
42+
// Must be a method we care about
43+
if (!isSupportedMethod(node)) return
44+
45+
context.report({
46+
messageId: 'preferLocator',
47+
node,
48+
})
49+
},
50+
}
51+
},
52+
meta: {
53+
docs: {
54+
category: 'Best Practices',
55+
description: 'Suggest locators over page methods',
56+
recommended: false,
57+
url: 'https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-locator.md',
58+
},
59+
messages: {
60+
preferLocator: 'Prefer locator methods instead of page methods',
61+
},
62+
schema: [],
63+
type: 'suggestion',
64+
},
65+
})

0 commit comments

Comments
 (0)