eslint-plugin-playwright 0.9.0 → 0.10.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 +139 -13
- package/lib/index.js +47 -41
- package/lib/rules/max-nested-describe.js +70 -0
- package/lib/rules/missing-playwright-await.js +57 -54
- package/lib/rules/no-conditional-in-test.js +61 -0
- package/lib/rules/no-element-handle.js +12 -6
- package/lib/rules/no-eval.js +8 -2
- package/lib/rules/no-focused-test.js +9 -2
- package/lib/rules/no-force-option.js +9 -2
- package/lib/rules/no-page-pause.js +5 -5
- package/lib/rules/no-skipped-test.js +9 -3
- package/lib/rules/no-wait-for-timeout.js +7 -2
- package/lib/utils/ast.js +76 -3
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -50,20 +50,20 @@ Identify false positives when async Playwright APIs are not properly awaited.
|
|
|
50
50
|
Example of **incorrect** code for this rule:
|
|
51
51
|
|
|
52
52
|
```js
|
|
53
|
-
expect(page).toMatchText(
|
|
53
|
+
expect(page).toMatchText('text');
|
|
54
54
|
|
|
55
|
-
test.step(
|
|
56
|
-
await page.click(
|
|
55
|
+
test.step('clicks the button', async () => {
|
|
56
|
+
await page.click('button');
|
|
57
57
|
});
|
|
58
58
|
```
|
|
59
59
|
|
|
60
60
|
Example of **correct** code for this rule:
|
|
61
61
|
|
|
62
62
|
```js
|
|
63
|
-
await expect(page).toMatchText(
|
|
63
|
+
await expect(page).toMatchText('text');
|
|
64
64
|
|
|
65
|
-
await test.step(
|
|
66
|
-
await page.click(
|
|
65
|
+
await test.step('clicks the button', async () => {
|
|
66
|
+
await page.click('button');
|
|
67
67
|
});
|
|
68
68
|
```
|
|
69
69
|
|
|
@@ -79,6 +79,7 @@ The rule accepts a non-required option which can be used to specify custom match
|
|
|
79
79
|
]
|
|
80
80
|
}
|
|
81
81
|
```
|
|
82
|
+
|
|
82
83
|
### `no-page-pause`
|
|
83
84
|
|
|
84
85
|
Prevent usage of `page.pause()`.
|
|
@@ -127,19 +128,23 @@ Disallow usage of `page.$eval` and `page.$$eval`.
|
|
|
127
128
|
Examples of **incorrect** code for this rule:
|
|
128
129
|
|
|
129
130
|
```js
|
|
130
|
-
const searchValue = await page.$eval('#search', el => el.value);
|
|
131
|
+
const searchValue = await page.$eval('#search', (el) => el.value);
|
|
131
132
|
|
|
132
|
-
const divCounts = await page.$$eval(
|
|
133
|
+
const divCounts = await page.$$eval(
|
|
134
|
+
'div',
|
|
135
|
+
(divs, min) => divs.length >= min,
|
|
136
|
+
10
|
|
137
|
+
);
|
|
133
138
|
|
|
134
|
-
await page.$eval('#search', el => el.value);
|
|
139
|
+
await page.$eval('#search', (el) => el.value);
|
|
135
140
|
|
|
136
|
-
await page.$$eval('#search', el => el.value);
|
|
141
|
+
await page.$$eval('#search', (el) => el.value);
|
|
137
142
|
```
|
|
138
143
|
|
|
139
144
|
Example of **correct** code for this rule:
|
|
140
145
|
|
|
141
146
|
```js
|
|
142
|
-
await page.locator('button').evaluate(node => node.innerText);
|
|
147
|
+
await page.locator('button').evaluate((node) => node.innerText);
|
|
143
148
|
|
|
144
149
|
await page.locator('div').evaluateAll((divs, min) => divs.length >= min, 10);
|
|
145
150
|
```
|
|
@@ -167,7 +172,6 @@ test.describe.serial.only('focus two tests in serial mode', () => {
|
|
|
167
172
|
test('one', async ({ page }) => {});
|
|
168
173
|
test('two', async ({ page }) => {});
|
|
169
174
|
});
|
|
170
|
-
|
|
171
175
|
```
|
|
172
176
|
|
|
173
177
|
Examples of **correct** code for this rule:
|
|
@@ -233,7 +237,6 @@ test.describe('skip test inside describe', () => {
|
|
|
233
237
|
test.describe('skip test conditionally', async ({ browserName }) => {
|
|
234
238
|
test.skip(browserName === 'firefox', 'Working on it');
|
|
235
239
|
});
|
|
236
|
-
|
|
237
240
|
```
|
|
238
241
|
|
|
239
242
|
Examples of **correct** code for this rule:
|
|
@@ -270,3 +273,126 @@ await page.locator('check').check();
|
|
|
270
273
|
|
|
271
274
|
await page.locator('input').fill('something');
|
|
272
275
|
```
|
|
276
|
+
|
|
277
|
+
### `max-nested-describe`
|
|
278
|
+
|
|
279
|
+
Enforces a maximum depth to nested `.describe()` calls. Useful for improving readability and parallelization of tests.
|
|
280
|
+
|
|
281
|
+
Uses a default max depth option of `{ "max": 5 }`.
|
|
282
|
+
|
|
283
|
+
Examples of **incorrect** code for this rule (using defaults):
|
|
284
|
+
|
|
285
|
+
```js
|
|
286
|
+
test.describe('level 1', () => {
|
|
287
|
+
test.describe('level 2', () => {
|
|
288
|
+
test.describe('level 3', () => {
|
|
289
|
+
test.describe('level 4', () => {
|
|
290
|
+
test.describe('level 5', () => {
|
|
291
|
+
test.describe('level 6', () => {
|
|
292
|
+
test('this test', async ({ page }) => {});
|
|
293
|
+
test('that test', async ({ page }) => {});
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Examples of **correct** code for this rule (using defaults):
|
|
303
|
+
|
|
304
|
+
```js
|
|
305
|
+
test.describe('first level', () => {
|
|
306
|
+
test.describe('second level', () => {
|
|
307
|
+
test('this test', async ({ page }) => {});
|
|
308
|
+
test('that test', async ({ page }) => {});
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
#### Options
|
|
314
|
+
|
|
315
|
+
The rule accepts a non-required option to override the default maximum nested describe depth (5).
|
|
316
|
+
|
|
317
|
+
```json
|
|
318
|
+
{
|
|
319
|
+
"playwright/max-nested-describe": ["error", { "max": 3 }]
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### `no-conditional-in-test`
|
|
324
|
+
|
|
325
|
+
Disallow conditional statements such as `if`, `switch`, and ternary expressions within tests.
|
|
326
|
+
|
|
327
|
+
Examples of **incorrect** code for this rule:
|
|
328
|
+
|
|
329
|
+
```js
|
|
330
|
+
test('foo', async ({ page }) => {
|
|
331
|
+
if (someCondition) {
|
|
332
|
+
bar();
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test('bar', async ({ page }) => {
|
|
337
|
+
switch (mode) {
|
|
338
|
+
case 'single':
|
|
339
|
+
generateOne();
|
|
340
|
+
break;
|
|
341
|
+
case 'double':
|
|
342
|
+
generateTwo();
|
|
343
|
+
break;
|
|
344
|
+
case 'multiple':
|
|
345
|
+
generateMany();
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
await expect(page.locator('.my-image').count()).toBeGreaterThan(0);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test('baz', async ({ page }) => {
|
|
353
|
+
const hotkey =
|
|
354
|
+
process.platform === 'linux' ? ['Control', 'Alt', 'f'] : ['Alt', 'f'];
|
|
355
|
+
await Promise.all(hotkey.map((x) => page.keyboard.down(x)));
|
|
356
|
+
|
|
357
|
+
expect(actionIsPerformed()).toBe(true);
|
|
358
|
+
});
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
Examples of **correct** code for this rule:
|
|
362
|
+
|
|
363
|
+
```js
|
|
364
|
+
test.describe('my tests', () => {
|
|
365
|
+
if (someCondition) {
|
|
366
|
+
test('foo', async ({ page }) => {
|
|
367
|
+
bar();
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
beforeEach(() => {
|
|
373
|
+
switch (mode) {
|
|
374
|
+
case 'single':
|
|
375
|
+
generateOne();
|
|
376
|
+
break;
|
|
377
|
+
case 'double':
|
|
378
|
+
generateTwo();
|
|
379
|
+
break;
|
|
380
|
+
case 'multiple':
|
|
381
|
+
generateMany();
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test('bar', async ({ page }) => {
|
|
387
|
+
await expect(page.locator('.my-image').count()).toBeGreaterThan(0);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
const hotkey =
|
|
391
|
+
process.platform === 'linux' ? ['Control', 'Alt', 'f'] : ['Alt', 'f'];
|
|
392
|
+
|
|
393
|
+
test('baz', async ({ page }) => {
|
|
394
|
+
await Promise.all(hotkey.map((x) => page.keyboard.down(x)));
|
|
395
|
+
|
|
396
|
+
expect(actionIsPerformed()).toBe(true);
|
|
397
|
+
});
|
|
398
|
+
```
|
package/lib/index.js
CHANGED
|
@@ -1,50 +1,54 @@
|
|
|
1
|
-
const missingPlaywrightAwait = require(
|
|
2
|
-
const noPagePause = require(
|
|
3
|
-
const noElementHandle = require(
|
|
4
|
-
const noEval = require(
|
|
5
|
-
const noFocusedTest = require(
|
|
6
|
-
const noSkippedTest = require(
|
|
7
|
-
const noWaitForTimeout = require(
|
|
8
|
-
const noForceOption = require(
|
|
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');
|
|
9
|
+
const maxNestedDescribe = require('./rules/max-nested-describe');
|
|
10
|
+
const noConditionalInTest = require('./rules/no-conditional-in-test');
|
|
9
11
|
|
|
10
12
|
module.exports = {
|
|
11
13
|
configs: {
|
|
12
|
-
|
|
13
|
-
plugins: [
|
|
14
|
+
'playwright-test': {
|
|
15
|
+
plugins: ['playwright'],
|
|
14
16
|
env: {
|
|
15
|
-
|
|
17
|
+
'shared-node-browser': true,
|
|
16
18
|
},
|
|
17
19
|
rules: {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
'no-empty-pattern': 'off',
|
|
21
|
+
'playwright/missing-playwright-await': 'error',
|
|
22
|
+
'playwright/no-page-pause': 'warn',
|
|
23
|
+
'playwright/no-element-handle': 'warn',
|
|
24
|
+
'playwright/no-eval': 'warn',
|
|
25
|
+
'playwright/no-focused-test': 'error',
|
|
26
|
+
'playwright/no-skipped-test': 'warn',
|
|
27
|
+
'playwright/no-wait-for-timeout': 'warn',
|
|
28
|
+
'playwright/no-force-option': 'warn',
|
|
29
|
+
'playwright/max-nested-describe': 'warn',
|
|
30
|
+
'playwright/no-conditional-in-test': 'warn',
|
|
27
31
|
},
|
|
28
32
|
},
|
|
29
|
-
|
|
30
|
-
plugins: [
|
|
33
|
+
'jest-playwright': {
|
|
34
|
+
plugins: ['jest', 'playwright'],
|
|
31
35
|
env: {
|
|
32
|
-
|
|
36
|
+
'shared-node-browser': true,
|
|
33
37
|
jest: true,
|
|
34
38
|
},
|
|
35
39
|
rules: {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
'playwright/missing-playwright-await': 'error',
|
|
41
|
+
'playwright/no-page-pause': 'warn',
|
|
42
|
+
'jest/no-standalone-expect': [
|
|
43
|
+
'error',
|
|
40
44
|
{
|
|
41
45
|
additionalTestBlockFunctions: [
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
'test.jestPlaywrightDebug',
|
|
47
|
+
'it.jestPlaywrightDebug',
|
|
48
|
+
'test.jestPlaywrightSkip',
|
|
49
|
+
'it.jestPlaywrightSkip',
|
|
50
|
+
'test.jestPlaywrightConfig',
|
|
51
|
+
'it.jestPlaywrightConfig',
|
|
48
52
|
],
|
|
49
53
|
},
|
|
50
54
|
],
|
|
@@ -60,13 +64,15 @@ module.exports = {
|
|
|
60
64
|
},
|
|
61
65
|
},
|
|
62
66
|
rules: {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
'missing-playwright-await': missingPlaywrightAwait,
|
|
68
|
+
'no-page-pause': noPagePause,
|
|
69
|
+
'no-element-handle': noElementHandle,
|
|
70
|
+
'no-eval': noEval,
|
|
71
|
+
'no-focused-test': noFocusedTest,
|
|
72
|
+
'no-skipped-test': noSkippedTest,
|
|
73
|
+
'no-wait-for-timeout': noWaitForTimeout,
|
|
74
|
+
'no-force-option': noForceOption,
|
|
75
|
+
'max-nested-describe': maxNestedDescribe,
|
|
76
|
+
'no-conditional-in-test': noConditionalInTest,
|
|
71
77
|
},
|
|
72
78
|
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const { isCallExpression, isDescribeCall } = require('../utils/ast');
|
|
2
|
+
|
|
3
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
4
|
+
module.exports = {
|
|
5
|
+
create(context) {
|
|
6
|
+
const { options } = context;
|
|
7
|
+
const defaultOptions = { max: 5 };
|
|
8
|
+
const { max } = options[0] || defaultOptions;
|
|
9
|
+
const describeCallbackStack = [];
|
|
10
|
+
|
|
11
|
+
function pushDescribeCallback(node) {
|
|
12
|
+
const { parent } = node;
|
|
13
|
+
|
|
14
|
+
if (!isCallExpression(parent) || !isDescribeCall(parent)) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describeCallbackStack.push(0);
|
|
19
|
+
|
|
20
|
+
if (describeCallbackStack.length > max) {
|
|
21
|
+
context.report({
|
|
22
|
+
node: parent,
|
|
23
|
+
messageId: 'exceededMaxDepth',
|
|
24
|
+
data: { depth: describeCallbackStack.length, max },
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function popDescribeCallback(node) {
|
|
30
|
+
const { parent } = node;
|
|
31
|
+
|
|
32
|
+
if (isCallExpression(parent) && isDescribeCall(parent)) {
|
|
33
|
+
describeCallbackStack.pop();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
FunctionExpression: pushDescribeCallback,
|
|
39
|
+
'FunctionExpression:exit': popDescribeCallback,
|
|
40
|
+
ArrowFunctionExpression: pushDescribeCallback,
|
|
41
|
+
'ArrowFunctionExpression:exit': popDescribeCallback,
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
meta: {
|
|
45
|
+
docs: {
|
|
46
|
+
category: 'Best Practices',
|
|
47
|
+
description: 'Enforces a maximum depth to nested describe calls',
|
|
48
|
+
recommended: false,
|
|
49
|
+
url: 'https://github.com/playwright-community/eslint-plugin-playwright#max-nested-describe',
|
|
50
|
+
},
|
|
51
|
+
messages: {
|
|
52
|
+
exceededMaxDepth:
|
|
53
|
+
'Maximum describe call depth exceeded ({{ depth }}). Maximum allowed is {{ max }}.',
|
|
54
|
+
},
|
|
55
|
+
type: 'suggestion',
|
|
56
|
+
fixable: 'code',
|
|
57
|
+
schema: [
|
|
58
|
+
{
|
|
59
|
+
type: 'object',
|
|
60
|
+
properties: {
|
|
61
|
+
max: {
|
|
62
|
+
type: 'integer',
|
|
63
|
+
minimum: 0,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
additionalProperties: false,
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
};
|
|
@@ -1,68 +1,71 @@
|
|
|
1
1
|
function getMemberPartName(node, part) {
|
|
2
|
-
return node[part].type ===
|
|
2
|
+
return node[part].type === 'Identifier' ? node[part].name : undefined;
|
|
3
3
|
}
|
|
4
4
|
|
|
5
5
|
function getMemberExpressionNode(node, matchers) {
|
|
6
|
-
const propertyName = getMemberPartName(node,
|
|
6
|
+
const propertyName = getMemberPartName(node, 'property');
|
|
7
7
|
|
|
8
|
-
if (getMemberPartName(node,
|
|
9
|
-
return propertyName ===
|
|
8
|
+
if (getMemberPartName(node, 'object') === 'test') {
|
|
9
|
+
return propertyName === 'step' ? { node, type: 'testStep' } : undefined;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
return matchers.has(propertyName) ? { node, type:
|
|
12
|
+
return matchers.has(propertyName) ? { node, type: 'expect' } : undefined;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
const validTypes = new Set([
|
|
16
|
+
'AwaitExpression',
|
|
17
|
+
'ReturnStatement',
|
|
18
|
+
'ArrowFunctionExpression',
|
|
19
|
+
]);
|
|
18
20
|
|
|
21
|
+
function isValid(node) {
|
|
19
22
|
return (
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
grandparentType === "ArrowFunctionExpression"
|
|
23
|
+
validTypes.has(node.parent?.type) ||
|
|
24
|
+
validTypes.has(node.parent?.parent?.type)
|
|
23
25
|
);
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
const expectPlaywrightMatchers = [
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
29
|
+
'toBeChecked',
|
|
30
|
+
'toBeDisabled',
|
|
31
|
+
'toBeEnabled',
|
|
32
|
+
'toEqualText', // deprecated
|
|
33
|
+
'toEqualUrl',
|
|
34
|
+
'toEqualValue',
|
|
35
|
+
'toHaveFocus',
|
|
36
|
+
'toHaveSelector',
|
|
37
|
+
'toHaveSelectorCount',
|
|
38
|
+
'toHaveText', // deprecated
|
|
39
|
+
'toMatchAttribute',
|
|
40
|
+
'toMatchComputedStyle',
|
|
41
|
+
'toMatchText',
|
|
42
|
+
'toMatchTitle',
|
|
43
|
+
'toMatchURL',
|
|
44
|
+
'toMatchValue',
|
|
43
45
|
];
|
|
44
46
|
|
|
45
47
|
const playwrightTestMatchers = [
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
48
|
+
'toBeChecked',
|
|
49
|
+
'toBeDisabled',
|
|
50
|
+
'toBeEditable',
|
|
51
|
+
'toBeEmpty',
|
|
52
|
+
'toBeEnabled',
|
|
53
|
+
'toBeFocused',
|
|
54
|
+
'toBeHidden',
|
|
55
|
+
'toBeVisible',
|
|
56
|
+
'toContainText',
|
|
57
|
+
'toHaveAttribute',
|
|
58
|
+
'toHaveClass',
|
|
59
|
+
'toHaveCount',
|
|
60
|
+
'toHaveCSS',
|
|
61
|
+
'toHaveId',
|
|
62
|
+
'toHaveJSProperty',
|
|
63
|
+
'toBeOK',
|
|
64
|
+
'toHaveScreenshot',
|
|
65
|
+
'toHaveText',
|
|
66
|
+
'toHaveTitle',
|
|
67
|
+
'toHaveURL',
|
|
68
|
+
'toHaveValue',
|
|
66
69
|
];
|
|
67
70
|
|
|
68
71
|
module.exports = {
|
|
@@ -81,7 +84,7 @@ module.exports = {
|
|
|
81
84
|
|
|
82
85
|
if (result && !isValid(result.node)) {
|
|
83
86
|
context.report({
|
|
84
|
-
fix: (fixer) => fixer.insertTextBefore(result.node,
|
|
87
|
+
fix: (fixer) => fixer.insertTextBefore(result.node, 'await '),
|
|
85
88
|
messageId: result.type,
|
|
86
89
|
node: result.node,
|
|
87
90
|
});
|
|
@@ -91,11 +94,11 @@ module.exports = {
|
|
|
91
94
|
},
|
|
92
95
|
meta: {
|
|
93
96
|
docs: {
|
|
94
|
-
category:
|
|
97
|
+
category: 'Possible Errors',
|
|
95
98
|
description: `Identify false positives when async Playwright APIs are not properly awaited.`,
|
|
96
99
|
recommended: true,
|
|
97
100
|
},
|
|
98
|
-
fixable:
|
|
101
|
+
fixable: 'code',
|
|
99
102
|
messages: {
|
|
100
103
|
expect: "'expect' matchers must be awaited or returned.",
|
|
101
104
|
testStep: "'test.step' must be awaited or returned.",
|
|
@@ -105,13 +108,13 @@ module.exports = {
|
|
|
105
108
|
additionalProperties: false,
|
|
106
109
|
properties: {
|
|
107
110
|
customMatchers: {
|
|
108
|
-
items: { type:
|
|
109
|
-
type:
|
|
111
|
+
items: { type: 'string' },
|
|
112
|
+
type: 'array',
|
|
110
113
|
},
|
|
111
114
|
},
|
|
112
|
-
type:
|
|
115
|
+
type: 'object',
|
|
113
116
|
},
|
|
114
117
|
],
|
|
115
|
-
type:
|
|
118
|
+
type: 'problem',
|
|
116
119
|
},
|
|
117
120
|
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const {
|
|
2
|
+
isTestIdentifier,
|
|
3
|
+
isDescribeCall,
|
|
4
|
+
isHookCall,
|
|
5
|
+
} = require('../utils/ast');
|
|
6
|
+
|
|
7
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
8
|
+
module.exports = {
|
|
9
|
+
create(context) {
|
|
10
|
+
let inTestCase = false;
|
|
11
|
+
|
|
12
|
+
function maybeReportConditional(node) {
|
|
13
|
+
if (inTestCase) {
|
|
14
|
+
context.report({
|
|
15
|
+
messageId: 'conditionalInTest',
|
|
16
|
+
node,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
CallExpression(node) {
|
|
23
|
+
const { callee } = node;
|
|
24
|
+
if (
|
|
25
|
+
isTestIdentifier(callee) &&
|
|
26
|
+
!isDescribeCall(node) &&
|
|
27
|
+
!isHookCall(node)
|
|
28
|
+
) {
|
|
29
|
+
inTestCase = true;
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
'CallExpression:exit'(node) {
|
|
33
|
+
const { callee } = node;
|
|
34
|
+
if (
|
|
35
|
+
isTestIdentifier(callee) &&
|
|
36
|
+
!isDescribeCall(node) &&
|
|
37
|
+
!isHookCall(node)
|
|
38
|
+
) {
|
|
39
|
+
inTestCase = false;
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
IfStatement: maybeReportConditional,
|
|
43
|
+
SwitchStatement: maybeReportConditional,
|
|
44
|
+
ConditionalExpression: maybeReportConditional,
|
|
45
|
+
LogicalExpression: maybeReportConditional,
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
meta: {
|
|
49
|
+
docs: {
|
|
50
|
+
category: 'Best Practices',
|
|
51
|
+
description: 'Disallow conditional logic in tests',
|
|
52
|
+
recommended: false,
|
|
53
|
+
url: 'https://github.com/playwright-community/eslint-plugin-playwright#no-conditional-in-test',
|
|
54
|
+
},
|
|
55
|
+
messages: {
|
|
56
|
+
conditionalInTest: 'Avoid having conditionals in tests',
|
|
57
|
+
},
|
|
58
|
+
type: 'problem',
|
|
59
|
+
schema: [],
|
|
60
|
+
},
|
|
61
|
+
};
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
const { isObject, isCalleeProperty } = require('../utils/ast');
|
|
2
2
|
|
|
3
3
|
function getRange(node) {
|
|
4
|
-
const start =
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
const start =
|
|
5
|
+
node.parent && node.parent.type === 'AwaitExpression'
|
|
6
|
+
? node.parent.range[0]
|
|
7
|
+
: node.callee.object.range[0];
|
|
7
8
|
|
|
8
9
|
return [start, node.callee.property.range[1]];
|
|
9
10
|
}
|
|
@@ -12,7 +13,10 @@ module.exports = {
|
|
|
12
13
|
create(context) {
|
|
13
14
|
return {
|
|
14
15
|
CallExpression(node) {
|
|
15
|
-
if (
|
|
16
|
+
if (
|
|
17
|
+
isObject(node, 'page') &&
|
|
18
|
+
(isCalleeProperty(node, '$') || isCalleeProperty(node, '$$'))
|
|
19
|
+
) {
|
|
16
20
|
context.report({
|
|
17
21
|
messageId: 'noElementHandle',
|
|
18
22
|
suggest: [
|
|
@@ -20,7 +24,8 @@ module.exports = {
|
|
|
20
24
|
messageId: isCalleeProperty(node, '$')
|
|
21
25
|
? 'replaceElementHandleWithLocator'
|
|
22
26
|
: 'replaceElementHandlesWithLocator',
|
|
23
|
-
fix: (fixer) =>
|
|
27
|
+
fix: (fixer) =>
|
|
28
|
+
fixer.replaceTextRange(getRange(node), 'page.locator'),
|
|
24
29
|
},
|
|
25
30
|
],
|
|
26
31
|
node,
|
|
@@ -32,7 +37,8 @@ module.exports = {
|
|
|
32
37
|
meta: {
|
|
33
38
|
docs: {
|
|
34
39
|
category: 'Possible Errors',
|
|
35
|
-
description:
|
|
40
|
+
description:
|
|
41
|
+
'The use of ElementHandle is discouraged, use Locator instead',
|
|
36
42
|
recommended: true,
|
|
37
43
|
url: 'https://github.com/playwright-community/eslint-plugin-playwright#no-element-handle',
|
|
38
44
|
},
|
package/lib/rules/no-eval.js
CHANGED
|
@@ -4,8 +4,14 @@ module.exports = {
|
|
|
4
4
|
create(context) {
|
|
5
5
|
return {
|
|
6
6
|
CallExpression(node) {
|
|
7
|
-
if (
|
|
8
|
-
|
|
7
|
+
if (
|
|
8
|
+
isObject(node, 'page') &&
|
|
9
|
+
(isCalleeProperty(node, '$eval') || isCalleeProperty(node, '$$eval'))
|
|
10
|
+
) {
|
|
11
|
+
context.report({
|
|
12
|
+
messageId: isCalleeProperty(node, '$eval') ? 'noEval' : 'noEvalAll',
|
|
13
|
+
node,
|
|
14
|
+
});
|
|
9
15
|
}
|
|
10
16
|
},
|
|
11
17
|
};
|
|
@@ -16,14 +16,21 @@ module.exports = {
|
|
|
16
16
|
create(context) {
|
|
17
17
|
return {
|
|
18
18
|
MemberExpression(node) {
|
|
19
|
-
if (
|
|
19
|
+
if (
|
|
20
|
+
(isTestIdentifier(node) || isTestGroup(node)) &&
|
|
21
|
+
hasAnnotation(node, 'only')
|
|
22
|
+
) {
|
|
20
23
|
context.report({
|
|
21
24
|
messageId: 'noFocusedTest',
|
|
22
25
|
suggest: [
|
|
23
26
|
{
|
|
24
27
|
messageId: 'removeFocusedTestAnnotation',
|
|
25
28
|
// - 1 to remove the `.only` annotation with dot notation
|
|
26
|
-
fix: (fixer) =>
|
|
29
|
+
fix: (fixer) =>
|
|
30
|
+
fixer.removeRange([
|
|
31
|
+
node.property.range[0] - 1,
|
|
32
|
+
node.property.range[1],
|
|
33
|
+
]),
|
|
27
34
|
},
|
|
28
35
|
],
|
|
29
36
|
node,
|
|
@@ -6,7 +6,10 @@ function isForceOptionEnabled({ parent }) {
|
|
|
6
6
|
parent.arguments.some(
|
|
7
7
|
(argument) =>
|
|
8
8
|
argument.type === 'ObjectExpression' &&
|
|
9
|
-
argument.properties.some(
|
|
9
|
+
argument.properties.some(
|
|
10
|
+
({ key, value }) =>
|
|
11
|
+
key && key.name === 'force' && value && value.value === true
|
|
12
|
+
)
|
|
10
13
|
)
|
|
11
14
|
);
|
|
12
15
|
}
|
|
@@ -31,7 +34,11 @@ module.exports = {
|
|
|
31
34
|
create(context) {
|
|
32
35
|
return {
|
|
33
36
|
MemberExpression(node) {
|
|
34
|
-
if (
|
|
37
|
+
if (
|
|
38
|
+
node.property &&
|
|
39
|
+
methodsWithForceOption.has(node.property.name) &&
|
|
40
|
+
isForceOptionEnabled(node)
|
|
41
|
+
) {
|
|
35
42
|
context.report({ messageId: 'noForceOption', node });
|
|
36
43
|
}
|
|
37
44
|
},
|
|
@@ -5,20 +5,20 @@ module.exports = {
|
|
|
5
5
|
return {
|
|
6
6
|
CallExpression(node) {
|
|
7
7
|
if (isObject(node, 'page') && isCalleeProperty(node, 'pause')) {
|
|
8
|
-
context.report({ messageId:
|
|
8
|
+
context.report({ messageId: 'noPagePause', node });
|
|
9
9
|
}
|
|
10
10
|
},
|
|
11
11
|
};
|
|
12
12
|
},
|
|
13
13
|
meta: {
|
|
14
14
|
docs: {
|
|
15
|
-
category:
|
|
16
|
-
description:
|
|
15
|
+
category: 'Possible Errors',
|
|
16
|
+
description: 'Prevent usage of page.pause()',
|
|
17
17
|
recommended: true,
|
|
18
18
|
},
|
|
19
19
|
messages: {
|
|
20
|
-
noPagePause:
|
|
20
|
+
noPagePause: 'Unexpected use of page.pause().',
|
|
21
21
|
},
|
|
22
|
-
type:
|
|
22
|
+
type: 'problem',
|
|
23
23
|
},
|
|
24
24
|
};
|
|
@@ -24,9 +24,12 @@ function getSkipRange(node) {
|
|
|
24
24
|
|
|
25
25
|
const isStandaloneSkip =
|
|
26
26
|
!node.parent.arguments.length ||
|
|
27
|
-
((isBinaryExpression(first) || isBooleanLiteral(first)) &&
|
|
27
|
+
((isBinaryExpression(first) || isBooleanLiteral(first)) &&
|
|
28
|
+
isStringLiteral(second));
|
|
28
29
|
|
|
29
|
-
return isStandaloneSkip
|
|
30
|
+
return isStandaloneSkip
|
|
31
|
+
? node.parent.parent.range
|
|
32
|
+
: [node.property.range[0] - 1, node.property.range[1]];
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
@@ -34,7 +37,10 @@ module.exports = {
|
|
|
34
37
|
create(context) {
|
|
35
38
|
return {
|
|
36
39
|
MemberExpression(node) {
|
|
37
|
-
if (
|
|
40
|
+
if (
|
|
41
|
+
(isTestIdentifier(node) || isObjectProperty(node, 'describe')) &&
|
|
42
|
+
hasAnnotation(node, 'skip')
|
|
43
|
+
) {
|
|
38
44
|
context.report({
|
|
39
45
|
messageId: 'noSkippedTest',
|
|
40
46
|
suggest: [
|
|
@@ -5,7 +5,10 @@ module.exports = {
|
|
|
5
5
|
create(context) {
|
|
6
6
|
return {
|
|
7
7
|
CallExpression(node) {
|
|
8
|
-
if (
|
|
8
|
+
if (
|
|
9
|
+
isObject(node, 'page') &&
|
|
10
|
+
isCalleeProperty(node, 'waitForTimeout')
|
|
11
|
+
) {
|
|
9
12
|
context.report({
|
|
10
13
|
messageId: 'noWaitForTimeout',
|
|
11
14
|
suggest: [
|
|
@@ -13,7 +16,9 @@ module.exports = {
|
|
|
13
16
|
messageId: 'removeWaitForTimeout',
|
|
14
17
|
fix: (fixer) =>
|
|
15
18
|
fixer.remove(
|
|
16
|
-
node.parent && node.parent.type !== 'AwaitExpression'
|
|
19
|
+
node.parent && node.parent.type !== 'AwaitExpression'
|
|
20
|
+
? node.parent
|
|
21
|
+
: node.parent.parent
|
|
17
22
|
),
|
|
18
23
|
},
|
|
19
24
|
],
|
package/lib/utils/ast.js
CHANGED
|
@@ -16,8 +16,12 @@ function isCalleeProperty({ callee }, name) {
|
|
|
16
16
|
);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
function isTestObject({ object }) {
|
|
20
|
+
return object && isIdentifier(object, 'test');
|
|
21
|
+
}
|
|
22
|
+
|
|
19
23
|
function isTestIdentifier(node) {
|
|
20
|
-
return node
|
|
24
|
+
return isIdentifier(node, 'test') || isTestObject(node);
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
function hasAnnotation(node, annotation) {
|
|
@@ -37,8 +41,13 @@ function isObjectProperty({ object }, name) {
|
|
|
37
41
|
);
|
|
38
42
|
}
|
|
39
43
|
|
|
40
|
-
function isStringLiteral(node) {
|
|
41
|
-
return
|
|
44
|
+
function isStringLiteral(node, value) {
|
|
45
|
+
return (
|
|
46
|
+
node &&
|
|
47
|
+
node.type === 'Literal' &&
|
|
48
|
+
typeof node.value === 'string' &&
|
|
49
|
+
(value === undefined || node.value === value)
|
|
50
|
+
);
|
|
42
51
|
}
|
|
43
52
|
|
|
44
53
|
function isBooleanLiteral(node) {
|
|
@@ -49,6 +58,66 @@ function isBinaryExpression(node) {
|
|
|
49
58
|
return node && node.type === 'BinaryExpression';
|
|
50
59
|
}
|
|
51
60
|
|
|
61
|
+
function isCallExpression(node) {
|
|
62
|
+
return node && node.type === 'CallExpression';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isMemberExpression(node) {
|
|
66
|
+
return node && node.type === 'MemberExpression';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isIdentifier(node, name) {
|
|
70
|
+
return (
|
|
71
|
+
node &&
|
|
72
|
+
node.type === 'Identifier' &&
|
|
73
|
+
(name === undefined || node.name === name)
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isDescribeAlias(node) {
|
|
78
|
+
return isIdentifier(node, 'describe');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isDescribeProperty(node) {
|
|
82
|
+
const describeProperties = ['parallel', 'serial', 'only', 'skip'];
|
|
83
|
+
return describeProperties.some((prop) => isIdentifier(node, prop));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function isDescribeCall(node) {
|
|
87
|
+
if (isDescribeAlias(node.callee)) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const callee =
|
|
92
|
+
node.callee.type === 'TaggedTemplateExpression'
|
|
93
|
+
? node.callee.tag
|
|
94
|
+
: node.callee.type === 'CallExpression'
|
|
95
|
+
? node.callee.callee
|
|
96
|
+
: node.callee;
|
|
97
|
+
|
|
98
|
+
if (callee.type === 'MemberExpression' && isDescribeAlias(callee.property)) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (
|
|
103
|
+
callee.type === 'MemberExpression' &&
|
|
104
|
+
isDescribeProperty(callee.property)
|
|
105
|
+
) {
|
|
106
|
+
return callee.object.type === 'MemberExpression'
|
|
107
|
+
? callee.object.object.type === 'MemberExpression'
|
|
108
|
+
? isDescribeAlias(callee.object.object.property)
|
|
109
|
+
: isDescribeAlias(callee.object.property)
|
|
110
|
+
: isDescribeAlias(callee.property) || isDescribeAlias(callee.object);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function isHookCall(node) {
|
|
117
|
+
const hooks = ['beforeAll', 'beforeEach', 'afterAll', 'afterEach'];
|
|
118
|
+
return hooks.some((hook) => isCalleeProperty(node, hook));
|
|
119
|
+
}
|
|
120
|
+
|
|
52
121
|
module.exports = {
|
|
53
122
|
isObject,
|
|
54
123
|
isCalleeProperty,
|
|
@@ -58,4 +127,8 @@ module.exports = {
|
|
|
58
127
|
isStringLiteral,
|
|
59
128
|
isBooleanLiteral,
|
|
60
129
|
isBinaryExpression,
|
|
130
|
+
isCallExpression,
|
|
131
|
+
isMemberExpression,
|
|
132
|
+
isDescribeCall,
|
|
133
|
+
isHookCall,
|
|
61
134
|
};
|
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.
|
|
4
|
+
"version": "0.10.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>",
|
|
@@ -10,11 +10,13 @@
|
|
|
10
10
|
"lib"
|
|
11
11
|
],
|
|
12
12
|
"scripts": {
|
|
13
|
+
"format": "prettier --write .",
|
|
13
14
|
"test": "jest"
|
|
14
15
|
},
|
|
15
16
|
"devDependencies": {
|
|
16
17
|
"eslint": "^8.4.1",
|
|
17
|
-
"jest": "^27.4.5"
|
|
18
|
+
"jest": "^27.4.5",
|
|
19
|
+
"prettier": "^2.7.1"
|
|
18
20
|
},
|
|
19
21
|
"peerDependencies": {
|
|
20
22
|
"eslint": ">=7",
|