eslint-plugin-playwright 2.7.1 → 2.9.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 +11 -11
  2. package/dist/index.cjs +338 -130
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -79,8 +79,8 @@ can configure this plugin to be aware of these additional names.
79
79
  "settings": {
80
80
  "playwright": {
81
81
  "globalAliases": {
82
- "test": ["myTest"],
83
- "expect": ["myExpect"]
82
+ "test": ["it"],
83
+ "expect": ["assert"]
84
84
  }
85
85
  }
86
86
  }
@@ -127,13 +127,13 @@ CLI option\
127
127
  | [no-commented-out-tests](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/no-commented-out-tests.md) | Disallow commented out tests | | | |
128
128
  | [no-conditional-expect](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/no-conditional-expect.md) | Disallow calling `expect` conditionally | ✅ | | |
129
129
  | [no-conditional-in-test](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/no-conditional-in-test.md) | Disallow conditional logic in tests | ✅ | | |
130
- | [no-duplicate-hooks](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/no-duplicate-hooks.md) | Disallow duplicate setup and teardown hooks | | | |
130
+ | [no-duplicate-hooks](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/no-duplicate-hooks.md) | Disallow duplicate setup and teardown hooks || | |
131
131
  | [no-duplicate-slow](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/no-duplicate-slow.md) | Disallow multiple `test.slow()` calls in the same test | ✅ | | |
132
132
  | [no-element-handle](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/no-element-handle.md) | Disallow usage of element handles | ✅ | | 💡 |
133
133
  | [no-eval](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/no-eval.md) | Disallow usage of `page.$eval()` and `page.$$eval()` | ✅ | | |
134
134
  | [no-focused-test](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/no-focused-test.md) | Disallow usage of `.only` annotation | ✅ | | 💡 |
135
135
  | [no-force-option](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/no-force-option.md) | Disallow usage of the `{ force: true }` option | ✅ | | |
136
- | [no-get-by-title](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/no-get-by-title.md) | Disallow using `getByTitle()` | | 🔧 | |
136
+ | [no-get-by-title](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/no-get-by-title.md) | Disallow using `getByTitle()` | | | |
137
137
  | [no-hooks](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/no-hooks.md) | Disallow setup and teardown hooks | | | |
138
138
  | [no-nested-step](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/no-nested-step.md) | Disallow nested `test.step()` methods | ✅ | | |
139
139
  | [no-networkidle](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/no-networkidle.md) | Disallow usage of the `networkidle` option | ✅ | | |
@@ -145,7 +145,7 @@ CLI option\
145
145
  | [no-restricted-roles](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/no-restricted-roles.md) | Disallow specific roles in `getByRole()` | | | |
146
146
  | [no-skipped-test](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/no-skipped-test.md) | Disallow usage of the `.skip` annotation | ✅ | | 💡 |
147
147
  | [no-slowed-test](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/no-slowed-test.md) | Disallow usage of the `.slow` annotation | | | 💡 |
148
- | [no-standalone-expect](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/no-standalone-expect.md) | Disallow using expect outside of `test` blocks | ✅ | | |
148
+ | [no-standalone-expect](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/no-standalone-expect.md) | Disallow using expect outside of `test` blocks | ✅ | 🔧 | |
149
149
  | [no-unsafe-references](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/no-unsafe-references.md) | Prevent unsafe variable references in `page.evaluate()` | ✅ | 🔧 | |
150
150
  | [no-unused-locators](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/no-unused-locators.md) | Disallow usage of page locators that are not used | ✅ | | |
151
151
  | [no-useless-await](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/no-useless-await.md) | Disallow unnecessary `await`s for Playwright methods | ✅ | 🔧 | |
@@ -155,16 +155,16 @@ CLI option\
155
155
  | [no-wait-for-timeout](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/no-wait-for-timeout.md) | Disallow usage of `page.waitForTimeout()` | ✅ | | 💡 |
156
156
  | [prefer-comparison-matcher](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/prefer-comparison-matcher.md) | Suggest using the built-in comparison matchers | | 🔧 | |
157
157
  | [prefer-equality-matcher](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/prefer-equality-matcher.md) | Suggest using the built-in equality matchers | | | 💡 |
158
- | [prefer-hooks-in-order](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/prefer-hooks-in-order.md) | Prefer having hooks in a consistent order | | | |
159
- | [prefer-hooks-on-top](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/prefer-hooks-on-top.md) | Suggest having hooks before any test cases | | | |
158
+ | [prefer-hooks-in-order](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/prefer-hooks-in-order.md) | Prefer having hooks in a consistent order || | |
159
+ | [prefer-hooks-on-top](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/prefer-hooks-on-top.md) | Suggest having hooks before any test cases || | |
160
160
  | [prefer-lowercase-title](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/prefer-lowercase-title.md) | Enforce lowercase test names | | 🔧 | |
161
161
  | [prefer-native-locators](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/prefer-native-locators.md) | Suggest built-in locators over `page.locator()` | | 🔧 | |
162
- | [prefer-locator](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/prefer-locator.md) | Suggest locators over page methods | | | |
163
- | [prefer-strict-equal](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/prefer-strict-equal.md) | Suggest using `toStrictEqual()` | | | 💡 |
162
+ | [prefer-locator](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/prefer-locator.md) | Suggest locators over page methods || | |
163
+ | [prefer-strict-equal](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/prefer-strict-equal.md) | Suggest using `toStrictEqual()` | | 🔧 | 💡 |
164
164
  | [prefer-to-be](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/prefer-to-be.md) | Suggest using `toBe()` | | 🔧 | |
165
165
  | [prefer-to-contain](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/prefer-to-contain.md) | Suggest using `toContain()` | | 🔧 | |
166
- | [prefer-to-have-count](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/prefer-to-have-count.md) | Suggest using `toHaveCount()` | | 🔧 | |
167
- | [prefer-to-have-length](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/prefer-to-have-length.md) | Suggest using `toHaveLength()` | | 🔧 | |
166
+ | [prefer-to-have-count](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/prefer-to-have-count.md) | Suggest using `toHaveCount()` || 🔧 | |
167
+ | [prefer-to-have-length](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/prefer-to-have-length.md) | Suggest using `toHaveLength()` || 🔧 | |
168
168
  | [prefer-web-first-assertions](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/prefer-web-first-assertions.md) | Suggest using web first assertions | ✅ | 🔧 | |
169
169
  | [require-hook](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/require-hook.md) | Require setup and teardown code to be within a hook | | | |
170
170
  | [require-soft-assertions](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/require-soft-assertions.md) | Require assertions to use `expect.soft()` | | 🔧 | |
package/dist/index.cjs CHANGED
@@ -118,27 +118,55 @@ var Chain = class {
118
118
  }
119
119
  }
120
120
  };
