eslint-plugin-playwright 2.1.0 → 2.2.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 +3 -0
  2. package/dist/index.cjs +293 -42
  3. package/package.json +1 -29
package/README.md CHANGED
@@ -139,10 +139,12 @@ CLI option\
139
139
  | [no-raw-locators](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-raw-locators.md) | Disallow using raw locators | | | |
140
140
  | [no-restricted-matchers](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-restricted-matchers.md) | Disallow specific matchers & modifiers | | | |
141
141
  | [no-skipped-test](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-skipped-test.md) | Disallow usage of the `.skip` annotation | ✅ | | 💡 |
142
+ | [no-slowed-test](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-slowed-test.md) | Disallow usage of the `.slow` annotation | | | 💡 |
142
143
  | [no-standalone-expect](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-standalone-expect.md) | Disallow using expect outside of `test` blocks | ✅ | | |
143
144
  | [no-unsafe-references](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-unsafe-references.md) | Prevent unsafe variable references in `page.evaluate()` | ✅ | 🔧 | |
144
145
  | [no-useless-await](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-useless-await.md) | Disallow unnecessary `await`s for Playwright methods | ✅ | 🔧 | |
145
146
  | [no-useless-not](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-useless-not.md) | Disallow usage of `not` matchers when a specific matcher exists | ✅ | 🔧 | |
147
+ | [no-wait-for-navigation](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-wait-for-navigation.md) | Disallow usage of `page.waitForNavigation()` | ✅ | | 💡 |
146
148
  | [no-wait-for-selector](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-wait-for-selector.md) | Disallow usage of `page.waitForSelector()` | ✅ | | 💡 |
147
149
  | [no-wait-for-timeout](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-wait-for-timeout.md) | Disallow usage of `page.waitForTimeout()` | ✅ | | 💡 |
148
150
  | [prefer-comparison-matcher](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-comparison-matcher.md) | Suggest using the built-in comparison matchers | | 🔧 | |
@@ -166,3 +168,4 @@ CLI option\
166
168
  | [valid-expect-in-promise](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/valid-expect-in-promise.md) | Require promises that have expectations in their chain to be valid | ✅ | | |
167
169
  | [valid-expect](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/valid-expect.md) | Enforce valid `expect()` usage | ✅ | | |
168
170
  | [valid-title](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/valid-title.md) | Enforce valid titles | ✅ | 🔧 | |
171
+ | [valid-test-tags](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/valid-test-tags.md) | Enforce valid tag format in test blocks | ✅ | | |
package/dist/index.cjs CHANGED
@@ -100,6 +100,7 @@ var VALID_CHAINS = /* @__PURE__ */ new Set([
100
100
  "test.only",
101
101
  "test.skip",
102
102
  "test.step",
103
+ "test.step.skip",
103
104
  "test.slow",
104
105
  "test.use"
105
106
  ]);
@@ -485,7 +486,7 @@ var expect_expect_default = createRule({
485
486
  },
486
487
  "Program:exit"() {
487
488
  unchecked.forEach((node) => {
488
- context.report({ messageId: "noAssertions", node });
489
+ context.report({ messageId: "noAssertions", node: node.callee });
489
490
  });
490
491
  }
491
492
  };
@@ -664,6 +665,7 @@ var expectPlaywrightMatchers = [
664
665
  "toPass"
665
666
  ];
