eslint-plugin-playwright 2.7.1 → 2.8.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 +2 -2
  2. package/dist/index.cjs +327 -124
  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
  }
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,8 +1720,9 @@ 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" || node.arguments[0]?.type === "Identifier") {
1618
1724
  return;
1725
+ }
1619
1726
  const method = getStringValue(node.callee.property);
1620
1727
  const arg = getStringValue(node.arguments[0]);
1621
1728
  const isLocator = isPageMethod(node, "locator") || method === "locator";
@@ -1673,7 +1780,7 @@ var no_restricted_locators_default = createRule({
1673
1780
  return;
1674
1781
  }
1675
1782
  for (const [restrictedType, message] of restrictionMap.entries()) {
1676
- if (isPageMethod(node, restrictedType)) {
1783
+ if (getStringValue(node.callee.property) === restrictedType) {
1677
1784
  context.report({
1678
1785
  data: {
1679
1786
  message: message ?? "",
@@ -1728,7 +1835,9 @@ var no_restricted_matchers_default = createRule({
1728
1835
  return {
1729
1836
  CallExpression(node) {
1730
1837
  const call = parseFnCall(context, node);
1731
- if (call?.type !== "expect") return;
1838
+ if (call?.type !== "expect") {
1839
+ return;
1840
+ }
1732
1841
  Object.entries(restrictedChains).map(([restriction, message]) => {
1733
1842
  const chain = call.members;
1734
1843
  const restrictionLinks = restriction.split(".").length;
@@ -1860,9 +1969,11 @@ var no_skipped_test_default = createRule({
1860
1969
  return;
1861
1970
  }
1862
1971
  const skipNode = call.members.find((s) => getStringValue(s) === "skip");
1863
- if (!skipNode) return;
1972
+ if (!skipNode) {
1973
+ return;
1974
+ }
1864
1975
  const isStandalone = call.type === "config";
1865
- if (isStandalone && allowConditional && (node.arguments.length !== 0 || findParent(node, "BlockStatement")?.parent?.type === "IfStatement")) {
1976
+ if (isStandalone && allowConditional && (node.arguments.length !== 0 || findParent(node, "BlockStatement")?.parent?.type === "IfStatement" || findParent(node, "SwitchCase") !== void 0)) {
1866
1977
  return;
1867
1978
  }
1868
1979
  context.report({
@@ -1922,7 +2033,9 @@ var no_slowed_test_default = createRule({
1922
2033
  return;
1923
2034
  }
1924
2035
  const slowNode = call.members.find((s) => getStringValue(s) === "slow");
1925
- if (!slowNode) return;
2036
+ if (!slowNode) {
2037
+ return;
2038
+ }
1926
2039
  const isStandalone = call.type === "config";
1927
2040
  if (isStandalone && allowConditional && (node.arguments.length !== 0 || findParent(node, "BlockStatement")?.parent?.type === "IfStatement")) {
1928
2041
  return;
@@ -2046,7 +2159,7 @@ var no_standalone_expect_default = createRule({
2046
2159
  },
2047
2160
  "CallExpression:exit"(node) {
2048
2161
  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")) {
2162
+ 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
2163
  callStack.pop();
2051
2164
  }
2052
2165
  }
@@ -2075,16 +2188,22 @@ var truthy = Boolean;
2075
2188
 
2076
2189
  // src/rules/no-unsafe-references.ts
2077
2190
  function collectVariables(scope) {
2078
- if (!scope || scope.type === "global") return [];
2191
+ if (!scope || scope.type === "global") {
2192
+ return [];
2193
+ }
2079
2194
  return [...collectVariables(scope.upper), ...scope.variables.map((ref) => ref.name)];
2080
2195
  }
2081
2196
  function addArgument(fixer, node, refs) {
2082
- if (!node.arguments.length) return;
2197
+ if (!node.arguments.length) {
2198
+ return;
2199
+ }
2083
2200
  if (node.arguments.length === 1) {
2084
2201
  return fixer.insertTextAfter(node.arguments[0], `, [${refs}]`);
2085
2202
  }
2086
2203
  const arg = node.arguments.at(-1);
2087
- if (!arg) return;
2204
+ if (!arg) {
2205
+ return;
2206
+ }
2088
2207
  if (arg.type !== "ArrayExpression") {
2089
2208
  return fixer.replaceText(arg, `[${getStringValue(arg)}, ${refs}]`);
2090
2209
  }
@@ -2110,9 +2229,13 @@ var no_unsafe_references_default = createRule({
2110
2229
  create(context) {
2111
2230
  return {
2112
2231
  CallExpression(node) {
2113
- if (!isPageMethod(node, "evaluate") && !isPageMethod(node, "addInitScript")) return;
2232
+ if (!isPageMethod(node, "evaluate") && !isPageMethod(node, "addInitScript")) {
2233
+ return;
2234
+ }
2114
2235
  const [fn] = node.arguments;
2115
- if (!fn || !isFunction(fn)) return;
2236
+ if (!fn || !isFunction(fn)) {
2237
+ return;
2238
+ }
2116
2239
  const { through, upper } = context.sourceCode.getScope(fn.body);
2117
2240
  const allRefs = new Set(collectVariables(upper));
2118
2241
  through.filter((ref) => {
@@ -2245,7 +2368,9 @@ var expectMatchers = /* @__PURE__ */ new Set([
2245
2368
  "toThrowError"
2246
2369
  ]);
2247
2370
  function isSupportedMethod(node) {
2248
- if (node.callee.type !== "MemberExpression") return false;
2371
+ if (node.callee.type !== "MemberExpression") {
2372
+ return false;
2373
+ }
2249
2374
  const name = getStringValue(node.callee.property);
2250
2375
  return locatorMethods.has(name) || pageMethods.has(name) && isPageMethod(node, name);
2251
2376
  }
@@ -2300,7 +2425,9 @@ function replaceAccessorFixer(fixer, node, text) {
2300
2425
  }
2301
2426
  function removePropertyFixer(fixer, property) {
2302
2427
  const parent = property.parent;
2303
- if (parent?.type !== "ObjectExpression") return;
2428
+ if (parent?.type !== "ObjectExpression") {
2429
+ return;
2430
+ }
2304
2431
  if (parent.properties.length === 1) {
2305
2432
  return fixer.remove(parent);
2306
2433
  }
@@ -2324,7 +2451,9 @@ var matcherConfig = {
2324
2451
  };
2325
2452
  function getOptions(call, name) {
2326
2453
  const [arg] = call.matcherArgs;
2327
- if (arg?.type !== "ObjectExpression") return;
2454
+ if (arg?.type !== "ObjectExpression") {
2455
+ return;
2456
+ }
2328
2457
  const property = arg.properties.find(
2329
2458
  (p) => p.type === "Property" && getStringValue(p.key) === name && isBooleanLiteral(p.value)
2330
2459
  );
@@ -2339,13 +2468,21 @@ var no_useless_not_default = createRule({
2339
2468
  return {
2340
2469
  CallExpression(node) {
2341
2470
  const call = parseFnCall(context, node);
2342
- if (call?.type !== "expect") return;
2471
+ if (call?.type !== "expect") {
2472
+ return;
2473
+ }
2343
2474
  const config = matcherConfig[call.matcherName];
2344
- if (!config) return;
2475
+ if (!config) {
2476
+ return;
2477
+ }
2345
2478
  const options = config.argName ? getOptions(call, config.argName) : void 0;
2346
- if (options?.arg && options.value === void 0) return;
2479
+ if (options?.arg && options.value === void 0) {
2480
+ return;
2481
+ }
2347
2482
  const notModifier = call.modifiers.find((mod) => getStringValue(mod) === "not");
2348
- if (!notModifier && !options?.property) return;
2483
+ if (!notModifier && !options?.property) {
2484
+ return;
2485
+ }
2349
2486
  const isInverted = !!notModifier !== (options?.value === false);
2350
2487
  const newMatcherName = isInverted ? config.inverse : call.matcherName;
2351
2488
  context.report({
@@ -2438,7 +2575,7 @@ var no_wait_for_selector_default = createRule({
2438
2575
  suggest: [
2439
2576
  {
2440
2577
  fix: (fixer) => fixer.remove(
2441
- node.parent && node.parent.type !== "AwaitExpression" ? node.parent : node.parent.parent
2578
+ node.parent && node.parent.type !== "AwaitExpression" && node.parent.type !== "VariableDeclarator" ? node.parent : node.parent.parent
2442
2579
  ),
2443
2580
  messageId: "removeWaitForSelector"
2444
2581
  }
@@ -2475,7 +2612,7 @@ var no_wait_for_timeout_default = createRule({
2475
2612
  suggest: [
2476
2613
  {
2477
2614
  fix: (fixer) => fixer.remove(
2478
- node.parent && node.parent.type !== "AwaitExpression" ? node.parent : node.parent.parent
2615
+ node.parent && node.parent.type !== "AwaitExpression" && node.parent.type !== "VariableDeclarator" ? node.parent : node.parent.parent
2479
2616
  ),
2480
2617
  messageId: "removeWaitForTimeout"
2481
2618
  }
@@ -2507,6 +2644,7 @@ var isString = (node) => {
2507
2644
  var isComparingToString = (expression) => {
2508
2645
  return isString(expression.left) || isString(expression.right);
2509
2646
  };
2647
+ var skipModifiers = /* @__PURE__ */ new Set(["not", "soft", "poll"]);
2510
2648
  var invertedOperators = {
2511
2649
  "<": ">=",
2512
2650
  "<=": ">",
@@ -2528,9 +2666,16 @@ var prefer_comparison_matcher_default = createRule({
2528
2666
  return {
2529
2667
  CallExpression(node) {
2530
2668
  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;
2669
+ if (call?.type !== "expect" || call.matcherArgs.length === 0) {
2670
+ return;
2671
+ }
2672
+ let expect = call.head.node.parent;
2673
+ while (expect?.type === "MemberExpression") {
2674
+ expect = expect.parent;
2675
+ }
2676
+ if (expect?.type !== "CallExpression") {
2677
+ return;
2678
+ }
2534
2679
  const [comparison] = expect.arguments;
2535
2680
  const expectCallEnd = expect.range[1];
2536
2681
  const [matcherArg] = call.matcherArgs;
@@ -2549,7 +2694,7 @@ var prefer_comparison_matcher_default = createRule({
2549
2694
  data: { preferredMatcher },
2550
2695
  fix(fixer) {
2551
2696
  const [modifier] = call.modifiers;
2552
- const modifierText = modifier && getStringValue(modifier) !== "not" ? `.${getStringValue(modifier)}` : "";
2697
+ const modifierText = modifier && !skipModifiers.has(getStringValue(modifier)) ? `.${getStringValue(modifier)}` : "";
2553
2698
  return [
2554
2699
  // Replace the comparison argument with the left-hand side of the comparison
2555
2700
  fixer.replaceText(comparison, context.sourceCode.getText(comparison.left)),
@@ -2588,9 +2733,13 @@ var prefer_equality_matcher_default = createRule({
2588
2733
  return {
2589
2734
  CallExpression(node) {
2590
2735
  const call = parseFnCall(context, node);
2591
- if (call?.type !== "expect" || call.matcherArgs.length === 0) return;
2736
+ if (call?.type !== "expect" || call.matcherArgs.length === 0) {
2737
+ return;
2738
+ }
2592
2739
  const expect = call.head.node.parent;
2593
- if (expect?.type !== "CallExpression") return;
2740
+ if (expect?.type !== "CallExpression") {
2741
+ return;
2742
+ }
2594
2743
  const [comparison] = expect.arguments;
2595
2744
  const expectCallEnd = expect.range[1];
2596
2745
  const [matcherArg] = call.matcherArgs;
@@ -2652,7 +2801,9 @@ var prefer_hooks_in_order_default = createRule({
2652
2801
  let inHook = false;
2653
2802
  return {
2654
2803
  "CallExpression"(node) {
2655
- if (inHook) return;
2804
+ if (inHook) {
2805
+ return;
2806
+ }
2656
2807
  const call = parseFnCall(context, node);
2657
2808
  if (call?.type !== "hook") {
2658
2809
  previousHookIndex = -1;
@@ -2679,7 +2830,9 @@ var prefer_hooks_in_order_default = createRule({
2679
2830
  inHook = false;
2680
2831
  return;
2681
2832
  }
2682
- if (inHook) return;
2833
+ if (inHook) {
2834
+ return;
2835
+ }
2683
2836
  previousHookIndex = -1;
2684
2837
  }
2685
2838
  };
@@ -2756,7 +2909,9 @@ var pageMethods2 = /* @__PURE__ */ new Set([
2756
2909
  "uncheck"
2757
2910
  ]);
2758
2911
  function isSupportedMethod2(node) {
2759
- if (node.callee.type !== "MemberExpression") return false;
2912
+ if (node.callee.type !== "MemberExpression") {
2913
+ return false;
2914
+ }
2760
2915
  const name = getStringValue(node.callee.property);
2761
2916
  return pageMethods2.has(name) && isPageMethod(node, name);
2762
2917
  }
@@ -2764,7 +2919,9 @@ var prefer_locator_default = createRule({
2764
2919
  create(context) {
2765
2920
  return {
2766
2921
  CallExpression(node) {
2767
- if (!isSupportedMethod2(node)) return;
2922
+ if (!isSupportedMethod2(node)) {
2923
+ return;
2924
+ }
2768
2925
  context.report({
2769
2926
  messageId: "preferLocator",
2770
2927
  node
@@ -2925,9 +3082,13 @@ var prefer_native_locators_default = createRule({
2925
3082
  const patterns = compilePatterns({ testIdAttribute });
2926
3083
  return {
2927
3084
  CallExpression(node) {
2928
- if (node.callee.type !== "MemberExpression") return;
3085
+ if (node.callee.type !== "MemberExpression") {
3086
+ return;
3087
+ }
2929
3088
  const query = getStringValue(node.arguments[0]);
2930
- if (!isPageMethod(node, "locator")) return;
3089
+ if (!isPageMethod(node, "locator")) {
3090
+ return;
3091
+ }
2931
3092
  for (const pattern of patterns) {
2932
3093
  const match = query.match(pattern.pattern);
2933
3094
  if (match) {
@@ -2985,7 +3146,9 @@ var prefer_strict_equal_default = createRule({
2985
3146
  return {
2986
3147
  CallExpression(node) {
2987
3148
  const call = parseFnCall(context, node);
2988
- if (call?.type !== "expect") return;
3149
+ if (call?.type !== "expect") {
3150
+ return;
3151
+ }
2989
3152
  if (call.matcherName === "toEqual") {
2990
3153
  context.report({
2991
3154
  messageId: "useToStrictEqual",
@@ -3053,7 +3216,9 @@ var prefer_to_be_default = createRule({
3053
3216
  return {
3054
3217
  CallExpression(node) {
3055
3218
  const call = parseFnCall(context, node);
3056
- if (call?.type !== "expect") return;
3219
+ if (call?.type !== "expect") {
3220
+ return;
3221
+ }
3057
3222
  const notMatchers = ["toBeUndefined", "toBeDefined"];
3058
3223
  const notModifier = call.modifiers.find((node2) => getStringValue(node2) === "not");
3059
3224
  if (notModifier && notMatchers.includes(call.matcherName)) {
@@ -3110,9 +3275,13 @@ var prefer_to_contain_default = createRule({
3110
3275
  return {
3111
3276
  CallExpression(node) {
3112
3277
  const call = parseFnCall(context, node);
3113
- if (call?.type !== "expect" || call.matcherArgs.length === 0) return;
3278
+ if (call?.type !== "expect" || call.matcherArgs.length === 0) {
3279
+ return;
3280
+ }
3114
3281
  const expect = call.head.node.parent;
3115
- if (expect?.type !== "CallExpression") return;
3282
+ if (expect?.type !== "CallExpression") {
3283
+ return;
3284
+ }
3116
3285
  const [includesCall] = expect.arguments;
3117
3286
  const { matcher } = call;
3118
3287
  const [matcherArg] = call.matcherArgs;
@@ -3307,19 +3476,29 @@ var prefer_web_first_assertions_default = createRule({
3307
3476
  return {
3308
3477
  CallExpression(node) {
3309
3478
  const fnCall = parseFnCall(context, node);
3310
- if (fnCall?.type !== "expect") return;
3479
+ if (fnCall?.type !== "expect") {
3480
+ return;
3481
+ }
3311
3482
  const expect = findParent(fnCall.head.node, "CallExpression");
3312
- if (!expect) return;
3483
+ if (!expect) {
3484
+ return;
3485
+ }
3313
3486
  const arg = dereference(context, fnCall.args[0]);
3314
- if (!arg) return;
3487
+ if (!arg) {
3488
+ return;
3489
+ }
3315
3490
  const call = arg.type === "AwaitExpression" ? arg.argument : arg;
3316
3491
  if (call.type !== "CallExpression" || call.callee.type !== "MemberExpression") {
3317
3492
  return;
3318
3493
  }
3319
- if (!supportedMatchers.has(fnCall.matcherName)) return;
3494
+ if (!supportedMatchers.has(fnCall.matcherName)) {
3495
+ return;
3496
+ }
3320
3497
  const method = getStringValue(call.callee.property);
3321
3498
  const methodConfig = methods3[method];
3322
- if (!Object.hasOwn(methods3, method)) return;
3499
+ if (!Object.hasOwn(methods3, method)) {
3500
+ return;
3501
+ }
3323
3502
  const notModifier = fnCall.modifiers.find((mod) => getStringValue(mod) === "not");
3324
3503
  const isFalsy = methodConfig.type === "boolean" && (!!fnCall.matcherArgs.length && isBooleanLiteral(fnCall.matcherArgs[0], false) || fnCall.matcherName === "toBeFalsy");
3325
3504
  const isInverse = methodConfig.inverse ? notModifier || isFalsy : notModifier && isFalsy;
@@ -3519,10 +3698,11 @@ function hasTagInOptions(node) {
3519
3698
  }
3520
3699
  function hasTagInTitle(node) {
3521
3700
  const title = node.arguments[0];
3522
- if (!title || title.type !== "Literal" || typeof title.value !== "string") {
3701
+ if (!title) {
3523
3702
  return false;
3524
3703
  }
3525
- return tagRegex.test(title.value);
3704
+ const value = getStringValue(title);
3705
+ return !!value && tagRegex.test(value);
3526
3706
  }
3527
3707
  function hasTags(node) {
3528
3708
  return hasTagInTitle(node) || hasTagInOptions(node);
@@ -3617,7 +3797,9 @@ var require_to_throw_message_default = createRule({
3617
3797
  return {
3618
3798
  CallExpression(node) {
3619
3799
  const call = parseFnCall(context, node);
3620
- if (call?.type !== "expect") return;
3800
+ if (call?.type !== "expect") {
3801
+ return;
3802
+ }
3621
3803
  if (call.matcherArgs.length === 0 && ["toThrow", "toThrowError"].includes(call.matcherName) && !call.modifiers.some((nod) => getStringValue(nod) === "not")) {
3622
3804
  context.report({
3623
3805
  data: { matcherName: call.matcherName },
@@ -3654,7 +3836,9 @@ var require_top_level_describe_default = createRule({
3654
3836
  return {
3655
3837
  "CallExpression"(node) {
3656
3838
  const call = parseFnCall(context, node);
3657
- if (!call) return;
3839
+ if (!call) {
3840
+ return;
3841
+ }
3658
3842
  if (call.type === "describe") {
3659
3843
  describeCount++;
3660
3844
  if (describeCount === 1) {
@@ -3723,7 +3907,9 @@ var valid_describe_callback_default = createRule({
3723
3907
  return {
3724
3908
  CallExpression(node) {
3725
3909
  const call = parseFnCall(context, node);
3726
- if (call?.group !== "describe") return;
3910
+ if (call?.group !== "describe") {
3911
+ return;
3912
+ }
3727
3913
  if (call.members.some((s) => getStringValue(s) === "configure")) {
3728
3914
  return;
3729
3915
  }
@@ -3826,7 +4012,9 @@ var isTestCaseCallWithCallbackArg = (context, node) => {
3826
4012
  };
3827
4013
  var isPromiseMethodThatUsesValue = (node, identifier) => {
3828
4014
  const name = getStringValue(identifier);
3829
- if (node.argument == null) return false;
4015
+ if (node.argument == null) {
4016
+ return false;
4017
+ }
3830
4018
  if (node.argument.type === "CallExpression" && node.argument.arguments.length > 0) {
3831
4019
  const nodeName = getNodeName(node.argument);
3832
4020
  if (["Promise.all", "Promise.allSettled"].includes(nodeName)) {
@@ -4083,7 +4271,9 @@ var valid_expect_default = createRule({
4083
4271
  return;
4084
4272
  }
4085
4273
  const { parent: expect } = call.head.node;
4086
- if (expect?.type !== "CallExpression") return;
4274
+ if (expect?.type !== "CallExpression") {
4275
+ return;
4276
+ }
4087
4277
  if (expect.arguments.length < minArgs) {
4088
4278
  const expectLength = getStringValue(call.head.node).length;
4089
4279
  const loc = {
@@ -4132,6 +4322,7 @@ var valid_expect_default = createRule({
4132
4322
  messages: {
4133
4323
  matcherNotCalled: "Matchers must be called to assert.",
4134
4324
  matcherNotFound: "Expect must have a corresponding matcher call.",
4325
+ modifierUnknown: "Expect has an unknown modifier.",
4135
4326
  notEnoughArgs: "Expect requires at least {{amount}} argument{{s}}.",
4136
4327
  tooManyArgs: "Expect takes at most {{amount}} argument{{s}}."
4137
4328
  },
@@ -4209,9 +4400,13 @@ var valid_test_tags_default = createRule({
4209
4400
  return {
4210
4401
  CallExpression(node) {
4211
4402
  const call = parseFnCall(context, node);
4212
- if (!call) return;
4403
+ if (!call) {
4404
+ return;
4405
+ }
4213
4406
  const { type } = call;
4214
- if (type !== "test" && type !== "describe" && type !== "step") return;
4407
+ if (type !== "test" && type !== "describe" && type !== "step") {
4408
+ return;
4409
+ }
4215
4410
  if (node.arguments.length > 0) {
4216
4411
  const titleArg = node.arguments[0];
4217
4412
  if (titleArg && titleArg.type === "Literal" && typeof titleArg.value === "string") {
@@ -4221,14 +4416,20 @@ var valid_test_tags_default = createRule({
4221
4416
  }
4222
4417
  }
4223
4418
  }
4224
- if (node.arguments.length < 2) return;
4419
+ if (node.arguments.length < 2) {
4420
+ return;
4421
+ }
4225
4422
  const optionsArg = node.arguments[1];
4226
- if (!optionsArg || optionsArg.type !== "ObjectExpression") return;
4423
+ if (!optionsArg || optionsArg.type !== "ObjectExpression") {
4424
+ return;
4425
+ }
4227
4426
  const tagProperty = optionsArg.properties.find(
4228
4427
  (prop) => prop.type === "Property" && !("argument" in prop) && // Ensure it's not a spread element
4229
4428
  prop.key.type === "Identifier" && prop.key.name === "tag"
4230
4429
  );
4231
- if (!tagProperty) return;
4430
+ if (!tagProperty) {
4431
+ return;
4432
+ }
4232
4433
  const tagValue = tagProperty.value;
4233
4434
  if (tagValue.type === "Literal") {
4234
4435
  if (typeof tagValue.value !== "string") {
@@ -4365,7 +4566,9 @@ var valid_title_default = createRule({
4365
4566
  }
4366
4567
  const [argument] = node.arguments;
4367
4568
  const title = dereference(context, argument) ?? argument;
4368
- if (!title) return;
4569
+ if (!title) {
4570
+ return;
4571
+ }
4369
4572
  if (!isStringNode(title)) {
4370
4573
  if (title.type === "BinaryExpression" && doesBinaryExpressionContainStringNode(title)) {
4371
4574
  return;
@@ -4414,15 +4617,15 @@ var valid_title_default = createRule({
4414
4617
  node: title
4415
4618
  });
4416
4619
  }
4417
- const [firstWord] = titleString.split(" ");
4620
+ const [firstWord, ...rest] = titleString.split(" ");
4418
4621
  if (firstWord.toLowerCase() === functionName) {
4419
4622
  context.report({
4420
- fix: (fixer) => [
4623
+ fix: rest.length > 0 ? (fixer) => [
4421
4624
  fixer.replaceTextRange(
4422
4625
  title.range,
4423
4626
  quoteStringValue(title).replace(/^([`'"]).+? /u, "$1")
4424
4627
  )
4425
- ],
4628
+ ] : void 0,
4426
4629
  messageId: "duplicatePrefix",
4427
4630
  node: title
4428
4631
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-playwright",
3
- "version": "2.7.1",
3
+ "version": "2.8.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
  },