121
- var resolvePossibleAliasedGlobal = (context, global) => {
121
+ function resolvePossibleAliasedGlobal(context, name) {
122
122
  const settings = context.settings;
123
123
  const globalAliases = settings?.playwright?.globalAliases ?? {};
124
- const alias = Object.entries(globalAliases).find(([, aliases]) => aliases.includes(global));
124
+ const alias = Object.entries(globalAliases).find(([, aliases]) => aliases.includes(name));
125
125
  return alias?.[0] ?? null;
126
- };
126
+ }
127
+ function resolveImportAlias(context, node) {
128
+ if (node.type !== "Identifier") {
129
+ return null;
130
+ }
131
+ const scope = context.sourceCode.getScope(node);
132
+ const ref = scope.references.find((r) => r.identifier === node);
133
+ for (const def of ref?.resolved?.defs ?? []) {
134
+ if (def.type === "ImportBinding" && def.node.type === "ImportSpecifier") {
135
+ const imported = getStringValue(def.node.imported);
136
+ if (imported !== node.name) {
137
+ return imported;
138
+ }
139
+ }
140
+ }
141
+ return null;
142
+ }
127
143
  var resolveToPlaywrightFn = (context, accessor) => {
128
144
  const ident = getStringValue(accessor);
129
145
  const resolved = /(^expect|Expect)$/.test(ident) ? "expect" : ident;
146
+ if (resolved === "test" || resolved === "expect") {
147
+ return { original: null, local: resolved };
148
+ }
130
149
  return {
131
- // eslint-disable-next-line sort/object-properties
132
- original: resolvePossibleAliasedGlobal(context, resolved),
150
+ original: resolvePossibleAliasedGlobal(context, resolved) ?? resolveImportAlias(context, accessor),
133
151
  local: resolved
134
152
  };
135
153
  };
136
154
  function determinePlaywrightFnGroup(name) {
137
- if (name === "step") return "step";
138
- if (name === "expect") return "expect";
139
- if (name === "describe") return "describe";
140
- if (name === "test") return "test";
141
- if (testHooks.has(name)) return "hook";
155
+ if (name === "step") {
156
+ return "step";
157
+ }
158
+ if (name === "expect") {
159
+ return "expect";
160
+ }
161
+ if (name === "describe") {
162
+ return "describe";
163
+ }
164
+ if (name === "test") {
165
+ return "test";
166
+ }
167
+ if (testHooks.has(name)) {
168
+ return "hook";
169
+ }
142
170
  return "unknown";
143
171
  }
144
172
  var modifiers = /* @__PURE__ */ new Set(["not", "resolves", "rejects"]);
@@ -213,14 +241,43 @@ var findTopMostCallExpression = (node) => {
213
241
  }
214
242
  return top;
215
243
  };
