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 +2 -0
- package/dist/index.d.mts +6 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +275 -30
- package/dist/index.mjs +302 -40
- package/package.json +2 -2
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
|
|
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
|
-
|
|
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
|
|
134
|
+
return __privateMethod(this, _buildChain, buildChain_fn).call(this, node.tag);
|
|
92
135
|
case "MemberExpression":
|
|
93
|
-
return joinChains(
|
|
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
|
|
141
|
+
return __privateMethod(this, _buildChain, buildChain_fn).call(this, node.callee, true);
|
|
142
|
+
default:
|
|
143
|
+
return null;
|
|
96
144
|
}
|
|
97
|
-
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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" || !
|
|
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,
|
|
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 = (
|
|
3483
|
-
if (typeof
|
|
3484
|
-
const compiledMatcher = compileMatcherPattern(
|
|
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:
|
|
3492
|
-
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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" || !
|
|
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,
|
|
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 = (
|
|
3896
|
-
if (typeof
|
|
3897
|
-
const compiledMatcher = compileMatcherPattern(
|
|
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:
|
|
3905
|
-
|
|
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.
|
|
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 .",
|