eslint-plugin-playwright 2.5.1 → 2.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +4 -0
  2. package/dist/index.cjs +266 -11
  3. package/package.json +14 -14
package/README.md CHANGED
@@ -128,6 +128,7 @@ CLI option\
128
128
  | [no-conditional-expect](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/no-conditional-expect.md) | Disallow calling `expect` conditionally | ✅ | | |
129
129
  | [no-conditional-in-test](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/no-conditional-in-test.md) | Disallow conditional logic in tests | ✅ | | |
130
130
  | [no-duplicate-hooks](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/no-duplicate-hooks.md) | Disallow duplicate setup and teardown hooks | | | |
131
+ | [no-duplicate-slow](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/no-duplicate-slow.md) | Disallow multiple `test.slow()` calls in the same test | ✅ | | |
131
132
  | [no-element-handle](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/no-element-handle.md) | Disallow usage of element handles | ✅ | | 💡 |
132
133
  | [no-eval](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/no-eval.md) | Disallow usage of `page.$eval()` and `page.$$eval()` | ✅ | | |
133
134
  | [no-focused-test](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/no-focused-test.md) | Disallow usage of `.only` annotation | ✅ | | 💡 |
@@ -141,6 +142,7 @@ CLI option\
141
142
  | [no-raw-locators](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/no-raw-locators.md) | Disallow using raw locators | | | |
142
143
  | [no-restricted-locators](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/no-restricted-locators.md) | Disallow specific locator methods | | | |
143
144
  | [no-restricted-matchers](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/no-restricted-matchers.md) | Disallow specific matchers & modifiers | | | |
145
+ | [no-restricted-roles](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/no-restricted-roles.md) | Disallow specific roles in `getByRole()` | | | |
144
146
  | [no-skipped-test](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/no-skipped-test.md) | Disallow usage of the `.skip` annotation | ✅ | | 💡 |
145
147
  | [no-slowed-test](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/no-slowed-test.md) | Disallow usage of the `.slow` annotation | | | 💡 |
146
148
  | [no-standalone-expect](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/no-standalone-expect.md) | Disallow using expect outside of `test` blocks | ✅ | | |
@@ -166,6 +168,8 @@ CLI option\
166
168
  | [prefer-web-first-assertions](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/prefer-web-first-assertions.md) | Suggest using web first assertions | ✅ | 🔧 | |
167
169
  | [require-hook](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/require-hook.md) | Require setup and teardown code to be within a hook | | | |
168
170
  | [require-soft-assertions](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/require-soft-assertions.md) | Require assertions to use `expect.soft()` | | 🔧 | |
171
+ | [require-tags](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/require-tags.md) | Require test blocks to have tags | | | |
172
+ | [require-to-pass-timeout](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/require-to-pass-timeout.md) | Require a timeout option for `toPass()` | | | |
169
173
  | [require-to-throw-message](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/require-to-throw-message.md) | Require a message for `toThrow()` | | | |
170
174
  | [require-top-level-describe](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/require-top-level-describe.md) | Require test cases and hooks to be inside a `test.describe` block | | | |
171
175
  | [valid-describe-callback](https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/valid-describe-callback.md) | Enforce valid `describe()` callback | ✅ | | |
package/dist/index.cjs CHANGED
@@ -22,7 +22,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
22
22
  mod
23
23
  ));
24
24
 
25
- // src/index.ts
25
+ // src/plugin.ts
26
26
  var import_globals = __toESM(require("globals"), 1);
27
27
 
28
28
  // src/utils/parseFnCall.ts
