eslint-plugin-playwright 2.7.0 → 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 +3 -3
  2. package/dist/index.cjs +329 -123
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # ESLint Plugin Playwright
2
2
 
3
- [![Test](https://github.com/mskelton/eslint-plugin-playwright/actions/workflows/test.yml/badge.svg)](https://github.com/mskelton/eslint-plugin-playwright/actions/workflows/test.yml)
3
+ [![CI](https://github.com/mskelton/eslint-plugin-playwright/actions/workflows/ci.yml/badge.svg)](https://github.com/mskelton/eslint-plugin-playwright/actions/workflows/ci.yml)
4
4
  [![npm](https://img.shields.io/npm/v/eslint-plugin-playwright)](https://www.npmjs.com/package/eslint-plugin-playwright)
5
5
  [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
6
6
 
@@ -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,26 +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;
977
+ if (validTypes.has(parent.type)) {
978
+ return true;
979
+ }
980
+ if (isPromiseAccessor(parent) && parent.parent?.type === "CallExpression") {
981
+ return checkValidity(parent.parent, visited);
982
+ }
983
+ if (parent.type === "CallExpression" && parent.callee === node && isPromiseAccessor(node)) {
984
+ return checkValidity(parent, visited);
985
+ }
881
986
  if (parent.type === "ArrayExpression") {
882
987
  return checkValidity(parent, visited);
883
988
  }
989
+ if (parent.type === "ConditionalExpression") {
990
+ return checkValidity(parent, visited);
991
+ }
992
+ if (parent.type === "SpreadElement") {
993
+ return checkValidity(parent, visited);
994
+ }
884
995
  if (parent.type === "CallExpression" && parent.callee.type === "MemberExpression" && isIdentifier(parent.callee.object, "Promise") && isIdentifier(parent.callee.property, "all")) {
885
996
  return true;
886
997
  }
887
998
  if (parent.type === "VariableDeclarator") {
888
- const scope = context.sourceCode.getScope(parent.parent);
889
- for (const ref of scope.references) {
890
- const refParent = ref.identifier.parent;
891
- if (visited.has(refParent)) continue;
892
- if (validTypes.has(refParent.type)) return true;
893
- if (checkValidity(refParent, visited)) return true;
894
- }
999
+ return isVariableConsumed(parent, checkValidity, validTypes, visited);
895
1000
  }
896
1001
  return false;
897
1002
  }
@@ -909,7 +1014,9 @@ var missing_playwright_await_default = createRule({
909
1014
  return;
910
1015
  }
911
1016
  const call = parseFnCall(context, node);
912
- if (call?.type !== "step" && call?.type !== "expect") return;
1017
+ if (call?.type !== "step" && call?.type !== "expect") {
1018
+ return;
1019
+ }
913
1020
  const result = getCallType(call, awaitableMatchers);
914
1021
  const isValid = result ? checkValidity(node, /* @__PURE__ */ new Set()) : false;
915
1022
  if (result && !isValid) {
@@ -967,7 +1074,9 @@ function hasTests(context, node) {
967
1074
  var no_commented_out_tests_default = createRule({
968
1075
  create(context) {
969
1076
  function checkNode(node) {
970
- if (!hasTests(context, node)) return;
1077
+ if (!hasTests(context, node)) {
1078
+ return;
1079
+ }
971
1080
  context.report({
972
1081
  messageId: "commentedTests",
973
1082
  node
@@ -994,18 +1103,6 @@ var no_commented_out_tests_default = createRule({
994
1103
 
995
1104
  // src/rules/no-conditional-expect.ts
996
1105
  var isCatchCall = (node) => node.callee.type === "MemberExpression" && isPropertyAccessor(node.callee, "catch");
997
- var getTestCallExpressionsFromDeclaredVariables = (context, declaredVariables) => {
998
- return declaredVariables.reduce(
999
- (acc, { references }) => [
1000
- ...acc,
1001
- ...references.map(({ identifier }) => identifier.parent).filter(
1002
- // ESLint types are infurating
1003
- (node) => node?.type === "CallExpression" && isTypeOfFnCall(context, node, ["test"])
1004
- )
1005
- ],
1006
- []
1007
- );
1008
- };
1009
1106
  var no_conditional_expect_default = createRule({
1010
1107
  create(context) {
1011
1108
  let conditionalDepth = 0;
@@ -1047,16 +1144,6 @@ var no_conditional_expect_default = createRule({
1047
1144
  "CatchClause:exit": decreaseConditionalDepth,
1048
1145
  "ConditionalExpression": increaseConditionalDepth,
1049
1146
  "ConditionalExpression:exit": decreaseConditionalDepth,
1050
- "FunctionDeclaration"(node) {
1051
- const declaredVariables = context.sourceCode.getDeclaredVariables(node);
1052
- const testCallExpressions = getTestCallExpressionsFromDeclaredVariables(
1053
- context,
1054
- declaredVariables
1055
- );
1056
- if (testCallExpressions.length > 0) {
1057
- inTestCase = true;
1058
- }
1059
- },
1060
1147
  "IfStatement": increaseConditionalDepth,
1061
1148
  "IfStatement:exit": decreaseConditionalDepth,
1062
1149
  "LogicalExpression": increaseConditionalDepth,
@@ -1086,11 +1173,15 @@ var no_conditional_in_test_default = createRule({
1086
1173
  return;
1087
1174
  }
1088
1175
  const call = findParent(node, "CallExpression");
1089
- if (!call) return;
1176
+ if (!call) {
1177
+ return;
1178
+ }
1090
1179
  if (isTypeOfFnCall(context, call, ["test", "step"])) {
1091
1180
  const testFunction = call.arguments[call.arguments.length - 1];
1092
1181
  const functionBody = findParent(node, "BlockStatement");
1093
- if (!functionBody) return;
1182
+ if (!functionBody) {
1183
+ return;
1184
+ }
1094
1185
  let currentParent = functionBody.parent;
1095
1186
  while (currentParent && currentParent !== testFunction) {
1096
1187
  currentParent = currentParent.parent;
@@ -1128,7 +1219,9 @@ var no_duplicate_hooks_default = createRule({
1128
1219
  return {
1129
1220
  "CallExpression"(node) {
1130
1221
  const call = parseFnCall(context, node);
1131
- if (!call) return;
1222
+ if (!call) {
1223
+ return;
1224
+ }
1132
1225
  if (call.type === "describe") {
1133
1226
  hookContexts.push({});
1134
1227
  }
@@ -1174,7 +1267,9 @@ var no_duplicate_slow_default = createRule({
1174
1267
  return {
1175
1268
  "CallExpression"(node) {
1176
1269
  const call = parseFnCall(context, node);
1177
- if (!call) return;
1270
+ if (!call) {
1271
+ return;
1272
+ }
1178
1273
  if (call.type === "test" || call.type === "describe") {
1179
1274
  scopes.push(scopes[scopes.length - 1]);
1180
1275
  }
@@ -1295,7 +1390,9 @@ var no_focused_test_default = createRule({
1295
1390
  return;
1296
1391
  }
1297
1392
  const onlyNode = call.members.find((s) => getStringValue(s) === "only");
1298
- if (!onlyNode) return;
1393
+ if (!onlyNode) {
1394
+ return;
1395
+ }
1299
1396
  context.report({
1300
1397
  messageId: "noFocusedTest",
1301
1398
  node: onlyNode,
@@ -1380,7 +1477,7 @@ var no_get_by_title_default = createRule({
1380
1477
  create(context) {
1381
1478
  return {
1382
1479
  CallExpression(node) {
1383
- if (isPageMethod(node, "getByTitle")) {
1480
+ if (node.callee.type === "MemberExpression" && getStringValue(node.callee.property) === "getByTitle") {
1384
1481
  context.report({ messageId: "noGetByTitle", node });
1385
1482
  }
1386
1483
  }
@@ -1409,7 +1506,9 @@ var no_hooks_default = createRule({
1409
1506
  return {
1410
1507
  CallExpression(node) {
1411
1508
  const call = parseFnCall(context, node);
1412
- if (!call) return;
1509
+ if (!call) {
1510
+ return;
1511
+ }
1413
1512
  if (call.type === "hook" && !options.allow.includes(call.name)) {
1414
1513
  context.report({
1415
1514
  data: { hookName: call.name },
@@ -1504,20 +1603,26 @@ var no_networkidle_default = createRule({
1504
1603
  create(context) {
1505
1604
  return {
1506
1605
  CallExpression(node) {
1507
- if (node.callee.type !== "MemberExpression") return;
1606
+ if (node.callee.type !== "MemberExpression") {
1607
+ return;
1608
+ }
1508
1609
  const methodName = getStringValue(node.callee.property);
1509
- if (!methods.has(methodName)) return;
1610
+ if (!methods.has(methodName)) {
1611
+ return;
1612
+ }
1510
1613
  if (methodName === "waitForLoadState") {
1511
1614
  const arg = node.arguments[0];
1512
- if (arg && isStringLiteral(arg, "networkidle")) {
1615
+ if (arg && isStringNode(arg, "networkidle")) {
1513
1616
  context.report({ messageId, node: arg });
1514
1617
  }
1515
1618
  return;
1516
1619
  }
1517
1620
  if (node.arguments.length >= 2) {
1518
1621
  const [_, arg] = node.arguments;
1519
- if (arg.type !== "ObjectExpression") return;
1520
- 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"));
1521
1626
  if (property) {
1522
1627
  context.report({ messageId, node: property.value });
1523
1628
  }
@@ -1544,9 +1649,13 @@ var no_nth_methods_default = createRule({
1544
1649
  create(context) {
1545
1650
  return {
1546
1651
  CallExpression(node) {
1547
- if (node.callee.type !== "MemberExpression") return;
1652
+ if (node.callee.type !== "MemberExpression") {
1653
+ return;
1654
+ }
1548
1655
  const method = getStringValue(node.callee.property);
1549
- if (!methods2.has(method)) return;
1656
+ if (!methods2.has(method)) {
1657
+ return;
1658
+ }
1550
1659
  context.report({
1551
1660
  data: { method },
1552
1661
  loc: {
@@ -1611,8 +1720,9 @@ var no_raw_locators_default = createRule({
1611
1720
  }
1612
1721
  return {
1613
1722
  CallExpression(node) {
1614
- if (node.callee.type !== "MemberExpression" || node.arguments[0]?.type === "Identifier")
1723
+ if (node.callee.type !== "MemberExpression" || node.arguments[0]?.type === "Identifier") {
1615
1724
  return;
1725
+ }
1616
1726
  const method = getStringValue(node.callee.property);
1617
1727
  const arg = getStringValue(node.arguments[0]);
1618
1728
  const isLocator = isPageMethod(node, "locator") || method === "locator";
@@ -1670,7 +1780,7 @@ var no_restricted_locators_default = createRule({
1670
1780
  return;
1671
1781
  }
1672
1782
  for (const [restrictedType, message] of restrictionMap.entries()) {
1673
- if (isPageMethod(node, restrictedType)) {
1783
+ if (getStringValue(node.callee.property) === restrictedType) {
1674
1784
  context.report({
1675
1785
  data: {
1676
1786
  message: message ?? "",
@@ -1725,7 +1835,9 @@ var no_restricted_matchers_default = createRule({
1725
1835
  return {
1726
1836
  CallExpression(node) {
1727
1837
  const call = parseFnCall(context, node);
1728
- if (call?.type !== "expect") return;
1838
+ if (call?.type !== "expect") {
1839
+ return;
1840
+ }
1729
1841
  Object.entries(restrictedChains).map(([restriction, message]) => {
1730
1842
  const chain = call.members;
1731
1843
  const restrictionLinks = restriction.split(".").length;
@@ -1857,9 +1969,11 @@ var no_skipped_test_default = createRule({
1857
1969
  return;
1858
1970
  }
1859
1971
  const skipNode = call.members.find((s) => getStringValue(s) === "skip");
1860
- if (!skipNode) return;
1972
+ if (!skipNode) {
1973
+ return;
1974
+ }
1861
1975
  const isStandalone = call.type === "config";
1862
- 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)) {
1863
1977
  return;
1864
1978
  }
1865
1979
  context.report({
@@ -1919,7 +2033,9 @@ var no_slowed_test_default = createRule({
1919
2033
  return;
1920
2034
  }
1921
2035
  const slowNode = call.members.find((s) => getStringValue(s) === "slow");
1922
- if (!slowNode) return;
2036
+ if (!slowNode) {
2037
+ return;
2038
+ }
1923
2039
  const isStandalone = call.type === "config";
1924
2040
  if (isStandalone && allowConditional && (node.arguments.length !== 0 || findParent(node, "BlockStatement")?.parent?.type === "IfStatement")) {
1925
2041
  return;
@@ -2043,7 +2159,7 @@ var no_standalone_expect_default = createRule({
2043
2159
  },
2044
2160
  "CallExpression:exit"(node) {
2045
2161
  const top = callStack.at(-1);
2046
- 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")) {
2047
2163
  callStack.pop();
2048
2164
  }
2049
2165
  }
@@ -2072,16 +2188,22 @@ var truthy = Boolean;
2072
2188
 
2073
2189
  // src/rules/no-unsafe-references.ts
2074
2190
  function collectVariables(scope) {
2075
- if (!scope || scope.type === "global") return [];
2191
+ if (!scope || scope.type === "global") {
2192
+ return [];
2193
+ }
2076
2194
  return [...collectVariables(scope.upper), ...scope.variables.map((ref) => ref.name)];
2077
2195
  }
2078
2196
  function addArgument(fixer, node, refs) {
2079
- if (!node.arguments.length) return;
2197
+ if (!node.arguments.length) {
2198
+ return;
2199
+ }
2080
2200
  if (node.arguments.length === 1) {
2081
2201
  return fixer.insertTextAfter(node.arguments[0], `, [${refs}]`);
2082
2202
  }
2083
2203
  const arg = node.arguments.at(-1);
2084
- if (!arg) return;
2204
+ if (!arg) {
2205
+ return;
2206
+ }
2085
2207
  if (arg.type !== "ArrayExpression") {
2086
2208
  return fixer.replaceText(arg, `[${getStringValue(arg)}, ${refs}]`);
2087
2209
  }
@@ -2107,9 +2229,13 @@ var no_unsafe_references_default = createRule({
2107
2229
  create(context) {
2108
2230
  return {
2109
2231
  CallExpression(node) {
2110
- if (!isPageMethod(node, "evaluate") && !isPageMethod(node, "addInitScript")) return;
2232
+ if (!isPageMethod(node, "evaluate") && !isPageMethod(node, "addInitScript")) {
2233
+ return;
2234
+ }
2111
2235
  const [fn] = node.arguments;
2112
- if (!fn || !isFunction(fn)) return;
2236
+ if (!fn || !isFunction(fn)) {
2237
+ return;
2238
+ }
2113
2239
  const { through, upper } = context.sourceCode.getScope(fn.body);
2114
2240
  const allRefs = new Set(collectVariables(upper));
2115
2241
  through.filter((ref) => {
@@ -2242,7 +2368,9 @@ var expectMatchers = /* @__PURE__ */ new Set([
2242
2368
  "toThrowError"
2243
2369
  ]);
2244
2370
  function isSupportedMethod(node) {
2245
- if (node.callee.type !== "MemberExpression") return false;
2371
+ if (node.callee.type !== "MemberExpression") {
2372
+ return false;
2373
+ }
2246
2374
  const name = getStringValue(node.callee.property);
2247
2375
  return locatorMethods.has(name) || pageMethods.has(name) && isPageMethod(node, name);
2248
2376
  }
@@ -2297,7 +2425,9 @@ function replaceAccessorFixer(fixer, node, text) {
2297
2425
  }
2298
2426
  function removePropertyFixer(fixer, property) {
2299
2427
  const parent = property.parent;
2300
- if (parent?.type !== "ObjectExpression") return;
2428
+ if (parent?.type !== "ObjectExpression") {
2429
+ return;
2430
+ }
2301
2431
  if (parent.properties.length === 1) {
2302
2432
  return fixer.remove(parent);
2303
2433
  }
@@ -2321,7 +2451,9 @@ var matcherConfig = {
2321
2451
  };
2322
2452
  function getOptions(call, name) {
2323
2453
  const [arg] = call.matcherArgs;
2324
- if (arg?.type !== "ObjectExpression") return;
2454
+ if (arg?.type !== "ObjectExpression") {
2455
+ return;
2456
+ }
2325
2457
  const property = arg.properties.find(
2326
2458
  (p) => p.type === "Property" && getStringValue(p.key) === name && isBooleanLiteral(p.value)
2327
2459
  );
@@ -2336,13 +2468,21 @@ var no_useless_not_default = createRule({
2336
2468
  return {
2337
2469
  CallExpression(node) {
2338
2470
  const call = parseFnCall(context, node);
2339
- if (call?.type !== "expect") return;
2471
+ if (call?.type !== "expect") {
2472
+ return;
2473
+ }
2340
2474
  const config = matcherConfig[call.matcherName];
2341
- if (!config) return;
2475
+ if (!config) {
2476
+ return;
2477
+ }
2342
2478
  const options = config.argName ? getOptions(call, config.argName) : void 0;
2343
- if (options?.arg && options.value === void 0) return;
2479
+ if (options?.arg && options.value === void 0) {
2480
+ return;
2481
+ }
2344
2482
  const notModifier = call.modifiers.find((mod) => getStringValue(mod) === "not");
2345
- if (!notModifier && !options?.property) return;
2483
+ if (!notModifier && !options?.property) {
2484
+ return;
2485
+ }
2346
2486
  const isInverted = !!notModifier !== (options?.value === false);
2347
2487
  const newMatcherName = isInverted ? config.inverse : call.matcherName;
2348
2488
  context.report({
@@ -2435,7 +2575,7 @@ var no_wait_for_selector_default = createRule({
2435
2575
  suggest: [
2436
2576
  {
2437
2577
  fix: (fixer) => fixer.remove(
2438
- 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
2439
2579
  ),
2440
2580
  messageId: "removeWaitForSelector"
2441
2581
  }
@@ -2472,7 +2612,7 @@ var no_wait_for_timeout_default = createRule({
2472
2612
  suggest: [
2473
2613
  {
2474
2614
  fix: (fixer) => fixer.remove(
2475
- 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
2476
2616
  ),
2477
2617
  messageId: "removeWaitForTimeout"
2478
2618
  }
@@ -2504,6 +2644,7 @@ var isString = (node) => {
2504
2644
  var isComparingToString = (expression) => {
2505
2645
  return isString(expression.left) || isString(expression.right);
2506
2646
  };
2647
+ var skipModifiers = /* @__PURE__ */ new Set(["not", "soft", "poll"]);
2507
2648
  var invertedOperators = {
2508
2649
  "<": ">=",
2509
2650
  "<=": ">",
@@ -2525,9 +2666,16 @@ var prefer_comparison_matcher_default = createRule({
2525
2666
  return {
2526
2667
  CallExpression(node) {
2527
2668
  const call = parseFnCall(context, node);
2528
- if (call?.type !== "expect" || call.matcherArgs.length === 0) return;
2529
- const expect = call.head.node.parent;
2530
- 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
+ }
2531
2679
  const [comparison] = expect.arguments;
2532
2680
  const expectCallEnd = expect.range[1];
2533
2681
  const [matcherArg] = call.matcherArgs;
@@ -2546,7 +2694,7 @@ var prefer_comparison_matcher_default = createRule({
2546
2694
  data: { preferredMatcher },
2547
2695
  fix(fixer) {
2548
2696
  const [modifier] = call.modifiers;
2549
- const modifierText = modifier && getStringValue(modifier) !== "not" ? `.${getStringValue(modifier)}` : "";
2697
+ const modifierText = modifier && !skipModifiers.has(getStringValue(modifier)) ? `.${getStringValue(modifier)}` : "";
2550
2698
  return [
2551
2699
  // Replace the comparison argument with the left-hand side of the comparison
2552
2700
  fixer.replaceText(comparison, context.sourceCode.getText(comparison.left)),
@@ -2585,9 +2733,13 @@ var prefer_equality_matcher_default = createRule({
2585
2733
  return {
2586
2734
  CallExpression(node) {
2587
2735
  const call = parseFnCall(context, node);
2588
- if (call?.type !== "expect" || call.matcherArgs.length === 0) return;
2736
+ if (call?.type !== "expect" || call.matcherArgs.length === 0) {
2737
+ return;
2738
+ }
2589
2739
  const expect = call.head.node.parent;
2590
- if (expect?.type !== "CallExpression") return;
2740
+ if (expect?.type !== "CallExpression") {
2741
+ return;
2742
+ }
2591
2743
  const [comparison] = expect.arguments;
2592
2744
  const expectCallEnd = expect.range[1];
2593
2745
  const [matcherArg] = call.matcherArgs;
@@ -2649,7 +2801,9 @@ var prefer_hooks_in_order_default = createRule({
2649
2801
  let inHook = false;
2650
2802
  return {
2651
2803
  "CallExpression"(node) {
2652
- if (inHook) return;
2804
+ if (inHook) {
2805
+ return;
2806
+ }
2653
2807
  const call = parseFnCall(context, node);
2654
2808
  if (call?.type !== "hook") {
2655
2809
  previousHookIndex = -1;
@@ -2676,7 +2830,9 @@ var prefer_hooks_in_order_default = createRule({
2676
2830
  inHook = false;
2677
2831
  return;
2678
2832
  }
2679
- if (inHook) return;
2833
+ if (inHook) {
2834
+ return;
2835
+ }
2680
2836
  previousHookIndex = -1;
2681
2837
  }
2682
2838
  };
@@ -2753,7 +2909,9 @@ var pageMethods2 = /* @__PURE__ */ new Set([
2753
2909
  "uncheck"
2754
2910
  ]);
2755
2911
  function isSupportedMethod2(node) {
2756
- if (node.callee.type !== "MemberExpression") return false;
2912
+ if (node.callee.type !== "MemberExpression") {
2913
+ return false;
2914
+ }
2757
2915
  const name = getStringValue(node.callee.property);
2758
2916
  return pageMethods2.has(name) && isPageMethod(node, name);
2759
2917
  }
@@ -2761,7 +2919,9 @@ var prefer_locator_default = createRule({
2761
2919
  create(context) {
2762
2920
  return {
2763
2921
  CallExpression(node) {
2764
- if (!isSupportedMethod2(node)) return;
2922
+ if (!isSupportedMethod2(node)) {
2923
+ return;
2924
+ }
2765
2925
  context.report({
2766
2926
  messageId: "preferLocator",
2767
2927
  node
@@ -2922,9 +3082,13 @@ var prefer_native_locators_default = createRule({
2922
3082
  const patterns = compilePatterns({ testIdAttribute });
2923
3083
  return {
2924
3084
  CallExpression(node) {
2925
- if (node.callee.type !== "MemberExpression") return;
3085
+ if (node.callee.type !== "MemberExpression") {
3086
+ return;
3087
+ }
2926
3088
  const query = getStringValue(node.arguments[0]);
2927
- if (!isPageMethod(node, "locator")) return;
3089
+ if (!isPageMethod(node, "locator")) {
3090
+ return;
3091
+ }
2928
3092
  for (const pattern of patterns) {
2929
3093
  const match = query.match(pattern.pattern);
2930
3094
  if (match) {
@@ -2982,7 +3146,9 @@ var prefer_strict_equal_default = createRule({
2982
3146
  return {
2983
3147
  CallExpression(node) {
2984
3148
  const call = parseFnCall(context, node);
2985
- if (call?.type !== "expect") return;
3149
+ if (call?.type !== "expect") {
3150
+ return;
3151
+ }
2986
3152
  if (call.matcherName === "toEqual") {
2987
3153
  context.report({
2988
3154
  messageId: "useToStrictEqual",
@@ -3050,7 +3216,9 @@ var prefer_to_be_default = createRule({
3050
3216
  return {
3051
3217
  CallExpression(node) {
3052
3218
  const call = parseFnCall(context, node);
3053
- if (call?.type !== "expect") return;
3219
+ if (call?.type !== "expect") {
3220
+ return;
3221
+ }
3054
3222
  const notMatchers = ["toBeUndefined", "toBeDefined"];
3055
3223
  const notModifier = call.modifiers.find((node2) => getStringValue(node2) === "not");
3056
3224
  if (notModifier && notMatchers.includes(call.matcherName)) {
@@ -3107,9 +3275,13 @@ var prefer_to_contain_default = createRule({
3107
3275
  return {
3108
3276
  CallExpression(node) {
3109
3277
  const call = parseFnCall(context, node);
3110
- if (call?.type !== "expect" || call.matcherArgs.length === 0) return;
3278
+ if (call?.type !== "expect" || call.matcherArgs.length === 0) {
3279
+ return;
3280
+ }
3111
3281
  const expect = call.head.node.parent;
3112
- if (expect?.type !== "CallExpression") return;
3282
+ if (expect?.type !== "CallExpression") {
3283
+ return;
3284
+ }
3113
3285
  const [includesCall] = expect.arguments;
3114
3286
  const { matcher } = call;
3115
3287
  const [matcherArg] = call.matcherArgs;
@@ -3304,19 +3476,29 @@ var prefer_web_first_assertions_default = createRule({
3304
3476
  return {
3305
3477
  CallExpression(node) {
3306
3478
  const fnCall = parseFnCall(context, node);
3307
- if (fnCall?.type !== "expect") return;
3479
+ if (fnCall?.type !== "expect") {
3480
+ return;
3481
+ }
3308
3482
  const expect = findParent(fnCall.head.node, "CallExpression");
3309
- if (!expect) return;
3483
+ if (!expect) {
3484
+ return;
3485
+ }
3310
3486
  const arg = dereference(context, fnCall.args[0]);
3311
- if (!arg) return;
3487
+ if (!arg) {
3488
+ return;
3489
+ }
3312
3490
  const call = arg.type === "AwaitExpression" ? arg.argument : arg;
3313
3491
  if (call.type !== "CallExpression" || call.callee.type !== "MemberExpression") {
3314
3492
  return;
3315
3493
  }
3316
- if (!supportedMatchers.has(fnCall.matcherName)) return;
3494
+ if (!supportedMatchers.has(fnCall.matcherName)) {
3495
+ return;
3496
+ }
3317
3497
  const method = getStringValue(call.callee.property);
3318
3498
  const methodConfig = methods3[method];
3319
- if (!Object.hasOwn(methods3, method)) return;
3499
+ if (!Object.hasOwn(methods3, method)) {
3500
+ return;
3501
+ }
3320
3502
  const notModifier = fnCall.modifiers.find((mod) => getStringValue(mod) === "not");
3321
3503
  const isFalsy = methodConfig.type === "boolean" && (!!fnCall.matcherArgs.length && isBooleanLiteral(fnCall.matcherArgs[0], false) || fnCall.matcherName === "toBeFalsy");
3322
3504
  const isInverse = methodConfig.inverse ? notModifier || isFalsy : notModifier && isFalsy;
@@ -3516,10 +3698,11 @@ function hasTagInOptions(node) {
3516
3698
  }
3517
3699
  function hasTagInTitle(node) {
3518
3700
  const title = node.arguments[0];
3519
- if (!title || title.type !== "Literal" || typeof title.value !== "string") {
3701
+ if (!title) {
3520
3702
  return false;
3521
3703
  }
3522
- return tagRegex.test(title.value);
3704
+ const value = getStringValue(title);
3705
+ return !!value && tagRegex.test(value);
3523
3706
  }
3524
3707
  function hasTags(node) {
3525
3708
  return hasTagInTitle(node) || hasTagInOptions(node);
@@ -3614,7 +3797,9 @@ var require_to_throw_message_default = createRule({
3614
3797
  return {
3615
3798
  CallExpression(node) {
3616
3799
  const call = parseFnCall(context, node);
3617
- if (call?.type !== "expect") return;
3800
+ if (call?.type !== "expect") {
3801
+ return;
3802
+ }
3618
3803
  if (call.matcherArgs.length === 0 && ["toThrow", "toThrowError"].includes(call.matcherName) && !call.modifiers.some((nod) => getStringValue(nod) === "not")) {
3619
3804
  context.report({
3620
3805
  data: { matcherName: call.matcherName },
@@ -3651,7 +3836,9 @@ var require_top_level_describe_default = createRule({
3651
3836
  return {
3652
3837
  "CallExpression"(node) {
3653
3838
  const call = parseFnCall(context, node);
3654
- if (!call) return;
3839
+ if (!call) {
3840
+ return;
3841
+ }
3655
3842
  if (call.type === "describe") {
3656
3843
  describeCount++;
3657
3844
  if (describeCount === 1) {
@@ -3720,7 +3907,9 @@ var valid_describe_callback_default = createRule({
3720
3907
  return {
3721
3908
  CallExpression(node) {
3722
3909
  const call = parseFnCall(context, node);
3723
- if (call?.group !== "describe") return;
3910
+ if (call?.group !== "describe") {
3911
+ return;
3912
+ }
3724
3913
  if (call.members.some((s) => getStringValue(s) === "configure")) {
3725
3914
  return;
3726
3915
  }
@@ -3823,7 +4012,9 @@ var isTestCaseCallWithCallbackArg = (context, node) => {
3823
4012
  };
3824
4013
  var isPromiseMethodThatUsesValue = (node, identifier) => {
3825
4014
  const name = getStringValue(identifier);
3826
- if (node.argument == null) return false;
4015
+ if (node.argument == null) {
4016
+ return false;
4017
+ }
3827
4018
  if (node.argument.type === "CallExpression" && node.argument.arguments.length > 0) {
3828
4019
  const nodeName = getNodeName(node.argument);
3829
4020
  if (["Promise.all", "Promise.allSettled"].includes(nodeName)) {
@@ -4080,7 +4271,9 @@ var valid_expect_default = createRule({
4080
4271
  return;
4081
4272
  }
4082
4273
  const { parent: expect } = call.head.node;
4083
- if (expect?.type !== "CallExpression") return;
4274
+ if (expect?.type !== "CallExpression") {
4275
+ return;
4276
+ }
4084
4277
  if (expect.arguments.length < minArgs) {
4085
4278
  const expectLength = getStringValue(call.head.node).length;
4086
4279
  const loc = {
@@ -4129,6 +4322,7 @@ var valid_expect_default = createRule({
4129
4322
  messages: {
4130
4323
  matcherNotCalled: "Matchers must be called to assert.",
4131
4324
  matcherNotFound: "Expect must have a corresponding matcher call.",
4325
+ modifierUnknown: "Expect has an unknown modifier.",
4132
4326
  notEnoughArgs: "Expect requires at least {{amount}} argument{{s}}.",
4133
4327
  tooManyArgs: "Expect takes at most {{amount}} argument{{s}}."
4134
4328
  },
@@ -4206,9 +4400,13 @@ var valid_test_tags_default = createRule({
4206
4400
  return {
4207
4401
  CallExpression(node) {
4208
4402
  const call = parseFnCall(context, node);
4209
- if (!call) return;
4403
+ if (!call) {
4404
+ return;
4405
+ }
4210
4406
  const { type } = call;
4211
- if (type !== "test" && type !== "describe" && type !== "step") return;
4407
+ if (type !== "test" && type !== "describe" && type !== "step") {
4408
+ return;
4409
+ }
4212
4410
  if (node.arguments.length > 0) {
4213
4411
  const titleArg = node.arguments[0];
4214
4412
  if (titleArg && titleArg.type === "Literal" && typeof titleArg.value === "string") {
@@ -4218,14 +4416,20 @@ var valid_test_tags_default = createRule({
4218
4416
  }
4219
4417
  }
4220
4418
  }
4221
- if (node.arguments.length < 2) return;
4419
+ if (node.arguments.length < 2) {
4420
+ return;
4421
+ }
4222
4422
  const optionsArg = node.arguments[1];
4223
- if (!optionsArg || optionsArg.type !== "ObjectExpression") return;
4423
+ if (!optionsArg || optionsArg.type !== "ObjectExpression") {
4424
+ return;
4425
+ }
4224
4426
  const tagProperty = optionsArg.properties.find(
4225
4427
  (prop) => prop.type === "Property" && !("argument" in prop) && // Ensure it's not a spread element
4226
4428
  prop.key.type === "Identifier" && prop.key.name === "tag"
4227
4429
  );
4228
- if (!tagProperty) return;
4430
+ if (!tagProperty) {
4431
+ return;
4432
+ }
4229
4433
  const tagValue = tagProperty.value;
4230
4434
  if (tagValue.type === "Literal") {
4231
4435
  if (typeof tagValue.value !== "string") {
@@ -4362,7 +4566,9 @@ var valid_title_default = createRule({
4362
4566
  }
4363
4567
  const [argument] = node.arguments;
4364
4568
  const title = dereference(context, argument) ?? argument;
4365
- if (!title) return;
4569
+ if (!title) {
4570
+ return;
4571
+ }
4366
4572
  if (!isStringNode(title)) {
4367
4573
  if (title.type === "BinaryExpression" && doesBinaryExpressionContainStringNode(title)) {
4368
4574
  return;
@@ -4411,15 +4617,15 @@ var valid_title_default = createRule({
4411
4617
  node: title
4412
4618
  });
4413
4619
  }
4414
- const [firstWord] = titleString.split(" ");
4620
+ const [firstWord, ...rest] = titleString.split(" ");
4415
4621
  if (firstWord.toLowerCase() === functionName) {
4416
4622
  context.report({
4417
- fix: (fixer) => [
4623
+ fix: rest.length > 0 ? (fixer) => [
4418
4624
  fixer.replaceTextRange(
4419
4625
  title.range,
4420
4626
  quoteStringValue(title).replace(/^([`'"]).+? /u, "$1")
4421
4627
  )
4422
- ],
4628
+ ] : void 0,
4423
4629
  messageId: "duplicatePrefix",
4424
4630
  node: title
4425
4631
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-playwright",
3
- "version": "2.7.0",
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>",