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.
- package/README.md +3 -0
- package/dist/index.cjs +293 -42
- 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
|
-
"
|
|
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
|
-
|
|
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" || !
|
|
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" &&
|
|
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.
|
|
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-
|
|
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
|
|
2961
|
-
if (
|
|
3069
|
+
const fnCall = parseFnCall(context, node);
|
|
3070
|
+
if (fnCall?.type !== "expect")
|
|
2962
3071
|
return;
|
|
2963
|
-
const expect = findParent(
|
|
3072
|
+
const expect = findParent(fnCall.head.node, "CallExpression");
|
|
2964
3073
|
if (!expect)
|
|
2965
3074
|
return;
|
|
2966
|
-
const arg = dereference(context,
|
|
2967
|
-
if (!arg
|
|
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(
|
|
3082
|
+
if (!supportedMatchers.has(fnCall.matcherName))
|
|
2971
3083
|
return;
|
|
2972
|
-
const method = getStringValue(
|
|
3084
|
+
const method = getStringValue(call.callee.property);
|
|
2973
3085
|
const methodConfig = methods3[method];
|
|
2974
3086
|
if (!methodConfig)
|
|
2975
3087
|
return;
|
|
2976
|
-
const notModifier =
|
|
3088
|
+
const notModifier = fnCall.modifiers.find(
|
|
2977
3089
|
(mod) => getStringValue(mod) === "not"
|
|
2978
3090
|
);
|
|
2979
|
-
const isFalsy = methodConfig.type === "boolean" && (!!
|
|
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 } =
|
|
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 =
|
|
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(
|
|
3119
|
+
fixes.push(fixer.insertTextBefore(fnCall.matcher, "not."));
|
|
3011
3120
|
}
|
|
3012
|
-
fixes.push(fixer.replaceText(
|
|
3013
|
-
const [matcherArg] =
|
|
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 =
|
|
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
|
|
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
|
}
|