eslint-plugin-playwright 2.2.2 → 2.4.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.
Files changed (3) hide show
  1. package/README.md +5 -4
  2. package/dist/index.cjs +100 -59
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -38,19 +38,19 @@ file patterns.
38
38
  (**eslint.config.js**)
39
39
 
40
40
  ```javascript
41
+ import { defineConfig } from '@eslint/config'
41
42
  import playwright from 'eslint-plugin-playwright'
42
43
 
43
- export default [
44
+ export default defineConfig([
44
45
  {
45
- ...playwright.configs['flat/recommended'],
46
46
  files: ['tests/**'],
47
+ extends: [playwright.configs['flat/recommended']],
47
48
  rules: {
48
- ...playwright.configs['flat/recommended'].rules,
49
49
  // Customize Playwright rules
50
50
  // ...
51
51
  },
52
52
  },
53
- ]
53
+ ])
54
54
  ```
55
55
 
56
56
  [Legacy config](https://eslint.org/docs/latest/use/configure/configuration-files)
@@ -142,6 +142,7 @@ CLI option\
142
142
  | [no-slowed-test](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-slowed-test.md) | Disallow usage of the `.slow` annotation | | | 💡 |
143
143
  | [no-standalone-expect](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-standalone-expect.md) | Disallow using expect outside of `test` blocks | ✅ | | |
144
144
  | [no-unsafe-references](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-unsafe-references.md) | Prevent unsafe variable references in `page.evaluate()` | ✅ | 🔧 | |
145
+ | [no-unused-locators](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-unused-locators.md) | Disallow usage of page locators that are not used | ✅ | | |
145
146
  | [no-useless-await](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-useless-await.md) | Disallow unnecessary `await`s for Playwright methods | ✅ | 🔧 | |
146
147
  | [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 | ✅ | 🔧 | |
147
148
  | [no-wait-for-navigation](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-wait-for-navigation.md) | Disallow usage of `page.waitForNavigation()` | ✅ | | 💡 |
package/dist/index.cjs CHANGED
@@ -21,28 +21,6 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
21
21
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
22
22
  mod
23
23
  ));
24
- var __accessCheck = (obj, member, msg) => {
25
- if (!member.has(obj))
26
- throw TypeError("Cannot " + msg);
27
- };
28
- var __privateGet = (obj, member, getter) => {
29
- __accessCheck(obj, member, "read from private field");
30
- return getter ? getter.call(obj) : member.get(obj);
31
- };
32
- var __privateAdd = (obj, member, value) => {
33
- if (member.has(obj))
34
- throw TypeError("Cannot add the same private member more than once");
35
- member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
36
- };
37
- var __privateSet = (obj, member, value, setter) => {
38
- __accessCheck(obj, member, "write to private field");
39
- setter ? setter.call(obj, value) : member.set(obj, value);
40
- return value;
41
- };
42
- var __privateMethod = (obj, member, method) => {
43
- __accessCheck(obj, member, "access private method");
44
- return method;
45
- };
46
24
 
47
25
  // src/index.ts
48
26
  var import_globals = __toESM(require("globals"), 1);
@@ -106,43 +84,38 @@ var VALID_CHAINS = /* @__PURE__ */ new Set([
106
84
  ]);
107
85
  var joinChains = (a, b) => a && b ? [...a, ...b] : null;
108
86
  var isSupportedAccessor = (node, value) => isIdentifier(node, value) || isStringNode(node, value);
