eslint-plugin-playwright 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -98,3 +98,175 @@ Example of **correct** code for this rule:
98
98
  await page.click('button');
99
99
  ```
100
100
 
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
+ ```
122
+
123
+ ### `no-eval`
124
+
125
+ Disallow usage of `page.$eval` and `page.$$eval`.
126
+
127
+ Examples of **incorrect** code for this rule:
128
+
129
+ ```js
130
+ const searchValue = await page.$eval('#search', el => el.value);
131
+
132
+ const divCounts = await page.$$eval('div', (divs, min) => divs.length >= min, 10);
133
+
134
+ await page.$eval('#search', el => el.value);
135
+
136
+ await page.$$eval('#search', el => el.value);
137
+ ```
138
+
139
+ Example of **correct** code for this rule:
140
+
141
+ ```js
142
+ await page.locator('button').evaluate(node => node.innerText);
143
+
144
+ await page.locator('div').evaluateAll((divs, min) => divs.length >= min, 10);
145
+ ```
146
+
147
+ ### `no-focused-test`
148
+
149
+ Disallow usage of `.only()` annotation
150
+
151
+ Examples of **incorrect** code for this rule:
152
+
153
+ ```js
154
+ test.only('focus this test', async ({ page }) => {});
155
+
156
+ test.describe.only('focus two tests', () => {
157
+ test('one', async ({ page }) => {});
158
+ test('two', async ({ page }) => {});
159
+ });
160
+
161
+ test.describe.parallel.only('focus two tests in parallel mode', () => {
162
+ test('one', async ({ page }) => {});
163
+ test('two', async ({ page }) => {});
164
+ });
165
+
166
+ test.describe.serial.only('focus two tests in serial mode', () => {
167
+ test('one', async ({ page }) => {});
168
+ test('two', async ({ page }) => {});
169
+ });
170
+
171
+ ```
172
+
173
+ Examples of **correct** code for this rule:
174
+
175
+ ```js
176
+ test('this test', async ({ page }) => {});
177
+
178
+ test.describe('two tests', () => {
179
+ test('one', async ({ page }) => {});
180
+ test('two', async ({ page }) => {});
181
+ });
182
+
183
+ test.describe.parallel('two tests in parallel mode', () => {
184
+ test('one', async ({ page }) => {});
185
+ test('two', async ({ page }) => {});
186
+ });
187
+
188
+ test.describe.serial('two tests in serial mode', () => {
189
+ test('one', async ({ page }) => {});
190
+ test('two', async ({ page }) => {});
191
+ });
192
+ ```
193
+
194
+ ### `no-wait-for-timeout`
195
+
196
+ Disallow usage of `page.waitForTimeout()`.
197
+
198
+ Example of **incorrect** code for this rule:
199
+
200
+ ```js
201
+ await page.waitForTimeout(5000);
202
+ ```
203
+
204
+ Examples of **correct** code for this rule:
205
+
206
+ ```js
207
+ // Use signals such as network events, selectors becoming visible and others instead.
208
+ await page.waitForLoadState();
209
+
210
+ await page.waitForUrl('/home');
211
+
212
+ await page.waitForFunction(() => window.innerWidth < 100);
213
+ ```
214
+
215
+ ### `no-skipped-test`
216
+
217
+ Disallow usage of the `.skip()` annotation.
218
+
219
+ Examples of **incorrect** code for this rule:
220
+
221
+ ```js
222
+ test.skip('skip this test', async ({ page }) => {});
223
+
224
+ test.describe.skip('skip two tests', () => {
225
+ test('one', async ({ page }) => {});
226
+ test('two', async ({ page }) => {});
227
+ });
228
+
229
+ test.describe('skip test inside describe', () => {
230
+ test.skip();
231
+ });
232
+
233
+ test.describe('skip test conditionally', async ({ browserName }) => {
234
+ test.skip(browserName === 'firefox', 'Working on it');
235
+ });
236
+
237
+ ```
238
+
239
+ Examples of **correct** code for this rule:
240
+
241
+ ```js
242
+ test('this test', async ({ page }) => {});
243
+
244
+ test.describe('two tests', () => {
245
+ test('one', async ({ page }) => {});
246
+ test('two', async ({ page }) => {});
247
+ });
248
+ ```
249
+
250
+ ### `no-force-option`
251
+
252
+ Disallow usage of the `{ force: true }` option.
253
+
254
+ Examples of **incorrect** code for this rule:
255
+
256
+ ```js
257
+ await page.locator('button').click({ force: true });
258
+
259
+ await page.locator('check').check({ force: true });
260
+
261
+ await page.locator('input').fill('something', { force: true });
262
+ ```
263
+
264
+ Examples of **correct** code for this rule:
265
+
266
+ ```js
267
+ await page.locator('button').click();
268
+
269
+ await page.locator('check').check();
270
+
271
+ await page.locator('input').fill('something');
272
+ ```
package/lib/index.js CHANGED
@@ -1,4 +1,11 @@
1
1
  const missingPlaywrightAwait = require("./rules/missing-playwright-await");
