eslint-plugin-playwright 0.11.2 → 0.13.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 +34 -25
- package/lib/index.js +32 -21
- package/lib/rules/max-nested-describe.js +1 -1
- package/lib/rules/missing-playwright-await.js +17 -5
- package/lib/rules/no-element-handle.js +20 -11
- package/lib/rules/no-eval.js +4 -4
- package/lib/rules/no-focused-test.js +1 -1
- package/lib/rules/no-force-option.js +6 -4
- package/lib/rules/no-page-pause.js +1 -1
- package/lib/rules/no-restricted-matchers.js +39 -20
- package/lib/rules/no-skipped-test.js +3 -2
- package/lib/rules/no-useless-not.js +29 -28
- package/lib/rules/no-wait-for-timeout.js +1 -2
- package/lib/rules/prefer-strict-equal.js +43 -0
- package/lib/rules/prefer-to-be.js +88 -0
- package/lib/rules/prefer-to-have-length.js +8 -12
- package/lib/rules/prefer-web-first-assertions.js +150 -0
- package/lib/rules/require-soft-assertions.js +31 -0
- package/lib/rules/require-top-level-describe.js +3 -3
- package/lib/rules/valid-expect.js +7 -14
- package/lib/utils/ast.js +33 -16
- package/lib/utils/fixer.js +16 -0
- package/lib/utils/parseExpectCall.js +38 -0
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -1,23 +1,28 @@
|
|
|
1
1
|
# ESLint Plugin Playwright
|
|
2
2
|
|
|
3
3
|
[](https://github.com/playwright-community/eslint-plugin-playwright/actions/workflows/test.yml)
|
|
4
|
-
[](https://www.npmjs.com/package/eslint-plugin-playwright)
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
> testing needs.
|
|
6
|
+
ESLint plugin for [Playwright](https://github.com/microsoft/playwright).
|
|
8
7
|
|
|
9
8
|
## Installation
|
|
10
9
|
|
|
10
|
+
npm
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install -D eslint-plugin-playwright
|
|
14
|
+
```
|
|
15
|
+
|
|
11
16
|
Yarn
|
|
12
17
|
|
|
13
18
|
```bash
|
|
14
19
|
yarn add -D eslint-plugin-playwright
|
|
15
20
|
```
|
|
16
21
|
|
|
17
|
-
|
|
22
|
+
pnpm
|
|
18
23
|
|
|
19
24
|
```bash
|
|
20
|
-
|
|
25
|
+
pnpm add -D eslint-plugin-playwright
|
|
21
26
|
```
|
|
22
27
|
|
|
23
28
|
## Usage
|
|
@@ -25,11 +30,11 @@ npm install -D eslint-plugin-playwright
|
|
|
25
30
|
This plugin bundles two configurations to work with both `@playwright/test` or
|
|
26
31
|
`jest-playwright`.
|
|
27
32
|
|
|
28
|
-
### With [Playwright test runner](https://playwright.dev/docs/
|
|
33
|
+
### With [Playwright test runner](https://playwright.dev/docs/writing-tests)
|
|
29
34
|
|
|
30
35
|
```json
|
|
31
36
|
{
|
|
32
|
-
"extends": ["plugin:playwright/
|
|
37
|
+
"extends": ["plugin:playwright/recommended"]
|
|
33
38
|
}
|
|
34
39
|
```
|
|
35
40
|
|
|
@@ -49,21 +54,25 @@ command line option.\
|
|
|
49
54
|
💡: Some problems reported by this rule are manually fixable by editor
|
|
50
55
|
[suggestions](https://eslint.org/docs/latest/developer-guide/working-with-rules#providing-suggestions).
|
|
51
56
|
|
|
52
|
-
| ✔ | 🔧 | 💡 | Rule
|
|
53
|
-
| :-: | :-: | :-: |
|
|
54
|
-
| ✔ | | | [max-nested-describe](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/max-nested-describe.md)
|
|
55
|
-
| ✔ | 🔧 | | [missing-playwright-await](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/missing-playwright-await.md)
|
|
56
|
-
| ✔ | | | [no-conditional-in-test](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-conditional-in-test.md)
|
|
57
|
-
| ✔ | | 💡 | [no-element-handle](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-element-handle.md)
|
|
58
|
-
| ✔ | | | [no-eval](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-eval.md)
|
|
59
|
-
| ✔ | | 💡 | [no-focused-test](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-focused-test.md)
|
|
60
|
-
| ✔ | | | [no-force-option](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-force-option.md)
|
|
61
|
-
| ✔ | | | [no-page-pause](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-page-pause.md)
|
|
62
|
-
| | | | [no-restricted-matchers](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-restricted-matchers.md)
|
|
63
|
-
| ✔ | | 💡 | [no-skipped-test](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-skipped-test.md)
|
|
64
|
-
| ✔ | 🔧 | | [no-useless-not](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-useless-not.md)
|
|
65
|
-
| ✔ | | 💡 | [no-wait-for-timeout](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-wait-for-timeout.md)
|
|
66
|
-
| |
|
|
67
|
-
| | 🔧 | | [prefer-
|
|
68
|
-
| |
|
|
69
|
-
|
|
|
57
|
+
| ✔ | 🔧 | 💡 | Rule | Description |
|
|
58
|
+
| :-: | :-: | :-: | --------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
|
|
59
|
+
| ✔ | | | [max-nested-describe](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/max-nested-describe.md) | Enforces a maximum depth to nested describe calls |
|
|
60
|
+
| ✔ | 🔧 | | [missing-playwright-await](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/missing-playwright-await.md) | Enforce Playwright APIs to be awaited |
|
|
61
|
+
| ✔ | | | [no-conditional-in-test](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-conditional-in-test.md) | Disallow conditional logic in tests |
|
|
62
|
+
| ✔ | | 💡 | [no-element-handle](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-element-handle.md) | Disallow usage of element handles |
|
|
63
|
+
| ✔ | | | [no-eval](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-eval.md) | Disallow usage of `page.$eval` and `page.$$eval` |
|
|
64
|
+
| ✔ | | 💡 | [no-focused-test](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-focused-test.md) | Disallow usage of `.only` annotation |
|
|
65
|
+
| ✔ | | | [no-force-option](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-force-option.md) | Disallow usage of the `{ force: true }` option |
|
|
66
|
+
| ✔ | | | [no-page-pause](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-page-pause.md) | Disallow using `page.pause` |
|
|
67
|
+
| | | | [no-restricted-matchers](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-restricted-matchers.md) | Disallow specific matchers & modifiers |
|
|
68
|
+
| ✔ | | 💡 | [no-skipped-test](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-skipped-test.md) | Disallow usage of the `.skip` annotation |
|
|
69
|
+
| ✔ | 🔧 | | [no-useless-not](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-useless-not.md) | Disallow usage of `not` matchers when a specific matcher exists |
|
|
70
|
+
| ✔ | | 💡 | [no-wait-for-timeout](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-wait-for-timeout.md) | Disallow usage of `page.waitForTimeout` |
|
|
71
|
+
| | | 💡 | [prefer-strict-equal](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-strict-equal.md) | Suggest using `toStrictEqual()` |
|
|
72
|
+
| | 🔧 | | [prefer-lowercase-title](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-lowercase-title.md) | Enforce lowercase test names |
|
|
73
|
+
| | 🔧 | | [prefer-to-be](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-to-be.md) | Suggest using `toBe()` |
|
|
74
|
+
| | 🔧 | | [prefer-to-have-length](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-to-have-length.md) | Suggest using `toHaveLength()` |
|
|
75
|
+
| ✔ | 🔧 | | [prefer-web-first-assertions](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-web-first-assertions.md) | Suggest using web first assertions |
|
|
76
|
+
| | | | [require-top-level-describe](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/require-top-level-describe.md) | Require test cases and hooks to be inside a `test.describe` block |
|
|
77
|
+
| | 🔧 | | [require-soft-assertions](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/require-soft-assertions.md) | Require assertions to use `expect.soft()` |
|
|
78
|
+
| ✔ | | | [valid-expect](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/valid-expect.md) | Enforce valid `expect()` usage |
|
package/lib/index.js
CHANGED
|
@@ -12,32 +12,39 @@ const no_conditional_in_test_1 = require("./rules/no-conditional-in-test");
|
|
|
12
12
|
const no_restricted_matchers_1 = require("./rules/no-restricted-matchers");
|
|
13
13
|
const no_useless_not_1 = require("./rules/no-useless-not");
|
|
14
14
|
const prefer_lowercase_title_1 = require("./rules/prefer-lowercase-title");
|
|
15
|
+
const prefer_to_be_1 = require("./rules/prefer-to-be");
|
|
15
16
|
const prefer_to_have_length_1 = require("./rules/prefer-to-have-length");
|
|
17
|
+
const prefer_strict_equal_1 = require("./rules/prefer-strict-equal");
|
|
18
|
+
const require_soft_assertions_1 = require("./rules/require-soft-assertions");
|
|
16
19
|
const require_top_level_describe_1 = require("./rules/require-top-level-describe");
|
|
17
20
|
const valid_expect_1 = require("./rules/valid-expect");
|
|
21
|
+
const prefer_web_first_assertions_1 = require("./rules/prefer-web-first-assertions");
|
|
22
|
+
const recommended = {
|
|
23
|
+
plugins: ['playwright'],
|
|
24
|
+
env: {
|
|
25
|
+
'shared-node-browser': true,
|
|
26
|
+
},
|
|
27
|
+
rules: {
|
|
28
|
+
'no-empty-pattern': 'off',
|
|
29
|
+
'playwright/missing-playwright-await': 'error',
|
|
30
|
+
'playwright/no-page-pause': 'warn',
|
|
31
|
+
'playwright/no-element-handle': 'warn',
|
|
32
|
+
'playwright/no-eval': 'warn',
|
|
33
|
+
'playwright/no-focused-test': 'error',
|
|
34
|
+
'playwright/no-skipped-test': 'warn',
|
|
35
|
+
'playwright/no-wait-for-timeout': 'warn',
|
|
36
|
+
'playwright/no-force-option': 'warn',
|
|
37
|
+
'playwright/max-nested-describe': 'warn',
|
|
38
|
+
'playwright/no-conditional-in-test': 'warn',
|
|
39
|
+
'playwright/no-useless-not': 'warn',
|
|
40
|
+
'playwright/prefer-web-first-assertions': 'error',
|
|
41
|
+
'playwright/valid-expect': 'error',
|
|
42
|
+
},
|
|
43
|
+
};
|
|
18
44
|
module.exports = {
|
|
19
45
|
configs: {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
env: {
|
|
23
|
-
'shared-node-browser': true,
|
|
24
|
-
},
|
|
25
|
-
rules: {
|
|
26
|
-
'no-empty-pattern': 'off',
|
|
27
|
-
'playwright/missing-playwright-await': 'error',
|
|
28
|
-
'playwright/no-page-pause': 'warn',
|
|
29
|
-
'playwright/no-element-handle': 'warn',
|
|
30
|
-
'playwright/no-eval': 'warn',
|
|
31
|
-
'playwright/no-focused-test': 'error',
|
|
32
|
-
'playwright/no-skipped-test': 'warn',
|
|
33
|
-
'playwright/no-wait-for-timeout': 'warn',
|
|
34
|
-
'playwright/no-force-option': 'warn',
|
|
35
|
-
'playwright/max-nested-describe': 'warn',
|
|
36
|
-
'playwright/no-conditional-in-test': 'warn',
|
|
37
|
-
'playwright/no-useless-not': 'warn',
|
|
38
|
-
'playwright/valid-expect': 'error',
|
|
39
|
-
},
|
|
40
|
-
},
|
|
46
|
+
recommended,
|
|
47
|
+
'playwright-test': recommended,
|
|
41
48
|
'jest-playwright': {
|
|
42
49
|
plugins: ['jest', 'playwright'],
|
|
43
50
|
env: {
|
|
@@ -85,8 +92,12 @@ module.exports = {
|
|
|
85
92
|
'no-useless-not': no_useless_not_1.default,
|
|
86
93
|
'no-restricted-matchers': no_restricted_matchers_1.default,
|
|
87
94
|
'prefer-lowercase-title': prefer_lowercase_title_1.default,
|
|
95
|
+
'prefer-strict-equal': prefer_strict_equal_1.default,
|
|
96
|
+
'prefer-to-be': prefer_to_be_1.default,
|
|
88
97
|
'prefer-to-have-length': prefer_to_have_length_1.default,
|
|
98
|
+
'prefer-web-first-assertions': prefer_web_first_assertions_1.default,
|
|
89
99
|
'require-top-level-describe': require_top_level_describe_1.default,
|
|
100
|
+
'require-soft-assertions': require_soft_assertions_1.default,
|
|
90
101
|
'valid-expect': valid_expect_1.default,
|
|
91
102
|
},
|
|
92
103
|
};
|
|
@@ -14,7 +14,7 @@ exports.default = {
|
|
|
14
14
|
describeCallbackStack.push(0);
|
|
15
15
|
if (describeCallbackStack.length > max) {
|
|
16
16
|
context.report({
|
|
17
|
-
node: node.parent,
|
|
17
|
+
node: node.parent.callee,
|
|
18
18
|
messageId: 'exceededMaxDepth',
|
|
19
19
|
data: {
|
|
20
20
|
depth: describeCallbackStack.length.toString(),
|
|
@@ -54,23 +54,35 @@ function getCallType(node, awaitableMatchers) {
|
|
|
54
54
|
(0, ast_1.isPropertyAccessor)(node.callee, 'step')) {
|
|
55
55
|
return { messageId: 'testStep' };
|
|
56
56
|
}
|
|
57
|
-
const expectType = (0, ast_1.
|
|
57
|
+
const expectType = (0, ast_1.getExpectType)(node);
|
|
58
58
|
if (!expectType)
|
|
59
59
|
return;
|
|
60
60
|
// expect.poll
|
|
61
61
|
if (expectType === 'poll') {
|
|
62
62
|
return { messageId: 'expectPoll' };
|
|
63
63
|
}
|
|
64
|
-
// expect with
|
|
64
|
+
// expect with awaitable matcher
|
|
65
65
|
const [lastMatcher] = (0, ast_1.getMatchers)(node).slice(-1);
|
|
66
66
|
const matcherName = (0, ast_1.getStringValue)(lastMatcher);
|
|
67
67
|
if (awaitableMatchers.has(matcherName)) {
|
|
68
68
|
return { messageId: 'expect', data: { matcherName } };
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
|
+
function isPromiseAll(node) {
|
|
72
|
+
return node.type === 'ArrayExpression' &&
|
|
73
|
+
node.parent.type === 'CallExpression' &&
|
|
74
|
+
node.parent.callee.type === 'MemberExpression' &&
|
|
75
|
+
(0, ast_1.isIdentifier)(node.parent.callee.object, 'Promise') &&
|
|
76
|
+
(0, ast_1.isIdentifier)(node.parent.callee.property, 'all')
|
|
77
|
+
? node.parent
|
|
78
|
+
: null;
|
|
79
|
+
}
|
|
71
80
|
function checkValidity(node) {
|
|
72
|
-
|
|
73
|
-
|
|
81
|
+
if (validTypes.has(node.parent.type))
|
|
82
|
+
return;
|
|
83
|
+
const promiseAll = isPromiseAll(node.parent);
|
|
84
|
+
return promiseAll
|
|
85
|
+
? checkValidity(promiseAll)
|
|
74
86
|
: node.parent.type === 'MemberExpression' ||
|
|
75
87
|
(node.parent.type === 'CallExpression' && node.parent.callee === node)
|
|
76
88
|
? checkValidity(node.parent)
|
|
@@ -94,7 +106,7 @@ exports.default = {
|
|
|
94
106
|
fix: (fixer) => fixer.insertTextBefore(node, 'await '),
|
|
95
107
|
messageId: result.messageId,
|
|
96
108
|
data: result.data,
|
|
97
|
-
node:
|
|
109
|
+
node: node.callee,
|
|
98
110
|
});
|
|
99
111
|
}
|
|
100
112
|
},
|
|
@@ -1,30 +1,39 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const ast_1 = require("../utils/ast");
|
|
4
|
-
function
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
: callee.object.range[0];
|
|
9
|
-
return [start, callee.range[1]];
|
|
4
|
+
function getPropertyRange(node) {
|
|
5
|
+
return node.type === 'Identifier'
|
|
6
|
+
? node.range
|
|
7
|
+
: [node.range[0] + 1, node.range[1] - 1];
|
|
10
8
|
}
|
|
11
9
|
exports.default = {
|
|
12
10
|
create(context) {
|
|
13
11
|
return {
|
|
14
12
|
CallExpression(node) {
|
|
15
|
-
if ((0, ast_1.
|
|
16
|
-
((0, ast_1.isCalleeProperty)(node, '$') || (0, ast_1.isCalleeProperty)(node, '$$'))) {
|
|
13
|
+
if ((0, ast_1.isPageMethod)(node, '$') || (0, ast_1.isPageMethod)(node, '$$')) {
|
|
17
14
|
context.report({
|
|
18
15
|
messageId: 'noElementHandle',
|
|
19
16
|
suggest: [
|
|
20
17
|
{
|
|
21
|
-
messageId: (0, ast_1.
|
|
18
|
+
messageId: (0, ast_1.isPageMethod)(node, '$')
|
|
22
19
|
? 'replaceElementHandleWithLocator'
|
|
23
20
|
: 'replaceElementHandlesWithLocator',
|
|
24
|
-
fix: (fixer) =>
|
|
21
|
+
fix: (fixer) => {
|
|
22
|
+
const { property } = node.callee;
|
|
23
|
+
// Replace $/$$ with locator
|
|
24
|
+
const fixes = [
|
|
25
|
+
fixer.replaceTextRange(getPropertyRange(property), 'locator'),
|
|
26
|
+
];
|
|
27
|
+
// Remove the await expression if it exists as locators do
|
|
28
|
+
// not need to be awaited.
|
|
29
|
+
if (node.parent.type === 'AwaitExpression') {
|
|
30
|
+
fixes.push(fixer.removeRange([node.parent.range[0], node.range[0]]));
|
|
31
|
+
}
|
|
32
|
+
return fixes;
|
|
33
|
+
},
|
|
25
34
|
},
|
|
26
35
|
],
|
|
27
|
-
node,
|
|
36
|
+
node: node.callee,
|
|
28
37
|
});
|
|
29
38
|
}
|
|
30
39
|
},
|
package/lib/rules/no-eval.js
CHANGED
|
@@ -5,11 +5,11 @@ exports.default = {
|
|
|
5
5
|
create(context) {
|
|
6
6
|
return {
|
|
7
7
|
CallExpression(node) {
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
const isEval = (0, ast_1.isPageMethod)(node, '$eval');
|
|
9
|
+
if (isEval || (0, ast_1.isPageMethod)(node, '$$eval')) {
|
|
10
10
|
context.report({
|
|
11
|
-
messageId:
|
|
12
|
-
node,
|
|
11
|
+
messageId: isEval ? 'noEval' : 'noEvalAll',
|
|
12
|
+
node: node.callee,
|
|
13
13
|
});
|
|
14
14
|
}
|
|
15
15
|
},
|
|
@@ -4,7 +4,7 @@ const ast_1 = require("../utils/ast");
|
|
|
4
4
|
function isForceOptionEnabled(node) {
|
|
5
5
|
const arg = node.arguments[node.arguments.length - 1];
|
|
6
6
|
return (arg?.type === 'ObjectExpression' &&
|
|
7
|
-
arg.properties.
|
|
7
|
+
arg.properties.find((property) => property.type === 'Property' &&
|
|
8
8
|
(0, ast_1.getStringValue)(property.key) === 'force' &&
|
|
9
9
|
(0, ast_1.isBooleanLiteral)(property.value, true)));
|
|
10
10
|
}
|
|
@@ -27,9 +27,11 @@ exports.default = {
|
|
|
27
27
|
return {
|
|
28
28
|
MemberExpression(node) {
|
|
29
29
|
if (methodsWithForceOption.has((0, ast_1.getStringValue)(node.property)) &&
|
|
30
|
-
node.parent.type === 'CallExpression'
|
|
31
|
-
isForceOptionEnabled(node.parent)
|
|
32
|
-
|
|
30
|
+
node.parent.type === 'CallExpression') {
|
|
31
|
+
const reportNode = isForceOptionEnabled(node.parent);
|
|
32
|
+
if (reportNode) {
|
|
33
|
+
context.report({ messageId: 'noForceOption', node: reportNode });
|
|
34
|
+
}
|
|
33
35
|
}
|
|
34
36
|
},
|
|
35
37
|
};
|
|
@@ -5,7 +5,7 @@ exports.default = {
|
|
|
5
5
|
create(context) {
|
|
6
6
|
return {
|
|
7
7
|
CallExpression(node) {
|
|
8
|
-
if ((0, ast_1.
|
|
8
|
+
if ((0, ast_1.isPageMethod)(node, 'pause')) {
|
|
9
9
|
context.report({ messageId: 'noPagePause', node });
|
|
10
10
|
}
|
|
11
11
|
},
|
|
@@ -1,31 +1,50 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const ast_1 = require("../utils/ast");
|
|
4
|
+
const parseExpectCall_1 = require("../utils/parseExpectCall");
|
|
4
5
|
exports.default = {
|
|
5
6
|
create(context) {
|
|
6
7
|
const restrictedChains = (context.options?.[0] ?? {});
|
|
7
8
|
return {
|
|
8
9
|
CallExpression(node) {
|
|
9
|
-
|
|
10
|
+
const expectCall = (0, parseExpectCall_1.parseExpectCall)(node);
|
|
11
|
+
if (!expectCall)
|
|
10
12
|
return;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
13
|
+
Object.entries(restrictedChains)
|
|
14
|
+
.map(([restriction, message]) => {
|
|
15
|
+
const chain = expectCall.members;
|
|
16
|
+
const restrictionLinks = restriction.split('.').length;
|
|
17
|
+
// Find in the full chain, where the restriction chain starts
|
|
18
|
+
const startIndex = chain.findIndex((_, i) => {
|
|
19
|
+
// Construct the partial chain to compare against the restriction
|
|
20
|
+
// chain string.
|
|
21
|
+
const partial = chain
|
|
22
|
+
.slice(i, i + restrictionLinks)
|
|
23
|
+
.map(ast_1.getStringValue)
|
|
24
|
+
.join('.');
|
|
25
|
+
return partial === restriction;
|
|
26
|
+
});
|
|
27
|
+
return {
|
|
28
|
+
// If the restriction chain was found, return the portion of the
|
|
29
|
+
// chain that matches the restriction chain.
|
|
30
|
+
chain: startIndex !== -1
|
|
31
|
+
? chain.slice(startIndex, startIndex + restrictionLinks)
|
|
32
|
+
: [],
|
|
33
|
+
restriction,
|
|
34
|
+
message,
|
|
35
|
+
};
|
|
36
|
+
})
|
|
37
|
+
.filter(({ chain }) => chain.length)
|
|
38
|
+
.forEach(({ chain, restriction, message }) => {
|
|
39
|
+
context.report({
|
|
40
|
+
messageId: message ? 'restrictedWithMessage' : 'restricted',
|
|
41
|
+
data: { message: message ?? '', restriction },
|
|
42
|
+
loc: {
|
|
43
|
+
start: chain[0].loc.start,
|
|
44
|
+
end: chain[chain.length - 1].loc.end,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
});
|
|
29
48
|
},
|
|
30
49
|
};
|
|
31
50
|
},
|
|
@@ -37,7 +56,7 @@ exports.default = {
|
|
|
37
56
|
url: 'https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-restricted-matchers.md',
|
|
38
57
|
},
|
|
39
58
|
messages: {
|
|
40
|
-
restricted: 'Use of `{{
|
|
59
|
+
restricted: 'Use of `{{restriction}}` is disallowed',
|
|
41
60
|
restrictedWithMessage: '{{message}}',
|
|
42
61
|
},
|
|
43
62
|
type: 'suggestion',
|
|
@@ -9,13 +9,14 @@ exports.default = {
|
|
|
9
9
|
if (((0, ast_1.isTestIdentifier)(callee) || (0, ast_1.isDescribeCall)(node)) &&
|
|
10
10
|
callee.type === 'MemberExpression' &&
|
|
11
11
|
(0, ast_1.isPropertyAccessor)(callee, 'skip')) {
|
|
12
|
+
const isHook = (0, ast_1.isTest)(node) || (0, ast_1.isDescribeCall)(node);
|
|
12
13
|
context.report({
|
|
13
14
|
messageId: 'noSkippedTest',
|
|
14
15
|
suggest: [
|
|
15
16
|
{
|
|
16
17
|
messageId: 'removeSkippedTestAnnotation',
|
|
17
18
|
fix: (fixer) => {
|
|
18
|
-
return
|
|
19
|
+
return isHook
|
|
19
20
|
? fixer.removeRange([
|
|
20
21
|
callee.property.range[0] - 1,
|
|
21
22
|
callee.range[1],
|
|
@@ -24,7 +25,7 @@ exports.default = {
|
|
|
24
25
|
},
|
|
25
26
|
},
|
|
26
27
|
],
|
|
27
|
-
node,
|
|
28
|
+
node: isHook ? callee.property : node,
|
|
28
29
|
});
|
|
29
30
|
}
|
|
30
31
|
},
|
|
@@ -1,43 +1,44 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const ast_1 = require("../utils/ast");
|
|
4
|
+
const fixer_1 = require("../utils/fixer");
|
|
5
|
+
const parseExpectCall_1 = require("../utils/parseExpectCall");
|
|
4
6
|
const matcherMap = {
|
|
5
7
|
toBeVisible: 'toBeHidden',
|
|
6
8
|
toBeHidden: 'toBeVisible',
|
|
7
9
|
toBeEnabled: 'toBeDisabled',
|
|
8
10
|
toBeDisabled: 'toBeEnabled',
|
|
9
11
|
};
|
|
10
|
-
const getRangeOffset = (node) => node.type === 'Identifier' ? 0 : 1;
|
|
11
12
|
exports.default = {
|
|
12
13
|
create(context) {
|
|
13
14
|
return {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
}
|
|
15
|
+
CallExpression(node) {
|
|
16
|
+
const expectCall = (0, parseExpectCall_1.parseExpectCall)(node);
|
|
17
|
+
if (!expectCall)
|
|
18
|
+
return;
|
|
19
|
+
// As the name implies, this rule only implies if the not modifier is
|
|
20
|
+
// part of the matcher chain
|
|
21
|
+
const notModifier = expectCall.modifiers.find((mod) => (0, ast_1.getStringValue)(mod) === 'not');
|
|
22
|
+
if (!notModifier)
|
|
23
|
+
return;
|
|
24
|
+
// This rule only applies to specific matchers that have opposites
|
|
25
|
+
if (expectCall.matcherName in matcherMap) {
|
|
26
|
+
const newMatcher = matcherMap[expectCall.matcherName];
|
|
27
|
+
context.report({
|
|
28
|
+
fix: (fixer) => [
|
|
29
|
+
fixer.removeRange([
|
|
30
|
+
notModifier.range[0] - (0, fixer_1.getRangeOffset)(notModifier),
|
|
31
|
+
notModifier.range[1] + 1,
|
|
32
|
+
]),
|
|
33
|
+
(0, fixer_1.replaceAccessorFixer)(fixer, expectCall.matcher, newMatcher),
|
|
34
|
+
],
|
|
35
|
+
messageId: 'noUselessNot',
|
|
36
|
+
data: { old: expectCall.matcherName, new: newMatcher },
|
|
37
|
+
loc: {
|
|
38
|
+
start: notModifier.loc.start,
|
|
39
|
+
end: expectCall.matcher.loc.end,
|
|
40
|
+
},
|
|
41
|
+
});
|
|
41
42
|
}
|
|
42
43
|
},
|
|
43
44
|
};
|
|
@@ -5,8 +5,7 @@ exports.default = {
|
|
|
5
5
|
create(context) {
|
|
6
6
|
return {
|
|
7
7
|
CallExpression(node) {
|
|
8
|
-
if ((0, ast_1.
|
|
9
|
-
(0, ast_1.isCalleeProperty)(node, 'waitForTimeout')) {
|
|
8
|
+
if ((0, ast_1.isPageMethod)(node, 'waitForTimeout')) {
|
|
10
9
|
context.report({
|
|
11
10
|
messageId: 'noWaitForTimeout',
|
|
12
11
|
suggest: [
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const fixer_1 = require("../utils/fixer");
|
|
4
|
+
const parseExpectCall_1 = require("../utils/parseExpectCall");
|
|
5
|
+
exports.default = {
|
|
6
|
+
create(context) {
|
|
7
|
+
return {
|
|
8
|
+
CallExpression(node) {
|
|
9
|
+
const expectCall = (0, parseExpectCall_1.parseExpectCall)(node);
|
|
10
|
+
if (expectCall?.matcherName === 'toEqual') {
|
|
11
|
+
context.report({
|
|
12
|
+
node: expectCall.matcher,
|
|
13
|
+
messageId: 'useToStrictEqual',
|
|
14
|
+
suggest: [
|
|
15
|
+
{
|
|
16
|
+
messageId: 'suggestReplaceWithStrictEqual',
|
|
17
|
+
fix: (fixer) => {
|
|
18
|
+
return (0, fixer_1.replaceAccessorFixer)(fixer, expectCall.matcher, 'toStrictEqual');
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
meta: {
|
|
28
|
+
docs: {
|
|
29
|
+
category: 'Best Practices',
|
|
30
|
+
description: 'Suggest using `toStrictEqual()`',
|
|
31
|
+
recommended: false,
|
|
32
|
+
url: 'https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-strict-equal.md',
|
|
33
|
+
},
|
|
34
|
+
messages: {
|
|
35
|
+
useToStrictEqual: 'Use toStrictEqual() instead',
|
|
36
|
+
suggestReplaceWithStrictEqual: 'Replace with `toStrictEqual()`',
|
|
37
|
+
},
|
|
38
|
+
fixable: 'code',
|
|
39
|
+
type: 'suggestion',
|
|
40
|
+
hasSuggestions: true,
|
|
41
|
+
schema: [],
|
|
42
|
+
},
|
|
43
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const ast_1 = require("../utils/ast");
|
|
4
|
+
const fixer_1 = require("../utils/fixer");
|
|
5
|
+
const parseExpectCall_1 = require("../utils/parseExpectCall");
|
|
6
|
+
function shouldUseToBe(expectCall) {
|
|
7
|
+
let arg = expectCall.args[0];
|
|
8
|
+
if (arg.type === 'UnaryExpression' && arg.operator === '-') {
|
|
9
|
+
arg = arg.argument;
|
|
10
|
+
}
|
|
11
|
+
if (arg.type === 'Literal') {
|
|
12
|
+
// regex literals are classed as literals, but they're actually objects
|
|
13
|
+
// which means "toBe" will give different results than other matchers
|
|
14
|
+
return !('regex' in arg);
|
|
15
|
+
}
|
|
16
|
+
return arg.type === 'TemplateLiteral';
|
|
17
|
+
}
|
|
18
|
+
function reportPreferToBe(context, expectCall, whatToBe, notModifier) {
|
|
19
|
+
context.report({
|
|
20
|
+
node: expectCall.matcher,
|
|
21
|
+
messageId: `useToBe${whatToBe}`,
|
|
22
|
+
fix(fixer) {
|
|
23
|
+
const fixes = [
|
|
24
|
+
(0, fixer_1.replaceAccessorFixer)(fixer, expectCall.matcher, `toBe${whatToBe}`),
|
|
25
|
+
];
|
|
26
|
+
if (expectCall.args?.length && whatToBe !== '') {
|
|
27
|
+
fixes.push(fixer.remove(expectCall.args[0]));
|
|
28
|
+
}
|
|
29
|
+
if (notModifier) {
|
|
30
|
+
const [start, end] = notModifier.range;
|
|
31
|
+
fixes.push(fixer.removeRange([start - 1, end]));
|
|
32
|
+
}
|
|
33
|
+
return fixes;
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
exports.default = {
|
|
38
|
+
create(context) {
|
|
39
|
+
return {
|
|
40
|
+
CallExpression(node) {
|
|
41
|
+
const expectCall = (0, parseExpectCall_1.parseExpectCall)(node);
|
|
42
|
+
if (!expectCall)
|
|
43
|
+
return;
|
|
44
|
+
const notMatchers = ['toBeUndefined', 'toBeDefined'];
|
|
45
|
+
const notModifier = expectCall.modifiers.find((node) => (0, ast_1.getStringValue)(node) === 'not');
|
|
46
|
+
if (notModifier && notMatchers.includes(expectCall.matcherName)) {
|
|
47
|
+
return reportPreferToBe(context, expectCall, expectCall.matcherName === 'toBeDefined' ? 'Undefined' : 'Defined', notModifier);
|
|
48
|
+
}
|
|
49
|
+
const argumentMatchers = ['toBe', 'toEqual', 'toStrictEqual'];
|
|
50
|
+
const firstArg = expectCall.args[0];
|
|
51
|
+
if (!argumentMatchers.includes(expectCall.matcherName) || !firstArg) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (firstArg.type === 'Literal' && firstArg.value === null) {
|
|
55
|
+
return reportPreferToBe(context, expectCall, 'Null');
|
|
56
|
+
}
|
|
57
|
+
if ((0, ast_1.isIdentifier)(firstArg, 'undefined')) {
|
|
58
|
+
const name = notModifier ? 'Defined' : 'Undefined';
|
|
59
|
+
return reportPreferToBe(context, expectCall, name, notModifier);
|
|
60
|
+
}
|
|
61
|
+
if ((0, ast_1.isIdentifier)(firstArg, 'NaN')) {
|
|
62
|
+
return reportPreferToBe(context, expectCall, 'NaN');
|
|
63
|
+
}
|
|
64
|
+
if (shouldUseToBe(expectCall) && expectCall.matcherName !== 'toBe') {
|
|
65
|
+
reportPreferToBe(context, expectCall, '');
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
meta: {
|
|
71
|
+
docs: {
|
|
72
|
+
category: 'Best Practices',
|
|
73
|
+
description: 'Suggest using `toBe()` for primitive literals',
|
|
74
|
+
recommended: false,
|
|
75
|
+
url: 'https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-to-be.md',
|
|
76
|
+
},
|
|
77
|
+
messages: {
|
|
78
|
+
useToBe: 'Use `toBe` when expecting primitive literals',
|
|
79
|
+
useToBeUndefined: 'Use `toBeUndefined` instead',
|
|
80
|
+
useToBeDefined: 'Use `toBeDefined` instead',
|
|
81
|
+
useToBeNull: 'Use `toBeNull` instead',
|
|
82
|
+
useToBeNaN: 'Use `toBeNaN` instead',
|
|
83
|
+
},
|
|
84
|
+
fixable: 'code',
|
|
85
|
+
type: 'suggestion',
|
|
86
|
+
schema: [],
|
|
87
|
+
},
|
|
88
|
+
};
|
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const ast_1 = require("../utils/ast");
|
|
4
|
-
const
|
|
5
|
-
const
|
|
4
|
+
const fixer_1 = require("../utils/fixer");
|
|
5
|
+
const parseExpectCall_1 = require("../utils/parseExpectCall");
|
|
6
|
+
const lengthMatchers = new Set(['toBe', 'toEqual', 'toStrictEqual']);
|
|
6
7
|
exports.default = {
|
|
7
8
|
create(context) {
|
|
8
9
|
return {
|
|
9
10
|
CallExpression(node) {
|
|
10
|
-
|
|
11
|
+
const expectCall = (0, parseExpectCall_1.parseExpectCall)(node);
|
|
12
|
+
if (!expectCall || !lengthMatchers.has(expectCall.matcherName)) {
|
|
11
13
|
return;
|
|
12
14
|
}
|
|
13
15
|
const [argument] = node.arguments;
|
|
14
|
-
|
|
15
|
-
if (!matcher ||
|
|
16
|
-
!matchers.has((0, ast_1.getStringValue)(matcher) ?? '') ||
|
|
17
|
-
argument?.type !== 'MemberExpression' ||
|
|
16
|
+
if (argument?.type !== 'MemberExpression' ||
|
|
18
17
|
!(0, ast_1.isPropertyAccessor)(argument, 'length')) {
|
|
19
18
|
return;
|
|
20
19
|
}
|
|
@@ -27,14 +26,11 @@ exports.default = {
|
|
|
27
26
|
argument.range[1],
|
|
28
27
|
]),
|
|
29
28
|
// replace the current matcher with "toHaveLength"
|
|
30
|
-
fixer.
|
|
31
|
-
matcher.range[0] + getRangeOffset(matcher),
|
|
32
|
-
matcher.range[1] - getRangeOffset(matcher),
|
|
33
|
-
], 'toHaveLength'),
|
|
29
|
+
(0, fixer_1.replaceAccessorFixer)(fixer, expectCall.matcher, 'toHaveLength'),
|
|
34
30
|
];
|
|
35
31
|
},
|
|
36
32
|
messageId: 'useToHaveLength',
|
|
37
|
-
node: matcher,
|
|
33
|
+
node: expectCall.matcher,
|
|
38
34
|
});
|
|
39
35
|
},
|
|
40
36
|
};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getMatcherCall = void 0;
|
|
4
|
+
const ast_1 = require("../utils/ast");
|
|
5
|
+
const parseExpectCall_1 = require("../utils/parseExpectCall");
|
|
6
|
+
const methods = {
|
|
7
|
+
innerText: { type: 'string', matcher: 'toHaveText' },
|
|
8
|
+
textContent: { type: 'string', matcher: 'toHaveText' },
|
|
9
|
+
inputValue: { type: 'string', matcher: 'toHaveValue' },
|
|
10
|
+
isEditable: { type: 'boolean', matcher: 'toBeEditable' },
|
|
11
|
+
isChecked: { type: 'boolean', matcher: 'toBeChecked' },
|
|
12
|
+
isDisabled: {
|
|
13
|
+
type: 'boolean',
|
|
14
|
+
matcher: 'toBeDisabled',
|
|
15
|
+
inverse: 'toBeEnabled',
|
|
16
|
+
},
|
|
17
|
+
isEnabled: {
|
|
18
|
+
type: 'boolean',
|
|
19
|
+
matcher: 'toBeEnabled',
|
|
20
|
+
inverse: 'toBeDisabled',
|
|
21
|
+
},
|
|
22
|
+
isHidden: {
|
|
23
|
+
type: 'boolean',
|
|
24
|
+
matcher: 'toBeHidden',
|
|
25
|
+
inverse: 'toBeVisible',
|
|
26
|
+
},
|
|
27
|
+
isVisible: {
|
|
28
|
+
type: 'boolean',
|
|
29
|
+
matcher: 'toBeVisible',
|
|
30
|
+
inverse: 'toBeHidden',
|
|
31
|
+
},
|
|
32
|
+
getAttribute: {
|
|
33
|
+
type: 'string',
|
|
34
|
+
matcher: 'toHaveAttribute',
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
const supportedMatchers = new Set([
|
|
38
|
+
'toBe',
|
|
39
|
+
'toEqual',
|
|
40
|
+
'toBeTruthy',
|
|
41
|
+
'toBeFalsy',
|
|
42
|
+
]);
|
|
43
|
+
function getMatcherCall(node) {
|
|
44
|
+
const grandparent = node.parent?.parent;
|
|
45
|
+
return grandparent.type === 'CallExpression' ? grandparent : undefined;
|
|
46
|
+
}
|
|
47
|
+
exports.getMatcherCall = getMatcherCall;
|
|
48
|
+
exports.default = {
|
|
49
|
+
create(context) {
|
|
50
|
+
return {
|
|
51
|
+
CallExpression(node) {
|
|
52
|
+
const expectCall = (0, parseExpectCall_1.parseExpectCall)(node);
|
|
53
|
+
if (!expectCall)
|
|
54
|
+
return;
|
|
55
|
+
const [arg] = node.arguments;
|
|
56
|
+
if (arg.type !== 'AwaitExpression' ||
|
|
57
|
+
arg.argument.type !== 'CallExpression' ||
|
|
58
|
+
arg.argument.callee.type !== 'MemberExpression') {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
// Matcher must be supported
|
|
62
|
+
if (!supportedMatchers.has(expectCall.matcherName))
|
|
63
|
+
return;
|
|
64
|
+
// Playwright method must be supported
|
|
65
|
+
const method = (0, ast_1.getStringValue)(arg.argument.callee.property);
|
|
66
|
+
const methodConfig = methods[method];
|
|
67
|
+
if (!methodConfig)
|
|
68
|
+
return;
|
|
69
|
+
// Change the matcher
|
|
70
|
+
const { args, matcher } = expectCall;
|
|
71
|
+
const notModifier = expectCall.modifiers.find((mod) => (0, ast_1.getStringValue)(mod) === 'not');
|
|
72
|
+
const isFalsy = methodConfig.type === 'boolean' &&
|
|
73
|
+
((!!args.length && (0, ast_1.isBooleanLiteral)(args[0], false)) ||
|
|
74
|
+
expectCall.matcherName === 'toBeFalsy');
|
|
75
|
+
const isInverse = methodConfig.inverse
|
|
76
|
+
? notModifier || isFalsy
|
|
77
|
+
: notModifier && isFalsy;
|
|
78
|
+
// Replace the old matcher with the new matcher. The inverse
|
|
79
|
+
// matcher should only be used if the old statement was not a
|
|
80
|
+
// double negation.
|
|
81
|
+
const newMatcher = (+!!notModifier ^ +isFalsy && methodConfig.inverse) ||
|
|
82
|
+
methodConfig.matcher;
|
|
83
|
+
const { callee } = arg.argument;
|
|
84
|
+
context.report({
|
|
85
|
+
node,
|
|
86
|
+
messageId: 'useWebFirstAssertion',
|
|
87
|
+
data: {
|
|
88
|
+
method: method,
|
|
89
|
+
matcher: newMatcher,
|
|
90
|
+
},
|
|
91
|
+
fix: (fixer) => {
|
|
92
|
+
const methodArgs = arg.argument.type === 'CallExpression'
|
|
93
|
+
? arg.argument.arguments
|
|
94
|
+
: [];
|
|
95
|
+
const methodEnd = methodArgs.length
|
|
96
|
+
? methodArgs[methodArgs.length - 1].range[1] + 1
|
|
97
|
+
: callee.property.range[1] + 2;
|
|
98
|
+
const fixes = [
|
|
99
|
+
// Add await to the expect call
|
|
100
|
+
fixer.insertTextBefore(node, 'await '),
|
|
101
|
+
// Remove the await keyword
|
|
102
|
+
fixer.replaceTextRange([arg.range[0], arg.argument.range[0]], ''),
|
|
103
|
+
// Remove the old Playwright method and any arguments
|
|
104
|
+
fixer.replaceTextRange([callee.property.range[0] - 1, methodEnd], ''),
|
|
105
|
+
];
|
|
106
|
+
// Remove not from matcher chain if no longer needed
|
|
107
|
+
if (isInverse && notModifier) {
|
|
108
|
+
const notRange = notModifier.range;
|
|
109
|
+
fixes.push(fixer.removeRange([notRange[0], notRange[1] + 1]));
|
|
110
|
+
}
|
|
111
|
+
// Add not to the matcher chain if no inverse matcher exists
|
|
112
|
+
if (!methodConfig.inverse && !notModifier && isFalsy) {
|
|
113
|
+
fixes.push(fixer.insertTextBefore(matcher, 'not.'));
|
|
114
|
+
}
|
|
115
|
+
fixes.push(fixer.replaceText(matcher, newMatcher));
|
|
116
|
+
// Remove boolean argument if it exists
|
|
117
|
+
const [matcherArg] = args ?? [];
|
|
118
|
+
if (matcherArg && (0, ast_1.isBooleanLiteral)(matcherArg)) {
|
|
119
|
+
fixes.push(fixer.remove(matcherArg));
|
|
120
|
+
}
|
|
121
|
+
// Add the new matcher arguments if needed
|
|
122
|
+
const hasOtherArgs = !!methodArgs.filter((arg) => !(0, ast_1.isBooleanLiteral)(arg)).length;
|
|
123
|
+
if (methodArgs) {
|
|
124
|
+
const range = matcher.range;
|
|
125
|
+
const stringArgs = methodArgs
|
|
126
|
+
.map((arg) => (0, ast_1.getRawValue)(arg))
|
|
127
|
+
.concat(hasOtherArgs ? '' : [])
|
|
128
|
+
.join(', ');
|
|
129
|
+
fixes.push(fixer.insertTextAfterRange([range[0], range[1] + 1], stringArgs));
|
|
130
|
+
}
|
|
131
|
+
return fixes;
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
},
|
|
137
|
+
meta: {
|
|
138
|
+
docs: {
|
|
139
|
+
category: 'Best Practices',
|
|
140
|
+
description: 'Prefer web first assertions',
|
|
141
|
+
recommended: true,
|
|
142
|
+
url: 'https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-web-first-assertions.md',
|
|
143
|
+
},
|
|
144
|
+
fixable: 'code',
|
|
145
|
+
messages: {
|
|
146
|
+
useWebFirstAssertion: 'Replace {{method}}() with {{matcher}}().',
|
|
147
|
+
},
|
|
148
|
+
type: 'suggestion',
|
|
149
|
+
},
|
|
150
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const ast_1 = require("../utils/ast");
|
|
4
|
+
exports.default = {
|
|
5
|
+
create(context) {
|
|
6
|
+
return {
|
|
7
|
+
CallExpression(node) {
|
|
8
|
+
if ((0, ast_1.getExpectType)(node) === 'standalone') {
|
|
9
|
+
context.report({
|
|
10
|
+
node: node.callee,
|
|
11
|
+
messageId: 'requireSoft',
|
|
12
|
+
fix: (fixer) => fixer.insertTextAfter(node.callee, '.soft'),
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
},
|
|
18
|
+
meta: {
|
|
19
|
+
docs: {
|
|
20
|
+
description: 'Require all assertions to use `expect.soft`',
|
|
21
|
+
recommended: false,
|
|
22
|
+
url: 'https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/require-soft-assertions.md',
|
|
23
|
+
},
|
|
24
|
+
messages: {
|
|
25
|
+
requireSoft: 'Unexpected non-soft assertion',
|
|
26
|
+
},
|
|
27
|
+
fixable: 'code',
|
|
28
|
+
type: 'suggestion',
|
|
29
|
+
schema: [],
|
|
30
|
+
},
|
|
31
|
+
};
|
|
@@ -18,7 +18,7 @@ exports.default = {
|
|
|
18
18
|
topLevelDescribeCount++;
|
|
19
19
|
if (topLevelDescribeCount > maxTopLevelDescribes) {
|
|
20
20
|
context.report({
|
|
21
|
-
node,
|
|
21
|
+
node: node.callee,
|
|
22
22
|
messageId: 'tooManyDescribes',
|
|
23
23
|
data: (0, misc_1.getAmountData)(maxTopLevelDescribes),
|
|
24
24
|
});
|
|
@@ -27,10 +27,10 @@ exports.default = {
|
|
|
27
27
|
}
|
|
28
28
|
else if (!describeCount) {
|
|
29
29
|
if ((0, ast_1.isTest)(node)) {
|
|
30
|
-
context.report({ node, messageId: 'unexpectedTest' });
|
|
30
|
+
context.report({ node: node.callee, messageId: 'unexpectedTest' });
|
|
31
31
|
}
|
|
32
32
|
else if ((0, ast_1.isTestHook)(node)) {
|
|
33
|
-
context.report({ node, messageId: 'unexpectedHook' });
|
|
33
|
+
context.report({ node: node.callee, messageId: 'unexpectedHook' });
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
},
|
|
@@ -2,16 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const ast_1 = require("../utils/ast");
|
|
4
4
|
const misc_1 = require("../utils/misc");
|
|
5
|
-
|
|
6
|
-
if (node.parent.type !== 'MemberExpression') {
|
|
7
|
-
return { found: false, node };
|
|
8
|
-
}
|
|
9
|
-
if ((0, ast_1.isPropertyAccessor)(node.parent, 'not') &&
|
|
10
|
-
node.parent.parent.type !== 'MemberExpression') {
|
|
11
|
-
return { found: false, node: node.parent };
|
|
12
|
-
}
|
|
13
|
-
return { found: true, node };
|
|
14
|
-
}
|
|
5
|
+
const parseExpectCall_1 = require("../utils/parseExpectCall");
|
|
15
6
|
function isMatcherCalled(node) {
|
|
16
7
|
if (node.parent.type !== 'MemberExpression') {
|
|
17
8
|
// Just asserting that the parent is a call expression is not enough as
|
|
@@ -40,15 +31,17 @@ exports.default = {
|
|
|
40
31
|
CallExpression(node) {
|
|
41
32
|
if (!(0, ast_1.isExpectCall)(node))
|
|
42
33
|
return;
|
|
43
|
-
const
|
|
44
|
-
if (!
|
|
45
|
-
context.report({ node
|
|
34
|
+
const expectCall = (0, parseExpectCall_1.parseExpectCall)(node);
|
|
35
|
+
if (!expectCall) {
|
|
36
|
+
context.report({ node, messageId: 'matcherNotFound' });
|
|
46
37
|
}
|
|
47
38
|
else {
|
|
48
39
|
const result = isMatcherCalled(node);
|
|
49
40
|
if (!result.called) {
|
|
50
41
|
context.report({
|
|
51
|
-
node: result.node
|
|
42
|
+
node: result.node.type === 'MemberExpression'
|
|
43
|
+
? result.node.property
|
|
44
|
+
: result.node,
|
|
52
45
|
messageId: 'matcherNotCalled',
|
|
53
46
|
});
|
|
54
47
|
}
|
package/lib/utils/ast.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getMatchers = exports.isExpectCall = exports.
|
|
3
|
+
exports.isPageMethod = exports.getMatchers = exports.isExpectCall = exports.getExpectType = exports.isTestHook = exports.isTest = exports.findParent = exports.isDescribeCall = exports.isTestIdentifier = exports.isPropertyAccessor = exports.isBooleanLiteral = exports.isStringLiteral = exports.isIdentifier = exports.getRawValue = exports.getStringValue = void 0;
|
|
4
4
|
function getStringValue(node) {
|
|
5
5
|
if (!node)
|
|
6
6
|
return '';
|
|
@@ -13,8 +13,13 @@ function getStringValue(node) {
|
|
|
13
13
|
: '';
|
|
14
14
|
}
|
|
15
15
|
exports.getStringValue = getStringValue;
|
|
16
|
+
function getRawValue(node) {
|
|
17
|
+
return node.type === 'Literal' ? node.raw : undefined;
|
|
18
|
+
}
|
|
19
|
+
exports.getRawValue = getRawValue;
|
|
16
20
|
function isIdentifier(node, name) {
|
|
17
|
-
return node.type === 'Identifier' &&
|
|
21
|
+
return (node.type === 'Identifier' &&
|
|
22
|
+
(typeof name === 'string' ? node.name === name : name.test(node.name)));
|
|
18
23
|
}
|
|
19
24
|
exports.isIdentifier = isIdentifier;
|
|
20
25
|
function isLiteral(node, type, value) {
|
|
@@ -35,16 +40,6 @@ function isPropertyAccessor(node, name) {
|
|
|
35
40
|
return getStringValue(node.property) === name;
|
|
36
41
|
}
|
|
37
42
|
exports.isPropertyAccessor = isPropertyAccessor;
|
|
38
|
-
function isCalleeObject(node, name) {
|
|
39
|
-
return (node.callee.type === 'MemberExpression' &&
|
|
40
|
-
isIdentifier(node.callee.object, name));
|
|
41
|
-
}
|
|
42
|
-
exports.isCalleeObject = isCalleeObject;
|
|
43
|
-
function isCalleeProperty(node, name) {
|
|
44
|
-
return (node.callee.type === 'MemberExpression' &&
|
|
45
|
-
isPropertyAccessor(node.callee, name));
|
|
46
|
-
}
|
|
47
|
-
exports.isCalleeProperty = isCalleeProperty;
|
|
48
43
|
function isTestIdentifier(node) {
|
|
49
44
|
return (isIdentifier(node, 'test') ||
|
|
50
45
|
(node.type === 'MemberExpression' && isIdentifier(node.object, 'test')));
|
|
@@ -96,7 +91,7 @@ function isTestHook(node) {
|
|
|
96
91
|
}
|
|
97
92
|
exports.isTestHook = isTestHook;
|
|
98
93
|
const expectSubCommands = new Set(['soft', 'poll']);
|
|
99
|
-
function
|
|
94
|
+
function getExpectType(node) {
|
|
100
95
|
if (isIdentifier(node.callee, 'expect')) {
|
|
101
96
|
return 'standalone';
|
|
102
97
|
}
|
|
@@ -106,15 +101,37 @@ function parseExpectCall(node) {
|
|
|
106
101
|
return expectSubCommands.has(type) ? type : undefined;
|
|
107
102
|
}
|
|
108
103
|
}
|
|
109
|
-
exports.
|
|
104
|
+
exports.getExpectType = getExpectType;
|
|
110
105
|
function isExpectCall(node) {
|
|
111
|
-
return !!
|
|
106
|
+
return !!getExpectType(node);
|
|
112
107
|
}
|
|
113
108
|
exports.isExpectCall = isExpectCall;
|
|
114
109
|
function getMatchers(node, chain = []) {
|
|
115
110
|
if (node.parent.type === 'MemberExpression' && node.parent.object === node) {
|
|
116
|
-
return getMatchers(node.parent, [
|
|
111
|
+
return getMatchers(node.parent, [
|
|
112
|
+
...chain,
|
|
113
|
+
node.parent.property,
|
|
114
|
+
]);
|
|
117
115
|
}
|
|
118
116
|
return chain;
|
|
119
117
|
}
|
|
120
118
|
exports.getMatchers = getMatchers;
|
|
119
|
+
/**
|
|
120
|
+
* Digs through a series of MemberExpressions and CallExpressions to find an
|
|
121
|
+
* Identifier with the given name.
|
|
122
|
+
*/
|
|
123
|
+
function dig(node, identifier) {
|
|
124
|
+
return node.type === 'MemberExpression'
|
|
125
|
+
? dig(node.property, identifier)
|
|
126
|
+
: node.type === 'CallExpression'
|
|
127
|
+
? dig(node.callee, identifier)
|
|
128
|
+
: node.type === 'Identifier'
|
|
129
|
+
? isIdentifier(node, identifier)
|
|
130
|
+
: false;
|
|
131
|
+
}
|
|
132
|
+
function isPageMethod(node, name) {
|
|
133
|
+
return (node.callee.type === 'MemberExpression' &&
|
|
134
|
+
dig(node.callee.object, /(^page|Page$)/) &&
|
|
135
|
+
isPropertyAccessor(node.callee, name));
|
|
136
|
+
}
|
|
137
|
+
exports.isPageMethod = isPageMethod;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.replaceAccessorFixer = exports.getRangeOffset = void 0;
|
|
4
|
+
const getRangeOffset = (node) => node.type === 'Identifier' ? 0 : 1;
|
|
5
|
+
exports.getRangeOffset = getRangeOffset;
|
|
6
|
+
/**
|
|
7
|
+
* Replaces an accessor node with the given `text`.
|
|
8
|
+
*
|
|
9
|
+
* This ensures that fixes produce valid code when replacing both dot-based and
|
|
10
|
+
* bracket-based property accessors.
|
|
11
|
+
*/
|
|
12
|
+
function replaceAccessorFixer(fixer, node, text) {
|
|
13
|
+
const [start, end] = node.range;
|
|
14
|
+
return fixer.replaceTextRange([start + (0, exports.getRangeOffset)(node), end - (0, exports.getRangeOffset)(node)], text);
|
|
15
|
+
}
|
|
16
|
+
exports.replaceAccessorFixer = replaceAccessorFixer;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseExpectCall = void 0;
|
|
4
|
+
const ast_1 = require("./ast");
|
|
5
|
+
const MODIFIER_NAMES = new Set(['not', 'resolves', 'rejects']);
|
|
6
|
+
function getExpectArguments(node) {
|
|
7
|
+
const grandparent = node.parent.parent;
|
|
8
|
+
return grandparent.type === 'CallExpression' ? grandparent.arguments : [];
|
|
9
|
+
}
|
|
10
|
+
function parseExpectCall(node) {
|
|
11
|
+
if (!(0, ast_1.isExpectCall)(node)) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const members = (0, ast_1.getMatchers)(node);
|
|
15
|
+
const modifiers = [];
|
|
16
|
+
let matcher;
|
|
17
|
+
// Separate the matchers (e.g. toBe) from modifiers (e.g. not)
|
|
18
|
+
members.forEach((item) => {
|
|
19
|
+
if (MODIFIER_NAMES.has((0, ast_1.getStringValue)(item))) {
|
|
20
|
+
modifiers.push(item);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
matcher = item;
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
// Rules only run against full expect calls with matchers
|
|
27
|
+
if (!matcher) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
members,
|
|
32
|
+
matcher,
|
|
33
|
+
matcherName: (0, ast_1.getStringValue)(matcher),
|
|
34
|
+
modifiers,
|
|
35
|
+
args: getExpectArguments(matcher),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
exports.parseExpectCall = parseExpectCall;
|
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-playwright",
|
|
3
3
|
"description": "ESLint plugin for Playwright testing.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.13.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>",
|
|
8
|
+
"contributors": [
|
|
9
|
+
"Mark Skelton <mark@mskelton.dev>"
|
|
10
|
+
],
|
|
8
11
|
"license": "MIT",
|
|
9
12
|
"files": [
|
|
10
13
|
"lib",
|