eslint-plugin-playwright 1.0.0 → 1.1.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
@@ -131,6 +131,7 @@ command line option.\
131
131
  | ✔ | | | [no-networkidle](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-networkidle.md) | Disallow usage of the `networkidle` option |
132
132
  | | | | [no-nth-methods](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-nth-methods.md) | Disallow usage of `first()`, `last()`, and `nth()` methods |
133
133
  | ✔ | | | [no-page-pause](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-page-pause.md) | Disallow using `page.pause()` |
134
+ | ✔ | 🔧 | | [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()` |
134
135
  | | 🔧 | | [no-get-by-title](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-get-by-title.md) | Disallow using `getByTitle()` |
135
136
  | | | | [no-raw-locators](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-raw-locators.md) | Disallow using raw locators |
136
137
  | ✔ | 🔧 | | [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 |
package/dist/index.d.mts CHANGED
@@ -18,6 +18,7 @@ declare const _default: {
18
18
  'no-eval': eslint.Rule.RuleModule;
19
19
  'no-focused-test': eslint.Rule.RuleModule;
20
20
  'no-force-option': eslint.Rule.RuleModule;
21
+ 'no-get-by-title': eslint.Rule.RuleModule;
21
22
  'no-nested-step': eslint.Rule.RuleModule;
22
23
  'no-networkidle': eslint.Rule.RuleModule;
23
24
  'no-nth-methods': eslint.Rule.RuleModule;
@@ -25,6 +26,7 @@ declare const _default: {
25
26
  'no-raw-locators': eslint.Rule.RuleModule;
26
27
  'no-restricted-matchers': eslint.Rule.RuleModule;
27
28
  'no-skipped-test': eslint.Rule.RuleModule;
29
+ 'no-unsafe-references': eslint.Rule.RuleModule;
28
30
  'no-useless-await': eslint.Rule.RuleModule;
29
31
  'no-useless-not': eslint.Rule.RuleModule;
30
32
  'no-wait-for-selector': eslint.Rule.RuleModule;
@@ -67,6 +69,7 @@ declare const _default: {
67
69
  'no-eval': eslint.Rule.RuleModule;
68
70
  'no-focused-test': eslint.Rule.RuleModule;
69
71
  'no-force-option': eslint.Rule.RuleModule;
72
+ 'no-get-by-title': eslint.Rule.RuleModule;
70
73
  'no-nested-step': eslint.Rule.RuleModule;
71
74
  'no-networkidle': eslint.Rule.RuleModule;
72
75
  'no-nth-methods': eslint.Rule.RuleModule;
@@ -74,6 +77,7 @@ declare const _default: {
74
77
  'no-raw-locators': eslint.Rule.RuleModule;
75
78
  'no-restricted-matchers': eslint.Rule.RuleModule;
76
79
  'no-skipped-test': eslint.Rule.RuleModule;
80
+ 'no-unsafe-references': eslint.Rule.RuleModule;
77
81
  'no-useless-await': eslint.Rule.RuleModule;
78
82
  'no-useless-not': eslint.Rule.RuleModule;
79
83
  'no-wait-for-selector': eslint.Rule.RuleModule;
@@ -106,6 +110,7 @@ declare const _default: {
106
110
  'playwright/no-networkidle': string;
107
111
  'playwright/no-page-pause': string;
108
112
  'playwright/no-skipped-test': string;
113
+ 'playwright/no-unsafe-references': string;
109
114
  'playwright/no-useless-await': string;
110
115
  'playwright/no-useless-not': string;
111
116
  'playwright/no-wait-for-selector': string;
@@ -156,6 +161,7 @@ declare const _default: {
156
161
  'playwright/no-networkidle': string;
157
162
  'playwright/no-page-pause': string;
158
163
  'playwright/no-skipped-test': string;
164
+ 'playwright/no-unsafe-references': string;
159
165
  'playwright/no-useless-await': string;
160
166
  'playwright/no-useless-not': string;
161
167
  'playwright/no-wait-for-selector': string;
@@ -184,6 +190,7 @@ declare const _default: {
184
190
  'playwright/no-networkidle': string;
185
191
  'playwright/no-page-pause': string;
186
192
  'playwright/no-skipped-test': string;
193
+ 'playwright/no-unsafe-references': string;
187
194
  'playwright/no-useless-await': string;
188
195
  'playwright/no-useless-not': string;
189
196
  'playwright/no-wait-for-selector': string;
@@ -203,6 +210,7 @@ declare const _default: {
203
210
  'no-eval': eslint.Rule.RuleModule;
204
211
  'no-focused-test': eslint.Rule.RuleModule;
205
212
  'no-force-option': eslint.Rule.RuleModule;
213
+ 'no-get-by-title': eslint.Rule.RuleModule;
206
214
  'no-nested-step': eslint.Rule.RuleModule;
207
215
  'no-networkidle': eslint.Rule.RuleModule;
208
216
  'no-nth-methods': eslint.Rule.RuleModule;
@@ -210,6 +218,7 @@ declare const _default: {
210
218
  'no-raw-locators': eslint.Rule.RuleModule;
211
219
  'no-restricted-matchers': eslint.Rule.RuleModule;
212
220
  'no-skipped-test': eslint.Rule.RuleModule;
221
+ 'no-unsafe-references': eslint.Rule.RuleModule;
213
222
  'no-useless-await': eslint.Rule.RuleModule;
214
223
  'no-useless-not': eslint.Rule.RuleModule;
215
224
  'no-wait-for-selector': eslint.Rule.RuleModule;
package/dist/index.d.ts CHANGED
@@ -18,6 +18,7 @@ declare const _default: {
18
18
  'no-eval': eslint.Rule.RuleModule;
19
19
  'no-focused-test': eslint.Rule.RuleModule;
20
20
  'no-force-option': eslint.Rule.RuleModule;
21
+ 'no-get-by-title': eslint.Rule.RuleModule;
21
22
  'no-nested-step': eslint.Rule.RuleModule;
22
23
  'no-networkidle': eslint.Rule.RuleModule;
23
24
  'no-nth-methods': eslint.Rule.RuleModule;
@@ -25,6 +26,7 @@ declare const _default: {
25
26
  'no-raw-locators': eslint.Rule.RuleModule;
26
27
  'no-restricted-matchers': eslint.Rule.RuleModule;
27
28
  'no-skipped-test': eslint.Rule.RuleModule;
29
+ 'no-unsafe-references': eslint.Rule.RuleModule;
28
30
  'no-useless-await': eslint.Rule.RuleModule;
29
31
  'no-useless-not': eslint.Rule.RuleModule;
30
32
  'no-wait-for-selector': eslint.Rule.RuleModule;
@@ -67,6 +69,7 @@ declare const _default: {
67
69
  'no-eval': eslint.Rule.RuleModule;
68
70
  'no-focused-test': eslint.Rule.RuleModule;
69
71
  'no-force-option': eslint.Rule.RuleModule;
72
+ 'no-get-by-title': eslint.Rule.RuleModule;
70
73
  'no-nested-step': eslint.Rule.RuleModule;
71
74
  'no-networkidle': eslint.Rule.RuleModule;
72
75
  'no-nth-methods': eslint.Rule.RuleModule;
@@ -74,6 +77,7 @@ declare const _default: {
74
77
  'no-raw-locators': eslint.Rule.RuleModule;
75
78
  'no-restricted-matchers': eslint.Rule.RuleModule;
76
79
  'no-skipped-test': eslint.Rule.RuleModule;
80
+ 'no-unsafe-references': eslint.Rule.RuleModule;
77
81
  'no-useless-await': eslint.Rule.RuleModule;
78
82
  'no-useless-not': eslint.Rule.RuleModule;
79
83
  'no-wait-for-selector': eslint.Rule.RuleModule;
@@ -106,6 +110,7 @@ declare const _default: {
106
110
  'playwright/no-networkidle': string;
107
111
  'playwright/no-page-pause': string;
108
112
  'playwright/no-skipped-test': string;
113
+ 'playwright/no-unsafe-references': string;
109
114
  'playwright/no-useless-await': string;
110
115
  'playwright/no-useless-not': string;
111
116
  'playwright/no-wait-for-selector': string;
@@ -156,6 +161,7 @@ declare const _default: {
156
161
  'playwright/no-networkidle': string;
157
162
  'playwright/no-page-pause': string;
158
163
  'playwright/no-skipped-test': string;
164
+ 'playwright/no-unsafe-references': string;
159
165
  'playwright/no-useless-await': string;
160
166
  'playwright/no-useless-not': string;
161
167
  'playwright/no-wait-for-selector': string;
@@ -184,6 +190,7 @@ declare const _default: {
184
190
  'playwright/no-networkidle': string;
185
191
  'playwright/no-page-pause': string;
186
192
  'playwright/no-skipped-test': string;
193
+ 'playwright/no-unsafe-references': string;
187
194
  'playwright/no-useless-await': string;
188
195
  'playwright/no-useless-not': string;
189
196
  'playwright/no-wait-for-selector': string;
@@ -203,6 +210,7 @@ declare const _default: {
203
210
  'no-eval': eslint.Rule.RuleModule;
204
211
  'no-focused-test': eslint.Rule.RuleModule;
205
212
  'no-force-option': eslint.Rule.RuleModule;
213
+ 'no-get-by-title': eslint.Rule.RuleModule;
206
214
  'no-nested-step': eslint.Rule.RuleModule;
207
215
  'no-networkidle': eslint.Rule.RuleModule;
208
216
  'no-nth-methods': eslint.Rule.RuleModule;
@@ -210,6 +218,7 @@ declare const _default: {
210
218
  'no-raw-locators': eslint.Rule.RuleModule;
211
219
  'no-restricted-matchers': eslint.Rule.RuleModule;
212
220
  'no-skipped-test': eslint.Rule.RuleModule;
221
+ 'no-unsafe-references': eslint.Rule.RuleModule;
213
222
  'no-useless-await': eslint.Rule.RuleModule;
214
223
  'no-useless-not': eslint.Rule.RuleModule;
215
224
  'no-wait-for-selector': eslint.Rule.RuleModule;
package/dist/index.js CHANGED
@@ -123,6 +123,19 @@ function dig(node, identifier) {
123
123
  function isPageMethod(node, name) {
124
124
  return node.callee.type === "MemberExpression" && dig(node.callee.object, /(^(page|frame)|(Page|Frame)$)/) && isPropertyAccessor(node.callee, name);
125
125
  }
126
+ function isFunction(node) {
127
+ return node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression";
128
+ }
129
+
130
+ // src/utils/misc.ts
131
+ var getAmountData = (amount) => ({
132
+ amount: amount.toString(),
133
+ s: amount === 1 ? "" : "s"
134
+ });
135
+ function getSourceCode(context) {
136
+ return context.sourceCode ?? context.getSourceCode();
137
+ }
138
+ var truthy = Boolean;
126
139
 
127
140
  // src/rules/expect-expect.ts
128
141
  function isAssertionCall(context, node, assertFunctionNames) {
@@ -134,7 +147,7 @@ var expect_expect_default = {
134
147
  assertFunctionNames: [],
135
148
  ...context.options?.[0] ?? {}
136
149
  };
137
- const sourceCode = context.sourceCode ?? context.getSourceCode();
150
+ const sourceCode = getSourceCode(context);
138
151
  const unchecked = [];
139
152
  function checkExpressions(nodes) {
140
153
  for (const node of nodes) {
@@ -323,7 +336,7 @@ function getCallType(context, node, awaitableMatchers) {
323
336
  }
324
337
  var missing_playwright_await_default = {
325
338
  create(context) {
326
- const sourceCode = context.sourceCode ?? context.getSourceCode();
339
+ const sourceCode = getSourceCode(context);
327
340
  const options = context.options[0] || {};
328
341
  const awaitableMatchers = /* @__PURE__ */ new Set([
329
342
  ...expectPlaywrightMatchers,
@@ -556,7 +569,7 @@ var no_focused_test_default = {
556
569
 
557
570
  // src/rules/no-force-option.ts
558
571
  function isForceOptionEnabled(node) {
559
- const arg = node.arguments[node.arguments.length - 1];
572
+ const arg = node.arguments.at(-1);
560
573
  return arg?.type === "ObjectExpression" && arg.properties.find(
561
574
  (property) => property.type === "Property" && getStringValue(property.key) === "force" && isBooleanLiteral(property.value, true)
562
575
  );
@@ -601,6 +614,31 @@ var no_force_option_default = {
601
614
  }
602
615
  };
603
616
 
617
+ // src/rules/no-get-by-title.ts
618
+ var no_get_by_title_default = {
619
+ create(context) {
620
+ return {
621
+ CallExpression(node) {
622
+ if (isPageMethod(node, "getByTitle")) {
623
+ context.report({ messageId: "noGetByTitle", node });
624
+ }
625
+ }
626
+ };
627
+ },
628
+ meta: {
629
+ docs: {
630
+ category: "Best Practices",
631
+ description: "Disallows the usage of getByTitle()",
632
+ recommended: false,
633
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-get-by-title.md"
634
+ },
635
+ messages: {
636
+ noGetByTitle: "The HTML title attribute is not an accessible name. Prefer getByRole() or getByLabelText() instead."
637
+ },
638
+ type: "suggestion"
639
+ }
640
+ };
641
+
604
642
  // src/rules/no-nested-step.ts
605
643
  function isStepCall(node) {
606
644
  const inner = node.type === "CallExpression" ? node.callee : node;
@@ -876,7 +914,7 @@ var no_restricted_matchers_default = {
876
914
  context.report({
877
915
  data: { message: message ?? "", restriction },
878
916
  loc: {
879
- end: chain[chain.length - 1].loc.end,
917
+ end: chain.at(-1).loc.end,
880
918
  start: chain[0].loc.start
881
919
  },
882
920
  messageId: message ? "restrictedWithMessage" : "restricted"
@@ -968,6 +1006,93 @@ var no_skipped_test_default = {
968
1006
  }
969
1007
  };
970
1008
 
1009
+ // src/rules/no-unsafe-references.ts
1010
+ function collectVariables(scope) {
1011
+ if (!scope)
1012
+ return [];
1013
+ return [
1014
+ ...collectVariables(scope.upper),
1015
+ ...scope.variables.map((ref) => ref.name)
1016
+ ];
1017
+ }
1018
+ function addArgument(fixer, node, refs) {
1019
+ if (!node.arguments.length)
1020
+ return;
1021
+ if (node.arguments.length === 1) {
1022
+ return fixer.insertTextAfter(node.arguments[0], `, [${refs}]`);
1023
+ }
1024
+ const arr = node.arguments.at(-1);
1025
+ if (!arr || arr.type !== "ArrayExpression")
1026
+ return;
1027
+ const lastItem = arr.elements.at(-1);
1028
+ return lastItem ? fixer.insertTextAfter(lastItem, `, ${refs}`) : fixer.replaceText(arr, `[${refs}]`);
1029
+ }
1030
+ function getParen(sourceCode, node) {
1031
+ let token = sourceCode.getFirstToken(node);
1032
+ while (token && token.value !== "(") {
1033
+ token = sourceCode.getTokenAfter(token);
1034
+ }
1035
+ return token;
1036
+ }
1037
+ function addParam(sourceCode, fixer, node, refs) {
1038
+ const lastParam = node.params.at(-1);
1039
+ if (lastParam) {
1040
+ return fixer.insertTextAfter(lastParam, `, ${refs}`);
1041
+ }
1042
+ const token = getParen(sourceCode, node);
1043
+ return token ? fixer.insertTextAfter(token, `[${refs}]`) : null;
1044
+ }
1045
+ var no_unsafe_references_default = {
1046
+ create(context) {
1047
+ return {
1048
+ CallExpression(node) {
1049
+ if (!isPageMethod(node, "evaluate"))
1050
+ return;
1051
+ const [fn] = node.arguments;
1052
+ if (!fn || !isFunction(fn))
1053
+ return;
1054
+ const sourceCode = getSourceCode(context);
1055
+ const { through, upper } = sourceCode.getScope(fn.body);
1056
+ const allRefs = new Set(collectVariables(upper));
1057
+ through.filter((ref) => allRefs.has(ref.identifier.name)).forEach((ref, i, arr) => {
1058
+ const descriptor = {
1059
+ data: { variable: ref.identifier.name },
1060
+ messageId: "noUnsafeReference",
1061
+ node: ref.identifier
1062
+ };
1063
+ if (i !== 0) {
1064
+ context.report(descriptor);
1065
+ return;
1066
+ }
1067
+ context.report({
1068
+ ...descriptor,
1069
+ fix(fixer) {
1070
+ const refs = arr.map((ref2) => ref2.identifier.name).join(", ");
1071
+ return [
1072
+ addArgument(fixer, node, refs),
1073
+ addParam(sourceCode, fixer, fn, refs)
1074
+ ].filter(truthy);
1075
+ }
1076
+ });
1077
+ });
1078
+ }
1079
+ };
1080
+ },
1081
+ meta: {
1082
+ docs: {
1083
+ category: "Possible Errors",
1084
+ description: "Prevent unsafe variable references in page.evaluate()",
1085
+ recommended: true,
1086
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-unsafe-references.md"
1087
+ },
1088
+ fixable: "code",
1089
+ messages: {
1090
+ noUnsafeReference: 'Unsafe reference to variable "{{ variable }}" in page.evaluate()'
1091
+ },
1092
+ type: "problem"
1093
+ }
1094
+ };
1095
+
971
1096
  // src/rules/no-useless-await.ts
972
1097
  var locatorMethods = /* @__PURE__ */ new Set([
973
1098
  "and",
@@ -1440,7 +1565,7 @@ var prefer_to_contain_default = {
1440
1565
  );
1441
1566
  context.report({
1442
1567
  fix(fixer) {
1443
- const sourceCode = context.sourceCode ?? context.getSourceCode();
1568
+ const sourceCode = getSourceCode(context);
1444
1569
  const addNotModifier = matcherArg.type === "Literal" && matcherArg.value === !!notModifier;
1445
1570
  const fixes = [
1446
1571
  // remove the "includes" call entirely
@@ -1636,6 +1761,19 @@ var supportedMatchers = /* @__PURE__ */ new Set([
1636
1761
  "toBeTruthy",
1637
1762
  "toBeFalsy"
1638
1763
  ]);
1764
+ function dereference(context, node) {
1765
+ if (node.type !== "Identifier") {
1766
+ return node;
1767
+ }
1768
+ const sourceCode = getSourceCode(context);
1769
+ const scope = sourceCode.getScope(node);
1770
+ for (const ref of scope.references) {
1771
+ const refParent = ref.identifier.parent;
1772
+ if (refParent.type === "VariableDeclarator") {
1773
+ return refParent.init;
1774
+ }
1775
+ }
1776
+ }
1639
1777
  var prefer_web_first_assertions_default = {
1640
1778
  create(context) {
1641
1779
  return {
@@ -1643,8 +1781,8 @@ var prefer_web_first_assertions_default = {
1643
1781
  const expectCall = parseExpectCall(context, node);
1644
1782
  if (!expectCall)
1645
1783
  return;
1646
- const [arg] = node.arguments;
1647
- if (arg.type !== "AwaitExpression" || arg.argument.type !== "CallExpression" || arg.argument.callee.type !== "MemberExpression") {
1784
+ const arg = dereference(context, node.arguments[0]);
1785
+ if (!arg || arg.type !== "AwaitExpression" || arg.argument.type !== "CallExpression" || arg.argument.callee.type !== "MemberExpression") {
1648
1786
  return;
1649
1787
  }
1650
1788
  if (!supportedMatchers.has(expectCall.matcherName))
@@ -1668,7 +1806,7 @@ var prefer_web_first_assertions_default = {
1668
1806
  },
1669
1807
  fix: (fixer) => {
1670
1808
  const methodArgs = arg.argument.type === "CallExpression" ? arg.argument.arguments : [];
1671
- const methodEnd = methodArgs.length ? methodArgs[methodArgs.length - 1].range[1] + 1 : callee.property.range[1] + 2;
1809
+ const methodEnd = methodArgs.length ? methodArgs.at(-1).range[1] + 1 : callee.property.range[1] + 2;
1672
1810
  const fixes = [
1673
1811
  // Add await to the expect call
1674
1812
  fixer.insertTextBefore(node, "await "),
@@ -1766,12 +1904,6 @@ var require_soft_assertions_default = {
1766
1904
  }
1767
1905
  };
1768
1906
 
1769
- // src/utils/misc.ts
1770
- var getAmountData = (amount) => ({
1771
- amount: amount.toString(),
1772
- s: amount === 1 ? "" : "s"
1773
- });
1774
-
1775
1907
  // src/rules/require-top-level-describe.ts
1776
1908
  var require_top_level_describe_default = {
1777
1909
  create(context) {
@@ -2147,6 +2279,7 @@ var index = {
2147
2279
  "no-eval": no_eval_default,
2148
2280
  "no-focused-test": no_focused_test_default,
2149
2281
  "no-force-option": no_force_option_default,
2282
+ "no-get-by-title": no_get_by_title_default,
2150
2283
  "no-nested-step": no_nested_step_default,
2151
2284
  "no-networkidle": no_networkidle_default,
2152
2285
  "no-nth-methods": no_nth_methods_default,
@@ -2154,6 +2287,7 @@ var index = {
2154
2287
  "no-raw-locators": no_raw_locators_default,
2155
2288
  "no-restricted-matchers": no_restricted_matchers_default,
2156
2289
  "no-skipped-test": no_skipped_test_default,
2290
+ "no-unsafe-references": no_unsafe_references_default,
2157
2291
  "no-useless-await": no_useless_await_default,
2158
2292
  "no-useless-not": no_useless_not_default,
2159
2293
  "no-wait-for-selector": no_wait_for_selector_default,
@@ -2186,6 +2320,7 @@ var sharedConfig = {
2186
2320
  "playwright/no-networkidle": "error",
2187
2321
  "playwright/no-page-pause": "warn",
2188
2322
  "playwright/no-skipped-test": "warn",
2323
+ "playwright/no-unsafe-references": "error",
2189
2324
  "playwright/no-useless-await": "warn",
2190
2325
  "playwright/no-useless-not": "warn",
2191
2326
  "playwright/no-wait-for-selector": "warn",
package/dist/index.mjs CHANGED
@@ -93,6 +93,9 @@ function dig(node, identifier) {
93
93
  function isPageMethod(node, name) {
94
94
  return node.callee.type === "MemberExpression" && dig(node.callee.object, /(^(page|frame)|(Page|Frame)$)/) && isPropertyAccessor(node.callee, name);
95
95
  }
96
+ function isFunction(node) {
97
+ return node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression";
98
+ }
96
99
  var isTemplateLiteral, describeProperties, testHooks, expectSubCommands;
97
100
  var init_ast = __esm({
98
101
  "src/utils/ast.ts"() {
@@ -111,6 +114,22 @@ var init_ast = __esm({
111
114
  }
112
115
  });
113
116
 
117
+ // src/utils/misc.ts
118
+ function getSourceCode(context) {
119
+ return context.sourceCode ?? context.getSourceCode();
120
+ }
121
+ var getAmountData, truthy;
122
+ var init_misc = __esm({
123
+ "src/utils/misc.ts"() {
124
+ "use strict";
125
+ getAmountData = (amount) => ({
126
+ amount: amount.toString(),
127
+ s: amount === 1 ? "" : "s"
128
+ });
129
+ truthy = Boolean;
130
+ }
131
+ });
132
+
114
133
  // src/rules/expect-expect.ts
115
134
  function isAssertionCall(context, node, assertFunctionNames) {
116
135
  return isExpectCall(context, node) || assertFunctionNames.find((name) => dig(node.callee, name));
@@ -120,13 +139,14 @@ var init_expect_expect = __esm({
120
139
  "src/rules/expect-expect.ts"() {
121
140
  "use strict";
122
141
  init_ast();
142
+ init_misc();
123
143
  expect_expect_default = {
124
144
  create(context) {
125
145
  const options = {
126
146
  assertFunctionNames: [],
127
147
  ...context.options?.[0] ?? {}
128
148
  };
129
- const sourceCode = context.sourceCode ?? context.getSourceCode();
149
+ const sourceCode = getSourceCode(context);
130
150
  const unchecked = [];
131
151
  function checkExpressions(nodes) {
132
152
  for (const node of nodes) {
@@ -275,6 +295,7 @@ var init_missing_playwright_await = __esm({
275
295
  "src/rules/missing-playwright-await.ts"() {
276
296
  "use strict";
277
297
  init_ast();
298
+ init_misc();
278
299
  validTypes = /* @__PURE__ */ new Set([
279
300
  "AwaitExpression",
280
301
  "ReturnStatement",
@@ -329,7 +350,7 @@ var init_missing_playwright_await = __esm({
329
350
  ];
330
351
  missing_playwright_await_default = {
331
352
  create(context) {
332
- const sourceCode = context.sourceCode ?? context.getSourceCode();
353
+ const sourceCode = getSourceCode(context);
333
354
  const options = context.options[0] || {};
334
355
  const awaitableMatchers = /* @__PURE__ */ new Set([
335
356
  ...expectPlaywrightMatchers,
@@ -592,7 +613,7 @@ var init_no_focused_test = __esm({
592
613
 
593
614
  // src/rules/no-force-option.ts
594
615
  function isForceOptionEnabled(node) {
595
- const arg = node.arguments[node.arguments.length - 1];
616
+ const arg = node.arguments.at(-1);
596
617
  return arg?.type === "ObjectExpression" && arg.properties.find(
597
618
  (property) => property.type === "Property" && getStringValue(property.key) === "force" && isBooleanLiteral(property.value, true)
598
619
  );
@@ -644,6 +665,38 @@ var init_no_force_option = __esm({
644
665
  }
645
666
  });
646
667
 
668
+ // src/rules/no-get-by-title.ts
669
+ var no_get_by_title_default;
670
+ var init_no_get_by_title = __esm({
671
+ "src/rules/no-get-by-title.ts"() {
672
+ "use strict";
673
+ init_ast();
674
+ no_get_by_title_default = {
675
+ create(context) {
676
+ return {
677
+ CallExpression(node) {
678
+ if (isPageMethod(node, "getByTitle")) {
679
+ context.report({ messageId: "noGetByTitle", node });
680
+ }
681
+ }
682
+ };
683
+ },
684
+ meta: {
685
+ docs: {
686
+ category: "Best Practices",
687
+ description: "Disallows the usage of getByTitle()",
688
+ recommended: false,
689
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-get-by-title.md"
690
+ },
691
+ messages: {
692
+ noGetByTitle: "The HTML title attribute is not an accessible name. Prefer getByRole() or getByLabelText() instead."
693
+ },
694
+ type: "suggestion"
695
+ }
696
+ };
697
+ }
698
+ });
699
+
647
700
  // src/rules/no-nested-step.ts
648
701
  function isStepCall(node) {
649
702
  const inner = node.type === "CallExpression" ? node.callee : node;
@@ -967,7 +1020,7 @@ var init_no_restricted_matchers = __esm({
967
1020
  context.report({
968
1021
  data: { message: message ?? "", restriction },
969
1022
  loc: {
970
- end: chain[chain.length - 1].loc.end,
1023
+ end: chain.at(-1).loc.end,
971
1024
  start: chain[0].loc.start
972
1025
  },
973
1026
  messageId: message ? "restrictedWithMessage" : "restricted"
@@ -1068,6 +1121,101 @@ var init_no_skipped_test = __esm({
1068
1121
  }
1069
1122
  });
1070
1123
 
1124
+ // src/rules/no-unsafe-references.ts
1125
+ function collectVariables(scope) {
1126
+ if (!scope)
1127
+ return [];
1128
+ return [
1129
+ ...collectVariables(scope.upper),
1130
+ ...scope.variables.map((ref) => ref.name)
1131
+ ];
1132
+ }
1133
+ function addArgument(fixer, node, refs) {
1134
+ if (!node.arguments.length)
1135
+ return;
1136
+ if (node.arguments.length === 1) {
1137
+ return fixer.insertTextAfter(node.arguments[0], `, [${refs}]`);
1138
+ }
1139
+ const arr = node.arguments.at(-1);
1140
+ if (!arr || arr.type !== "ArrayExpression")
1141
+ return;
1142
+ const lastItem = arr.elements.at(-1);
1143
+ return lastItem ? fixer.insertTextAfter(lastItem, `, ${refs}`) : fixer.replaceText(arr, `[${refs}]`);
1144
+ }
1145
+ function getParen(sourceCode, node) {
1146
+ let token = sourceCode.getFirstToken(node);
1147
+ while (token && token.value !== "(") {
1148
+ token = sourceCode.getTokenAfter(token);
1149
+ }
1150
+ return token;
1151
+ }
1152
+ function addParam(sourceCode, fixer, node, refs) {
1153
+ const lastParam = node.params.at(-1);
1154
+ if (lastParam) {
1155
+ return fixer.insertTextAfter(lastParam, `, ${refs}`);
1156
+ }
1157
+ const token = getParen(sourceCode, node);
1158
+ return token ? fixer.insertTextAfter(token, `[${refs}]`) : null;
1159
+ }
1160
+ var no_unsafe_references_default;
1161
+ var init_no_unsafe_references = __esm({
1162
+ "src/rules/no-unsafe-references.ts"() {
1163
+ "use strict";
1164
+ init_ast();
1165
+ init_misc();
1166
+ no_unsafe_references_default = {
1167
+ create(context) {
1168
+ return {
1169
+ CallExpression(node) {
1170
+ if (!isPageMethod(node, "evaluate"))
1171
+ return;
1172
+ const [fn] = node.arguments;
1173
+ if (!fn || !isFunction(fn))
1174
+ return;
1175
+ const sourceCode = getSourceCode(context);
1176
+ const { through, upper } = sourceCode.getScope(fn.body);
1177
+ const allRefs = new Set(collectVariables(upper));
1178
+ through.filter((ref) => allRefs.has(ref.identifier.name)).forEach((ref, i, arr) => {
1179
+ const descriptor = {
1180
+ data: { variable: ref.identifier.name },
1181
+ messageId: "noUnsafeReference",
1182
+ node: ref.identifier
1183
+ };
1184
+ if (i !== 0) {
1185
+ context.report(descriptor);
1186
+ return;
1187
+ }
1188
+ context.report({
1189
+ ...descriptor,
1190
+ fix(fixer) {
1191
+ const refs = arr.map((ref2) => ref2.identifier.name).join(", ");
1192
+ return [
1193
+ addArgument(fixer, node, refs),
1194
+ addParam(sourceCode, fixer, fn, refs)
1195
+ ].filter(truthy);
1196
+ }
1197
+ });
1198
+ });
1199
+ }
1200
+ };
1201
+ },
1202
+ meta: {
1203
+ docs: {
1204
+ category: "Possible Errors",
1205
+ description: "Prevent unsafe variable references in page.evaluate()",
1206
+ recommended: true,
1207
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-unsafe-references.md"
1208
+ },
1209
+ fixable: "code",
1210
+ messages: {
1211
+ noUnsafeReference: 'Unsafe reference to variable "{{ variable }}" in page.evaluate()'
1212
+ },
1213
+ type: "problem"
1214
+ }
1215
+ };
1216
+ }
1217
+ });
1218
+
1071
1219
  // src/rules/no-useless-await.ts
1072
1220
  function isSupportedMethod(node) {
1073
1221
  if (node.callee.type !== "MemberExpression")
@@ -1585,6 +1733,7 @@ var init_prefer_to_contain = __esm({
1585
1733
  "src/rules/prefer-to-contain.ts"() {
1586
1734
  "use strict";
1587
1735
  init_ast();
1736
+ init_misc();
1588
1737
  init_parseExpectCall();
1589
1738
  matchers = /* @__PURE__ */ new Set(["toBe", "toEqual", "toStrictEqual"]);
1590
1739
  isFixableIncludesCallExpression = (node) => node.type === "CallExpression" && node.callee.type === "MemberExpression" && isPropertyAccessor(node.callee, "includes") && node.arguments.length === 1 && node.arguments[0].type !== "SpreadElement";
@@ -1606,7 +1755,7 @@ var init_prefer_to_contain = __esm({
1606
1755
  );
1607
1756
  context.report({
1608
1757
  fix(fixer) {
1609
- const sourceCode = context.sourceCode ?? context.getSourceCode();
1758
+ const sourceCode = getSourceCode(context);
1610
1759
  const addNotModifier = matcherArg.type === "Literal" && matcherArg.value === !!notModifier;
1611
1760
  const fixes = [
1612
1761
  // remove the "includes" call entirely
@@ -1781,11 +1930,25 @@ var init_prefer_to_have_length = __esm({
1781
1930
  });
1782
1931
 
1783
1932
  // src/rules/prefer-web-first-assertions.ts
1933
+ function dereference(context, node) {
1934
+ if (node.type !== "Identifier") {
1935
+ return node;
1936
+ }
1937
+ const sourceCode = getSourceCode(context);
1938
+ const scope = sourceCode.getScope(node);
1939
+ for (const ref of scope.references) {
1940
+ const refParent = ref.identifier.parent;
1941
+ if (refParent.type === "VariableDeclarator") {
1942
+ return refParent.init;
1943
+ }
1944
+ }
1945
+ }
1784
1946
  var methods3, supportedMatchers, prefer_web_first_assertions_default;
1785
1947
  var init_prefer_web_first_assertions = __esm({
1786
1948
  "src/rules/prefer-web-first-assertions.ts"() {
1787
1949
  "use strict";
1788
1950
  init_ast();
1951
+ init_misc();
1789
1952
  init_parseExpectCall();
1790
1953
  methods3 = {
1791
1954
  getAttribute: {
@@ -1835,8 +1998,8 @@ var init_prefer_web_first_assertions = __esm({
1835
1998
  const expectCall = parseExpectCall(context, node);
1836
1999
  if (!expectCall)
1837
2000
  return;
1838
- const [arg] = node.arguments;
1839
- if (arg.type !== "AwaitExpression" || arg.argument.type !== "CallExpression" || arg.argument.callee.type !== "MemberExpression") {
2001
+ const arg = dereference(context, node.arguments[0]);
2002
+ if (!arg || arg.type !== "AwaitExpression" || arg.argument.type !== "CallExpression" || arg.argument.callee.type !== "MemberExpression") {
1840
2003
  return;
1841
2004
  }
1842
2005
  if (!supportedMatchers.has(expectCall.matcherName))
@@ -1860,7 +2023,7 @@ var init_prefer_web_first_assertions = __esm({
1860
2023
  },
1861
2024
  fix: (fixer) => {
1862
2025
  const methodArgs = arg.argument.type === "CallExpression" ? arg.argument.arguments : [];
1863
- const methodEnd = methodArgs.length ? methodArgs[methodArgs.length - 1].range[1] + 1 : callee.property.range[1] + 2;
2026
+ const methodEnd = methodArgs.length ? methodArgs.at(-1).range[1] + 1 : callee.property.range[1] + 2;
1864
2027
  const fixes = [
1865
2028
  // Add await to the expect call
1866
2029
  fixer.insertTextBefore(node, "await "),
@@ -1967,18 +2130,6 @@ var init_require_soft_assertions = __esm({
1967
2130
  }
1968
2131
  });
1969
2132
 
1970
- // src/utils/misc.ts
1971
- var getAmountData;
1972
- var init_misc = __esm({
1973
- "src/utils/misc.ts"() {
1974
- "use strict";
1975
- getAmountData = (amount) => ({
1976
- amount: amount.toString(),
1977
- s: amount === 1 ? "" : "s"
1978
- });
1979
- }
1980
- });
1981
-
1982
2133
  // src/rules/require-top-level-describe.ts
1983
2134
  var require_top_level_describe_default;
1984
2135
  var init_require_top_level_describe = __esm({
@@ -2378,6 +2529,7 @@ var require_src = __commonJS({
2378
2529
  init_no_eval();
2379
2530
  init_no_focused_test();
2380
2531
  init_no_force_option();
2532
+ init_no_get_by_title();
2381
2533
  init_no_nested_step();
2382
2534
  init_no_networkidle();
2383
2535
  init_no_nth_methods();
@@ -2385,6 +2537,7 @@ var require_src = __commonJS({
2385
2537
  init_no_raw_locators();
2386
2538
  init_no_restricted_matchers();
2387
2539
  init_no_skipped_test();
2540
+ init_no_unsafe_references();
2388
2541
  init_no_useless_await();
2389
2542
  init_no_useless_not();
2390
2543
  init_no_wait_for_selector();
@@ -2411,6 +2564,7 @@ var require_src = __commonJS({
2411
2564
  "no-eval": no_eval_default,
2412
2565
  "no-focused-test": no_focused_test_default,
2413
2566
  "no-force-option": no_force_option_default,
2567
+ "no-get-by-title": no_get_by_title_default,
2414
2568
  "no-nested-step": no_nested_step_default,
2415
2569
  "no-networkidle": no_networkidle_default,
2416
2570
  "no-nth-methods": no_nth_methods_default,
@@ -2418,6 +2572,7 @@ var require_src = __commonJS({
2418
2572
  "no-raw-locators": no_raw_locators_default,
2419
2573
  "no-restricted-matchers": no_restricted_matchers_default,
2420
2574
  "no-skipped-test": no_skipped_test_default,
2575
+ "no-unsafe-references": no_unsafe_references_default,
2421
2576
  "no-useless-await": no_useless_await_default,
2422
2577
  "no-useless-not": no_useless_not_default,
2423
2578
  "no-wait-for-selector": no_wait_for_selector_default,
@@ -2450,6 +2605,7 @@ var require_src = __commonJS({
2450
2605
  "playwright/no-networkidle": "error",
2451
2606
  "playwright/no-page-pause": "warn",
2452
2607
  "playwright/no-skipped-test": "warn",
2608
+ "playwright/no-unsafe-references": "error",
2453
2609
  "playwright/no-useless-await": "warn",
2454
2610
  "playwright/no-useless-not": "warn",
2455
2611
  "playwright/no-wait-for-selector": "warn",
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": "1.0.0",
4
+ "version": "1.1.0",
5
5
  "repository": "https://github.com/playwright-community/eslint-plugin-playwright",
6
6
  "author": "Mark Skelton <mark@mskelton.dev>",
7
7
  "packageManager": "pnpm@8.12.0",
@@ -12,6 +12,9 @@
12
12
  "workspaces": [
13
13
  "examples"
14
14
  ],
15
+ "engines": {
16
+ "node": ">=16.6.0"
17
+ },
15
18
  "types": "./dist/index.d.ts",
16
19
  "exports": {
17
20
  "import": {
@@ -35,9 +38,12 @@
35
38
  "ts": "tsc --noEmit"
36
39
  },
37
40
  "devDependencies": {
41
+ "@jest/globals": "^29.7.0",
38
42
  "@mskelton/eslint-config": "^8.4.0",
43
+ "@mskelton/semantic-release-config": "^1.0.1",
39
44
  "@types/eslint": "^8.44.3",
40
45
  "@types/estree": "^1.0.2",
46
+ "@types/node": "^20.11.17",
41
47
  "@typescript-eslint/eslint-plugin": "^6.7.3",
42
48
  "@typescript-eslint/parser": "^6.7.3",
43
49
  "dedent": "^1.5.1",
@@ -45,7 +51,8 @@
45
51
  "eslint-plugin-sort": "^2.10.0",
46
52
  "jest": "^29.7.0",
47
53
  "prettier": "^3.0.3",
48
- "semantic-release": "^22.0.5",
54
+ "prettier-plugin-jsdoc": "^1.3.0",
55
+ "semantic-release": "^23.0.2",
49
56
  "ts-jest": "^29.1.1",
50
57
  "tsup": "^8.0.1",
51
58
  "typescript": "^5.2.2"