eslint-plugin-playwright 1.6.2 → 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;
@@ -2240,6 +2307,66 @@ var prefer_hooks_on_top_default = createRule({
2240
2307
  }
2241
2308
  });
2242
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
+
2243
2370
  // src/rules/prefer-lowercase-title.ts
2244
2371
  var prefer_lowercase_title_default = createRule({
2245
2372
  create(context) {
@@ -2335,6 +2462,113 @@ var prefer_lowercase_title_default = createRule({
2335
2462
  }
2336
2463
  });
2337
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
+
2338
2572
  // src/rules/prefer-strict-equal.ts
2339
2573
  var prefer_strict_equal_default = createRule({
2340
2574
  create(context) {
@@ -2544,16 +2778,18 @@ var prefer_to_contain_default = createRule({
2544
2778
  });
2545
2779
 
2546
2780
  // src/rules/prefer-to-have-count.ts
2781
+ var matchers = /* @__PURE__ */ new Set([...equalityMatchers, "toHaveLength"]);
2547
2782
  var prefer_to_have_count_default = createRule({
2548
2783
  create(context) {
2549
2784
  return {
2550
2785
  CallExpression(node) {
2551
2786
  const call = parseFnCall(context, node);
2552
- if (call?.type !== "expect" || !equalityMatchers.has(call.matcherName)) {
2787
+ if (call?.type !== "expect" || !matchers.has(call.matcherName)) {
2553
2788
  return;
2554
2789
  }
2790
+ const accessor = call.matcherName === "toHaveLength" ? "all" : "count";
2555
2791
  const argument = dereference(context, call.args[0]);
2556
- if (argument?.type !== "AwaitExpression" || argument.argument.type !== "CallExpression" || argument.argument.callee.type !== "MemberExpression" || !isPropertyAccessor(argument.argument.callee, "count")) {
2792
+ if (argument?.type !== "AwaitExpression" || argument.argument.type !== "CallExpression" || argument.argument.callee.type !== "MemberExpression" || !isPropertyAccessor(argument.argument.callee, accessor)) {
2557
2793
  return;
2558
2794
  }
2559
2795
  const callee = argument.argument.callee;
@@ -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;
@@ -2516,6 +2582,74 @@ var init_prefer_hooks_on_top = __esm({
2516
2582
  }
2517
2583
  });
2518
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
+
2519
2653
  // src/rules/prefer-lowercase-title.ts
2520
2654
  var prefer_lowercase_title_default;
2521
2655
  var init_prefer_lowercase_title = __esm({
@@ -2620,6 +2754,121 @@ var init_prefer_lowercase_title = __esm({
2620
2754
  }
2621
2755
  });
2622
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
+
2623
2872
  // src/rules/prefer-strict-equal.ts
2624
2873
  var prefer_strict_equal_default;
2625
2874
  var init_prefer_strict_equal = __esm({
@@ -2857,7 +3106,7 @@ var init_prefer_to_contain = __esm({
2857
3106
  });
2858
3107
 
2859
3108
  // src/rules/prefer-to-have-count.ts
2860
- var prefer_to_have_count_default;
3109
+ var matchers, prefer_to_have_count_default;
2861
3110
  var init_prefer_to_have_count = __esm({
2862
3111
  "src/rules/prefer-to-have-count.ts"() {
2863
3112
  "use strict";
@@ -2865,16 +3114,18 @@ var init_prefer_to_have_count = __esm({
2865
3114
  init_createRule();
2866
3115
  init_fixer();
2867
3116
  init_parseFnCall();
3117
+ matchers = /* @__PURE__ */ new Set([...equalityMatchers, "toHaveLength"]);
2868
3118
  prefer_to_have_count_default = createRule({
2869
3119
  create(context) {
2870
3120
  return {
2871
3121
  CallExpression(node) {
2872
3122
  const call = parseFnCall(context, node);
2873
- if (call?.type !== "expect" || !equalityMatchers.has(call.matcherName)) {
3123
+ if (call?.type !== "expect" || !matchers.has(call.matcherName)) {
2874
3124
  return;
2875
3125
  }
3126
+ const accessor = call.matcherName === "toHaveLength" ? "all" : "count";
2876
3127
  const argument = dereference(context, call.args[0]);
2877
- if (argument?.type !== "AwaitExpression" || argument.argument.type !== "CallExpression" || argument.argument.callee.type !== "MemberExpression" || !isPropertyAccessor(argument.argument.callee, "count")) {
3128
+ if (argument?.type !== "AwaitExpression" || argument.argument.type !== "CallExpression" || argument.argument.callee.type !== "MemberExpression" || !isPropertyAccessor(argument.argument.callee, accessor)) {
2878
3129
  return;
2879
3130
  }
2880
3131
  const callee = argument.argument.callee;
@@ -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.2",
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 .",