2
+ const noPagePause = require("./rules/no-page-pause");
3
+ const noElementHandle = require("./rules/no-element-handle");
4
+ const noEval = require("./rules/no-eval");
5
+ const noFocusedTest = require("./rules/no-focused-test");
6
+ const noSkippedTest = require("./rules/no-skipped-test");
7
+ const noWaitForTimeout = require("./rules/no-wait-for-timeout");
8
+ const noForceOption = require("./rules/no-force-option");
2
9
 
3
10
  module.exports = {
4
11
  configs: {
@@ -11,6 +18,12 @@ module.exports = {
11
18
  "no-empty-pattern": "off",
12
19
  "playwright/missing-playwright-await": "error",
13
20
  "playwright/no-page-pause": "warn",
21
+ "playwright/no-element-handle": "warn",
22
+ "playwright/no-eval": "warn",
23
+ "playwright/no-focused-test": "error",
24
+ "playwright/no-skipped-test": "warn",
25
+ "playwright/no-wait-for-timeout": "warn",
26
+ "playwright/no-force-option": "warn",
14
27
  },
15
28
  },
16
29
  "jest-playwright": {
@@ -49,5 +62,11 @@ module.exports = {
49
62
  rules: {
50
63
  "missing-playwright-await": missingPlaywrightAwait,
51
64
  "no-page-pause": noPagePause,
65
+ "no-element-handle": noElementHandle,
66
+ "no-eval": noEval,
67
+ "no-focused-test": noFocusedTest,
68
+ "no-skipped-test": noSkippedTest,
69
+ "no-wait-for-timeout": noWaitForTimeout,
70
+ "no-force-option": noForceOption,
52
71
  },
53
72
  };
@@ -58,6 +58,7 @@ const playwrightTestMatchers = [
58
58
  "toHaveCSS",
59
59
  "toHaveId",
60
60
  "toHaveJSProperty",
61
+ "toBeOK",
61
62
  "toHaveText",
62
63
  "toHaveTitle",
63
64
  "toHaveURL",
@@ -0,0 +1,47 @@
1
+ const { isObject, isCalleeProperty } = require('../utils/ast');
2
+
3
+ function getRange(node) {
4
+ const start = node.parent && node.parent.type === 'AwaitExpression'
5
+ ? node.parent.range[0]
6
+ : node.callee.object.range[0];
7
+
8
+ return [start, node.callee.property.range[1]];
9
+ }
10
+
11
+ module.exports = {
12
+ create(context) {
13
+ return {
14
+ CallExpression(node) {
15
+ if (isObject(node, 'page') && (isCalleeProperty(node, '$') || isCalleeProperty(node, '$$'))) {
16
+ context.report({
17
+ messageId: 'noElementHandle',
18
+ suggest: [
19
+ {
20
+ messageId: isCalleeProperty(node, '$')
21
+ ? 'replaceElementHandleWithLocator'
22
+ : 'replaceElementHandlesWithLocator',
23
+ fix: (fixer) => fixer.replaceTextRange(getRange(node), 'page.locator'),
24
+ },
25
+ ],
26
+ node,
27
+ });
28
+ }
29
+ },
30
+ };
31
+ },
32
+ meta: {
33
+ docs: {
34
+ category: 'Possible Errors',
35
+ description: 'The use of ElementHandle is discouraged, use Locator instead',
36
+ recommended: true,
37
+ url: 'https://github.com/playwright-community/eslint-plugin-playwright#no-element-handle',
38
+ },
39
+ hasSuggestions: true,
40
+ messages: {
41
+ noElementHandle: 'Unexpected use of element handles.',
42
+ replaceElementHandleWithLocator: 'Replace `page.$` with `page.locator`',
43
+ replaceElementHandlesWithLocator: 'Replace `page.$$` with `page.locator`',
44
+ },
45
+ type: 'suggestion',
46
+ },
47
+ };
@@ -0,0 +1,27 @@
1
+ const { isObject, isCalleeProperty } = require('../utils/ast');
2
+
3
+ module.exports = {
4
+ create(context) {
5
+ return {
6
+ CallExpression(node) {
7
+ if (isObject(node, 'page') && (isCalleeProperty(node, '$eval') || isCalleeProperty(node, '$$eval'))) {
8
+ context.report({ messageId: isCalleeProperty(node, '$eval') ? 'noEval' : 'noEvalAll', node });
9
+ }
10
+ },
11
+ };
12
+ },
13
+ meta: {
14
+ docs: {
15
+ category: 'Possible Errors',
16
+ description:
17
+ 'The use of `page.$eval` and `page.$$eval` are discouraged, use `locator.evaluate` or `locator.evaluateAll` instead',
18
+ recommended: true,
19
+ url: 'https://github.com/playwright-community/eslint-plugin-playwright#no-eval',
20
+ },
21
+ messages: {
22
+ noEval: 'Unexpected use of page.$eval().',
23
+ noEvalAll: 'Unexpected use of page.$$eval().',
24
+ },
25
+ type: 'problem',
26
+ },
27
+ };
@@ -0,0 +1,49 @@
1
+ const { isTestIdentifier, hasAnnotation } = require('../utils/ast');
2
+
3
+ function isTestGroup(node) {
4
+ const testGroups = new Set(['describe', 'parallel', 'serial']);
5
+
6
+ return (
7
+ node.object &&
8
+ node.object.type === 'MemberExpression' &&
9
+ node.object.property.type === 'Identifier' &&
10
+ testGroups.has(node.object.property.name)
11
+ );
12
+ }
13
+
14
+ /** @type {import('eslint').Rule.RuleModule} */
15
+ module.exports = {
16
+ create(context) {
17
+ return {
18
+ MemberExpression(node) {
19
+ if ((isTestIdentifier(node) || isTestGroup(node)) && hasAnnotation(node, 'only')) {
20
+ context.report({
21
+ messageId: 'noFocusedTest',
22
+ suggest: [
23
+ {
24
+ messageId: 'removeFocusedTestAnnotation',
25
+ // - 1 to remove the `.only` annotation with dot notation
26
+ fix: (fixer) => fixer.removeRange([node.property.range[0] - 1, node.property.range[1]]),
27
+ },
28
+ ],
29
+ node,
30
+ });
31
+ }
32
+ },
33
+ };
34
+ },
35
+ meta: {
36
+ docs: {
37
+ category: 'Possible Errors',
38
+ description: 'Prevent usage of `.only()` focus test annotation',
39
+ recommended: true,
40
+ url: 'https://github.com/playwright-community/eslint-plugin-playwright#no-focused-test',
41
+ },
42
+ hasSuggestions: true,
43
+ messages: {
44
+ noFocusedTest: 'Unexpected use of .only() annotation.',
45
+ removeFocusedTestAnnotation: 'Remove .only() annotation',
46
+ },
47
+ type: 'problem',
48
+ },
49
+ };
@@ -0,0 +1,52 @@
1
+ function isForceOptionEnabled({ parent }) {
2
+ return (
3
+ parent &&
4
+ parent.arguments &&
5
+ parent.arguments.length &&
6
+ parent.arguments.some(
7
+ (argument) =>
8
+ argument.type === 'ObjectExpression' &&
9
+ argument.properties.some(({ key, value }) => key && key.name === 'force' && value && value.value === true)
10
+ )
11
+ );
12
+ }
13
+
14
+ // https://playwright.dev/docs/api/class-locator
15
+ const methodsWithForceOption = new Set([
16
+ 'check',
17
+ 'uncheck',
18
+ 'click',
19
+ 'dblclick',
20
+ 'dragTo',
21
+ 'fill',
22
+ 'hover',
23
+ 'selectOption',
24
+ 'selectText',
25
+ 'setChecked',
26
+ 'tap',
27
+ ]);
28
+
29
+ /** @type {import('eslint').Rule.RuleModule} */
30
+ module.exports = {
31
+ create(context) {
32
+ return {
33
+ MemberExpression(node) {
34
+ if (node.property && methodsWithForceOption.has(node.property.name) && isForceOptionEnabled(node)) {
35
+ context.report({ messageId: 'noForceOption', node });
36
+ }
37
+ },
38
+ };
39
+ },
40
+ meta: {
41
+ docs: {
42
+ category: 'Best Practices',
43
+ description: 'Prevent usage of `{ force: true }` option.',
44
+ recommended: true,
45
+ url: 'https://github.com/playwright-community/eslint-plugin-playwright#no-force-option',
46
+ },
47
+ messages: {
48
+ noForceOption: 'Unexpected use of { force: true } option.',
49
+ },
50
+ type: 'suggestion',
51
+ },
52
+ };
@@ -1,8 +1,10 @@
1
+ const { isObject, isCalleeProperty } = require('../utils/ast');
2
+
1
3
  module.exports = {
2
4
  create(context) {
3
5
  return {
4
- MemberExpression(node) {
5
- if (node.object.name === "page" && node.property.name === "pause") {
6
+ CallExpression(node) {
7
+ if (isObject(node, 'page') && isCalleeProperty(node, 'pause')) {
6
8
  context.report({ messageId: "noPagePause", node });
7
9
  }
8
10
  },
@@ -0,0 +1,66 @@
1
+ const {
2
+ isTestIdentifier,
3
+ isObjectProperty,
4
+ hasAnnotation,
5
+ isStringLiteral,
6
+ isBooleanLiteral,
7
+ isBinaryExpression,
8
+ } = require('../utils/ast');
9
+
10
+ /**
11
+ * This function returns needed range to remove skip annotation.
12
+ *
13
+ * To cover the standalone cases:
14
+ * 1. test.skip() => when there's no arguments
15
+ * 2. test.skip(browserName === 'firefox', 'Working on it') => when there's first argument is a binary expression and the second argument is a string literal
16
+ * 3. test.skip(true, 'Working on it') => when there's first argument is a boolean literal and the second argument is a string literal
17
+ *
18
+ * So, if it's standalone skip then we need to remove the whole line
19
+ *
20
+ * Otherwise we need to remove the range of `.skip` annotation - 1 (dot notation).
21
+ */
22
+ function getSkipRange(node) {
23
+ const [first, second] = node.parent.arguments;
24
+
25
+ const isStandaloneSkip =
26
+ !node.parent.arguments.length ||
27
+ ((isBinaryExpression(first) || isBooleanLiteral(first)) && isStringLiteral(second));
28
+
29
+ return isStandaloneSkip ? node.parent.parent.range : [node.property.range[0] - 1, node.property.range[1]];
30
+ }
31
+
32
+ /** @type {import('eslint').Rule.RuleModule} */
33
+ module.exports = {
34
+ create(context) {
35
+ return {
36
+ MemberExpression(node) {
37
+ if ((isTestIdentifier(node) || isObjectProperty(node, 'describe')) && hasAnnotation(node, 'skip')) {
38
+ context.report({
39
+ messageId: 'noSkippedTest',
40
+ suggest: [
41
+ {
42
+ messageId: 'removeSkippedTestAnnotation',
43
+ fix: (fixer) => fixer.removeRange(getSkipRange(node)),
44
+ },
45
+ ],
46
+ node,
47
+ });
48
+ }
49
+ },
50
+ };
51
+ },
52
+ meta: {
53
+ docs: {
54
+ category: 'Best Practices',
55
+ description: 'Prevent usage of the `.skip()` skip test annotation.',
56
+ recommended: true,
57
+ url: 'https://github.com/playwright-community/eslint-plugin-playwright#no-skipped-test',
58
+ },
59
+ hasSuggestions: true,
60
+ messages: {
61
+ noSkippedTest: 'Unexpected use of the `.skip()` annotation.',
62
+ removeSkippedTestAnnotation: 'Remove the `.skip()` annotation.',
63
+ },
64
+ type: 'suggestion',
65
+ },
66
+ };
@@ -0,0 +1,40 @@
1
+ const { isObject, isCalleeProperty } = require('../utils/ast');
2
+
3
+ /** @type {import('eslint').Rule.RuleModule} */
4
+ module.exports = {
5
+ create(context) {
6
+ return {
7
+ CallExpression(node) {
8
+ if (isObject(node, 'page') && isCalleeProperty(node, 'waitForTimeout')) {
9
+ context.report({
10
+ messageId: 'noWaitForTimeout',
11
+ suggest: [
12
+ {
13
+ messageId: 'removeWaitForTimeout',
14
+ fix: (fixer) =>
15
+ fixer.remove(
16
+ node.parent && node.parent.type !== 'AwaitExpression' ? node.parent : node.parent.parent
17
+ ),
18
+ },
19
+ ],
20
+ node,
21
+ });
22
+ }
23
+ },
24
+ };
25
+ },
26
+ meta: {
27
+ docs: {
28
+ category: 'Best Practices',
29
+ description: 'Prevent usage of page.waitForTimeout()',
30
+ recommended: true,
31
+ url: 'https://github.com/playwright-community/eslint-plugin-playwright#no-wait-for-timeout',
32
+ },
33
+ hasSuggestions: true,
34
+ messages: {
35
+ noWaitForTimeout: 'Unexpected use of page.waitForTimeout().',
36
+ removeWaitForTimeout: 'Remove the page.waitForTimeout() method.',
37
+ },
38
+ type: 'suggestion',
39
+ },
40
+ };
@@ -0,0 +1,61 @@
1
+ function isObject({ callee }, name) {
2
+ return (
3
+ callee &&
4
+ callee.type === 'MemberExpression' &&
5
+ callee.object.type === 'Identifier' &&
6
+ callee.object.name === name
7
+ );
8
+ }
9
+
10
+ function isCalleeProperty({ callee }, name) {
11
+ return (
12
+ callee &&
13
+ callee.property &&
14
+ callee.property.type === 'Identifier' &&
15
+ callee.property.name === name
16
+ );
17
+ }
18
+
19
+ function isTestIdentifier(node) {
20
+ return node.object && node.object.type === 'Identifier' && node.object.name === 'test';
21
+ }
22
+
23
+ function hasAnnotation(node, annotation) {
24
+ return (
25
+ node.property &&
26
+ node.property.type === 'Identifier' &&
27
+ node.property.name === annotation
28
+ );
29
+ }
30
+
31
+ function isObjectProperty({ object }, name) {
32
+ return (
33
+ object &&
34
+ object.type === 'MemberExpression' &&
35
+ object.property.type === 'Identifier' &&
36
+ object.property.name === name
37
+ );
38
+ }
39
+
40
+ function isStringLiteral(node) {
41
+ return node && node.type === 'Literal' && typeof node.value === 'string';
42
+ }
43
+
44
+ function isBooleanLiteral(node) {
45
+ return node && node.type === 'Literal' && typeof node.value === 'boolean';
46
+ }
47
+
48
+ function isBinaryExpression(node) {
49
+ return node && node.type === 'BinaryExpression';
50
+ }
51
+
52
+ module.exports = {
53
+ isObject,
54
+ isCalleeProperty,
55
+ isTestIdentifier,
56
+ hasAnnotation,
57
+ isObjectProperty,
58
+ isStringLiteral,
59
+ isBooleanLiteral,
60
+ isBinaryExpression,
61
+ };
@@ -0,0 +1,34 @@
1
+ const { RuleTester } = require('eslint');
2
+
3
+ /**
4
+ *
5
+ * @param {string} name - The name of the rule
6
+ * @param {string} rule - Path to the rule to test
7
+ * @param {Object} tests - The tests to run
8
+ * @param {string[]} tests.valid - Valid tests
9
+ * @param {string[]} tests.invalid - Invalid tests
10
+ *
11
+ * @example
12
+ * const rule = require('../lib/rules/missing-playwright-await');
13
+ *
14
+ * runRuleTester('missing-playwright-await', rule, {
15
+ * valid: ['await expect(page.locator('checkbox')).toBeChecked()'],
16
+ * invalid: ['expect(page.locator('checkbox')).toBeChecked()'],
17
+ * });
18
+ */
19
+ function runRuleTester(name, rule, tests) {
20
+ const config = {
21
+ parserOptions: {
22
+ ecmaVersion: 2018,
23
+ },
24
+ };
25
+
26
+ return new RuleTester(config).run(name, rule, tests);
27
+ }
28
+
29
+ const wrapInTest = (input) => `test('test', async () => { ${input} })`;
30
+
31
+ module.exports = {
32
+ runRuleTester,
33
+ wrapInTest,
34
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "eslint-plugin-playwright",
3
3
  "description": "ESLint plugin for Playwright testing.",
4
- "version": "0.7.0",
4
+ "version": "0.9.0",
5
5
  "main": "lib/index.js",
6
6
  "repository": "https://github.com/playwright-community/eslint-plugin-playwright",
7
7
  "author": "Max Schmitt <max@schmitt.mx>",