Skip to content

Commit 81d7f54

Browse files
Haberkampmskelton
andauthored
feat: Add no-raw-locators rule (#160)
* feat: add prefer-user-facing-locators rule * docs: update docs of no-raw-selector to include all usage of .locator() * chore: rename rule from no-raw-selectors to no-raw-locators * Update test/spec/no-raw-locators.spec.ts --------- Co-authored-by: Mark Skelton <[email protected]>
1 parent a122c85 commit 81d7f54

File tree

4 files changed

+123
-0
lines changed

4 files changed

+123
-0
lines changed

docs/rules/no-raw-locators.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
## Disallow using raw locators (`no-raw-locators`)
2+
3+
Prefer using user-facing locators over raw locators to make tests more robust.
4+
5+
Check out the [Playwright documentation](https://playwright.dev/docs/locators)
6+
for more information.
7+
8+
## Rule Details
9+
10+
Example of **incorrect** code for this rule:
11+
12+
```javascript
13+
await page.locator('button').click();
14+
```
15+
16+
Example of **correct** code for this rule:
17+
18+
```javascript
19+
await page.getByRole('button').click();
20+
```
21+
22+
```javascript
23+
await page.getByRole('button', {
24+
name: 'Submit',
25+
});
26+
```

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import noNestedStep from './rules/no-nested-step';
1010
import noNetworkidle from './rules/no-networkidle';
1111
import noNthMethods from './rules/no-nth-methods';
1212
import noPagePause from './rules/no-page-pause';
13+
import noRawLocators from './rules/no-raw-locators';
1314
import noRestrictedMatchers from './rules/no-restricted-matchers';
1415
import noSkippedTest from './rules/no-skipped-test';
1516
import noUselessAwait from './rules/no-useless-await';
@@ -102,6 +103,7 @@ export = {
102103
'no-networkidle': noNetworkidle,
103104
'no-nth-methods': noNthMethods,
104105
'no-page-pause': noPagePause,
106+
'no-raw-locators': noRawLocators,
105107
'no-restricted-matchers': noRestrictedMatchers,
106108
'no-skipped-test': noSkippedTest,
107109
'no-useless-await': noUselessAwait,

src/rules/no-raw-locators.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Rule } from 'eslint';
2+
import { getStringValue, isPageMethod } from '../utils/ast';
3+
4+
export default {
5+
create(context) {
6+
return {
7+
CallExpression(node) {
8+
if (node.callee.type !== 'MemberExpression') return;
9+
const method = getStringValue(node.callee.property);
10+
11+
if (isPageMethod(node, 'locator') || method === 'locator') {
12+
context.report({ messageId: 'noRawLocator', node });
13+
}
14+
},
15+
};
16+
},
17+
meta: {
18+
docs: {
19+
category: 'Best Practices',
20+
description: 'Disallows the usage of raw locators',
21+
recommended: false,
22+
url: 'https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-raw-locators.md',
23+
},
24+
messages: {
25+
noRawLocator:
26+
'Usage of raw locator detected. Use methods like .getByRole() or .getByText() instead of raw locators.',
27+
},
28+
type: 'suggestion',
29+
},
30+
} as Rule.RuleModule;

test/spec/no-raw-locators.spec.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import rule from '../../src/rules/no-raw-locators';
2+
import { runRuleTester, test } from '../utils/rule-tester';
3+
4+
const messageId = 'noRawLocator';
5+
6+
runRuleTester('no-raw-locators', rule, {
7+
invalid: [
8+
{
9+
code: test('await page.locator()'),
10+
errors: [{ column: 34, endColumn: 48, line: 1, messageId }],
11+
},
12+
{
13+
code: test('await this.page.locator()'),
14+
errors: [{ column: 34, endColumn: 53, line: 1, messageId }],
15+
},
16+
{
17+
code: test("await page.locator('.btn')"),
18+
errors: [{ column: 34, endColumn: 54, line: 1, messageId }],
19+
},
20+
{
21+
code: test('await page["locator"](".btn")'),
22+
errors: [{ column: 34, endColumn: 57, line: 1, messageId }],
23+
},
24+
{
25+
code: test('await page[`locator`](".btn")'),
26+
errors: [{ column: 34, endColumn: 57, line: 1, messageId }],
27+
},
28+
29+
{
30+
code: test('await frame.locator()'),
31+
errors: [{ column: 34, endColumn: 49, line: 1, messageId }],
32+
},
33+
34+
{
35+
code: test(
36+
'const section = await page.getByRole("section"); section.locator(".btn")'
37+
),
38+
errors: [{ column: 77, endColumn: 100, line: 1, messageId }],
39+
},
40+
],
41+
valid: [
42+
test('await page.click()'),
43+
test('await this.page.click()'),
44+
test('await page["hover"]()'),
45+
test('await page[`check`]()'),
46+
47+
// Preferred user facing locators
48+
test('await page.getByText("lorem ipsum")'),
49+
test('await page.getByLabel(/Email/)'),
50+
test('await page.getByRole("button", { name: /submit/i })'),
51+
test('await page.getByTestId("my-test-button").click()'),
52+
test(
53+
'await page.getByRole("button").filter({ hasText: "Add to cart" }).click()'
54+
),
55+
56+
test('await frame.getByRole("button")'),
57+
58+
test(
59+
'const section = page.getByRole("section"); section.getByRole("button")'
60+
),
61+
62+
// bare calls
63+
test('() => page.locator'),
64+
],
65+
});

0 commit comments

Comments
 (0)