109
- var _nodes, _leaves, _buildChain, buildChain_fn;
110
87
  var Chain = class {
88
+ #nodes = null;
89
+ #leaves = /* @__PURE__ */ new WeakSet();
111
90
  constructor(node) {
112
- __privateAdd(this, _buildChain);
113
- __privateAdd(this, _nodes, null);
114
- __privateAdd(this, _leaves, /* @__PURE__ */ new WeakSet());
115
- __privateSet(this, _nodes, __privateMethod(this, _buildChain, buildChain_fn).call(this, node));
91
+ this.#nodes = this.#buildChain(node);
116
92
  }
117
93
  isLeaf(node) {
118
- return __privateGet(this, _leaves).has(node);
94
+ return this.#leaves.has(node);
119
95
  }
120
96
  get nodes() {
121
- return __privateGet(this, _nodes);
97
+ return this.#nodes;
122
98
  }
123
- };
124
- _nodes = new WeakMap();
125
- _leaves = new WeakMap();
126
- _buildChain = new WeakSet();
127
- buildChain_fn = function(node, insideCall = false) {
128
- if (isSupportedAccessor(node)) {
129
- if (insideCall) {
130
- __privateGet(this, _leaves).add(node);
99
+ #buildChain(node, insideCall = false) {
100
+ if (isSupportedAccessor(node)) {
101
+ if (insideCall) {
102
+ this.#leaves.add(node);
103
+ }
104
+ return [node];
105
+ }
106
+ switch (node.type) {
107
+ case "TaggedTemplateExpression":
108
+ return this.#buildChain(node.tag);
109
+ case "MemberExpression":
110
+ return joinChains(
111
+ this.#buildChain(node.object),
112
+ this.#buildChain(node.property, insideCall)
113
+ );
114
+ case "CallExpression":
115
+ return this.#buildChain(node.callee, true);
116
+ default:
117
+ return null;
131
118
  }
132
- return [node];
133
- }
134
- switch (node.type) {
135
- case "TaggedTemplateExpression":
136
- return __privateMethod(this, _buildChain, buildChain_fn).call(this, node.tag);
137
- case "MemberExpression":
138
- return joinChains(
139
- __privateMethod(this, _buildChain, buildChain_fn).call(this, node.object),
140
- __privateMethod(this, _buildChain, buildChain_fn).call(this, node.property, insideCall)
141
- );
142
- case "CallExpression":
143
- return __privateMethod(this, _buildChain, buildChain_fn).call(this, node.callee, true);
144
- default:
145
- return null;
146
119
  }
147
120
  };
148
121
  var resolvePossibleAliasedGlobal = (context, global) => {
@@ -462,8 +435,12 @@ var expect_expect_default = createRule({
462
435
  create(context) {
463
436
  const options = {
464
437
  assertFunctionNames: [],
438
+ assertFunctionPatterns: [],
465
439
  ...context.options?.[0] ?? {}
466
440
  };
441
+ const patterns = options.assertFunctionPatterns.map(
442
+ (pattern) => new RegExp(pattern)
443
+ );
467
444
  const unchecked = [];
468
445
  function checkExpressions(nodes) {
469
446
  for (const node of nodes) {
@@ -474,12 +451,21 @@ var expect_expect_default = createRule({
474
451
  }
475
452
  }
476
453
  }
454
+ function matches(node) {
455
+ if (options.assertFunctionNames.some((name) => dig(node.callee, name))) {
456
+ return true;
457
+ }
458
+ if (patterns.some((pattern) => dig(node.callee, pattern))) {
459
+ return true;
460
+ }
461
+ return false;
462
+ }
477
463
  return {
478
464
  CallExpression(node) {
479
465
  const call = parseFnCall(context, node);
480
466
  if (call?.type === "test") {
481
467
  unchecked.push(node);
482
- } else if (call?.type === "expect" || options.assertFunctionNames.find((name) => dig(node.callee, name))) {
468
+ } else if (call?.type === "expect" || matches(node)) {
483
469
  const ancestors = context.sourceCode.getAncestors(node);
484
470
  checkExpressions(ancestors);
485
471
  }
@@ -508,6 +494,10 @@ var expect_expect_default = createRule({
508
494
  assertFunctionNames: {
509
495
  items: [{ type: "string" }],
510
496
  type: "array"
497
+ },
498
+ assertFunctionPatterns: {
499
+ items: [{ type: "string" }],
500
+ type: "array"
511
501
  }
512
502
  },
513
503
  type: "object"
@@ -720,14 +710,17 @@ var missing_playwright_await_default = createRule({
720
710
  // Add any custom matchers to the set
721
711
  ...options.customMatchers || []
722
712
  ]);
723
- function checkValidity(node) {
713
+ function checkValidity(node, visited) {
724
714
  const parent = getParent(node);
725
715
  if (!parent)
726
716
  return false;
717
+ if (visited.has(parent))
718
+ return false;
719
+ visited.add(parent);
727
720
  if (validTypes.has(parent.type))
728
721
  return true;
729
722
  if (parent.type === "ArrayExpression") {
730
- return checkValidity(parent);
723
+ return checkValidity(parent, visited);
731
724
  }
732
725
  if (parent.type === "CallExpression" && parent.callee.type === "MemberExpression" && isIdentifier(parent.callee.object, "Promise") && isIdentifier(parent.callee.property, "all")) {
733
726
  return true;
@@ -736,9 +729,11 @@ var missing_playwright_await_default = createRule({
736
729
  const scope = context.sourceCode.getScope(parent.parent);
737
730
  for (const ref of scope.references) {
738
731
  const refParent = ref.identifier.parent;
732
+ if (visited.has(refParent))
733
+ continue;
739
734
  if (validTypes.has(refParent.type))
740
735
  return true;
741
- if (checkValidity(refParent))
736
+ if (checkValidity(refParent, visited))
742
737
  return true;
743
738
  }
744
739
  }
@@ -750,7 +745,7 @@ var missing_playwright_await_default = createRule({
750
745
  if (call?.type !== "step" && call?.type !== "expect")
751
746
  return;
752
747
  const result = getCallType(call, awaitableMatchers);
753
- const isValid = result ? checkValidity(node) : false;
748
+ const isValid = result ? checkValidity(node, /* @__PURE__ */ new Set()) : false;
754
749
  if (result && !isValid) {
755
750
  context.report({
756
751
  data: result.data,
@@ -982,7 +977,7 @@ var no_duplicate_hooks_default = createRule({
982
977
  }
983
978
  const currentLayer = hookContexts[hookContexts.length - 1];
984
979
  const name = node.callee.type === "MemberExpression" ? getStringValue(node.callee.property) : "";
985
- currentLayer[name] || (currentLayer[name] = 0);
980
+ currentLayer[name] ||= 0;
986
981
  currentLayer[name] += 1;
987
982
  if (currentLayer[name] > 1) {
988
983
  context.report({
@@ -1729,13 +1724,16 @@ var no_standalone_expect_default = createRule({
1729
1724
  if (call?.type === "hook") {
1730
1725
  callStack.push("hook");
1731
1726
  }
1727
+ if (node.callee.type === "MemberExpression" && isPropertyAccessor(node.callee, "extend")) {
1728
+ callStack.push("fixture");
1729
+ }
1732
1730
  if (node.callee.type === "TaggedTemplateExpression") {
1733
1731
  callStack.push("template");
1734
1732
  }
1735
1733
  },
1736
1734
  "CallExpression:exit"(node) {
1737
1735
  const top = callStack.at(-1);
1738
- if (top === "test" && isTypeOfFnCall(context, node, ["test"]) && node.callee.type !== "MemberExpression" || top === "template" && node.callee.type === "TaggedTemplateExpression") {
1736
+ if (top === "test" && isTypeOfFnCall(context, node, ["test"]) && node.callee.type !== "MemberExpression" || top === "template" && node.callee.type === "TaggedTemplateExpression" || top === "fixture" && node.callee.type === "MemberExpression" && isPropertyAccessor(node.callee, "extend")) {
1739
1737
  callStack.pop();
1740
1738
  }
1741
1739
  }
@@ -1856,6 +1854,35 @@ var no_unsafe_references_default = createRule({
1856
1854
  }
1857
1855
  });
1858
1856
 
1857
+ // src/rules/no-unused-locators.ts
1858
+ var LOCATOR_REGEX = /locator|getBy(Role|Text|Label|Placeholder|AltText|Title|TestId)/;
1859
+ var no_unused_locators_default = createRule({
1860
+ create(context) {
1861
+ return {
1862
+ CallExpression(node) {
1863
+ if (!isPageMethod(node, LOCATOR_REGEX)) {
1864
+ return;
1865
+ }
1866
+ if (node.parent.type === "ExpressionStatement" || node.parent.type === "AwaitExpression") {
1867
+ context.report({ messageId: "noUnusedLocator", node });
1868
+ }
1869
+ }
1870
+ };
1871
+ },
1872
+ meta: {
1873
+ docs: {
1874
+ category: "Possible Errors",
1875
+ description: `Disallow usage of page locators that are not used`,
1876
+ recommended: true,
1877
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-unused-locators.md"
1878
+ },
1879
+ messages: {
1880
+ noUnusedLocator: "Unused locator"
1881
+ },
1882
+ type: "problem"
1883
+ }
1884
+ });
1885
+
1859
1886
  // src/rules/no-useless-await.ts
1860
1887
  var locatorMethods = /* @__PURE__ */ new Set([
1861
1888
  "and",
@@ -3857,6 +3884,9 @@ var valid_test_tags_default = createRule({
3857
3884
  );
3858
3885
  }
3859
3886
  }
3887
+ const extractTagsFromTitle = (title) => {
3888
+ return title.match(/@[\S]+/g) || [];
3889
+ };
3860
3890
  const validateTag = (tag, node) => {
3861
3891
  if (!tag.startsWith("@")) {
3862
3892
  context.report({
@@ -3899,6 +3929,15 @@ var valid_test_tags_default = createRule({
3899
3929
  const { type } = call;
3900
3930
  if (type !== "test" && type !== "describe" && type !== "step")
3901
3931
  return;
3932
+ if (node.arguments.length > 0) {
3933
+ const titleArg = node.arguments[0];
3934
+ if (titleArg && titleArg.type === "Literal" && typeof titleArg.value === "string") {
3935
+ const titleTags = extractTagsFromTitle(titleArg.value);
3936
+ for (const tag of titleTags) {
3937
+ validateTag(tag, node);
3938
+ }
3939
+ }
3940
+ }
3902
3941
  if (node.arguments.length < 2)
3903
3942
  return;
3904
3943
  const optionsArg = node.arguments[1];
@@ -3938,7 +3977,7 @@ var valid_test_tags_default = createRule({
3938
3977
  },
3939
3978
  meta: {
3940
3979
  docs: {
3941
- description: "Enforce valid tag format in Playwright test blocks",
3980
+ description: "Enforce valid tag format in Playwright test blocks and titles",
3942
3981
  recommended: true
3943
3982
  },
3944
3983
  messages: {
@@ -4231,6 +4270,7 @@ var index = {
4231
4270
  "no-slowed-test": no_slowed_test_default,
4232
4271
  "no-standalone-expect": no_standalone_expect_default,
4233
4272
  "no-unsafe-references": no_unsafe_references_default,
4273
+ "no-unused-locators": no_unused_locators_default,
4234
4274
  "no-useless-await": no_useless_await_default,
4235
4275
  "no-useless-not": no_useless_not_default,
4236
4276
  "no-wait-for-navigation": no_wait_for_navigation_default,
@@ -4278,6 +4318,7 @@ var sharedConfig = {
4278
4318
  "playwright/no-skipped-test": "warn",
4279
4319
  "playwright/no-standalone-expect": "error",
4280
4320
  "playwright/no-unsafe-references": "error",
4321
+ "playwright/no-unused-locators": "error",
4281
4322
  "playwright/no-useless-await": "warn",
4282
4323
  "playwright/no-useless-not": "warn",
4283
4324
  "playwright/no-wait-for-navigation": "error",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "eslint-plugin-playwright",
3
3
  "description": "ESLint plugin for Playwright testing.",
4
- "version": "2.2.2",
4
+ "version": "2.4.0",
5
5
  "repository": "https://github.com/playwright-community/eslint-plugin-playwright",
6
6
  "author": "Mark Skelton <mark@mskelton.dev>",
7
7
  "contributors": [
@@ -9,7 +9,7 @@
9
9
  ],
10
10
  "license": "MIT",
11
11
  "engines": {
12
- "node": ">=16.6.0"
12
+ "node": ">=16.9.0"
13
13
  },
14
14
  "type": "module",
15
15
  "types": "./index.d.ts",
@@ -30,6 +30,6 @@
30
30
  "eslint": ">=8.40.0"
31
31
  },
32
32
  "dependencies": {
33
- "globals": "^13.23.0"
33
+ "globals": "^16.4.0"
34
34
  }
35
35
  }