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 CHANGED
@@ -1,23 +1,28 @@
1
1
  # ESLint Plugin Playwright
2
2
 
3
3
  [![Test](https://github.com/playwright-community/eslint-plugin-playwright/actions/workflows/test.yml/badge.svg)](https://github.com/playwright-community/eslint-plugin-playwright/actions/workflows/test.yml)
4
- [![NPM](https://img.shields.io/npm/v/eslint-plugin-playwright)](https://www.npmjs.com/package/eslint-plugin-playwright)
4
+ [![npm](https://img.shields.io/npm/v/eslint-plugin-playwright)](https://www.npmjs.com/package/eslint-plugin-playwright)
5
5
 
6
- > ESLint plugin for your [Playwright](https://github.com/microsoft/playwright)
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
- NPM
22
+ pnpm
18
23
 
19
24
  ```bash
20
- npm install -D eslint-plugin-playwright
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/test-intro)
33
+ ### With [Playwright test runner](https://playwright.dev/docs/writing-tests)
29
34
 
30
35
  ```json
31
36
  {
32
- "extends": ["plugin:playwright/playwright-test"]
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 | Description |
53
- | :-: | :-: | :-: | ------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
54
- | ✔ | | | [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 |
55
- | ✔ | 🔧 | | [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 |
56
- | ✔ | | | [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 |
57
- | ✔ | | 💡 | [no-element-handle](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-element-handle.md) | Disallow usage of element handles |
58
- | ✔ | | | [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` |
59
- | ✔ | | 💡 | [no-focused-test](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-focused-test.md) | Disallow usage of `.only` annotation |
60
- | ✔ | | | [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 |
61
- | ✔ | | | [no-page-pause](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-page-pause.md) | Disallow using `page.pause` |
62
- | | | | [no-restricted-matchers](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-restricted-matchers.md) | Disallow specific matchers & modifiers |
63
- | ✔ | | 💡 | [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 |
64
- | ✔ | 🔧 | | [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 |
65
- | ✔ | | 💡 | [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` |
66
- | | 🔧 | | [prefer-lowercase-title](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-lowercase-title.md) | Enforce lowercase test names |
67
- | | 🔧 | | [prefer-to-have-length](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-to-have-length.md) | Suggest using `toHaveLength()` |
68
- | | | | [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 |
69
- | | | | [valid-expect](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/valid-expect.md) | Enforce valid `expect()` usage |
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
- 'playwright-test': {
21
- plugins: ['playwright'],
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.parseExpectCall)(node);
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 awitable matcher
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
- return validTypes.has(node.parent.type)
73
- ? undefined
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: reportNode,
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 getRange(node) {
5
- const callee = node.callee;
6
- const start = node.parent.type === 'AwaitExpression'
7
- ? node.parent.range[0]
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.isCalleeObject)(node, 'page') &&
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.isCalleeProperty)(node, '$')
18
+ messageId: (0, ast_1.isPageMethod)(node, '$')
22
19
  ? 'replaceElementHandleWithLocator'
23
20
  : 'replaceElementHandlesWithLocator',
24
- fix: (fixer) => fixer.replaceTextRange(getRange(node), 'page.locator'),
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
  },
@@ -5,11 +5,11 @@ exports.default = {
5
5
  create(context) {
6
6
  return {
7
7
  CallExpression(node) {
8
- if ((0, ast_1.isCalleeObject)(node, 'page') &&
9
- ((0, ast_1.isCalleeProperty)(node, '$eval') || (0, ast_1.isCalleeProperty)(node, '$$eval'))) {
8
+ const isEval = (0, ast_1.isPageMethod)(node, '$eval');
9
+ if (isEval || (0, ast_1.isPageMethod)(node, '$$eval')) {
10
10
  context.report({
11
- messageId: (0, ast_1.isCalleeProperty)(node, '$eval') ? 'noEval' : 'noEvalAll',
12
- node,
11
+ messageId: isEval ? 'noEval' : 'noEvalAll',
12
+ node: node.callee,
13
13
  });
14
14
  }
15
15
  },
@@ -21,7 +21,7 @@ exports.default = {
21
21
  ]),
22
22
  },
23
23
  ],
24
- node,
24
+ node: node.callee.property,
25
25
  });
26
26
  }
27
27
  },
@@ -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.some((property) => property.type === 'Property' &&
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
- context.report({ messageId: 'noForceOption', node });
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.isCalleeObject)(node, 'page') && (0, ast_1.isCalleeProperty)(node, 'pause')) {
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
- if (!(0, ast_1.isExpectCall)(node)) {
10
+ const expectCall = (0, parseExpectCall_1.parseExpectCall)(node);
11
+ if (!expectCall)
10
12
  return;
11
- }
12
- const matchers = (0, ast_1.getMatchers)(node);
13
- const permutations = matchers.map((_, i) => matchers.slice(0, i + 1));
14
- for (const permutation of permutations) {
15
- const chain = permutation.map(ast_1.getStringValue).join('.');
16
- if (chain in restrictedChains) {
17
- const message = restrictedChains[chain];
18
- context.report({
19
- messageId: message ? 'restrictedWithMessage' : 'restricted',
20
- data: { message: message ?? '', chain },
21
- loc: {
22
- start: permutation[0].loc.start,
23
- end: permutation[permutation.length - 1].loc.end,
24
- },
25
- });
26
- break;
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 `{{chain}}` is disallowed',
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 (0, ast_1.isTest)(node) || (0, ast_1.isDescribeCall)(node)
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
- MemberExpression(node) {
15
- if (node.object.type === 'MemberExpression' &&
16
- node.object.object.type === 'CallExpression' &&
17
- (0, ast_1.isExpectCall)(node.object.object) &&
18
- (0, ast_1.isPropertyAccessor)(node.object, 'not')) {
19
- const matcher = (0, ast_1.getStringValue)(node.property);
20
- if (matcher && matcher in matcherMap) {
21
- const { property } = node.object;
22
- context.report({
23
- fix: (fixer) => [
24
- fixer.removeRange([
25
- property.range[0] - getRangeOffset(property),
26
- property.range[1] + 1,
27
- ]),
28
- fixer.replaceTextRange([
29
- node.property.range[0] + getRangeOffset(node.property),
30
- node.property.range[1] - getRangeOffset(node.property),
31
- ], matcherMap[matcher]),
32
- ],
33
- messageId: 'noUselessNot',
34
- node: node,
35
- data: {
36
- old: matcher,
37
- new: matcherMap[matcher],
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.isCalleeObject)(node, 'page') &&
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 matchers = new Set(['toBe', 'toEqual', 'toStrictEqual']);
5
- const getRangeOffset = (node) => node.type === 'Identifier' ? 0 : 1;
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
- if (!(0, ast_1.isExpectCall)(node)) {
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
- const [matcher] = (0, ast_1.getMatchers)(node).slice(-1);
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.replaceTextRange([
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
- function isMatcherFound(node) {
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 result = isMatcherFound(node);
44
- if (!result.found) {
45
- context.report({ node: result.node, messageId: 'matcherNotFound' });
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.parseExpectCall = exports.isTestHook = exports.isTest = exports.findParent = exports.isDescribeCall = exports.isTestIdentifier = exports.isCalleeProperty = exports.isCalleeObject = exports.isPropertyAccessor = exports.isBooleanLiteral = exports.isStringLiteral = exports.isIdentifier = exports.getStringValue = void 0;
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' && node.name === name;
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 parseExpectCall(node) {
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.parseExpectCall = parseExpectCall;
104
+ exports.getExpectType = getExpectType;
110
105
  function isExpectCall(node) {
111
- return !!parseExpectCall(node);
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, [...chain, node.parent.property]);
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.11.2",
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",