Skip to content

Commit 0ec8932

Browse files
authored
Add new "no element handle" rule (#40)
* rule: add new noElementHandle rule * chore: move end range to const * fix: getRange method for non-awaiting statements * test: add additional tests for the noElementHandle rule * chore: improve noElementHandle rule * docs: update the wording of noElementHandle rule * rule: update message to match with other rule naming conventions * rule: change noElementHandle to warn level * rule: noElementHandle change fixable to suggestion * test: add valid tests for noElementHandle rule * rule: change type for noElementHandle to suggestion * rule: update noElementHandle rule's message for suggestions * rule: update noElementHandle with getRange method
1 parent 488c9b5 commit 0ec8932

File tree

4 files changed

+202
-0
lines changed

4 files changed

+202
-0
lines changed

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,24 @@ Example of **correct** code for this rule:
9898
await page.click('button');
9999
```
100100

101+
### `no-element-handle`
102+
103+
Disallow the creation of element handles with `page.$` or `page.$$`.
104+
105+
Examples of **incorrect** code for this rule:
106+
107+
```js
108+
// Element Handle
109+
const buttonHandle = await page.$('button');
110+
await buttonHandle.click();
111+
112+
// Element Handles
113+
const linkHandles = await page.$$('a');
114+
```
115+
116+
Example of **correct** code for this rule:
117+
118+
```js
119+
const buttonLocator = page.locator('button');
120+
await buttonLocator.click();
121+
```

lib/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const missingPlaywrightAwait = require("./rules/missing-playwright-await");
22
const noPagePause = require("./rules/no-page-pause");
3+
const noElementHandle = require("./rules/no-element-handle");
34

45
module.exports = {
56
configs: {
@@ -12,6 +13,7 @@ module.exports = {
1213
"no-empty-pattern": "off",
1314
"playwright/missing-playwright-await": "error",
1415
"playwright/no-page-pause": "warn",
16+
"playwright/no-element-handle": "warn",
1517
},
1618
},
1719
"jest-playwright": {
@@ -50,5 +52,6 @@ module.exports = {
5052
rules: {
5153
"missing-playwright-await": missingPlaywrightAwait,
5254
"no-page-pause": noPagePause,
55+
"no-element-handle": noElementHandle
5356
},
5457
};

lib/rules/no-element-handle.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
function isPageIdentifier({ callee }) {
2+
return (
3+
callee &&
4+
callee.type === 'MemberExpression' &&
5+
callee.object.type === 'Identifier' &&
6+
callee.object.name === 'page'
7+
);
8+
}
9+
10+
function isElementHandleIdentifier({ callee }) {
11+
return (
12+
callee &&
13+
callee.property &&
14+
callee.property.type === 'Identifier' &&
15+
callee.property.name === '$'
16+
);
17+
}
18+
19+
function isElementHandlesIdentifier({ callee }) {
20+
return (
21+
callee &&
22+
callee.property &&
23+
callee.property.type === 'Identifier' &&
24+
callee.property.name === '$$'
25+
);
26+
}
27+
28+
function getRange(node) {
29+
const start = node.parent && node.parent.type === 'AwaitExpression'
30+
? node.parent.range[0]
31+
: node.callee.object.range[0];
32+
33+
return [start, node.callee.property.range[1]];
34+
}
35+
36+
module.exports = {
37+
create(context) {
38+
return {
39+
CallExpression(node) {
40+
if (isPageIdentifier(node) && (isElementHandleIdentifier(node) || isElementHandlesIdentifier(node))) {
41+
context.report({
42+
messageId: 'noElementHandle',
43+
suggest: [
44+
{
45+
messageId: isElementHandleIdentifier(node)
46+
? 'replaceElementHandleWithLocator'
47+
: 'replaceElementHandlesWithLocator',
48+
fix: (fixer) => fixer.replaceTextRange(getRange(node), 'page.locator'),
49+
},
50+
],
51+
node,
52+
});
53+
}
54+
},
55+
};
56+
},
57+
meta: {
58+
docs: {
59+
category: 'Possible Errors',
60+
description: 'The use of ElementHandle is discouraged, use Locator instead',
61+
recommended: true,
62+
url: 'https://github.com/playwright-community/eslint-plugin-playwright#no-element-handle',
63+
},
64+
hasSuggestions: true,
65+
messages: {
66+
noElementHandle: 'Unexpected use of element handles.',
67+
replaceElementHandleWithLocator: 'Replace `page.$` with `page.locator`',
68+
replaceElementHandlesWithLocator: 'Replace `page.$$` with `page.locator`',
69+
},
70+
type: 'suggestion',
71+
},
72+
};

test/no-element-handle.spec.js

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
const { RuleTester } = require('eslint');
2+
const rule = require('../lib/rules/no-element-handle');
3+
4+
RuleTester.setDefaultConfig({
5+
parserOptions: {
6+
ecmaVersion: 2018,
7+
},
8+
});
9+
10+
const wrapInTest = (input) => `test('verify noElementHandle rule', async () => { ${input} })`;
11+
12+
const invalid = (code, output) => ({
13+
code: wrapInTest(code),
14+
errors: [
15+
{
16+
messageId: 'noElementHandle',
17+
suggestions: [
18+
{
19+
messageId: code.includes('page.$$') ? 'replaceElementHandlesWithLocator' : 'replaceElementHandleWithLocator',
20+
output: wrapInTest(output),
21+
},
22+
],
23+
},
24+
],
25+
});
26+
27+
const valid = (code) => ({
28+
code: wrapInTest(code),
29+
});
30+
31+
new RuleTester().run('no-element-handle', rule, {
32+
invalid: [
33+
// element handle as const
34+
invalid('const handle = await page.$("text=Submit");', 'const handle = page.locator("text=Submit");'),
35+
36+
// element handle as let
37+
invalid('let handle = await page.$("text=Submit");', 'let handle = page.locator("text=Submit");'),
38+
39+
// element handle as expression statement without await
40+
invalid('page.$("div")', 'page.locator("div")'),
41+
42+
// element handles as expression statement without await
43+
invalid('page.$$("div")', 'page.locator("div")'),
44+
45+
// element handle as expression statement
46+
invalid('await page.$("div")', 'page.locator("div")'),
47+
48+
// element handle click
49+
invalid('await (await page.$$("div")).click();', 'await (page.locator("div")).click();'),
50+
51+
// element handles as const
52+
invalid('const handles = await page.$$("a")', 'const handles = page.locator("a")'),
53+
54+
// element handles as let
55+
invalid('let handles = await page.$$("a")', 'let handles = page.locator("a")'),
56+
57+
// element handles as expression statement
58+
invalid('await page.$$("a")', 'page.locator("a")'),
59+
60+
// return element handle without awaiting it
61+
invalid(
62+
'function getHandle() { return page.$("button"); }',
63+
'function getHandle() { return page.locator("button"); }'
64+
),
65+
66+
// return element handles without awaiting it
67+
invalid(
68+
'function getHandle() { return page.$$("button"); }',
69+
'function getHandle() { return page.locator("button"); }'
70+
),
71+
72+
// missed return for the element handle
73+
invalid('function getHandle() { page.$("button"); }', 'function getHandle() { page.locator("button"); }'),
74+
75+
// arrow function return element handle without awaiting it
76+
invalid('const getHandles = () => page.$("links");', 'const getHandles = () => page.locator("links");'),
77+
78+
// arrow function return element handles without awaiting it
79+
invalid('const getHandles = () => page.$$("links");', 'const getHandles = () => page.locator("links");'),
80+
],
81+
valid: [
82+
// page locator
83+
valid('page.locator("a")'),
84+
85+
// page locator with action
86+
valid('await page.locator("a").click();'),
87+
88+
// const $
89+
valid('const $ = "text";'),
90+
91+
// $ as a method
92+
valid('$("a");'),
93+
94+
// this.$ as a method
95+
valid('this.$("a");'),
96+
97+
// internalPage.$ method
98+
valid('internalPage.$("a");'),
99+
100+
// this.page.$$$ method
101+
valid('this.page.$$$("div");'),
102+
103+
// page.$$$ method
104+
valid('page.$$$("div");'),
105+
],
106+
});

0 commit comments

Comments
 (0)