666
667
  var playwrightTestMatchers = [
668
+ "toBeAttached",
667
669
  "toBeChecked",
668
670
  "toBeDisabled",
669
671
  "toBeEditable",
@@ -671,23 +673,24 @@ var playwrightTestMatchers = [
671
673
  "toBeEnabled",
672
674
  "toBeFocused",
673
675
  "toBeHidden",
676
+ "toBeInViewport",
677
+ "toBeOK",
674
678
  "toBeVisible",
675
679
  "toContainText",
680
+ "toHaveAccessibleErrorMessage",
676
681
  "toHaveAttribute",
682
+ "toHaveCSS",
677
683
  "toHaveClass",
678
684
  "toHaveCount",
679
- "toHaveCSS",
680
685
  "toHaveId",
681
686
  "toHaveJSProperty",
682
- "toBeOK",
683
687
  "toHaveScreenshot",
684
688
  "toHaveText",
685
689
  "toHaveTitle",
686
690
  "toHaveURL",
687
691
  "toHaveValue",
688
692
  "toHaveValues",
689
- "toBeAttached",
690
- "toBeInViewport"
693
+ "toContainClass"
691
694
  ];
692
695
  function getReportNode(node) {
693
696
  const parent = getParent(node);
@@ -927,7 +930,17 @@ var no_conditional_in_test_default = createRule({
927
930
  if (!call)
928
931
  return;
929
932
  if (isTypeOfFnCall(context, call, ["test", "step"])) {
930
- context.report({ messageId: "conditionalInTest", node });
933
+ const testFunction = call.arguments[call.arguments.length - 1];
934
+ const functionBody = findParent(node, "BlockStatement");
935
+ if (!functionBody)
936
+ return;
937
+ let currentParent = functionBody.parent;
938
+ while (currentParent && currentParent !== testFunction) {
939
+ currentParent = currentParent.parent;
940
+ }
941
+ if (currentParent === testFunction) {
942
+ context.report({ messageId: "conditionalInTest", node });
943
+ }
931
944
  }
932
945
  }
933
946
  return {
@@ -1254,18 +1267,11 @@ var no_hooks_default = createRule({
1254
1267
  });
1255
1268
 
1256
1269
  // src/rules/no-nested-step.ts
1257
- function isStepCall(node) {
1258
- const inner = node.type === "CallExpression" ? node.callee : node;
1259
- if (inner.type !== "MemberExpression") {
1260
- return false;
1261
- }
1262
- return isPropertyAccessor(inner, "step");
1263
- }
1264
1270
  var no_nested_step_default = createRule({
1265
1271
  create(context) {
1266
1272
  const stack = [];
1267
1273
  function pushStepCallback(node) {
1268
- if (node.parent.type !== "CallExpression" || !isStepCall(node.parent)) {
1274
+ if (node.parent.type !== "CallExpression" || !isTypeOfFnCall(context, node.parent, ["step"])) {
1269
1275
  return;
1270
1276
  }
1271
1277
  stack.push(0);
@@ -1278,7 +1284,7 @@ var no_nested_step_default = createRule({
1278
1284
  }
1279
1285
  function popStepCallback(node) {
1280
1286
  const { parent } = node;
1281
- if (parent.type === "CallExpression" && isStepCall(parent)) {
1287
+ if (parent.type === "CallExpression" && isTypeOfFnCall(context, parent, ["step"])) {
1282
1288
  stack.pop();
1283
1289
  }
1284
1290
  }
@@ -1434,7 +1440,7 @@ var no_raw_locators_default = createRule({
1434
1440
  }
1435
1441
  return {
1436
1442
  CallExpression(node) {
1437
- if (node.callee.type !== "MemberExpression")
1443
+ if (node.callee.type !== "MemberExpression" || node.arguments[0]?.type === "Identifier")
1438
1444
  return;
1439
1445
  const method = getStringValue(node.callee.property);
1440
1446
  const arg = getStringValue(node.arguments[0]);
@@ -1538,14 +1544,14 @@ var no_skipped_test_default = createRule({
1538
1544
  const options = context.options[0] || {};
1539
1545
  const allowConditional = !!options.allowConditional;
1540
1546
  const call = parseFnCall(context, node);
1541
- if (call?.group !== "test" && call?.group !== "describe") {
1547
+ if (call?.group !== "test" && call?.group !== "describe" && call?.group !== "step") {
1542
1548
  return;
1543
1549
  }
1544
1550
  const skipNode = call.members.find((s) => getStringValue(s) === "skip");
1545
1551
  if (!skipNode)
1546
1552
  return;
1547
1553
  const isStandalone = call.type === "config";
1548
- if (isStandalone && allowConditional) {
1554
+ if (isStandalone && allowConditional && (node.arguments.length !== 0 || findParent(node, "BlockStatement")?.parent?.type === "IfStatement")) {
1549
1555
  return;
1550
1556
  }
1551
1557
  context.report({
@@ -1594,6 +1600,70 @@ var no_skipped_test_default = createRule({
1594
1600
  }
1595
1601
  });
1596
1602
 
1603
+ // src/rules/no-slowed-test.ts
1604
+ var no_slowed_test_default = createRule({
1605
+ create(context) {
1606
+ return {
1607
+ CallExpression(node) {
1608
+ const options = context.options[0] || {};
1609
+ const allowConditional = !!options.allowConditional;
1610
+ const call = parseFnCall(context, node);
1611
+ if (call?.group !== "test") {
1612
+ return;
1613
+ }
1614
+ const slowNode = call.members.find((s) => getStringValue(s) === "slow");
1615
+ if (!slowNode)
1616
+ return;
1617
+ const isStandalone = call.type === "config";
1618
+ if (isStandalone && allowConditional && (node.arguments.length !== 0 || findParent(node, "BlockStatement")?.parent?.type === "IfStatement")) {
1619
+ return;
1620
+ }
1621
+ context.report({
1622
+ messageId: "noSlowedTest",
1623
+ node: isStandalone ? node : slowNode,
1624
+ suggest: [
1625
+ {
1626
+ fix: (fixer) => {
1627
+ return isStandalone ? fixer.remove(node.parent) : fixer.removeRange([
1628
+ slowNode.range[0] - 1,
1629
+ slowNode.range[1] + Number(slowNode.type !== "Identifier")
1630
+ ]);
1631
+ },
1632
+ messageId: "removeSlowedTestAnnotation"
1633
+ }
1634
+ ]
1635
+ });
1636
+ }
1637
+ };
1638
+ },
1639
+ meta: {
1640
+ docs: {
1641
+ category: "Best Practices",
1642
+ description: "Prevent usage of the `.slow()` slow test annotation.",
1643
+ recommended: false,
1644
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-slowed-test.md"
1645
+ },
1646
+ hasSuggestions: true,
1647
+ messages: {
1648
+ noSlowedTest: "Unexpected use of the `.slow()` annotation.",
1649
+ removeSlowedTestAnnotation: "Remove the `.slow()` annotation."
1650
+ },
1651
+ schema: [
1652
+ {
1653
+ additionalProperties: false,
1654
+ properties: {
1655
+ allowConditional: {
1656
+ default: false,
1657
+ type: "boolean"
1658
+ }
1659
+ },
1660
+ type: "object"
1661
+ }
1662
+ ],
1663
+ type: "suggestion"
1664
+ }
1665
+ });
1666
+
1597
1667
  // src/rules/no-standalone-expect.ts
1598
1668
  var getBlockType = (context, statement) => {
1599
1669
  const func = getParent(statement);
@@ -1736,7 +1806,7 @@ var no_unsafe_references_default = createRule({
1736
1806
  create(context) {
1737
1807
  return {
1738
1808
  CallExpression(node) {
1739
- if (!isPageMethod(node, "evaluate"))
1809
+ if (!isPageMethod(node, "evaluate") && !isPageMethod(node, "addInitScript"))
1740
1810
  return;
1741
1811
  const [fn] = node.arguments;
1742
1812
  if (!fn || !isFunction(fn))
@@ -1747,8 +1817,9 @@ var no_unsafe_references_default = createRule({
1747
1817
  const parent = getParent(ref.identifier);
1748
1818
  return parent?.type !== "TSTypeReference";
1749
1819
  }).filter((ref) => allRefs.has(ref.identifier.name)).forEach((ref, i, arr) => {
1820
+ const methodName = isPageMethod(node, "evaluate") ? "evaluate" : "addInitScript";
1750
1821
  const descriptor = {
1751
- data: { variable: ref.identifier.name },
1822
+ data: { method: methodName, variable: ref.identifier.name },
1752
1823
  messageId: "noUnsafeReference",
1753
1824
  node: ref.identifier
1754
1825
  };
@@ -1773,13 +1844,13 @@ var no_unsafe_references_default = createRule({
1773
1844
  meta: {
1774
1845
  docs: {
1775
1846
  category: "Possible Errors",
1776
- description: "Prevent unsafe variable references in page.evaluate()",
1847
+ description: "Prevent unsafe variable references in page.evaluate() and page.addInitScript()",
1777
1848
  recommended: true,
1778
1849
  url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-unsafe-references.md"
1779
1850
  },
1780
1851
  fixable: "code",
1781
1852
  messages: {
1782
- noUnsafeReference: 'Unsafe reference to variable "{{ variable }}" in page.evaluate()'
1853
+ noUnsafeReference: 'Unsafe reference to variable "{{ variable }}" in page.{{ method }}()'
1783
1854
  },
1784
1855
  type: "problem"
1785
1856
  }
@@ -2006,6 +2077,44 @@ var no_useless_not_default = createRule({
2006
2077
  }
2007
2078
  });
2008
2079
 
2080
+ // src/rules/no-wait-for-navigation.ts
2081
+ var no_wait_for_navigation_default = createRule({
2082
+ create(context) {
2083
+ return {
2084
+ CallExpression(node) {
2085
+ if (isPageMethod(node, "waitForNavigation")) {
2086
+ context.report({
2087
+ messageId: "noWaitForNavigation",
2088
+ node,
2089
+ suggest: [
2090
+ {
2091
+ fix: (fixer) => fixer.remove(
2092
+ node.parent && node.parent.type !== "AwaitExpression" && node.parent.type !== "VariableDeclarator" ? node.parent : node.parent.parent
2093
+ ),
2094
+ messageId: "removeWaitForNavigation"
2095
+ }
2096
+ ]
2097
+ });
2098
+ }
2099
+ }
2100
+ };
2101
+ },
2102
+ meta: {
2103
+ docs: {
2104
+ category: "Possible Errors",
2105
+ description: "Prevent usage of page.waitForNavigation()",
2106
+ recommended: true,
2107
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-wait-for-navigation.md"
2108
+ },
2109
+ hasSuggestions: true,
2110
+ messages: {
2111
+ noWaitForNavigation: "Unexpected use of page.waitForNavigation().",
2112
+ removeWaitForNavigation: "Remove the page.waitForNavigation() method."
2113
+ },
2114
+ type: "suggestion"
2115
+ }
2116
+ });
2117
+
2009
2118
  // src/rules/no-wait-for-selector.ts
2010
2119
  var no_wait_for_selector_default = createRule({
2011
2120
  create(context) {
@@ -2165,7 +2274,7 @@ var prefer_comparison_matcher_default = createRule({
2165
2274
  category: "Best Practices",
2166
2275
  description: "Suggest using the built-in comparison matchers",
2167
2276
  recommended: false,
2168
- url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-comparision-matcher.md"
2277
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-comparison-matcher.md"
2169
2278
  },
2170
2279
  fixable: "code",
2171
2280
  messages: {
@@ -2957,45 +3066,45 @@ var prefer_web_first_assertions_default = createRule({
2957
3066
  create(context) {
2958
3067
  return {
2959
3068
  CallExpression(node) {
2960
- const call = parseFnCall(context, node);
2961
- if (call?.type !== "expect")
3069
+ const fnCall = parseFnCall(context, node);
3070
+ if (fnCall?.type !== "expect")
2962
3071
  return;
2963
- const expect = findParent(call.head.node, "CallExpression");
3072
+ const expect = findParent(fnCall.head.node, "CallExpression");
2964
3073
  if (!expect)
2965
3074
  return;
2966
- const arg = dereference(context, call.args[0]);
2967
- if (!arg || arg.type !== "AwaitExpression" || arg.argument.type !== "CallExpression" || arg.argument.callee.type !== "MemberExpression") {
3075
+ const arg = dereference(context, fnCall.args[0]);
3076
+ if (!arg)
3077
+ return;
3078
+ const call = arg.type === "AwaitExpression" ? arg.argument : arg;
3079
+ if (call.type !== "CallExpression" || call.callee.type !== "MemberExpression") {
2968
3080
  return;
2969
3081
  }
2970
- if (!supportedMatchers.has(call.matcherName))
3082
+ if (!supportedMatchers.has(fnCall.matcherName))
2971
3083
  return;
2972
- const method = getStringValue(arg.argument.callee.property);
3084
+ const method = getStringValue(call.callee.property);
2973
3085
  const methodConfig = methods3[method];
2974
3086
  if (!methodConfig)
2975
3087
  return;
2976
- const notModifier = call.modifiers.find(
3088
+ const notModifier = fnCall.modifiers.find(
2977
3089
  (mod) => getStringValue(mod) === "not"
2978
3090
  );
2979
- const isFalsy = methodConfig.type === "boolean" && (!!call.matcherArgs.length && isBooleanLiteral(call.matcherArgs[0], false) || call.matcherName === "toBeFalsy");
3091
+ const isFalsy = methodConfig.type === "boolean" && (!!fnCall.matcherArgs.length && isBooleanLiteral(fnCall.matcherArgs[0], false) || fnCall.matcherName === "toBeFalsy");
2980
3092
  const isInverse = methodConfig.inverse ? notModifier || isFalsy : notModifier && isFalsy;
2981
3093
  const newMatcher = +!!notModifier ^ +isFalsy && methodConfig.inverse || methodConfig.matcher;
2982
- const { callee } = arg.argument;
3094
+ const { callee } = call;
2983
3095
  context.report({
2984
3096
  data: {
2985
3097
  matcher: newMatcher,
2986
3098
  method
2987
3099
  },
2988
3100
  fix: (fixer) => {
2989
- const methodArgs = arg.argument.type === "CallExpression" ? arg.argument.arguments : [];
3101
+ const methodArgs = call.type === "CallExpression" ? call.arguments : [];
2990
3102
  const methodEnd = methodArgs.length ? methodArgs.at(-1).range[1] + 1 : callee.property.range[1] + 2;
2991
3103
  const fixes = [
2992
3104
  // Add await to the expect call
2993
3105
  fixer.insertTextBefore(expect, "await "),
2994
3106
  // Remove the await keyword
2995
- fixer.replaceTextRange(
2996
- [arg.range[0], arg.argument.range[0]],
2997
- ""
2998
- ),
3107
+ fixer.replaceTextRange([arg.range[0], call.range[0]], ""),
2999
3108
  // Remove the old Playwright method and any arguments
3000
3109
  fixer.replaceTextRange(
3001
3110
  [callee.property.range[0] - 1, methodEnd],
@@ -3007,10 +3116,10 @@ var prefer_web_first_assertions_default = createRule({
3007
3116
  fixes.push(fixer.removeRange([notRange[0], notRange[1] + 1]));
3008
3117
  }
3009
3118
  if (!methodConfig.inverse && !notModifier && isFalsy) {
3010
- fixes.push(fixer.insertTextBefore(call.matcher, "not."));
3119
+ fixes.push(fixer.insertTextBefore(fnCall.matcher, "not."));
3011
3120
  }
3012
- fixes.push(fixer.replaceText(call.matcher, newMatcher));
3013
- const [matcherArg] = call.matcherArgs ?? [];
3121
+ fixes.push(fixer.replaceText(fnCall.matcher, newMatcher));
3122
+ const [matcherArg] = fnCall.matcherArgs ?? [];
3014
3123
  if (matcherArg && isBooleanLiteral(matcherArg)) {
3015
3124
  fixes.push(fixer.remove(matcherArg));
3016
3125
  } else if (methodConfig.prop && matcherArg) {
@@ -3023,7 +3132,7 @@ var prefer_web_first_assertions_default = createRule({
3023
3132
  (arg2) => !isBooleanLiteral(arg2)
3024
3133
  ).length;
3025
3134
  if (methodArgs) {
3026
- const range = call.matcher.range;
3135
+ const range = fnCall.matcher.range;
3027
3136
  const stringArgs = methodArgs.map((arg2) => getRawValue(arg2)).concat(hasOtherArgs ? "" : []).join(", ");
3028
3137
  fixes.push(
3029
3138
  fixer.insertTextAfterRange(
@@ -3730,6 +3839,144 @@ var valid_expect_in_promise_default = createRule({
3730
3839
  }
3731
3840
  });
3732
3841
 
3842
+ // src/rules/valid-test-tags.ts
3843
+ var valid_test_tags_default = createRule({
3844
+ create(context) {
3845
+ const options = context.options[0] || {};
3846
+ const allowedTags = options.allowedTags || [];
3847
+ const disallowedTags = options.disallowedTags || [];
3848
+ if (allowedTags.length > 0 && disallowedTags.length > 0) {
3849
+ throw new Error(
3850
+ "The allowedTags and disallowedTags options cannot be used together"
3851
+ );
3852
+ }
3853
+ for (const tag of [...allowedTags, ...disallowedTags]) {
3854
+ if (typeof tag === "string" && !tag.startsWith("@")) {
3855
+ throw new Error(
3856
+ `Invalid tag "${tag}" in configuration: tags must start with @`
3857
+ );
3858
+ }
3859
+ }
3860
+ const validateTag = (tag, node) => {
3861
+ if (!tag.startsWith("@")) {
3862
+ context.report({
3863
+ messageId: "invalidTagFormat",
3864
+ node
3865
+ });
3866
+ return;
3867
+ }
3868
+ if (allowedTags.length > 0) {
3869
+ const isAllowed = allowedTags.some(
3870
+ (pattern) => pattern instanceof RegExp ? pattern.test(tag) : pattern === tag
3871
+ );
3872
+ if (!isAllowed) {
3873
+ context.report({
3874
+ data: { tag },
3875
+ messageId: "unknownTag",
3876
+ node
3877
+ });
3878
+ return;
3879
+ }
3880
+ }
3881
+ if (disallowedTags.length > 0) {
3882
+ const isDisallowed = disallowedTags.some(
3883
+ (pattern) => pattern instanceof RegExp ? pattern.test(tag) : pattern === tag
3884
+ );
3885
+ if (isDisallowed) {
3886
+ context.report({
3887
+ data: { tag },
3888
+ messageId: "disallowedTag",
3889
+ node
3890
+ });
3891
+ }
3892
+ }
3893
+ };
3894
+ return {
3895
+ CallExpression(node) {
3896
+ const call = parseFnCall(context, node);
3897
+ if (!call)
3898
+ return;
3899
+ const { type } = call;
3900
+ if (type !== "test" && type !== "describe" && type !== "step")
3901
+ return;
3902
+ if (node.arguments.length < 2)
3903
+ return;
3904
+ const optionsArg = node.arguments[1];
3905
+ if (!optionsArg || optionsArg.type !== "ObjectExpression")
3906
+ return;
3907
+ const tagProperty = optionsArg.properties.find(
3908
+ (prop) => prop.type === "Property" && !("argument" in prop) && // Ensure it's not a spread element
3909
+ prop.key.type === "Identifier" && prop.key.name === "tag"
3910
+ );
3911
+ if (!tagProperty)
3912
+ return;
3913
+ const tagValue = tagProperty.value;
3914
+ if (tagValue.type === "Literal") {
3915
+ if (typeof tagValue.value !== "string") {
3916
+ context.report({
3917
+ messageId: "invalidTagValue",
3918
+ node
3919
+ });
3920
+ return;
3921
+ }
3922
+ validateTag(tagValue.value, node);
3923
+ } else if (tagValue.type === "ArrayExpression") {
3924
+ for (const element of tagValue.elements) {
3925
+ if (!element || element.type !== "Literal" || typeof element.value !== "string") {
3926
+ return;
3927
+ }
3928
+ validateTag(element.value, node);
3929
+ }
3930
+ } else {
3931
+ context.report({
3932
+ messageId: "invalidTagValue",
3933
+ node
3934
+ });
3935
+ }
3936
+ }
3937
+ };
3938
+ },
3939
+ meta: {
3940
+ docs: {
3941
+ description: "Enforce valid tag format in Playwright test blocks",
3942
+ recommended: true
3943
+ },
3944
+ messages: {
3945
+ disallowedTag: 'Tag "{{tag}}" is not allowed',
3946
+ invalidTagFormat: "Tag must start with @",
3947
+ invalidTagValue: "Tag must be a string or array of strings",
3948
+ unknownTag: 'Unknown tag "{{tag}}"'
3949
+ },
3950
+ schema: [
3951
+ {
3952
+ additionalProperties: false,
3953
+ properties: {
3954
+ allowedTags: {
3955
+ items: {
3956
+ oneOf: [
3957
+ { type: "string" },
3958
+ { properties: { source: { type: "string" } }, type: "object" }
3959
+ ]
3960
+ },
3961
+ type: "array"
3962
+ },
3963
+ disallowedTags: {
3964
+ items: {
3965
+ oneOf: [
3966
+ { type: "string" },
3967
+ { properties: { source: { type: "string" } }, type: "object" }
3968
+ ]
3969
+ },
3970
+ type: "array"
3971
+ }
3972
+ },
3973
+ type: "object"
3974
+ }
3975
+ ],
3976
+ type: "problem"
3977
+ }
3978
+ });
3979
+
3733
3980
  // src/rules/valid-title.ts
3734
3981
  var doesBinaryExpressionContainStringNode = (binaryExp) => {
3735
3982
  if (isStringNode(binaryExp.right)) {
@@ -3973,10 +4220,12 @@ var index = {
3973
4220
  "no-raw-locators": no_raw_locators_default,
3974
4221
  "no-restricted-matchers": no_restricted_matchers_default,
3975
4222
  "no-skipped-test": no_skipped_test_default,
4223
+ "no-slowed-test": no_slowed_test_default,
3976
4224
  "no-standalone-expect": no_standalone_expect_default,
3977
4225
  "no-unsafe-references": no_unsafe_references_default,
3978
4226
  "no-useless-await": no_useless_await_default,
3979
4227
  "no-useless-not": no_useless_not_default,
4228
+ "no-wait-for-navigation": no_wait_for_navigation_default,
3980
4229
  "no-wait-for-selector": no_wait_for_selector_default,
3981
4230
  "no-wait-for-timeout": no_wait_for_timeout_default,
3982
4231
  "prefer-comparison-matcher": prefer_comparison_matcher_default,
@@ -3999,6 +4248,7 @@ var index = {
3999
4248
  "valid-describe-callback": valid_describe_callback_default,
4000
4249
  "valid-expect": valid_expect_default,
4001
4250
  "valid-expect-in-promise": valid_expect_in_promise_default,
4251
+ "valid-test-tags": valid_test_tags_default,
4002
4252
  "valid-title": valid_title_default
4003
4253
  }
4004
4254
  };
@@ -4022,6 +4272,7 @@ var sharedConfig = {
4022
4272
  "playwright/no-unsafe-references": "error",
4023
4273
  "playwright/no-useless-await": "warn",
4024
4274
  "playwright/no-useless-not": "warn",
4275
+ "playwright/no-wait-for-navigation": "error",
4025
4276
  "playwright/no-wait-for-selector": "warn",
4026
4277
  "playwright/no-wait-for-timeout": "warn",
4027
4278
  "playwright/prefer-web-first-assertions": "error",
package/package.json CHANGED
@@ -1,17 +1,13 @@
1
1
  {
2
2
  "name": "eslint-plugin-playwright",
3
3
  "description": "ESLint plugin for Playwright testing.",
4
- "version": "2.1.0",
4
+ "version": "2.2.1",
5
5
  "repository": "https://github.com/playwright-community/eslint-plugin-playwright",
6
6
  "author": "Mark Skelton <mark@mskelton.dev>",
7
- "packageManager": "pnpm@8.12.0",
8
7
  "contributors": [
9
8
  "Max Schmitt <max@schmitt.mx>"
10
9
  ],
11
10
  "license": "MIT",
12
- "workspaces": [
13
- "examples"
14
- ],
15
11
  "engines": {
16
12
  "node": ">=16.6.0"
17
13
  },
@@ -30,34 +26,10 @@
30
26
  "index.cjs",
31
27
  "index.d.ts"
32
28
  ],
33
- "scripts": {
34
- "build": "tsup src/index.ts --format cjs --out-dir dist",
35
- "lint": "eslint .",
36
- "fmt": "prettier --write .",
37
- "fmt:check": "prettier --check .",
38
- "test": "vitest",
39
- "test:watch": "vitest --reporter=dot",
40
- "ts": "tsc --noEmit"
41
- },
42
29
  "peerDependencies": {
43
30
  "eslint": ">=8.40.0"
44
31
  },
45
32
  "dependencies": {
46
33
  "globals": "^13.23.0"
47
- },
48
- "devDependencies": {
49
- "@mskelton/eslint-config": "^9.0.1",
50
- "@mskelton/semantic-release-config": "^1.0.1",
51
- "@types/estree": "^1.0.6",
52
- "@types/node": "^20.11.17",
53
- "@typescript-eslint/parser": "^8.11.0",
54
- "dedent": "^1.5.1",
55
- "eslint": "^9.13.0",
56
- "prettier": "^3.0.3",
57
- "prettier-plugin-jsdoc": "^1.3.0",
58
- "semantic-release": "^23.0.2",
59
- "tsup": "^8.0.1",
60
- "typescript": "^5.2.2",
61
- "vitest": "^1.3.1"
62
34
  }
63
35
  }