244
+ function isTestExtendCall(context, node) {
245
+ if (node.type !== "CallExpression" || node.callee.type !== "MemberExpression" || !isPropertyAccessor(node.callee, "extend")) {
246
+ return false;
247
+ }
248
+ const object = node.callee.object;
249
+ if (object.type === "Identifier") {
250
+ const resolved = resolveToPlaywrightFn(context, object);
251
+ if ((resolved?.original ?? resolved?.local) === "test") {
252
+ return true;
253
+ }
254
+ const dereferenced = dereference(context, object);
255
+ if (dereferenced) {
256
+ return isTestExtendCall(context, dereferenced);
257
+ }
258
+ }
259
+ return false;
260
+ }
216
261
  function parse(context, node) {
217
262
  const chain = new Chain(node);
218
- if (!chain.nodes?.length) return null;
263
+ if (!chain.nodes?.length) {
264
+ return null;
265
+ }
219
266
  const [first, ...rest] = chain.nodes;
220
- const resolved = resolveToPlaywrightFn(context, first);
221
- if (!resolved) return null;
267
+ let resolved = resolveToPlaywrightFn(context, first);
268
+ if (!resolved) {
269
+ return null;
270
+ }
222
271
  let name = resolved.original ?? resolved.local;
223
272
  const links = [name, ...rest.map((link) => getStringValue(link))];
273
+ if (determinePlaywrightFnGroup(name) === "unknown") {
274
+ const dereferenced = dereference(context, first);
275
+ if (dereferenced && isTestExtendCall(context, dereferenced)) {
276
+ name = "test";
277
+ links[0] = "test";
278
+ resolved = { local: resolved.local, original: "test" };
279
+ }
280
+ }
224
281
  if (name === "test" && links.length > 1) {
225
282
  const nextLinkName = links[1];
226
283
  const nextLinkGroup = determinePlaywrightFnGroup(nextLinkName);
@@ -246,7 +303,9 @@ function parse(context, node) {
246
303
  parsedFnCall.members.shift();
247
304
  }
248
305
  const result = parseExpectCall(chain, parsedFnCall, stage);
249
- if (!result) return null;
306
+ if (!result) {
307
+ return null;
308
+ }
250
309
  if (typeof result === "string" && findTopMostCallExpression(node) !== node) {
251
310
  return null;
252
311
  }
@@ -294,7 +353,9 @@ var isTypeOfFnCall = (context, node, types) => {
294
353
 
295
354
  // src/utils/ast.ts
296
355
  function getStringValue(node) {
297
- if (!node) return "";
356
+ if (!node) {
357
+ return "";
358
+ }
298
359
  return node.type === "Identifier" ? node.name : node.type === "TemplateLiteral" ? node.quasis[0].value.raw : node.type === "Literal" && typeof node.value === "string" ? node.value : "";
299
360
  }
300
361
  function getRawValue(node) {
@@ -323,7 +384,9 @@ function isPropertyAccessor(node, name) {
323
384
  }
324
385
  function findParent(node, type) {
325
386
  const parent = node.parent;
326
- if (!parent) return;
387
+ if (!parent) {
388
+ return;
389
+ }
327
390
  return parent.type === type ? parent : findParent(parent, type);
328
391
  }
329
392
  function dig(node, identifier) {
@@ -394,6 +457,9 @@ var getPaddingLineSequences = (prevNode, nextNode, sourceCode) => {
394
457
  return pairs;
395
458
  };
396
459
  var areTokensOnSameLine = (left, right) => left.loc.end.line === right.loc.start.line;
460
+ var isPromiseAccessor = (node) => {
461
+ return node.type === "MemberExpression" && isIdentifier(node.property, /^(then|catch|finally)$/);
462
+ };
397
463
 
398
464
  // src/utils/createRule.ts
399
465
  function interpolate(str, data) {
@@ -683,7 +749,7 @@ var max_expects_default = createRule({
683
749
  "ArrowFunctionExpression:exit": maybeResetCount,
684
750
  "CallExpression"(node) {
685
751
  const call = parseFnCall(context, node);
686
- if (call?.type !== "expect" || call.head.node.parent?.type === "MemberExpression") {
752
+ if (call?.type !== "expect") {
687
753
  return;
688
754
  }
689
755
  count += 1;
@@ -872,29 +938,65 @@ var missing_playwright_await_default = createRule({
872
938
  // Add any custom matchers to the set
873
939
  ...options.customMatchers || []
874
940
  ]);
941
+ function isVariableConsumed(declarator, checkValidity2, validTypes2, visited) {
942
+ const variables = context.sourceCode.getDeclaredVariables(declarator);
943
+ for (const variable of variables) {
944
+ for (const ref of variable.references) {
945
+ if (!ref.isRead()) {
946
+ continue;
947
+ }
948
+ const refParent = ref.identifier.parent;
949
+ if (visited.has(refParent)) {
950
+ continue;
951
+ }
952
+ if (validTypes2.has(refParent.type)) {
953
+ return true;
954
+ }
955
+ if (refParent.type === "VariableDeclarator") {
956
+ if (checkValidity2(ref.identifier, visited)) {
957
+ return true;
958
+ }
959
+ continue;
960
+ }
961
+ if (checkValidity2(refParent, visited)) {
962
+ return true;
963
+ }
964
+ }
965
+ }
966
+ return false;
967
+ }
875
968
  function checkValidity(node, visited) {
876
969
  const parent = node.parent;
877
- if (!parent) return false;
878
- if (visited.has(parent)) return false;
970
+ if (!parent) {
971
+ return false;
972
+ }
973
+ if (visited.has(parent)) {
974
+ return false;
975
+ }
879
976
  visited.add(parent);
880
- if (validTypes.has(parent.type)) return true;
881
- if (parent.type === "MemberExpression" && isIdentifier(parent.property, /^(then|catch|finally)$/) && parent.parent?.type === "CallExpression") {
977
+ if (validTypes.has(parent.type)) {
978
+ return true;
979
+ }
980
+ if (isPromiseAccessor(parent) && parent.parent?.type === "CallExpression") {
882
981
  return checkValidity(parent.parent, visited);
883
982
  }
983
+ if (parent.type === "CallExpression" && parent.callee === node && isPromiseAccessor(node)) {
984
+ return checkValidity(parent, visited);
985
+ }
884
986
  if (parent.type === "ArrayExpression") {
885
987
  return checkValidity(parent, visited);
886
988
  }
989
+ if (parent.type === "ConditionalExpression") {
990
+ return checkValidity(parent, visited);
991
+ }
992
+ if (parent.type === "SpreadElement") {
993
+ return checkValidity(parent, visited);
994
+ }
887
995
  if (parent.type === "CallExpression" && parent.callee.type === "MemberExpression" && isIdentifier(parent.callee.object, "Promise") && isIdentifier(parent.callee.property, "all")) {
888
996
  return true;
889
997
  }
890
998
  if (parent.type === "VariableDeclarator") {
891
- const scope = context.sourceCode.getScope(parent.parent);
892
- for (const ref of scope.references) {
893
- const refParent = ref.identifier.parent;
894
- if (visited.has(refParent)) continue;
895
- if (validTypes.has(refParent.type)) return true;
896
- if (checkValidity(refParent, visited)) return true;
897
- }
999
+ return isVariableConsumed(parent, checkValidity, validTypes, visited);
898
1000
  }
899
1001
  return false;
900
1002
  }
@@ -912,7 +1014,9 @@ var missing_playwright_await_default = createRule({
912
1014
  return;
913
1015
  }
914
1016
  const call = parseFnCall(context, node);
915
- if (call?.type !== "step" && call?.type !== "expect") return;
1017
+ if (call?.type !== "step" && call?.type !== "expect") {
1018
+ return;
1019
+ }
916
1020
  const result = getCallType(call, awaitableMatchers);
917
1021
  const isValid = result ? checkValidity(node, /* @__PURE__ */ new Set()) : false;
918
1022
  if (result && !isValid) {
@@ -970,7 +1074,9 @@ function hasTests(context, node) {
970
1074
  var no_commented_out_tests_default = createRule({
971
1075
  create(context) {
972
1076
  function checkNode(node) {
973
- if (!hasTests(context, node)) return;
1077
+ if (!hasTests(context, node)) {
1078
+ return;
1079
+ }
974
1080
  context.report({
975
1081
  messageId: "commentedTests",
976
1082
  node
@@ -997,18 +1103,6 @@ var no_commented_out_tests_default = createRule({
997
1103
 
998
1104
  // src/rules/no-conditional-expect.ts
999
1105
  var isCatchCall = (node) => node.callee.type === "MemberExpression" && isPropertyAccessor(node.callee, "catch");
1000
- var getTestCallExpressionsFromDeclaredVariables = (context, declaredVariables) => {
1001
- return declaredVariables.reduce(
1002
- (acc, { references }) => [
1003
- ...acc,
1004
- ...references.map(({ identifier }) => identifier.parent).filter(
1005
- // ESLint types are infurating
1006
- (node) => node?.type === "CallExpression" && isTypeOfFnCall(context, node, ["test"])
1007
- )
1008
- ],
1009
- []
1010
- );
1011
- };
1012
1106
  var no_conditional_expect_default = createRule({
1013
1107
  create(context) {
1014
1108
  let conditionalDepth = 0;
@@ -1050,16 +1144,6 @@ var no_conditional_expect_default = createRule({
1050
1144
  "CatchClause:exit": decreaseConditionalDepth,
1051
1145
  "ConditionalExpression": increaseConditionalDepth,
1052
1146
  "ConditionalExpression:exit": decreaseConditionalDepth,
1053
- "FunctionDeclaration"(node) {
1054
- const declaredVariables = context.sourceCode.getDeclaredVariables(node);
1055
- const testCallExpressions = getTestCallExpressionsFromDeclaredVariables(
1056
- context,
1057
- declaredVariables
1058
- );
1059
- if (testCallExpressions.length > 0) {
1060
- inTestCase = true;
1061
- }
1062
- },
1063
1147
  "IfStatement": increaseConditionalDepth,
1064
1148
  "IfStatement:exit": decreaseConditionalDepth,
1065
1149
  "LogicalExpression": increaseConditionalDepth,
@@ -1089,11 +1173,15 @@ var no_conditional_in_test_default = createRule({
1089
1173
  return;
1090
1174
  }
1091
1175
  const call = findParent(node, "CallExpression");
1092
- if (!call) return;
1176
+ if (!call) {
1177
+ return;
1178
+ }
1093
1179
  if (isTypeOfFnCall(context, call, ["test", "step"])) {
1094
1180
  const testFunction = call.arguments[call.arguments.length - 1];
1095
1181
  const functionBody = findParent(node, "BlockStatement");
1096
- if (!functionBody) return;
1182
+ if (!functionBody) {
1183
+ return;
1184
+ }
1097
1185
  let currentParent = functionBody.parent;
1098
1186
  while (currentParent && currentParent !== testFunction) {
1099
1187
  currentParent = currentParent.parent;
@@ -1131,7 +1219,9 @@ var no_duplicate_hooks_default = createRule({
1131
1219
  return {
1132
1220
  "CallExpression"(node) {
1133
1221
  const call = parseFnCall(context, node);
1134
- if (!call) return;
1222
+ if (!call) {
1223
+ return;
1224
+ }
1135
1225
  if (call.type === "describe") {
1136
1226
  hookContexts.push({});
1137
1227
  }
@@ -1177,7 +1267,9 @@ var no_duplicate_slow_default = createRule({
1177
1267
  return {
1178
1268
  "CallExpression"(node) {
1179
1269
  const call = parseFnCall(context, node);
1180
- if (!call) return;
1270
+ if (!call) {
1271
+ return;
1272
+ }
1181
1273
  if (call.type === "test" || call.type === "describe") {
1182
1274
  scopes.push(scopes[scopes.length - 1]);
1183
1275
  }
@@ -1298,7 +1390,9 @@ var no_focused_test_default = createRule({
1298
1390
  return;
1299
1391
  }
1300
1392
  const onlyNode = call.members.find((s) => getStringValue(s) === "only");
1301
- if (!onlyNode) return;
1393
+ if (!onlyNode) {
1394
+ return;
1395
+ }
1302
1396
  context.report({
1303
1397
  messageId: "noFocusedTest",
1304
1398
  node: onlyNode,
@@ -1383,7 +1477,7 @@ var no_get_by_title_default = createRule({
1383
1477
  create(context) {
1384
1478
  return {
1385
1479
  CallExpression(node) {
1386
- if (isPageMethod(node, "getByTitle")) {
1480
+ if (node.callee.type === "MemberExpression" && getStringValue(node.callee.property) === "getByTitle") {
1387
1481
  context.report({ messageId: "noGetByTitle", node });
1388
1482
  }
1389
1483
  }
@@ -1412,7 +1506,9 @@ var no_hooks_default = createRule({
1412
1506
  return {
1413
1507
  CallExpression(node) {
1414
1508
  const call = parseFnCall(context, node);
1415
- if (!call) return;
1509
+ if (!call) {
1510
+ return;
1511
+ }
1416
1512
  if (call.type === "hook" && !options.allow.includes(call.name)) {
1417
1513
  context.report({
1418
1514
  data: { hookName: call.name },
@@ -1507,20 +1603,26 @@ var no_networkidle_default = createRule({
1507
1603
  create(context) {
1508
1604
  return {
1509
1605
  CallExpression(node) {
1510
- if (node.callee.type !== "MemberExpression") return;
1606
+ if (node.callee.type !== "MemberExpression") {
1607
+ return;
1608
+ }
1511
1609
  const methodName = getStringValue(node.callee.property);
1512
- if (!methods.has(methodName)) return;
1610
+ if (!methods.has(methodName)) {
1611
+ return;
1612
+ }
1513
1613
  if (methodName === "waitForLoadState") {
1514
1614
  const arg = node.arguments[0];
1515
- if (arg && isStringLiteral(arg, "networkidle")) {
1615
+ if (arg && isStringNode(arg, "networkidle")) {
1516
1616
  context.report({ messageId, node: arg });
1517
1617
  }
1518
1618
  return;
1519
1619
  }
1520
1620
  if (node.arguments.length >= 2) {
1521
1621
  const [_, arg] = node.arguments;
1522
- if (arg.type !== "ObjectExpression") return;
1523
- const property = arg.properties.filter((p) => p.type === "Property").find((p) => isStringLiteral(p.value, "networkidle"));
1622
+ if (arg.type !== "ObjectExpression") {
1623
+ return;
1624
+ }
1625
+ const property = arg.properties.filter((p) => p.type === "Property").find((p) => isStringNode(p.value, "networkidle"));
1524
1626
  if (property) {
1525
1627
  context.report({ messageId, node: property.value });
1526
1628
  }
@@ -1547,9 +1649,13 @@ var no_nth_methods_default = createRule({
1547
1649
  create(context) {
1548
1650
  return {
1549
1651
  CallExpression(node) {
1550
- if (node.callee.type !== "MemberExpression") return;
1652
+ if (node.callee.type !== "MemberExpression") {
1653
+ return;
1654
+ }
1551
1655
  const method = getStringValue(node.callee.property);
1552
- if (!methods2.has(method)) return;
1656
+ if (!methods2.has(method)) {
1657
+ return;
1658
+ }
1553
1659
  context.report({
1554
1660
  data: { method },
1555
1661
  loc: {
@@ -1614,13 +1720,12 @@ var no_raw_locators_default = createRule({
1614
1720
  }
1615
1721
  return {
1616
1722
  CallExpression(node) {
1617
- if (node.callee.type !== "MemberExpression" || node.arguments[0]?.type === "Identifier")
1723
+ if (node.callee.type !== "MemberExpression" || !isPropertyAccessor(node.callee, "locator")) {
1618
1724
  return;
1619
- const method = getStringValue(node.callee.property);
1620
- const arg = getStringValue(node.arguments[0]);
1621
- const isLocator = isPageMethod(node, "locator") || method === "locator";
1622
- if (isLocator && !isAllowed(arg)) {
1725
+ }
1726
+ if ((node.arguments.length === 0 || isStringNode(node.arguments[0])) && !isAllowed(getStringValue(node.arguments[0]))) {
1623
1727
  context.report({ messageId: "noRawLocator", node });
1728
+ return;
1624
1729
  }
1625
1730
  }
1626
1731
  };
@@ -1673,7 +1778,7 @@ var no_restricted_locators_default = createRule({
1673
1778
  return;
1674
1779
  }
1675
1780
  for (const [restrictedType, message] of restrictionMap.entries()) {
1676
- if (isPageMethod(node, restrictedType)) {
1781
+ if (getStringValue(node.callee.property) === restrictedType) {
1677
1782
  context.report({
1678
1783
  data: {
1679
1784
  message: message ?? "",
@@ -1728,7 +1833,9 @@ var no_restricted_matchers_default = createRule({
1728
1833
  return {
1729
1834
  CallExpression(node) {
1730
1835
  const call = parseFnCall(context, node);
1731
- if (call?.type !== "expect") return;
1836
+ if (call?.type !== "expect") {
1837
+ return;
1838
+ }
1732
1839
  Object.entries(restrictedChains).map(([restriction, message]) => {
1733
1840
  const chain = call.members;
1734
1841
  const restrictionLinks = restriction.split(".").length;
@@ -1797,7 +1904,7 @@ var no_restricted_roles_default = createRule({
1797
1904
  }
1798
1905
  return {
1799
1906
  CallExpression(node) {
1800
- if (!isPageMethod(node, "getByRole")) {
1907
+ if (node.callee.type !== "MemberExpression" || !isPropertyAccessor(node.callee, "getByRole")) {
1801
1908
  return;
1802
1909
  }
1803
1910
  const role = getStringValue(node.arguments[0]);
@@ -1860,9 +1967,11 @@ var no_skipped_test_default = createRule({
1860
1967
  return;
1861
1968
  }
1862
1969
  const skipNode = call.members.find((s) => getStringValue(s) === "skip");
1863
- if (!skipNode) return;
1970
+ if (!skipNode) {
1971
+ return;
1972
+ }
1864
1973
  const isStandalone = call.type === "config";
1865
- if (isStandalone && allowConditional && (node.arguments.length !== 0 || findParent(node, "BlockStatement")?.parent?.type === "IfStatement")) {
1974
+ if (isStandalone && allowConditional && (node.arguments.length !== 0 || findParent(node, "BlockStatement")?.parent?.type === "IfStatement" || findParent(node, "SwitchCase") !== void 0)) {
1866
1975
  return;
1867
1976
  }
1868
1977
  context.report({
@@ -1922,7 +2031,9 @@ var no_slowed_test_default = createRule({
1922
2031
  return;
1923
2032
  }
1924
2033
  const slowNode = call.members.find((s) => getStringValue(s) === "slow");
1925
- if (!slowNode) return;
2034
+ if (!slowNode) {
2035
+ return;
2036
+ }
1926
2037
  const isStandalone = call.type === "config";
1927
2038
  if (isStandalone && allowConditional && (node.arguments.length !== 0 || findParent(node, "BlockStatement")?.parent?.type === "IfStatement")) {
1928
2039
  return;
@@ -2046,7 +2157,7 @@ var no_standalone_expect_default = createRule({
2046
2157
  },
2047
2158
  "CallExpression:exit"(node) {
2048
2159
  const top = callStack.at(-1);
2049
- 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")) {
2160
+ if (top === "test" && isTypeOfFnCall(context, node, ["test"]) || top === "hook" && isTypeOfFnCall(context, node, ["hook"]) || top === "template" && node.callee.type === "TaggedTemplateExpression" || top === "fixture" && node.callee.type === "MemberExpression" && isPropertyAccessor(node.callee, "extend")) {
2050
2161
  callStack.pop();
2051
2162
  }
2052
2163
  }
@@ -2075,16 +2186,22 @@ var truthy = Boolean;
2075
2186
 
2076
2187
  // src/rules/no-unsafe-references.ts
2077
2188
  function collectVariables(scope) {
2078
- if (!scope || scope.type === "global") return [];
2189
+ if (!scope || scope.type === "global") {
2190
+ return [];
2191
+ }
2079
2192
  return [...collectVariables(scope.upper), ...scope.variables.map((ref) => ref.name)];
2080
2193
  }
2081
2194
  function addArgument(fixer, node, refs) {
2082
- if (!node.arguments.length) return;
2195
+ if (!node.arguments.length) {
2196
+ return;
2197
+ }
2083
2198
  if (node.arguments.length === 1) {
2084
2199
  return fixer.insertTextAfter(node.arguments[0], `, [${refs}]`);
2085
2200
  }
2086
2201
  const arg = node.arguments.at(-1);
2087
- if (!arg) return;
2202
+ if (!arg) {
2203
+ return;
2204
+ }
2088
2205
  if (arg.type !== "ArrayExpression") {
2089
2206
  return fixer.replaceText(arg, `[${getStringValue(arg)}, ${refs}]`);
2090
2207
  }
@@ -2110,9 +2227,13 @@ var no_unsafe_references_default = createRule({
2110
2227
  create(context) {
2111
2228
  return {
2112
2229
  CallExpression(node) {
2113
- if (!isPageMethod(node, "evaluate") && !isPageMethod(node, "addInitScript")) return;
2230
+ if (!isPageMethod(node, "evaluate") && !isPageMethod(node, "addInitScript")) {
2231
+ return;
2232
+ }
2114
2233
  const [fn] = node.arguments;
2115
- if (!fn || !isFunction(fn)) return;
2234
+ if (!fn || !isFunction(fn)) {
2235
+ return;
2236
+ }
2116
2237
  const { through, upper } = context.sourceCode.getScope(fn.body);
2117
2238
  const allRefs = new Set(collectVariables(upper));
2118
2239
  through.filter((ref) => {
@@ -2162,7 +2283,7 @@ var no_unused_locators_default = createRule({
2162
2283
  create(context) {
2163
2284
  return {
2164
2285
  CallExpression(node) {
2165
- if (!isPageMethod(node, LOCATOR_REGEX)) {
2286
+ if (node.callee.type !== "MemberExpression" || !isPropertyAccessor(node.callee, LOCATOR_REGEX)) {
2166
2287
  return;
2167
2288
  }
2168
2289
  if (node.parent.type === "ExpressionStatement" || node.parent.type === "AwaitExpression") {
@@ -2245,7 +2366,9 @@ var expectMatchers = /* @__PURE__ */ new Set([
2245
2366
  "toThrowError"
2246
2367
  ]);
2247
2368
  function isSupportedMethod(node) {
2248
- if (node.callee.type !== "MemberExpression") return false;
2369
+ if (node.callee.type !== "MemberExpression") {
2370
+ return false;
2371
+ }
2249
2372
  const name = getStringValue(node.callee.property);
2250
2373
  return locatorMethods.has(name) || pageMethods.has(name) && isPageMethod(node, name);
2251
2374
  }
@@ -2300,7 +2423,9 @@ function replaceAccessorFixer(fixer, node, text) {
2300
2423
  }
2301
2424
  function removePropertyFixer(fixer, property) {
2302
2425
  const parent = property.parent;
2303
- if (parent?.type !== "ObjectExpression") return;
2426
+ if (parent?.type !== "ObjectExpression") {
2427
+ return;
2428
+ }
2304
2429
  if (parent.properties.length === 1) {
2305
2430
  return fixer.remove(parent);
2306
2431
  }
@@ -2324,7 +2449,9 @@ var matcherConfig = {
2324
2449
  };
2325
2450
  function getOptions(call, name) {
2326
2451
  const [arg] = call.matcherArgs;
2327
- if (arg?.type !== "ObjectExpression") return;
2452
+ if (arg?.type !== "ObjectExpression") {
2453
+ return;
2454
+ }
2328
2455
  const property = arg.properties.find(
2329
2456
  (p) => p.type === "Property" && getStringValue(p.key) === name && isBooleanLiteral(p.value)
2330
2457
  );
@@ -2339,13 +2466,21 @@ var no_useless_not_default = createRule({
2339
2466
  return {
2340
2467
  CallExpression(node) {
2341
2468
  const call = parseFnCall(context, node);
2342
- if (call?.type !== "expect") return;
2469
+ if (call?.type !== "expect") {
2470
+ return;
2471
+ }
2343
2472
  const config = matcherConfig[call.matcherName];
2344
- if (!config) return;
2473
+ if (!config) {
2474
+ return;
2475
+ }
2345
2476
  const options = config.argName ? getOptions(call, config.argName) : void 0;
2346
- if (options?.arg && options.value === void 0) return;
2477
+ if (options?.arg && options.value === void 0) {
2478
+ return;
2479
+ }
2347
2480
  const notModifier = call.modifiers.find((mod) => getStringValue(mod) === "not");
2348
- if (!notModifier && !options?.property) return;
2481
+ if (!notModifier && !options?.property) {
2482
+ return;
2483
+ }
2349
2484
  const isInverted = !!notModifier !== (options?.value === false);
2350
2485
  const newMatcherName = isInverted ? config.inverse : call.matcherName;
2351
2486
  context.report({
@@ -2438,7 +2573,7 @@ var no_wait_for_selector_default = createRule({
2438
2573
  suggest: [
2439
2574
  {
2440
2575
  fix: (fixer) => fixer.remove(
2441
- node.parent && node.parent.type !== "AwaitExpression" ? node.parent : node.parent.parent
2576
+ node.parent && node.parent.type !== "AwaitExpression" && node.parent.type !== "VariableDeclarator" ? node.parent : node.parent.parent
2442
2577
  ),
2443
2578
  messageId: "removeWaitForSelector"
2444
2579
  }
@@ -2475,7 +2610,7 @@ var no_wait_for_timeout_default = createRule({
2475
2610
  suggest: [
2476
2611
  {
2477
2612
  fix: (fixer) => fixer.remove(
2478
- node.parent && node.parent.type !== "AwaitExpression" ? node.parent : node.parent.parent
2613
+ node.parent && node.parent.type !== "AwaitExpression" && node.parent.type !== "VariableDeclarator" ? node.parent : node.parent.parent
2479
2614
  ),
2480
2615
  messageId: "removeWaitForTimeout"
2481
2616
  }
@@ -2507,6 +2642,7 @@ var isString = (node) => {
2507
2642
  var isComparingToString = (expression) => {
2508
2643
  return isString(expression.left) || isString(expression.right);
2509
2644
  };
2645
+ var skipModifiers = /* @__PURE__ */ new Set(["not", "soft", "poll"]);
2510
2646
  var invertedOperators = {
2511
2647
  "<": ">=",
2512
2648
  "<=": ">",
@@ -2528,9 +2664,16 @@ var prefer_comparison_matcher_default = createRule({
2528
2664
  return {
2529
2665
  CallExpression(node) {
2530
2666
  const call = parseFnCall(context, node);
2531
- if (call?.type !== "expect" || call.matcherArgs.length === 0) return;
2532
- const expect = call.head.node.parent;
2533
- if (expect?.type !== "CallExpression") return;
2667
+ if (call?.type !== "expect" || call.matcherArgs.length === 0) {
2668
+ return;
2669
+ }
2670
+ let expect = call.head.node.parent;
2671
+ while (expect?.type === "MemberExpression") {
2672
+ expect = expect.parent;
2673
+ }
2674
+ if (expect?.type !== "CallExpression") {
2675
+ return;
2676
+ }
2534
2677
  const [comparison] = expect.arguments;
2535
2678
  const expectCallEnd = expect.range[1];
2536
2679
  const [matcherArg] = call.matcherArgs;
@@ -2549,7 +2692,7 @@ var prefer_comparison_matcher_default = createRule({
2549
2692
  data: { preferredMatcher },
2550
2693
  fix(fixer) {
2551
2694
  const [modifier] = call.modifiers;
2552
- const modifierText = modifier && getStringValue(modifier) !== "not" ? `.${getStringValue(modifier)}` : "";
2695
+ const modifierText = modifier && !skipModifiers.has(getStringValue(modifier)) ? `.${getStringValue(modifier)}` : "";
2553
2696
  return [
2554
2697
  // Replace the comparison argument with the left-hand side of the comparison
2555
2698
  fixer.replaceText(comparison, context.sourceCode.getText(comparison.left)),
@@ -2588,9 +2731,13 @@ var prefer_equality_matcher_default = createRule({
2588
2731
  return {
2589
2732
  CallExpression(node) {
2590
2733
  const call = parseFnCall(context, node);
2591
- if (call?.type !== "expect" || call.matcherArgs.length === 0) return;
2734
+ if (call?.type !== "expect" || call.matcherArgs.length === 0) {
2735
+ return;
2736
+ }
2592
2737
  const expect = call.head.node.parent;
2593
- if (expect?.type !== "CallExpression") return;
2738
+ if (expect?.type !== "CallExpression") {
2739
+ return;
2740
+ }
2594
2741
  const [comparison] = expect.arguments;
2595
2742
  const expectCallEnd = expect.range[1];
2596
2743
  const [matcherArg] = call.matcherArgs;
@@ -2652,7 +2799,9 @@ var prefer_hooks_in_order_default = createRule({
2652
2799
  let inHook = false;
2653
2800
  return {
2654
2801
  "CallExpression"(node) {
2655
- if (inHook) return;
2802
+ if (inHook) {
2803
+ return;
2804
+ }
2656
2805
  const call = parseFnCall(context, node);
2657
2806
  if (call?.type !== "hook") {
2658
2807
  previousHookIndex = -1;
@@ -2679,7 +2828,9 @@ var prefer_hooks_in_order_default = createRule({
2679
2828
  inHook = false;
2680
2829
  return;
2681
2830
  }
2682
- if (inHook) return;
2831
+ if (inHook) {
2832
+ return;
2833
+ }
2683
2834
  previousHookIndex = -1;
2684
2835
  }
2685
2836
  };
@@ -2756,7 +2907,9 @@ var pageMethods2 = /* @__PURE__ */ new Set([
2756
2907
  "uncheck"
2757
2908
  ]);
2758
2909
  function isSupportedMethod2(node) {
2759
- if (node.callee.type !== "MemberExpression") return false;
2910
+ if (node.callee.type !== "MemberExpression") {
2911
+ return false;
2912
+ }
2760
2913
  const name = getStringValue(node.callee.property);
2761
2914
  return pageMethods2.has(name) && isPageMethod(node, name);
2762
2915
  }
@@ -2764,7 +2917,9 @@ var prefer_locator_default = createRule({
2764
2917
  create(context) {
2765
2918
  return {
2766
2919
  CallExpression(node) {
2767
- if (!isSupportedMethod2(node)) return;
2920
+ if (!isSupportedMethod2(node)) {
2921
+ return;
2922
+ }
2768
2923
  context.report({
2769
2924
  messageId: "preferLocator",
2770
2925
  node
@@ -2925,9 +3080,13 @@ var prefer_native_locators_default = createRule({
2925
3080
  const patterns = compilePatterns({ testIdAttribute });
2926
3081
  return {
2927
3082
  CallExpression(node) {
2928
- if (node.callee.type !== "MemberExpression") return;
3083
+ if (node.callee.type !== "MemberExpression") {
3084
+ return;
3085
+ }
2929
3086
  const query = getStringValue(node.arguments[0]);
2930
- if (!isPageMethod(node, "locator")) return;
3087
+ if (!isPropertyAccessor(node.callee, "locator")) {
3088
+ return;
3089
+ }
2931
3090
  for (const pattern of patterns) {
2932
3091
  const match = query.match(pattern.pattern);
2933
3092
  if (match) {
@@ -2985,7 +3144,9 @@ var prefer_strict_equal_default = createRule({
2985
3144
  return {
2986
3145
  CallExpression(node) {
2987
3146
  const call = parseFnCall(context, node);
2988
- if (call?.type !== "expect") return;
3147
+ if (call?.type !== "expect") {
3148
+ return;
3149
+ }
2989
3150
  if (call.matcherName === "toEqual") {
2990
3151
  context.report({
2991
3152
  messageId: "useToStrictEqual",
@@ -3053,7 +3214,9 @@ var prefer_to_be_default = createRule({
3053
3214
  return {
3054
3215
  CallExpression(node) {
3055
3216
  const call = parseFnCall(context, node);
3056
- if (call?.type !== "expect") return;
3217
+ if (call?.type !== "expect") {
3218
+ return;
3219
+ }
3057
3220
  const notMatchers = ["toBeUndefined", "toBeDefined"];
3058
3221
  const notModifier = call.modifiers.find((node2) => getStringValue(node2) === "not");
3059
3222
  if (notModifier && notMatchers.includes(call.matcherName)) {
@@ -3110,9 +3273,13 @@ var prefer_to_contain_default = createRule({
3110
3273
  return {
3111
3274
  CallExpression(node) {
3112
3275
  const call = parseFnCall(context, node);
3113
- if (call?.type !== "expect" || call.matcherArgs.length === 0) return;
3276
+ if (call?.type !== "expect" || call.matcherArgs.length === 0) {
3277
+ return;
3278
+ }
3114
3279
  const expect = call.head.node.parent;
3115
- if (expect?.type !== "CallExpression") return;
3280
+ if (expect?.type !== "CallExpression") {
3281
+ return;
3282
+ }
3116
3283
  const [includesCall] = expect.arguments;
3117
3284
  const { matcher } = call;
3118
3285
  const [matcherArg] = call.matcherArgs;
@@ -3307,19 +3474,29 @@ var prefer_web_first_assertions_default = createRule({
3307
3474
  return {
3308
3475
  CallExpression(node) {
3309
3476
  const fnCall = parseFnCall(context, node);
3310
- if (fnCall?.type !== "expect") return;
3477
+ if (fnCall?.type !== "expect") {
3478
+ return;
3479
+ }
3311
3480
  const expect = findParent(fnCall.head.node, "CallExpression");
3312
- if (!expect) return;
3481
+ if (!expect) {
3482
+ return;
3483
+ }
3313
3484
  const arg = dereference(context, fnCall.args[0]);
3314
- if (!arg) return;
3485
+ if (!arg) {
3486
+ return;
3487
+ }
3315
3488
  const call = arg.type === "AwaitExpression" ? arg.argument : arg;
3316
3489
  if (call.type !== "CallExpression" || call.callee.type !== "MemberExpression") {
3317
3490
  return;
3318
3491
  }
3319
- if (!supportedMatchers.has(fnCall.matcherName)) return;
3492
+ if (!supportedMatchers.has(fnCall.matcherName)) {
3493
+ return;
3494
+ }
3320
3495
  const method = getStringValue(call.callee.property);
3321
3496
  const methodConfig = methods3[method];
3322
- if (!Object.hasOwn(methods3, method)) return;
3497
+ if (!Object.hasOwn(methods3, method)) {
3498
+ return;
3499
+ }
3323
3500
  const notModifier = fnCall.modifiers.find((mod) => getStringValue(mod) === "not");
3324
3501
  const isFalsy = methodConfig.type === "boolean" && (!!fnCall.matcherArgs.length && isBooleanLiteral(fnCall.matcherArgs[0], false) || fnCall.matcherName === "toBeFalsy");
3325
3502
  const isInverse = methodConfig.inverse ? notModifier || isFalsy : notModifier && isFalsy;
@@ -3519,10 +3696,11 @@ function hasTagInOptions(node) {
3519
3696
  }
3520
3697
  function hasTagInTitle(node) {
3521
3698
  const title = node.arguments[0];
3522
- if (!title || title.type !== "Literal" || typeof title.value !== "string") {
3699
+ if (!title) {
3523
3700
  return false;
3524
3701
  }
3525
- return tagRegex.test(title.value);
3702
+ const value = getStringValue(title);
3703
+ return !!value && tagRegex.test(value);
3526
3704
  }
3527
3705
  function hasTags(node) {
3528
3706
  return hasTagInTitle(node) || hasTagInOptions(node);
@@ -3617,7 +3795,9 @@ var require_to_throw_message_default = createRule({
3617
3795
  return {
3618
3796
  CallExpression(node) {
3619
3797
  const call = parseFnCall(context, node);
3620
- if (call?.type !== "expect") return;
3798
+ if (call?.type !== "expect") {
3799
+ return;
3800
+ }
3621
3801
  if (call.matcherArgs.length === 0 && ["toThrow", "toThrowError"].includes(call.matcherName) && !call.modifiers.some((nod) => getStringValue(nod) === "not")) {
3622
3802
  context.report({
3623
3803
  data: { matcherName: call.matcherName },
@@ -3654,7 +3834,9 @@ var require_top_level_describe_default = createRule({
3654
3834
  return {
3655
3835
  "CallExpression"(node) {
3656
3836
  const call = parseFnCall(context, node);
3657
- if (!call) return;
3837
+ if (!call) {
3838
+ return;
3839
+ }
3658
3840
  if (call.type === "describe") {
3659
3841
  describeCount++;
3660
3842
  if (describeCount === 1) {
@@ -3723,7 +3905,9 @@ var valid_describe_callback_default = createRule({
3723
3905
  return {
3724
3906
  CallExpression(node) {
3725
3907
  const call = parseFnCall(context, node);
3726
- if (call?.group !== "describe") return;
3908
+ if (call?.group !== "describe") {
3909
+ return;
3910
+ }
3727
3911
  if (call.members.some((s) => getStringValue(s) === "configure")) {
3728
3912
  return;
3729
3913
  }
@@ -3826,7 +4010,9 @@ var isTestCaseCallWithCallbackArg = (context, node) => {
3826
4010
  };
3827
4011
  var isPromiseMethodThatUsesValue = (node, identifier) => {
3828
4012
  const name = getStringValue(identifier);
3829
- if (node.argument == null) return false;
4013
+ if (node.argument == null) {
4014
+ return false;
4015
+ }
3830
4016
  if (node.argument.type === "CallExpression" && node.argument.arguments.length > 0) {
3831
4017
  const nodeName = getNodeName(node.argument);
3832
4018
  if (["Promise.all", "Promise.allSettled"].includes(nodeName)) {
@@ -4083,7 +4269,9 @@ var valid_expect_default = createRule({
4083
4269
  return;
4084
4270
  }
4085
4271
  const { parent: expect } = call.head.node;
4086
- if (expect?.type !== "CallExpression") return;
4272
+ if (expect?.type !== "CallExpression") {
4273
+ return;
4274
+ }
4087
4275
  if (expect.arguments.length < minArgs) {
4088
4276
  const expectLength = getStringValue(call.head.node).length;
4089
4277
  const loc = {
@@ -4132,6 +4320,7 @@ var valid_expect_default = createRule({
4132
4320
  messages: {
4133
4321
  matcherNotCalled: "Matchers must be called to assert.",
4134
4322
  matcherNotFound: "Expect must have a corresponding matcher call.",
4323
+ modifierUnknown: "Expect has an unknown modifier.",
4135
4324
  notEnoughArgs: "Expect requires at least {{amount}} argument{{s}}.",
4136
4325
  tooManyArgs: "Expect takes at most {{amount}} argument{{s}}."
4137
4326
  },
@@ -4209,9 +4398,13 @@ var valid_test_tags_default = createRule({
4209
4398
  return {
4210
4399
  CallExpression(node) {
4211
4400
  const call = parseFnCall(context, node);
4212
- if (!call) return;
4401
+ if (!call) {
4402
+ return;
4403
+ }
4213
4404
  const { type } = call;
4214
- if (type !== "test" && type !== "describe" && type !== "step") return;
4405
+ if (type !== "test" && type !== "describe" && type !== "step") {
4406
+ return;
4407
+ }
4215
4408
  if (node.arguments.length > 0) {
4216
4409
  const titleArg = node.arguments[0];
4217
4410
  if (titleArg && titleArg.type === "Literal" && typeof titleArg.value === "string") {
@@ -4221,14 +4414,20 @@ var valid_test_tags_default = createRule({
4221
4414
  }
4222
4415
  }
4223
4416
  }
4224
- if (node.arguments.length < 2) return;
4417
+ if (node.arguments.length < 2) {
4418
+ return;
4419
+ }
4225
4420
  const optionsArg = node.arguments[1];
4226
- if (!optionsArg || optionsArg.type !== "ObjectExpression") return;
4421
+ if (!optionsArg || optionsArg.type !== "ObjectExpression") {
4422
+ return;
4423
+ }
4227
4424
  const tagProperty = optionsArg.properties.find(
4228
4425
  (prop) => prop.type === "Property" && !("argument" in prop) && // Ensure it's not a spread element
4229
4426
  prop.key.type === "Identifier" && prop.key.name === "tag"
4230
4427
  );
4231
- if (!tagProperty) return;
4428
+ if (!tagProperty) {
4429
+ return;
4430
+ }
4232
4431
  const tagValue = tagProperty.value;
4233
4432
  if (tagValue.type === "Literal") {
4234
4433
  if (typeof tagValue.value !== "string") {
@@ -4365,7 +4564,9 @@ var valid_title_default = createRule({
4365
4564
  }
4366
4565
  const [argument] = node.arguments;
4367
4566
  const title = dereference(context, argument) ?? argument;
4368
- if (!title) return;
4567
+ if (!title) {
4568
+ return;
4569
+ }
4369
4570
  if (!isStringNode(title)) {
4370
4571
  if (title.type === "BinaryExpression" && doesBinaryExpressionContainStringNode(title)) {
4371
4572
  return;
@@ -4414,15 +4615,15 @@ var valid_title_default = createRule({
4414
4615
  node: title
4415
4616
  });
4416
4617
  }
4417
- const [firstWord] = titleString.split(" ");
4618
+ const [firstWord, ...rest] = titleString.split(" ");
4418
4619
  if (firstWord.toLowerCase() === functionName) {
4419
4620
  context.report({
4420
- fix: (fixer) => [
4621
+ fix: rest.length > 0 ? (fixer) => [
4421
4622
  fixer.replaceTextRange(
4422
4623
  title.range,
4423
4624
  quoteStringValue(title).replace(/^([`'"]).+? /u, "$1")
4424
4625
  )
4425
- ],
4626
+ ] : void 0,
4426
4627
  messageId: "duplicatePrefix",
4427
4628
  node: title
4428
4629
  });
@@ -4594,6 +4795,8 @@ var sharedConfig = {
4594
4795
  "playwright/missing-playwright-await": "error",
4595
4796
  "playwright/no-conditional-expect": "warn",
4596
4797
  "playwright/no-conditional-in-test": "warn",
4798
+ "playwright/no-duplicate-hooks": "warn",
4799
+ "playwright/no-duplicate-slow": "warn",
4597
4800
  "playwright/no-element-handle": "warn",
4598
4801
  "playwright/no-eval": "warn",
4599
4802
  "playwright/no-focused-test": "error",
@@ -4610,6 +4813,11 @@ var sharedConfig = {
4610
4813
  "playwright/no-wait-for-navigation": "error",
4611
4814
  "playwright/no-wait-for-selector": "warn",
4612
4815
  "playwright/no-wait-for-timeout": "warn",
4816
+ "playwright/prefer-hooks-in-order": "warn",
4817
+ "playwright/prefer-hooks-on-top": "warn",
4818
+ "playwright/prefer-locator": "warn",
4819
+ "playwright/prefer-to-have-count": "warn",
4820
+ "playwright/prefer-to-have-length": "warn",
4613
4821
  "playwright/prefer-web-first-assertions": "error",
4614
4822
  "playwright/valid-describe-callback": "error",
4615
4823
  "playwright/valid-expect": "error",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-playwright",
3
- "version": "2.7.1",
3
+ "version": "2.9.0",
4
4
  "description": "ESLint plugin for Playwright testing.",
5
5
  "license": "MIT",
6
6
  "author": "Mark Skelton <mark@mskelton.dev>",
@@ -31,7 +31,7 @@
31
31
  "lint": "eslint .",
32
32
  "format": "oxfmt .",
33
33
  "format:check": "oxfmt --check .",
34
- "test": "vitest --reporter dot --hideSkippedTests",
34
+ "test": "vitest --hideSkippedTests",
35
35
  "typecheck": "tsc --noEmit",
36
36
  "ci": "yarn format:check && yarn lint && yarn typecheck"
37
37
  },