@@ -599,9 +599,9 @@ var expect_expect_default = createRule({
599
599
  const unchecked = [];
600
600
  function checkExpressions(nodes) {
601
601
  for (const node of nodes) {
602
- const index2 = node.type === "CallExpression" ? unchecked.indexOf(node) : -1;
603
- if (index2 !== -1) {
604
- unchecked.splice(index2, 1);
602
+ const index = node.type === "CallExpression" ? unchecked.indexOf(node) : -1;
603
+ if (index !== -1) {
604
+ unchecked.splice(index, 1);
605
605
  break;
606
606
  }
607
607
  }
@@ -784,6 +784,18 @@ var max_nested_describe_default = createRule({
784
784
 
785
785
  // src/rules/missing-playwright-await.ts
786
786
  var validTypes = /* @__PURE__ */ new Set(["AwaitExpression", "ReturnStatement", "ArrowFunctionExpression"]);
787
+ var waitForMethods = [
788
+ "waitForConsoleMessage",
789
+ "waitForDownload",
790
+ "waitForEvent",
791
+ "waitForFileChooser",
792
+ "waitForFunction",
793
+ "waitForPopup",
794
+ "waitForRequest",
795
+ "waitForResponse",
796
+ "waitForWebSocket"
797
+ ];
798
+ var waitForMethodsRegex = new RegExp(`^(${waitForMethods.join("|")})$`);
787
799
  var expectPlaywrightMatchers = [
788
800
  "toBeChecked",
789
801
  "toBeDisabled",
@@ -886,6 +898,17 @@ var missing_playwright_await_default = createRule({
886
898
  }
887
899
  return {
888
900
  CallExpression(node) {
901
+ if (isPageMethod(node, waitForMethodsRegex)) {
902
+ if (!checkValidity(node, /* @__PURE__ */ new Set())) {
903
+ const methodName = getStringValue(node.callee.property);
904
+ context.report({
905
+ data: { methodName },
906
+ messageId: "waitFor",
907
+ node
908
+ });
909
+ }
910
+ return;
911
+ }
889
912
  const call = parseFnCall(context, node);
890
913
  if (call?.type !== "step" && call?.type !== "expect") return;
891
914
  const result = getCallType(call, awaitableMatchers);
@@ -912,7 +935,8 @@ var missing_playwright_await_default = createRule({
912
935
  messages: {
913
936
  expect: "'{{matcherName}}' must be awaited or returned.",
914
937
  expectPoll: "'expect.poll' matchers must be awaited or returned.",
915
- testStep: "'test.step' must be awaited or returned."
938
+ testStep: "'test.step' must be awaited or returned.",
939
+ waitFor: "'{{methodName}}' must be awaited or returned."
916
940
  },
917
941
  schema: [
918
942
  {
@@ -1149,6 +1173,50 @@ var no_duplicate_hooks_default = createRule({
1149
1173
  }
1150
1174
  });
1151
1175
 
1176
+ // src/rules/no-duplicate-slow.ts
1177
+ var no_duplicate_slow_default = createRule({
1178
+ create(context) {
1179
+ const scopes = [false];
1180
+ return {
1181
+ "CallExpression"(node) {
1182
+ const call = parseFnCall(context, node);
1183
+ if (!call) return;
1184
+ if (call.type === "test" || call.type === "describe") {
1185
+ scopes.push(scopes[scopes.length - 1]);
1186
+ }
1187
+ if (call.group === "test" && call.type === "config") {
1188
+ const isSlowCall = call.members.some((s) => getStringValue(s) === "slow");
1189
+ if (isSlowCall) {
1190
+ const current = scopes.length - 1;
1191
+ if (scopes[current]) {
1192
+ context.report({ messageId: "noDuplicateSlow", node });
1193
+ } else {
1194
+ scopes[current] = true;
1195
+ }
1196
+ }
1197
+ }
1198
+ },
1199
+ "CallExpression:exit"(node) {
1200
+ if (isTypeOfFnCall(context, node, ["test", "describe"])) {
1201
+ scopes.pop();
1202
+ }
1203
+ }
1204
+ };
1205
+ },
1206
+ meta: {
1207
+ docs: {
1208
+ category: "Best Practices",
1209
+ description: "Disallow multiple `test.slow()` calls in the same test",
1210
+ recommended: true,
1211
+ url: "https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/no-duplicate-slow.md"
1212
+ },
1213
+ messages: {
1214
+ noDuplicateSlow: "Multiple `test.slow()` calls will multiply the timeout. Use only one `test.slow()` per test."
1215
+ },
1216
+ type: "problem"
1217
+ }
1218
+ });
1219
+
1152
1220
  // src/rules/no-element-handle.ts
1153
1221
  function getPropertyRange(node) {
1154
1222
  return node.type === "Identifier" ? node.range : [node.range[0] + 1, node.range[1] - 1];
@@ -1727,6 +1795,77 @@ var no_restricted_matchers_default = createRule({
1727
1795
  }
1728
1796
  });
1729
1797
 
1798
+ // src/rules/no-restricted-roles.ts
1799
+ var no_restricted_roles_default = createRule({
1800
+ create(context) {
1801
+ const options = context.options?.[0] ?? [];
1802
+ const restrictions = options.map((option) => {
1803
+ if (typeof option === "string") {
1804
+ return { message: null, role: option };
1805
+ }
1806
+ return {
1807
+ message: option.message ?? null,
1808
+ role: option.role
1809
+ };
1810
+ });
1811
+ const restrictionMap = /* @__PURE__ */ new Map();
1812
+ for (const restriction of restrictions) {
1813
+ restrictionMap.set(restriction.role, restriction.message);
1814
+ }
1815
+ return {
1816
+ CallExpression(node) {
1817
+ if (!isPageMethod(node, "getByRole")) {
1818
+ return;
1819
+ }
1820
+ const role = getStringValue(node.arguments[0]);
1821
+ if (!role) {
1822
+ return;
1823
+ }
1824
+ if (restrictionMap.has(role)) {
1825
+ const message = restrictionMap.get(role) ?? "";
1826
+ context.report({
1827
+ data: { message, role },
1828
+ messageId: message ? "restrictedWithMessage" : "restricted",
1829
+ node
1830
+ });
1831
+ }
1832
+ }
1833
+ };
1834
+ },
1835
+ meta: {
1836
+ docs: {
1837
+ category: "Best Practices",
1838
+ description: "Disallows the usage of specific roles in getByRole()",
1839
+ recommended: false,
1840
+ url: "https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/no-restricted-roles.md"
1841
+ },
1842
+ messages: {
1843
+ restricted: "Usage of role `{{role}}` in getByRole() is disallowed",
1844
+ restrictedWithMessage: "{{message}}"
1845
+ },
1846
+ schema: [
1847
+ {
1848
+ items: {
1849
+ oneOf: [
1850
+ { type: "string" },
1851
+ {
1852
+ additionalProperties: false,
1853
+ properties: {
1854
+ message: { type: "string" },
1855
+ role: { type: "string" }
1856
+ },
1857
+ required: ["role"],
1858
+ type: "object"
1859
+ }
1860
+ ]
1861
+ },
1862
+ type: "array"
1863
+ }
1864
+ ],
1865
+ type: "suggestion"
1866
+ }
1867
+ });
1868
+
1730
1869
  // src/rules/no-skipped-test.ts
1731
1870
  var no_skipped_test_default = createRule({
1732
1871
  create(context) {
@@ -2189,8 +2328,8 @@ function removePropertyFixer(fixer, property) {
2189
2328
  if (parent.properties.length === 1) {
2190
2329
  return fixer.remove(parent);
2191
2330
  }
2192
- const index2 = parent.properties.indexOf(property);
2193
- const range = index2 ? [parent.properties[index2 - 1].range[1], property.range[1]] : [property.range[0], parent.properties[1].range[0]];
2331
+ const index = parent.properties.indexOf(property);
2332
+ const range = index ? [parent.properties[index - 1].range[1], property.range[1]] : [property.range[0], parent.properties[1].range[0]];
2194
2333
  return fixer.removeRange(range);
2195
2334
  }
2196
2335
 
@@ -3409,6 +3548,113 @@ var require_soft_assertions_default = createRule({
3409
3548
  }
3410
3549
  });
3411
3550
 
3551
+ // src/rules/require-tags.ts
3552
+ var tagRegex = /@[\S]+/;
3553
+ function hasTagInOptions(node) {
3554
+ const options = node.arguments[1];
3555
+ if (!options || options.type !== "ObjectExpression") {
3556
+ return false;
3557
+ }
3558
+ return options.properties.some(
3559
+ (prop) => prop.type === "Property" && prop.key.type === "Identifier" && prop.key.name === "tag"
3560
+ );
3561
+ }
3562
+ function hasTagInTitle(node) {
3563
+ const title = node.arguments[0];
3564
+ if (!title || title.type !== "Literal" || typeof title.value !== "string") {
3565
+ return false;
3566
+ }
3567
+ return tagRegex.test(title.value);
3568
+ }
3569
+ function hasTags(node) {
3570
+ return hasTagInTitle(node) || hasTagInOptions(node);
3571
+ }
3572
+ var require_tags_default = createRule({
3573
+ create(context) {
3574
+ const describeStack = [];
3575
+ return {
3576
+ "CallExpression"(node) {
3577
+ const call = parseFnCall(context, node);
3578
+ if (!call) {
3579
+ return;
3580
+ }
3581
+ if (call.type === "describe") {
3582
+ describeStack.push(hasTags(node) || !!describeStack.at(-1));
3583
+ return;
3584
+ }
3585
+ if (call.type === "test") {
3586
+ if (hasTags(node) || !!describeStack.at(-1)) {
3587
+ return;
3588
+ }
3589
+ context.report({ messageId: "missingTag", node });
3590
+ }
3591
+ },
3592
+ "CallExpression:exit"(node) {
3593
+ if (isTypeOfFnCall(context, node, ["describe"])) {
3594
+ describeStack.pop();
3595
+ }
3596
+ }
3597
+ };
3598
+ },
3599
+ meta: {
3600
+ docs: {
3601
+ category: "Best Practices",
3602
+ description: "Require test blocks to have tags",
3603
+ recommended: false,
3604
+ url: "https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/require-tags.md"
3605
+ },
3606
+ messages: {
3607
+ missingTag: "Test must have at least one tag"
3608
+ },
3609
+ schema: [],
3610
+ type: "suggestion"
3611
+ }
3612
+ });
3613
+
3614
+ // src/rules/require-to-pass-timeout.ts
3615
+ var require_to_pass_timeout_default = createRule({
3616
+ create(context) {
3617
+ return {
3618
+ CallExpression(node) {
3619
+ const call = parseFnCall(context, node);
3620
+ if (call?.type !== "expect" || call.matcherName !== "toPass") {
3621
+ return;
3622
+ }
3623
+ const [options] = call.matcherArgs;
3624
+ if (!options || options.type !== "ObjectExpression") {
3625
+ context.report({
3626
+ messageId: "addTimeoutOption",
3627
+ node: call.matcher
3628
+ });
3629
+ return;
3630
+ }
3631
+ const hasTimeout = options.properties.some(
3632
+ (prop) => prop.type === "Property" && prop.key.type === "Identifier" && prop.key.name === "timeout"
3633
+ );
3634
+ if (!hasTimeout) {
3635
+ context.report({
3636
+ messageId: "addTimeoutOption",
3637
+ node: call.matcher
3638
+ });
3639
+ }
3640
+ }
3641
+ };
3642
+ },
3643
+ meta: {
3644
+ docs: {
3645
+ category: "Best Practices",
3646
+ description: "Require a timeout option for `toPass()`",
3647
+ recommended: false,
3648
+ url: "https://github.com/mskelton/eslint-plugin-playwright/tree/main/docs/rules/require-to-pass-timeout.md"
3649
+ },
3650
+ messages: {
3651
+ addTimeoutOption: "Add a timeout option to toPass()"
3652
+ },
3653
+ schema: [],
3654
+ type: "suggestion"
3655
+ }
3656
+ });
3657
+
3412
3658
  // src/rules/require-to-throw-message.ts
3413
3659
  var require_to_throw_message_default = createRule({
3414
3660
  create(context) {
@@ -4174,6 +4420,9 @@ var valid_title_default = createRule({
4174
4420
  if (title.type === "BinaryExpression" && doesBinaryExpressionContainStringNode(title)) {
4175
4421
  return;
4176
4422
  }
4423
+ if (title.type === "Identifier" || title.type === "MemberExpression") {
4424
+ return;
4425
+ }
4177
4426
  if (!(call.type === "describe" && ignoreTypeOfDescribeName || call.type === "test" && ignoreTypeOfTestName || call.type === "step" && ignoreTypeOfStepName) && title.type !== "TemplateLiteral") {
4178
4427
  context.report({
4179
4428
  loc: title.loc,
@@ -4323,8 +4572,8 @@ var valid_title_default = createRule({
4323
4572
  }
4324
4573
  });
4325
4574
 
4326
- // src/index.ts
4327
- var index = {
4575
+ // src/plugin.ts
4576
+ var plugin = {
4328
4577
  configs: {},
4329
4578
  rules: {
4330
4579
  "consistent-spacing-between-blocks": consistent_spacing_between_blocks_default,
@@ -4336,6 +4585,7 @@ var index = {
4336
4585
  "no-conditional-expect": no_conditional_expect_default,
4337
4586
  "no-conditional-in-test": no_conditional_in_test_default,
4338
4587
  "no-duplicate-hooks": no_duplicate_hooks_default,
4588
+ "no-duplicate-slow": no_duplicate_slow_default,
4339
4589
  "no-element-handle": no_element_handle_default,
4340
4590
  "no-eval": no_eval_default,
4341
4591
  "no-focused-test": no_focused_test_default,
@@ -4349,6 +4599,7 @@ var index = {
4349
4599
  "no-raw-locators": no_raw_locators_default,
4350
4600
  "no-restricted-locators": no_restricted_locators_default,
4351
4601
  "no-restricted-matchers": no_restricted_matchers_default,
4602
+ "no-restricted-roles": no_restricted_roles_default,
4352
4603
  "no-skipped-test": no_skipped_test_default,
4353
4604
  "no-slowed-test": no_slowed_test_default,
4354
4605
  "no-standalone-expect": no_standalone_expect_default,
@@ -4374,6 +4625,8 @@ var index = {
4374
4625
  "prefer-web-first-assertions": prefer_web_first_assertions_default,
4375
4626
  "require-hook": require_hook_default,
4376
4627
  "require-soft-assertions": require_soft_assertions_default,
4628
+ "require-tags": require_tags_default,
4629
+ "require-to-pass-timeout": require_to_pass_timeout_default,
4377
4630
  "require-to-throw-message": require_to_throw_message_default,
4378
4631
  "require-top-level-describe": require_top_level_describe_default,
4379
4632
  "valid-describe-callback": valid_describe_callback_default,
@@ -4429,11 +4682,13 @@ var flatConfig = {
4429
4682
  globals: import_globals.default["shared-node-browser"]
4430
4683
  },
4431
4684
  plugins: {
4432
- playwright: index
4685
+ playwright: plugin
4433
4686
  }
4434
4687
  };
4688
+
4689
+ // src/index.ts
4435
4690
  module.exports = {
4436
- ...index,
4691
+ ...plugin,
4437
4692
  configs: {
4438
4693
  "flat/recommended": flatConfig,
4439
4694
  "playwright-test": legacyConfig,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-playwright",
3
- "version": "2.5.1",
3
+ "version": "2.6.1",
4
4
  "description": "ESLint plugin for Playwright testing.",
5
5
  "license": "MIT",
6
6
  "author": "Mark Skelton <mark@mskelton.dev>",
@@ -32,24 +32,24 @@
32
32
  "format": "oxfmt .",
33
33
  "format:check": "oxfmt --check .",
34
34
  "test": "vitest --hideSkippedTests",
35
- "test:watch": "vitest --reporter=dot",
36
- "ts": "tsc --noEmit"
35
+ "typecheck": "tsc --noEmit",
36
+ "ci": "yarn format:check && yarn lint && yarn typecheck"
37
37
  },
38
38
  "dependencies": {
39
- "globals": "^16.4.0"
39
+ "globals": "^17.3.0"
40
40
  },
41
41
  "devDependencies": {
42
- "@mskelton/eslint-config": "^9.0.1",
43
- "@types/estree": "^1.0.6",
44
- "@types/node": "^20.11.17",
45
- "@typescript-eslint/parser": "^8.11.0",
46
- "dedent": "^1.5.1",
42
+ "@mskelton/eslint-config": "^9.3.0",
43
+ "@types/estree": "^1.0.8",
44
+ "@types/node": "^25.2.3",
45
+ "@typescript-eslint/parser": "^8.56.0",
46
+ "dedent": "^1.7.1",
47
47
  "eslint": "^9.13.0",
48
- "oxfmt": "^0.23.0",
49
- "semantic-release": "^25.0.2",
50
- "tsup": "^8.0.1",
51
- "typescript": "^5.2.2",
52
- "vitest": "^4.0.17"
48
+ "oxfmt": "^0.33.0",
49
+ "semantic-release": "^25.0.3",
50
+ "tsup": "^8.5.1",
51
+ "typescript": "^5.9.3",
52
+ "vitest": "^4.0.18"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "eslint": ">=8.40.0"