eslint-plugin-playwright 1.6.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -194,6 +194,8 @@ CLI option\
194
194
  | [prefer-hooks-in-order](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-hooks-in-order.md) | Prefer having hooks in a consistent order | | | |
195
195
  | [prefer-hooks-on-top](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-hooks-on-top.md) | Suggest having hooks before any test cases | | | |
196
196
  | [prefer-lowercase-title](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-lowercase-title.md) | Enforce lowercase test names | | 🔧 | |
197
+ | [prefer-native-locators](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-native-locators.md) | Suggest built-in locators over `page.locator()` | | 🔧 | |
198
+ | [prefer-locator](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-locator.md) | Suggest locators over page methods | | | |
197
199
  | [prefer-strict-equal](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-strict-equal.md) | Suggest using `toStrictEqual()` | | | 💡 |
198
200
  | [prefer-to-be](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-to-be.md) | Suggest using `toBe()` | | 🔧 | |
199
201
  | [prefer-to-contain](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-to-contain.md) | Suggest using `toContain()` | | 🔧 | |
package/dist/index.d.mts CHANGED
@@ -41,7 +41,9 @@ declare const _default: {
41
41
  'prefer-equality-matcher': eslint.Rule.RuleModule;
42
42
  'prefer-hooks-in-order': eslint.Rule.RuleModule;
43
43
  'prefer-hooks-on-top': eslint.Rule.RuleModule;
44
+ 'prefer-locator': eslint.Rule.RuleModule;
44
45
  'prefer-lowercase-title': eslint.Rule.RuleModule;
46
+ 'prefer-native-locators': eslint.Rule.RuleModule;
45
47
  'prefer-strict-equal': eslint.Rule.RuleModule;
46
48
  'prefer-to-be': eslint.Rule.RuleModule;
47
49
  'prefer-to-contain': eslint.Rule.RuleModule;
@@ -106,7 +108,9 @@ declare const _default: {
106
108
  'prefer-equality-matcher': eslint.Rule.RuleModule;
107
109
  'prefer-hooks-in-order': eslint.Rule.RuleModule;
108
110
  'prefer-hooks-on-top': eslint.Rule.RuleModule;
111
+ 'prefer-locator': eslint.Rule.RuleModule;
109
112
  'prefer-lowercase-title': eslint.Rule.RuleModule;
113
+ 'prefer-native-locators': eslint.Rule.RuleModule;
110
114
  'prefer-strict-equal': eslint.Rule.RuleModule;
111
115
  'prefer-to-be': eslint.Rule.RuleModule;
112
116
  'prefer-to-contain': eslint.Rule.RuleModule;
@@ -273,7 +277,9 @@ declare const _default: {
273
277
  'prefer-equality-matcher': eslint.Rule.RuleModule;
274
278
  'prefer-hooks-in-order': eslint.Rule.RuleModule;
275
279
  'prefer-hooks-on-top': eslint.Rule.RuleModule;
280
+ 'prefer-locator': eslint.Rule.RuleModule;
276
281
  'prefer-lowercase-title': eslint.Rule.RuleModule;
282
+ 'prefer-native-locators': eslint.Rule.RuleModule;
277
283
  'prefer-strict-equal': eslint.Rule.RuleModule;
278
284
  'prefer-to-be': eslint.Rule.RuleModule;
279
285
  'prefer-to-contain': eslint.Rule.RuleModule;
package/dist/index.d.ts CHANGED
@@ -41,7 +41,9 @@ declare const _default: {
41
41
  'prefer-equality-matcher': eslint.Rule.RuleModule;
42
42
  'prefer-hooks-in-order': eslint.Rule.RuleModule;
43
43
  'prefer-hooks-on-top': eslint.Rule.RuleModule;
44
+ 'prefer-locator': eslint.Rule.RuleModule;
44
45
  'prefer-lowercase-title': eslint.Rule.RuleModule;
46
+ 'prefer-native-locators': eslint.Rule.RuleModule;
45
47
  'prefer-strict-equal': eslint.Rule.RuleModule;
46
48
  'prefer-to-be': eslint.Rule.RuleModule;
47
49
  'prefer-to-contain': eslint.Rule.RuleModule;
@@ -106,7 +108,9 @@ declare const _default: {
106
108
  'prefer-equality-matcher': eslint.Rule.RuleModule;
107
109
  'prefer-hooks-in-order': eslint.Rule.RuleModule;
108
110
  'prefer-hooks-on-top': eslint.Rule.RuleModule;
111
+ 'prefer-locator': eslint.Rule.RuleModule;
109
112
  'prefer-lowercase-title': eslint.Rule.RuleModule;
113
+ 'prefer-native-locators': eslint.Rule.RuleModule;
110
114
  'prefer-strict-equal': eslint.Rule.RuleModule;
111
115
  'prefer-to-be': eslint.Rule.RuleModule;
112
116
  'prefer-to-contain': eslint.Rule.RuleModule;
@@ -273,7 +277,9 @@ declare const _default: {
273
277
  'prefer-equality-matcher': eslint.Rule.RuleModule;
274
278
  'prefer-hooks-in-order': eslint.Rule.RuleModule;
275
279
  'prefer-hooks-on-top': eslint.Rule.RuleModule;
280
+ 'prefer-locator': eslint.Rule.RuleModule;
276
281
  'prefer-lowercase-title': eslint.Rule.RuleModule;
282
+ 'prefer-native-locators': eslint.Rule.RuleModule;
277
283
  'prefer-strict-equal': eslint.Rule.RuleModule;
278
284
  'prefer-to-be': eslint.Rule.RuleModule;
279
285
  'prefer-to-contain': eslint.Rule.RuleModule;
@@ -291,4 +297,4 @@ declare const _default: {
291
297
  };
292
298
  };
293
299
 
294
- export { _default as default };
300
+ export = _default;
package/dist/index.js CHANGED
@@ -21,6 +21,28 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
21
21
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
22
22
  mod
23
23
  ));
24
+ var __accessCheck = (obj, member, msg) => {
25
+ if (!member.has(obj))
26
+ throw TypeError("Cannot " + msg);
27
+ };
28
+ var __privateGet = (obj, member, getter) => {
29
+ __accessCheck(obj, member, "read from private field");
30
+ return getter ? getter.call(obj) : member.get(obj);
31
+ };
32
+ var __privateAdd = (obj, member, value) => {
33
+ if (member.has(obj))
34
+ throw TypeError("Cannot add the same private member more than once");
35
+ member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
36
+ };
37
+ var __privateSet = (obj, member, value, setter) => {
38
+ __accessCheck(obj, member, "write to private field");
39
+ setter ? setter.call(obj, value) : member.set(obj, value);
40
+ return value;
41
+ };
42
+ var __privateMethod = (obj, member, method) => {
43
+ __accessCheck(obj, member, "access private method");
44
+ return method;
45
+ };
24
46
 
25
47
  // src/index.ts
26
48
  var import_globals = __toESM(require("globals"));
@@ -82,20 +104,45 @@ var VALID_CHAINS = /* @__PURE__ */ new Set([
82
104
  ]);
83
105
  var joinChains = (a, b) => a && b ? [...a, ...b] : null;
84
106
  var isSupportedAccessor = (node, value) => isIdentifier(node, value) || isStringNode(node, value);
85
- function getNodeChain(node) {
107
+ var _nodes, _leaves, _buildChain, buildChain_fn;
108
+ var Chain = class {
109
+ constructor(node) {
110
+ __privateAdd(this, _buildChain);
111
+ __privateAdd(this, _nodes, null);
112
+ __privateAdd(this, _leaves, /* @__PURE__ */ new WeakSet());
113
+ __privateSet(this, _nodes, __privateMethod(this, _buildChain, buildChain_fn).call(this, node));
114
+ }
115
+ isLeaf(node) {
116
+ return __privateGet(this, _leaves).has(node);
117
+ }
118
+ get nodes() {
119
+ return __privateGet(this, _nodes);
120
+ }
121
+ };
122
+ _nodes = new WeakMap();
123
+ _leaves = new WeakMap();
124
+ _buildChain = new WeakSet();
125
+ buildChain_fn = function(node, insideCall = false) {
86
126
  if (isSupportedAccessor(node)) {
127
+ if (insideCall) {
128
+ __privateGet(this, _leaves).add(node);
129
+ }
87
130
  return [node];
88
131
  }
89
132
  switch (node.type) {
90
133
  case "TaggedTemplateExpression":
91
- return getNodeChain(node.tag);
134
+ return __privateMethod(this, _buildChain, buildChain_fn).call(this, node.tag);
92
135
  case "MemberExpression":
93
- return joinChains(getNodeChain(node.object), getNodeChain(node.property));
136
+ return joinChains(
137
+ __privateMethod(this, _buildChain, buildChain_fn).call(this, node.object),
138
+ __privateMethod(this, _buildChain, buildChain_fn).call(this, node.property, insideCall)
139
+ );
94
140
  case "CallExpression":
95
- return getNodeChain(node.callee);
141
+ return __privateMethod(this, _buildChain, buildChain_fn).call(this, node.callee, true);
142
+ default:
143
+ return null;
96
144
  }
97
- return null;
98
- }
145
+ };
99
146
  var resolvePossibleAliasedGlobal = (context, global) => {
100
147
  const globalAliases = context.settings.playwright?.globalAliases ?? {};
101
148
  const alias = Object.entries(globalAliases).find(
@@ -126,7 +173,7 @@ function determinePlaywrightFnGroup(name) {
126
173
  return "unknown";
127
174
  }
128
175
  var modifiers = /* @__PURE__ */ new Set(["not", "resolves", "rejects"]);
129
- var findModifiersAndMatcher = (members) => {
176
+ var findModifiersAndMatcher = (chain, members, stage) => {
130
177
  const modifiers2 = [];
131
178
  for (const member of members) {
132
179
  const name = getStringValue(member);
@@ -140,6 +187,9 @@ var findModifiersAndMatcher = (members) => {
140
187
  return "modifier-unknown";
141
188
  }
142
189
  } else if (name !== "not") {
190
+ if (stage === "modifiers") {
191
+ return null;
192
+ }
143
193
  if (member.parent?.type === "MemberExpression" && member.parent.parent?.type === "CallExpression") {
144
194
  return {
145
195
  matcher: member,
@@ -150,6 +200,9 @@ var findModifiersAndMatcher = (members) => {
150
200
  }
151
201
  return "modifier-unknown";
152
202
  }
203
+ if (chain.isLeaf(member)) {
204
+ stage = "matchers";
205
+ }
153
206
  modifiers2.push(member);
154
207
  }
155
208
  return "matcher-not-found";
@@ -157,8 +210,15 @@ var findModifiersAndMatcher = (members) => {
157
210
  function getExpectArguments(call) {
158
211
  return findParent(call.head.node, "CallExpression")?.arguments ?? [];
159
212
  }
160
- var parseExpectCall = (call) => {
161
- const modifiersAndMatcher = findModifiersAndMatcher(call.members);
213
+ var parseExpectCall = (chain, call, stage) => {
214
+ const modifiersAndMatcher = findModifiersAndMatcher(
215
+ chain,
216
+ call.members,
217
+ stage
218
+ );
219
+ if (!modifiersAndMatcher) {
220
+ return null;
221
+ }
162
222
  if (typeof modifiersAndMatcher === "string") {
163
223
  return modifiersAndMatcher;
164
224
  }
@@ -190,19 +250,15 @@ var findTopMostCallExpression = (node) => {
190
250
  return top;
191
251
  };
192
252
  function parse(context, node) {
193
- const chain = getNodeChain(node);
194
- if (!chain?.length) {
253
+ const chain = new Chain(node);
254
+ if (!chain.nodes?.length)
195
255
  return null;
196
- }
197
- const [first, ...rest] = chain;
256
+ const [first, ...rest] = chain.nodes;
198
257
  const resolved = resolveToPlaywrightFn(context, first);
199
258
  if (!resolved)
200
259
  return null;
201
260
  let name = resolved.original ?? resolved.local;
202
261
  const links = [name, ...rest.map((link) => getStringValue(link))];
203
- if (name !== "expect" && !VALID_CHAINS.has(links.join("."))) {
204
- return null;
205
- }
206
262
  if (name === "test" && links.length > 1) {
207
263
  const nextLinkName = links[1];
208
264
  const nextLinkGroup = determinePlaywrightFnGroup(nextLinkName);
@@ -210,6 +266,9 @@ function parse(context, node) {
210
266
  name = nextLinkName;
211
267
  }
212
268
  }
269
+ if (name !== "expect" && !VALID_CHAINS.has(links.join("."))) {
270
+ return null;
271
+ }
213
272
  const parsedFnCall = {
214
273
  head: { ...resolved, node: first },
215
274
  // every member node must have a member expression as their parent
@@ -219,7 +278,14 @@ function parse(context, node) {
219
278
  };
220
279
  const group = determinePlaywrightFnGroup(name);
221
280
  if (group === "expect") {
222
- const result = parseExpectCall(parsedFnCall);
281
+ let stage = chain.isLeaf(parsedFnCall.head.node) ? "matchers" : "modifiers";
282
+ if (isIdentifier(rest[0], "expect")) {
283
+ stage = chain.isLeaf(rest[0]) ? "matchers" : "modifiers";
284
+ parsedFnCall.members.shift();
285
+ }
286
+ const result = parseExpectCall(chain, parsedFnCall, stage);
287
+ if (!result)
288
+ return null;
223
289
  if (typeof result === "string" && findTopMostCallExpression(node) !== node) {
224
290
  return null;
225
291
  }
@@ -230,7 +296,7 @@ function parse(context, node) {
230
296
  }
231
297
  return result;
232
298
  }
233
- if (chain.slice(0, chain.length - 1).some((n) => getParent(n)?.type !== "MemberExpression")) {
299
+ if (chain.nodes.slice(0, chain.nodes.length - 1).some((n) => getParent(n)?.type !== "MemberExpression")) {
234
300
  return null;
235
301
  }
236
302
  const parent = getParent(node);
@@ -275,7 +341,7 @@ function getRawValue(node) {
275
341
  return node.type === "Literal" ? node.raw : void 0;
276
342
  }
277
343
  function isIdentifier(node, name) {
278
- return node.type === "Identifier" && (!name || (typeof name === "string" ? node.name === name : name.test(node.name)));
344
+ return node?.type === "Identifier" && (!name || (typeof name === "string" ? node.name === name : name.test(node.name)));
279
345
  }
280
346
  function isLiteral(node, type, value) {
281
347
  return node.type === "Literal" && (value === void 0 ? typeof node.value === type : node.value === value);
@@ -292,7 +358,8 @@ function isStringNode(node, value) {
292
358
  return node && (isStringLiteral(node, value) || isTemplateLiteral(node, value));
293
359
  }
294
360
  function isPropertyAccessor(node, name) {
295
- return getStringValue(node.property) === name;
361
+ const value = getStringValue(node.property);
362
+ return typeof name === "string" ? value === name : name.test(value);
296
363
  }
297
364
  function getParent(node) {
298
365
  return node.parent;
@@ -329,6 +396,24 @@ function getNodeName(node) {
329
396
  }
330
397
  return null;
331
398
  }
399
+ var isVariableDeclarator = (node) => node.type === "VariableDeclarator";
400
+ var isAssignmentExpression = (node) => node.type === "AssignmentExpression";
401
+ function isNodeLastAssignment(node, assignment) {
402
+ if (node.range && assignment.range && node.range[0] < assignment.range[1]) {
403
+ return false;
404
+ }
405
+ return assignment.left.type === "Identifier" && assignment.left.name === node.name;
406
+ }
407
+ function dereference(context, node) {
408
+ if (node?.type !== "Identifier") {
409
+ return node;
410
+ }
411
+ const scope = context.sourceCode.getScope(node);
412
+ const parents = scope.references.map((ref) => ref.identifier).map((ident) => ident.parent);
413
+ const decl = parents.filter(isVariableDeclarator).find((p) => p.id.type === "Identifier" && p.id.name === node.name);
414
+ const expr = parents.filter(isAssignmentExpression).reverse().find((assignment) => isNodeLastAssignment(node, assignment));
415
+ return expr?.right ?? decl?.init;
416
+ }
332
417
 
333
418
  // src/utils/createRule.ts
334
419
  function interpolate(str, data) {
@@ -2222,6 +2307,66 @@ var prefer_hooks_on_top_default = createRule({
2222
2307
  }
2223
2308
  });
2224
2309
 
2310
+ // src/rules/prefer-locator.ts
2311
+ var pageMethods2 = /* @__PURE__ */ new Set([
2312
+ "click",
2313
+ "dblclick",
2314
+ "dispatchEvent",
2315
+ "fill",
2316
+ "focus",
2317
+ "getAttribute",
2318
+ "hover",
2319
+ "innerHTML",
2320
+ "innerText",
2321
+ "inputValue",
2322
+ "isChecked",
2323
+ "isDisabled",
2324
+ "isEditable",
2325
+ "isEnabled",
2326
+ "isHidden",
2327
+ "isVisible",
2328
+ "press",
2329
+ "selectOption",
2330
+ "setChecked",
2331
+ "setInputFiles",
2332
+ "tap",
2333
+ "textContent",
2334
+ "uncheck"
2335
+ ]);
2336
+ function isSupportedMethod2(node) {
2337
+ if (node.callee.type !== "MemberExpression")
2338
+ return false;
2339
+ const name = getStringValue(node.callee.property);
2340
+ return pageMethods2.has(name) && isPageMethod(node, name);
2341
+ }
2342
+ var prefer_locator_default = createRule({
2343
+ create(context) {
2344
+ return {
2345
+ CallExpression(node) {
2346
+ if (!isSupportedMethod2(node))
2347
+ return;
2348
+ context.report({
2349
+ messageId: "preferLocator",
2350
+ node
2351
+ });
2352
+ }
2353
+ };
2354
+ },
2355
+ meta: {
2356
+ docs: {
2357
+ category: "Best Practices",
2358
+ description: "Suggest locators over page methods",
2359
+ recommended: false,
2360
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-locator.md"
2361
+ },
2362
+ messages: {
2363
+ preferLocator: "Prefer locator methods instead of page methods"
2364
+ },
2365
+ schema: [],
2366
+ type: "suggestion"
2367
+ }
2368
+ });
2369
+
2225
2370
  // src/rules/prefer-lowercase-title.ts
2226
2371
  var prefer_lowercase_title_default = createRule({
2227
2372
  create(context) {
@@ -2317,6 +2462,113 @@ var prefer_lowercase_title_default = createRule({
2317
2462
  }
2318
2463
  });
2319
2464
 
2465
+ // src/rules/prefer-native-locators.ts
2466
+ var compilePatterns = ({
2467
+ testIdAttribute
2468
+ }) => {
2469
+ const patterns = [
2470
+ {
2471
+ attribute: "aria-label",
2472
+ messageId: "unexpectedLabelQuery",
2473
+ replacement: "getByLabel"
2474
+ },
2475
+ {
2476
+ attribute: "role",
2477
+ messageId: "unexpectedRoleQuery",
2478
+ replacement: "getByRole"
2479
+ },
2480
+ {
2481
+ attribute: "placeholder",
2482
+ messageId: "unexpectedPlaceholderQuery",
2483
+ replacement: "getByPlaceholder"
2484
+ },
2485
+ {
2486
+ attribute: "alt",
2487
+ messageId: "unexpectedAltTextQuery",
2488
+ replacement: "getByAltText"
2489
+ },
2490
+ {
2491
+ attribute: "title",
2492
+ messageId: "unexpectedTitleQuery",
2493
+ replacement: "getByTitle"
2494
+ },
2495
+ {
2496
+ attribute: testIdAttribute,
2497
+ messageId: "unexpectedTestIdQuery",
2498
+ replacement: "getByTestId"
2499
+ }
2500
+ ];
2501
+ return patterns.map(({ attribute, ...pattern }) => ({
2502
+ ...pattern,
2503
+ pattern: new RegExp(`^\\[${attribute}=['"]?(.+?)['"]?\\]$`)
2504
+ }));
2505
+ };
2506
+ var prefer_native_locators_default = createRule({
2507
+ create(context) {
2508
+ const { testIdAttribute } = {
2509
+ testIdAttribute: "data-testid",
2510
+ ...context.options?.[0] ?? {}
2511
+ };
2512
+ const patterns = compilePatterns({ testIdAttribute });
2513
+ return {
2514
+ CallExpression(node) {
2515
+ if (node.callee.type !== "MemberExpression")
2516
+ return;
2517
+ const query = getStringValue(node.arguments[0]);
2518
+ if (!isPageMethod(node, "locator"))
2519
+ return;
2520
+ for (const pattern of patterns) {
2521
+ const match = query.match(pattern.pattern);
2522
+ if (match) {
2523
+ context.report({
2524
+ fix(fixer) {
2525
+ const start = node.callee.type === "MemberExpression" ? node.callee.property.range[0] : node.range[0];
2526
+ const end = node.range[1];
2527
+ const rangeToReplace = [start, end];
2528
+ const newText = `${pattern.replacement}("${match[1]}")`;
2529
+ return fixer.replaceTextRange(rangeToReplace, newText);
2530
+ },
2531
+ messageId: pattern.messageId,
2532
+ node
2533
+ });
2534
+ return;
2535
+ }
2536
+ }
2537
+ }
2538
+ };
2539
+ },
2540
+ meta: {
2541
+ docs: {
2542
+ category: "Best Practices",
2543
+ description: "Prefer native locator functions",
2544
+ recommended: false,
2545
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-native-locators.md"
2546
+ },
2547
+ fixable: "code",
2548
+ messages: {
2549
+ unexpectedAltTextQuery: "Use getByAltText() instead",
2550
+ unexpectedLabelQuery: "Use getByLabel() instead",
2551
+ unexpectedPlaceholderQuery: "Use getByPlaceholder() instead",
2552
+ unexpectedRoleQuery: "Use getByRole() instead",
2553
+ unexpectedTestIdQuery: "Use getByTestId() instead",
2554
+ unexpectedTitleQuery: "Use getByTitle() instead"
2555
+ },
2556
+ schema: [
2557
+ {
2558
+ additionalProperties: false,
2559
+ properties: {
2560
+ testIdAttribute: {
2561
+ default: "data-testid",
2562
+ type: "string"
2563
+ }
2564
+ },
2565
+ type: "object"
2566
+ }
2567
+ ],
2568
+ type: "suggestion"
2569
+ }
2570
+ });
2571
+
2320
2572
  // src/rules/prefer-strict-equal.ts
2321
2573
  var prefer_strict_equal_default = createRule({
2322
2574
  create(context) {
@@ -2526,16 +2778,18 @@ var prefer_to_contain_default = createRule({
2526
2778
  });
2527
2779
 
2528
2780
  // src/rules/prefer-to-have-count.ts
2781
+ var matchers = /* @__PURE__ */ new Set([...equalityMatchers, "toHaveLength"]);
2529
2782
  var prefer_to_have_count_default = createRule({
2530
2783
  create(context) {
2531
2784
  return {
2532
2785
  CallExpression(node) {
2533
2786
  const call = parseFnCall(context, node);
2534
- if (call?.type !== "expect" || !equalityMatchers.has(call.matcherName)) {
2787
+ if (call?.type !== "expect" || !matchers.has(call.matcherName)) {
2535
2788
  return;
2536
2789
  }
2537
- const [argument] = call.args;
2538
- if (argument?.type !== "AwaitExpression" || argument.argument.type !== "CallExpression" || argument.argument.callee.type !== "MemberExpression" || !isPropertyAccessor(argument.argument.callee, "count")) {
2790
+ const accessor = call.matcherName === "toHaveLength" ? "all" : "count";
2791
+ const argument = dereference(context, call.args[0]);
2792
+ if (argument?.type !== "AwaitExpression" || argument.argument.type !== "CallExpression" || argument.argument.callee.type !== "MemberExpression" || !isPropertyAccessor(argument.argument.callee, accessor)) {
2539
2793
  return;
2540
2794
  }
2541
2795
  const callee = argument.argument.callee;
@@ -2669,24 +2923,6 @@ var supportedMatchers = /* @__PURE__ */ new Set([
2669
2923
  "toBeTruthy",
2670
2924
  "toBeFalsy"
2671
2925
  ]);
2672
- var isVariableDeclarator = (node) => node.type === "VariableDeclarator";
2673
- var isAssignmentExpression = (node) => node.type === "AssignmentExpression";
2674
- function isNodeLastAssignment(node, assignment) {
2675
- if (node.range && assignment.range && node.range[0] < assignment.range[1]) {
2676
- return false;
2677
- }
2678
- return assignment.left.type === "Identifier" && assignment.left.name === node.name;
2679
- }
2680
- function dereference(context, node) {
2681
- if (node?.type !== "Identifier") {
2682
- return node;
2683
- }
2684
- const scope = context.sourceCode.getScope(node);
2685
- const parents = scope.references.map((ref) => ref.identifier).map((ident) => ident.parent);
2686
- const decl = parents.filter(isVariableDeclarator).find((p) => p.id.type === "Identifier" && p.id.name === node.name);
2687
- const expr = parents.filter(isAssignmentExpression).reverse().find((assignment) => isNodeLastAssignment(node, assignment));
2688
- return expr?.right ?? decl?.init;
2689
- }
2690
2926
  var prefer_web_first_assertions_default = createRule({
2691
2927
  create(context) {
2692
2928
  return {
@@ -3479,17 +3715,19 @@ var compileMatcherPattern = (matcherMaybeWithMessage) => {
3479
3715
  const [matcher, message] = Array.isArray(matcherMaybeWithMessage) ? matcherMaybeWithMessage : [matcherMaybeWithMessage];
3480
3716
  return [new RegExp(matcher, "u"), message];
3481
3717
  };
3482
- var compileMatcherPatterns = (matchers) => {
3483
- if (typeof matchers === "string" || Array.isArray(matchers)) {
3484
- const compiledMatcher = compileMatcherPattern(matchers);
3718
+ var compileMatcherPatterns = (matchers2) => {
3719
+ if (typeof matchers2 === "string" || Array.isArray(matchers2)) {
3720
+ const compiledMatcher = compileMatcherPattern(matchers2);
3485
3721
  return {
3486
3722
  describe: compiledMatcher,
3723
+ step: compiledMatcher,
3487
3724
  test: compiledMatcher
3488
3725
  };
3489
3726
  }
3490
3727
  return {
3491
- describe: matchers.describe ? compileMatcherPattern(matchers.describe) : null,
3492
- test: matchers.test ? compileMatcherPattern(matchers.test) : null
3728
+ describe: matchers2.describe ? compileMatcherPattern(matchers2.describe) : null,
3729
+ step: matchers2.step ? compileMatcherPattern(matchers2.step) : null,
3730
+ test: matchers2.test ? compileMatcherPattern(matchers2.test) : null
3493
3731
  };
3494
3732
  };
3495
3733
  var MatcherAndMessageSchema = {
@@ -3506,6 +3744,7 @@ var valid_title_default = createRule({
3506
3744
  disallowedWords = [],
3507
3745
  ignoreSpaces = false,
3508
3746
  ignoreTypeOfDescribeName = false,
3747
+ ignoreTypeOfStepName = true,
3509
3748
  ignoreTypeOfTestName = false,
3510
3749
  mustMatch,
3511
3750
  mustNotMatch
@@ -3519,7 +3758,7 @@ var valid_title_default = createRule({
3519
3758
  return {
3520
3759
  CallExpression(node) {
3521
3760
  const call = parseFnCall(context, node);
3522
- if (call?.type !== "test" && call?.type !== "describe") {
3761
+ if (call?.type !== "test" && call?.type !== "describe" && call?.type !== "step") {
3523
3762
  return;
3524
3763
  }
3525
3764
  const [argument] = node.arguments;
@@ -3529,7 +3768,7 @@ var valid_title_default = createRule({
3529
3768
  if (argument.type === "BinaryExpression" && doesBinaryExpressionContainStringNode(argument)) {
3530
3769
  return;
3531
3770
  }
3532
- if (!(call.type === "describe" && ignoreTypeOfDescribeName || call.type === "test" && ignoreTypeOfTestName) && argument.type !== "TemplateLiteral") {
3771
+ if (!(call.type === "describe" && ignoreTypeOfDescribeName || call.type === "test" && ignoreTypeOfTestName || call.type === "step" && ignoreTypeOfStepName) && argument.type !== "TemplateLiteral") {
3533
3772
  context.report({
3534
3773
  loc: argument.loc,
3535
3774
  messageId: "titleMustBeString"
@@ -3643,7 +3882,7 @@ var valid_title_default = createRule({
3643
3882
  additionalProperties: {
3644
3883
  oneOf: [{ type: "string" }, MatcherAndMessageSchema]
3645
3884
  },
3646
- propertyNames: { enum: ["describe", "test"] },
3885
+ propertyNames: { enum: ["describe", "test", "step"] },
3647
3886
  type: "object"
3648
3887
  }
3649
3888
  ]
@@ -3662,6 +3901,10 @@ var valid_title_default = createRule({
3662
3901
  default: false,
3663
3902
  type: "boolean"
3664
3903
  },
3904
+ ignoreTypeOfStepName: {
3905
+ default: true,
3906
+ type: "boolean"
3907
+ },
3665
3908
  ignoreTypeOfTestName: {
3666
3909
  default: false,
3667
3910
  type: "boolean"
@@ -3709,7 +3952,9 @@ var index = {
3709
3952
  "prefer-equality-matcher": prefer_equality_matcher_default,
3710
3953
  "prefer-hooks-in-order": prefer_hooks_in_order_default,
3711
3954
  "prefer-hooks-on-top": prefer_hooks_on_top_default,
3955
+ "prefer-locator": prefer_locator_default,
3712
3956
  "prefer-lowercase-title": prefer_lowercase_title_default,
3957
+ "prefer-native-locators": prefer_native_locators_default,
3713
3958
  "prefer-strict-equal": prefer_strict_equal_default,
3714
3959
  "prefer-to-be": prefer_to_be_default,
3715
3960
  "prefer-to-contain": prefer_to_contain_default,
package/dist/index.mjs CHANGED
@@ -5,22 +5,30 @@ var __esm = (fn, res) => function __init() {
5
5
  var __commonJS = (cb, mod) => function __require() {
6
6
  return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
7
7
  };
8
+ var __accessCheck = (obj, member, msg) => {
9
+ if (!member.has(obj))
10
+ throw TypeError("Cannot " + msg);
11
+ };
12
+ var __privateGet = (obj, member, getter) => {
13
+ __accessCheck(obj, member, "read from private field");
14
+ return getter ? getter.call(obj) : member.get(obj);
15
+ };
16
+ var __privateAdd = (obj, member, value) => {
17
+ if (member.has(obj))
18
+ throw TypeError("Cannot add the same private member more than once");
19
+ member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
20
+ };
21
+ var __privateSet = (obj, member, value, setter) => {
22
+ __accessCheck(obj, member, "write to private field");
23
+ setter ? setter.call(obj, value) : member.set(obj, value);
24
+ return value;
25
+ };
26
+ var __privateMethod = (obj, member, method) => {
27
+ __accessCheck(obj, member, "access private method");
28
+ return method;
29
+ };
8
30
 
9
31
  // src/utils/parseFnCall.ts
10
- function getNodeChain(node) {
11
- if (isSupportedAccessor(node)) {
12
- return [node];
13
- }
14
- switch (node.type) {
15
- case "TaggedTemplateExpression":
16
- return getNodeChain(node.tag);
17
- case "MemberExpression":
18
- return joinChains(getNodeChain(node.object), getNodeChain(node.property));
19
- case "CallExpression":
20
- return getNodeChain(node.callee);
21
- }
22
- return null;
23
- }
24
32
  function determinePlaywrightFnGroup(name) {
25
33
  if (name === "step")
26
34
  return "step";
@@ -38,19 +46,15 @@ function getExpectArguments(call) {
38
46
  return findParent(call.head.node, "CallExpression")?.arguments ?? [];
39
47
  }
40
48
  function parse(context, node) {
41
- const chain = getNodeChain(node);
42
- if (!chain?.length) {
49
+ const chain = new Chain(node);
50
+ if (!chain.nodes?.length)
43
51
  return null;
44
- }
45
- const [first, ...rest] = chain;
52
+ const [first, ...rest] = chain.nodes;
46
53
  const resolved = resolveToPlaywrightFn(context, first);
47
54
  if (!resolved)
48
55
  return null;
49
56
  let name = resolved.original ?? resolved.local;
50
57
  const links = [name, ...rest.map((link) => getStringValue(link))];
51
- if (name !== "expect" && !VALID_CHAINS.has(links.join("."))) {
52
- return null;
53
- }
54
58
  if (name === "test" && links.length > 1) {
55
59
  const nextLinkName = links[1];
56
60
  const nextLinkGroup = determinePlaywrightFnGroup(nextLinkName);
@@ -58,6 +62,9 @@ function parse(context, node) {
58
62
  name = nextLinkName;
59
63
  }
60
64
  }
65
+ if (name !== "expect" && !VALID_CHAINS.has(links.join("."))) {
66
+ return null;
67
+ }
61
68
  const parsedFnCall = {
62
69
  head: { ...resolved, node: first },
63
70
  // every member node must have a member expression as their parent
@@ -67,7 +74,14 @@ function parse(context, node) {
67
74
  };
68
75
  const group = determinePlaywrightFnGroup(name);
69
76
  if (group === "expect") {
70
- const result = parseExpectCall(parsedFnCall);
77
+ let stage = chain.isLeaf(parsedFnCall.head.node) ? "matchers" : "modifiers";
78
+ if (isIdentifier(rest[0], "expect")) {
79
+ stage = chain.isLeaf(rest[0]) ? "matchers" : "modifiers";
80
+ parsedFnCall.members.shift();
81
+ }
82
+ const result = parseExpectCall(chain, parsedFnCall, stage);
83
+ if (!result)
84
+ return null;
71
85
  if (typeof result === "string" && findTopMostCallExpression(node) !== node) {
72
86
  return null;
73
87
  }
@@ -78,7 +92,7 @@ function parse(context, node) {
78
92
  }
79
93
  return result;
80
94
  }
81
- if (chain.slice(0, chain.length - 1).some((n) => getParent(n)?.type !== "MemberExpression")) {
95
+ if (chain.nodes.slice(0, chain.nodes.length - 1).some((n) => getParent(n)?.type !== "MemberExpression")) {
82
96
  return null;
83
97
  }
84
98
  const parent = getParent(node);
@@ -107,7 +121,7 @@ function parseFnCall(context, node) {
107
121
  const call = parseFnCallWithReason(context, node);
108
122
  return typeof call === "string" ? null : call;
109
123
  }
110
- var testHooks, VALID_CHAINS, joinChains, isSupportedAccessor, resolvePossibleAliasedGlobal, resolveToPlaywrightFn, modifiers, findModifiersAndMatcher, parseExpectCall, findTopMostCallExpression, cache, isTypeOfFnCall;
124
+ var testHooks, VALID_CHAINS, joinChains, isSupportedAccessor, _nodes, _leaves, _buildChain, buildChain_fn, Chain, resolvePossibleAliasedGlobal, resolveToPlaywrightFn, modifiers, findModifiersAndMatcher, parseExpectCall, findTopMostCallExpression, cache, isTypeOfFnCall;
111
125
  var init_parseFnCall = __esm({
112
126
  "src/utils/parseFnCall.ts"() {
113
127
  "use strict";
@@ -168,6 +182,44 @@ var init_parseFnCall = __esm({
168
182
  ]);
169
183
  joinChains = (a, b) => a && b ? [...a, ...b] : null;
170
184
  isSupportedAccessor = (node, value) => isIdentifier(node, value) || isStringNode(node, value);
185
+ Chain = class {
186
+ constructor(node) {
187
+ __privateAdd(this, _buildChain);
188
+ __privateAdd(this, _nodes, null);
189
+ __privateAdd(this, _leaves, /* @__PURE__ */ new WeakSet());
190
+ __privateSet(this, _nodes, __privateMethod(this, _buildChain, buildChain_fn).call(this, node));
191
+ }
192
+ isLeaf(node) {
193
+ return __privateGet(this, _leaves).has(node);
194
+ }
195
+ get nodes() {
196
+ return __privateGet(this, _nodes);
197
+ }
198
+ };
199
+ _nodes = new WeakMap();
200
+ _leaves = new WeakMap();
201
+ _buildChain = new WeakSet();
202
+ buildChain_fn = function(node, insideCall = false) {
203
+ if (isSupportedAccessor(node)) {
204
+ if (insideCall) {
205
+ __privateGet(this, _leaves).add(node);
206
+ }
207
+ return [node];
208
+ }
209
+ switch (node.type) {
210
+ case "TaggedTemplateExpression":
211
+ return __privateMethod(this, _buildChain, buildChain_fn).call(this, node.tag);
212
+ case "MemberExpression":
213
+ return joinChains(
214
+ __privateMethod(this, _buildChain, buildChain_fn).call(this, node.object),
215
+ __privateMethod(this, _buildChain, buildChain_fn).call(this, node.property, insideCall)
216
+ );
217
+ case "CallExpression":
218
+ return __privateMethod(this, _buildChain, buildChain_fn).call(this, node.callee, true);
219
+ default:
220
+ return null;
221
+ }
222
+ };
171
223
  resolvePossibleAliasedGlobal = (context, global) => {
172
224
  const globalAliases = context.settings.playwright?.globalAliases ?? {};
173
225
  const alias = Object.entries(globalAliases).find(
@@ -185,7 +237,7 @@ var init_parseFnCall = __esm({
185
237
  };
186
238
  };
187
239
  modifiers = /* @__PURE__ */ new Set(["not", "resolves", "rejects"]);
188
- findModifiersAndMatcher = (members) => {
240
+ findModifiersAndMatcher = (chain, members, stage) => {
189
241
  const modifiers2 = [];
190
242
  for (const member of members) {
191
243
  const name = getStringValue(member);
@@ -199,6 +251,9 @@ var init_parseFnCall = __esm({
199
251
  return "modifier-unknown";
200
252
  }
201
253
  } else if (name !== "not") {
254
+ if (stage === "modifiers") {
255
+ return null;
256
+ }
202
257
  if (member.parent?.type === "MemberExpression" && member.parent.parent?.type === "CallExpression") {
203
258
  return {
204
259
  matcher: member,
@@ -209,12 +264,22 @@ var init_parseFnCall = __esm({
209
264
  }
210
265
  return "modifier-unknown";
211
266
  }
267
+ if (chain.isLeaf(member)) {
268
+ stage = "matchers";
269
+ }
212
270
  modifiers2.push(member);
213
271
  }
214
272
  return "matcher-not-found";
215
273
  };
216
- parseExpectCall = (call) => {
217
- const modifiersAndMatcher = findModifiersAndMatcher(call.members);
274
+ parseExpectCall = (chain, call, stage) => {
275
+ const modifiersAndMatcher = findModifiersAndMatcher(
276
+ chain,
277
+ call.members,
278
+ stage
279
+ );
280
+ if (!modifiersAndMatcher) {
281
+ return null;
282
+ }
218
283
  if (typeof modifiersAndMatcher === "string") {
219
284
  return modifiersAndMatcher;
220
285
  }
@@ -263,7 +328,7 @@ function getRawValue(node) {
263
328
  return node.type === "Literal" ? node.raw : void 0;
264
329
  }
265
330
  function isIdentifier(node, name) {
266
- return node.type === "Identifier" && (!name || (typeof name === "string" ? node.name === name : name.test(node.name)));
331
+ return node?.type === "Identifier" && (!name || (typeof name === "string" ? node.name === name : name.test(node.name)));
267
332
  }
268
333
  function isLiteral(node, type, value) {
269
334
  return node.type === "Literal" && (value === void 0 ? typeof node.value === type : node.value === value);
@@ -278,7 +343,8 @@ function isStringNode(node, value) {
278
343
  return node && (isStringLiteral(node, value) || isTemplateLiteral(node, value));
279
344
  }
280
345
  function isPropertyAccessor(node, name) {
281
- return getStringValue(node.property) === name;
346
+ const value = getStringValue(node.property);
347
+ return typeof name === "string" ? value === name : name.test(value);
282
348
  }
283
349
  function getParent(node) {
284
350
  return node.parent;
@@ -313,7 +379,23 @@ function getNodeName(node) {
313
379
  }
314
380
  return null;
315
381
  }
316
- var isTemplateLiteral, equalityMatchers, joinNames;
382
+ function isNodeLastAssignment(node, assignment) {
383
+ if (node.range && assignment.range && node.range[0] < assignment.range[1]) {
384
+ return false;
385
+ }
386
+ return assignment.left.type === "Identifier" && assignment.left.name === node.name;
387
+ }
388
+ function dereference(context, node) {
389
+ if (node?.type !== "Identifier") {
390
+ return node;
391
+ }
392
+ const scope = context.sourceCode.getScope(node);
393
+ const parents = scope.references.map((ref) => ref.identifier).map((ident) => ident.parent);
394
+ const decl = parents.filter(isVariableDeclarator).find((p) => p.id.type === "Identifier" && p.id.name === node.name);
395
+ const expr = parents.filter(isAssignmentExpression).reverse().find((assignment) => isNodeLastAssignment(node, assignment));
396
+ return expr?.right ?? decl?.init;
397
+ }
398
+ var isTemplateLiteral, equalityMatchers, joinNames, isVariableDeclarator, isAssignmentExpression;
317
399
  var init_ast = __esm({
318
400
  "src/utils/ast.ts"() {
319
401
  "use strict";
@@ -322,6 +404,8 @@ var init_ast = __esm({
322
404
  (value === void 0 || node.quasis[0].value.raw === value);
323
405
  equalityMatchers = /* @__PURE__ */ new Set(["toBe", "toEqual", "toStrictEqual"]);
324
406
  joinNames = (a, b) => a && b ? `${a}.${b}` : null;
407
+ isVariableDeclarator = (node) => node.type === "VariableDeclarator";
408
+ isAssignmentExpression = (node) => node.type === "AssignmentExpression";
325
409
  }
326
410
  });
327
411
 
@@ -2498,6 +2582,74 @@ var init_prefer_hooks_on_top = __esm({
2498
2582
  }
2499
2583
  });
2500
2584
 
2585
+ // src/rules/prefer-locator.ts
2586
+ function isSupportedMethod2(node) {
2587
+ if (node.callee.type !== "MemberExpression")
2588
+ return false;
2589
+ const name = getStringValue(node.callee.property);
2590
+ return pageMethods2.has(name) && isPageMethod(node, name);
2591
+ }
2592
+ var pageMethods2, prefer_locator_default;
2593
+ var init_prefer_locator = __esm({
2594
+ "src/rules/prefer-locator.ts"() {
2595
+ "use strict";
2596
+ init_ast();
2597
+ init_createRule();
2598
+ pageMethods2 = /* @__PURE__ */ new Set([
2599
+ "click",
2600
+ "dblclick",
2601
+ "dispatchEvent",
2602
+ "fill",
2603
+ "focus",
2604
+ "getAttribute",
2605
+ "hover",
2606
+ "innerHTML",
2607
+ "innerText",
2608
+ "inputValue",
2609
+ "isChecked",
2610
+ "isDisabled",
2611
+ "isEditable",
2612
+ "isEnabled",
2613
+ "isHidden",
2614
+ "isVisible",
2615
+ "press",
2616
+ "selectOption",
2617
+ "setChecked",
2618
+ "setInputFiles",
2619
+ "tap",
2620
+ "textContent",
2621
+ "uncheck"
2622
+ ]);
2623
+ prefer_locator_default = createRule({
2624
+ create(context) {
2625
+ return {
2626
+ CallExpression(node) {
2627
+ if (!isSupportedMethod2(node))
2628
+ return;
2629
+ context.report({
2630
+ messageId: "preferLocator",
2631
+ node
2632
+ });
2633
+ }
2634
+ };
2635
+ },
2636
+ meta: {
2637
+ docs: {
2638
+ category: "Best Practices",
2639
+ description: "Suggest locators over page methods",
2640
+ recommended: false,
2641
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-locator.md"
2642
+ },
2643
+ messages: {
2644
+ preferLocator: "Prefer locator methods instead of page methods"
2645
+ },
2646
+ schema: [],
2647
+ type: "suggestion"
2648
+ }
2649
+ });
2650
+ }
2651
+ });
2652
+
2501
2653
  // src/rules/prefer-lowercase-title.ts
2502
2654
  var prefer_lowercase_title_default;
2503
2655
  var init_prefer_lowercase_title = __esm({
@@ -2602,6 +2754,121 @@ var init_prefer_lowercase_title = __esm({
2602
2754
  }
2603
2755
  });
2604
2756
 
2757
+ // src/rules/prefer-native-locators.ts
2758
+ var compilePatterns, prefer_native_locators_default;
2759
+ var init_prefer_native_locators = __esm({
2760
+ "src/rules/prefer-native-locators.ts"() {
2761
+ "use strict";
2762
+ init_ast();
2763
+ init_createRule();
2764
+ compilePatterns = ({
2765
+ testIdAttribute
2766
+ }) => {
2767
+ const patterns = [
2768
+ {
2769
+ attribute: "aria-label",
2770
+ messageId: "unexpectedLabelQuery",
2771
+ replacement: "getByLabel"
2772
+ },
2773
+ {
2774
+ attribute: "role",
2775
+ messageId: "unexpectedRoleQuery",
2776
+ replacement: "getByRole"
2777
+ },
2778
+ {
2779
+ attribute: "placeholder",
2780
+ messageId: "unexpectedPlaceholderQuery",
2781
+ replacement: "getByPlaceholder"
2782
+ },
2783
+ {
2784
+ attribute: "alt",
2785
+ messageId: "unexpectedAltTextQuery",
2786
+ replacement: "getByAltText"
2787
+ },
2788
+ {
2789
+ attribute: "title",
2790
+ messageId: "unexpectedTitleQuery",
2791
+ replacement: "getByTitle"
2792
+ },
2793
+ {
2794
+ attribute: testIdAttribute,
2795
+ messageId: "unexpectedTestIdQuery",
2796
+ replacement: "getByTestId"
2797
+ }
2798
+ ];
2799
+ return patterns.map(({ attribute, ...pattern }) => ({
2800
+ ...pattern,
2801
+ pattern: new RegExp(`^\\[${attribute}=['"]?(.+?)['"]?\\]$`)
2802
+ }));
2803
+ };
2804
+ prefer_native_locators_default = createRule({
2805
+ create(context) {
2806
+ const { testIdAttribute } = {
2807
+ testIdAttribute: "data-testid",
2808
+ ...context.options?.[0] ?? {}
2809
+ };
2810
+ const patterns = compilePatterns({ testIdAttribute });
2811
+ return {
2812
+ CallExpression(node) {
2813
+ if (node.callee.type !== "MemberExpression")
2814
+ return;
2815
+ const query = getStringValue(node.arguments[0]);
2816
+ if (!isPageMethod(node, "locator"))
2817
+ return;
2818
+ for (const pattern of patterns) {
2819
+ const match = query.match(pattern.pattern);
2820
+ if (match) {
2821
+ context.report({
2822
+ fix(fixer) {
2823
+ const start = node.callee.type === "MemberExpression" ? node.callee.property.range[0] : node.range[0];
2824
+ const end = node.range[1];
2825
+ const rangeToReplace = [start, end];
2826
+ const newText = `${pattern.replacement}("${match[1]}")`;
2827
+ return fixer.replaceTextRange(rangeToReplace, newText);
2828
+ },
2829
+ messageId: pattern.messageId,
2830
+ node
2831
+ });
2832
+ return;
2833
+ }
2834
+ }
2835
+ }
2836
+ };
2837
+ },
2838
+ meta: {
2839
+ docs: {
2840
+ category: "Best Practices",
2841
+ description: "Prefer native locator functions",
2842
+ recommended: false,
2843
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-native-locators.md"
2844
+ },
2845
+ fixable: "code",
2846
+ messages: {
2847
+ unexpectedAltTextQuery: "Use getByAltText() instead",
2848
+ unexpectedLabelQuery: "Use getByLabel() instead",
2849
+ unexpectedPlaceholderQuery: "Use getByPlaceholder() instead",
2850
+ unexpectedRoleQuery: "Use getByRole() instead",
2851
+ unexpectedTestIdQuery: "Use getByTestId() instead",
2852
+ unexpectedTitleQuery: "Use getByTitle() instead"
2853
+ },
2854
+ schema: [
2855
+ {
2856
+ additionalProperties: false,
2857
+ properties: {
2858
+ testIdAttribute: {
2859
+ default: "data-testid",
2860
+ type: "string"
2861
+ }
2862
+ },
2863
+ type: "object"
2864
+ }
2865
+ ],
2866
+ type: "suggestion"
2867
+ }
2868
+ });
2869
+ }
2870
+ });
2871
+
2605
2872
  // src/rules/prefer-strict-equal.ts
2606
2873
  var prefer_strict_equal_default;
2607
2874
  var init_prefer_strict_equal = __esm({
@@ -2839,7 +3106,7 @@ var init_prefer_to_contain = __esm({
2839
3106
  });
2840
3107
 
2841
3108
  // src/rules/prefer-to-have-count.ts
2842
- var prefer_to_have_count_default;
3109
+ var matchers, prefer_to_have_count_default;
2843
3110
  var init_prefer_to_have_count = __esm({
2844
3111
  "src/rules/prefer-to-have-count.ts"() {
2845
3112
  "use strict";
@@ -2847,16 +3114,18 @@ var init_prefer_to_have_count = __esm({
2847
3114
  init_createRule();
2848
3115
  init_fixer();
2849
3116
  init_parseFnCall();
3117
+ matchers = /* @__PURE__ */ new Set([...equalityMatchers, "toHaveLength"]);
2850
3118
  prefer_to_have_count_default = createRule({
2851
3119
  create(context) {
2852
3120
  return {
2853
3121
  CallExpression(node) {
2854
3122
  const call = parseFnCall(context, node);
2855
- if (call?.type !== "expect" || !equalityMatchers.has(call.matcherName)) {
3123
+ if (call?.type !== "expect" || !matchers.has(call.matcherName)) {
2856
3124
  return;
2857
3125
  }
2858
- const [argument] = call.args;
2859
- if (argument?.type !== "AwaitExpression" || argument.argument.type !== "CallExpression" || argument.argument.callee.type !== "MemberExpression" || !isPropertyAccessor(argument.argument.callee, "count")) {
3126
+ const accessor = call.matcherName === "toHaveLength" ? "all" : "count";
3127
+ const argument = dereference(context, call.args[0]);
3128
+ if (argument?.type !== "AwaitExpression" || argument.argument.type !== "CallExpression" || argument.argument.callee.type !== "MemberExpression" || !isPropertyAccessor(argument.argument.callee, accessor)) {
2860
3129
  return;
2861
3130
  }
2862
3131
  const callee = argument.argument.callee;
@@ -2961,23 +3230,7 @@ var init_prefer_to_have_length = __esm({
2961
3230
  });
2962
3231
 
2963
3232
  // src/rules/prefer-web-first-assertions.ts
2964
- function isNodeLastAssignment(node, assignment) {
2965
- if (node.range && assignment.range && node.range[0] < assignment.range[1]) {
2966
- return false;
2967
- }
2968
- return assignment.left.type === "Identifier" && assignment.left.name === node.name;
2969
- }
2970
- function dereference(context, node) {
2971
- if (node?.type !== "Identifier") {
2972
- return node;
2973
- }
2974
- const scope = context.sourceCode.getScope(node);
2975
- const parents = scope.references.map((ref) => ref.identifier).map((ident) => ident.parent);
2976
- const decl = parents.filter(isVariableDeclarator).find((p) => p.id.type === "Identifier" && p.id.name === node.name);
2977
- const expr = parents.filter(isAssignmentExpression).reverse().find((assignment) => isNodeLastAssignment(node, assignment));
2978
- return expr?.right ?? decl?.init;
2979
- }
2980
- var methods3, supportedMatchers, isVariableDeclarator, isAssignmentExpression, prefer_web_first_assertions_default;
3233
+ var methods3, supportedMatchers, prefer_web_first_assertions_default;
2981
3234
  var init_prefer_web_first_assertions = __esm({
2982
3235
  "src/rules/prefer-web-first-assertions.ts"() {
2983
3236
  "use strict";
@@ -3025,8 +3278,6 @@ var init_prefer_web_first_assertions = __esm({
3025
3278
  "toBeTruthy",
3026
3279
  "toBeFalsy"
3027
3280
  ]);
3028
- isVariableDeclarator = (node) => node.type === "VariableDeclarator";
3029
- isAssignmentExpression = (node) => node.type === "AssignmentExpression";
3030
3281
  prefer_web_first_assertions_default = createRule({
3031
3282
  create(context) {
3032
3283
  return {
@@ -3892,17 +4143,19 @@ var init_valid_title = __esm({
3892
4143
  const [matcher, message] = Array.isArray(matcherMaybeWithMessage) ? matcherMaybeWithMessage : [matcherMaybeWithMessage];
3893
4144
  return [new RegExp(matcher, "u"), message];
3894
4145
  };
3895
- compileMatcherPatterns = (matchers) => {
3896
- if (typeof matchers === "string" || Array.isArray(matchers)) {
3897
- const compiledMatcher = compileMatcherPattern(matchers);
4146
+ compileMatcherPatterns = (matchers2) => {
4147
+ if (typeof matchers2 === "string" || Array.isArray(matchers2)) {
4148
+ const compiledMatcher = compileMatcherPattern(matchers2);
3898
4149
  return {
3899
4150
  describe: compiledMatcher,
4151
+ step: compiledMatcher,
3900
4152
  test: compiledMatcher
3901
4153
  };
3902
4154
  }
3903
4155
  return {
3904
- describe: matchers.describe ? compileMatcherPattern(matchers.describe) : null,
3905
- test: matchers.test ? compileMatcherPattern(matchers.test) : null
4156
+ describe: matchers2.describe ? compileMatcherPattern(matchers2.describe) : null,
4157
+ step: matchers2.step ? compileMatcherPattern(matchers2.step) : null,
4158
+ test: matchers2.test ? compileMatcherPattern(matchers2.test) : null
3906
4159
  };
3907
4160
  };
3908
4161
  MatcherAndMessageSchema = {
@@ -3919,6 +4172,7 @@ var init_valid_title = __esm({
3919
4172
  disallowedWords = [],
3920
4173
  ignoreSpaces = false,
3921
4174
  ignoreTypeOfDescribeName = false,
4175
+ ignoreTypeOfStepName = true,
3922
4176
  ignoreTypeOfTestName = false,
3923
4177
  mustMatch,
3924
4178
  mustNotMatch
@@ -3932,7 +4186,7 @@ var init_valid_title = __esm({
3932
4186
  return {
3933
4187
  CallExpression(node) {
3934
4188
  const call = parseFnCall(context, node);
3935
- if (call?.type !== "test" && call?.type !== "describe") {
4189
+ if (call?.type !== "test" && call?.type !== "describe" && call?.type !== "step") {
3936
4190
  return;
3937
4191
  }
3938
4192
  const [argument] = node.arguments;
@@ -3942,7 +4196,7 @@ var init_valid_title = __esm({
3942
4196
  if (argument.type === "BinaryExpression" && doesBinaryExpressionContainStringNode(argument)) {
3943
4197
  return;
3944
4198
  }
3945
- if (!(call.type === "describe" && ignoreTypeOfDescribeName || call.type === "test" && ignoreTypeOfTestName) && argument.type !== "TemplateLiteral") {
4199
+ if (!(call.type === "describe" && ignoreTypeOfDescribeName || call.type === "test" && ignoreTypeOfTestName || call.type === "step" && ignoreTypeOfStepName) && argument.type !== "TemplateLiteral") {
3946
4200
  context.report({
3947
4201
  loc: argument.loc,
3948
4202
  messageId: "titleMustBeString"
@@ -4056,7 +4310,7 @@ var init_valid_title = __esm({
4056
4310
  additionalProperties: {
4057
4311
  oneOf: [{ type: "string" }, MatcherAndMessageSchema]
4058
4312
  },
4059
- propertyNames: { enum: ["describe", "test"] },
4313
+ propertyNames: { enum: ["describe", "test", "step"] },
4060
4314
  type: "object"
4061
4315
  }
4062
4316
  ]
@@ -4075,6 +4329,10 @@ var init_valid_title = __esm({
4075
4329
  default: false,
4076
4330
  type: "boolean"
4077
4331
  },
4332
+ ignoreTypeOfStepName: {
4333
+ default: true,
4334
+ type: "boolean"
4335
+ },
4078
4336
  ignoreTypeOfTestName: {
4079
4337
  default: false,
4080
4338
  type: "boolean"
@@ -4124,7 +4382,9 @@ var require_src = __commonJS({
4124
4382
  init_prefer_equality_matcher();
4125
4383
  init_prefer_hooks_in_order();
4126
4384
  init_prefer_hooks_on_top();
4385
+ init_prefer_locator();
4127
4386
  init_prefer_lowercase_title();
4387
+ init_prefer_native_locators();
4128
4388
  init_prefer_strict_equal();
4129
4389
  init_prefer_to_be();
4130
4390
  init_prefer_to_contain();
@@ -4173,7 +4433,9 @@ var require_src = __commonJS({
4173
4433
  "prefer-equality-matcher": prefer_equality_matcher_default,
4174
4434
  "prefer-hooks-in-order": prefer_hooks_in_order_default,
4175
4435
  "prefer-hooks-on-top": prefer_hooks_on_top_default,
4436
+ "prefer-locator": prefer_locator_default,
4176
4437
  "prefer-lowercase-title": prefer_lowercase_title_default,
4438
+ "prefer-native-locators": prefer_native_locators_default,
4177
4439
  "prefer-strict-equal": prefer_strict_equal_default,
4178
4440
  "prefer-to-be": prefer_to_be_default,
4179
4441
  "prefer-to-contain": prefer_to_contain_default,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "eslint-plugin-playwright",
3
3
  "description": "ESLint plugin for Playwright testing.",
4
- "version": "1.6.1",
4
+ "version": "1.7.0",
5
5
  "repository": "https://github.com/playwright-community/eslint-plugin-playwright",
6
6
  "author": "Mark Skelton <mark@mskelton.dev>",
7
7
  "packageManager": "pnpm@8.12.0",
@@ -30,7 +30,7 @@
30
30
  "dist"
31
31
  ],
32
32
  "scripts": {
33
- "build": "tsup src/index.ts --format cjs,esm --dts --out-dir dist",
33
+ "build": "tsup src/index.ts --format cjs,esm --dts --out-dir dist --cjsInterop",
34
34
  "lint": "eslint .",
35
35
  "fmt": "prettier --write .",
36
36
  "fmt:check": "prettier --check .",