eslint-plugin-playwright 1.1.2 → 1.3.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/dist/index.js CHANGED
@@ -54,9 +54,12 @@ function isStringNode(node) {
54
54
  function isPropertyAccessor(node, name) {
55
55
  return getStringValue(node.property) === name;
56
56
  }
57
- function isTestIdentifier(context, node) {
57
+ function getTestNames(context) {
58
58
  const aliases = context.settings.playwright?.globalAliases?.test ?? [];
59
- const testNames = ["test", ...aliases];
59
+ return ["test", ...aliases];
60
+ }
61
+ function isTestIdentifier(context, node) {
62
+ const testNames = getTestNames(context);
60
63
  const regex = new RegExp(`^(${testNames.join("|")})$`);
61
64
  return isIdentifier(node, regex) || node.type === "MemberExpression" && isIdentifier(node.object, regex);
62
65
  }
@@ -86,14 +89,34 @@ function findParent(node, type) {
86
89
  return node.parent.type === type ? node.parent : findParent(node.parent, type);
87
90
  }
88
91
  function isTestCall(context, node, modifiers) {
89
- return isTestIdentifier(context, node.callee) && !isDescribeCall(node) && (node.callee.type !== "MemberExpression" || !modifiers || modifiers?.includes(getStringValue(node.callee.property))) && node.arguments.length === 2 && ["ArrowFunctionExpression", "FunctionExpression"].includes(
90
- node.arguments[1].type
91
- );
92
+ return isTestIdentifier(context, node.callee) && !isDescribeCall(node) && (node.callee.type !== "MemberExpression" || !modifiers || modifiers?.includes(getStringValue(node.callee.property))) && node.arguments.length === 2 && isFunction(node.arguments[1]);
92
93
  }
93
94
  var testHooks = /* @__PURE__ */ new Set(["afterAll", "afterEach", "beforeAll", "beforeEach"]);
94
95
  function isTestHook(context, node) {
95
96
  return node.callee.type === "MemberExpression" && isTestIdentifier(context, node.callee.object) && testHooks.has(getStringValue(node.callee.property));
96
97
  }
98
+ function parseFnCall(context, node) {
99
+ if (isTestCall(context, node)) {
100
+ return {
101
+ fn: node.arguments[1],
102
+ name: getStringValue(node.callee),
103
+ type: "test"
104
+ };
105
+ }
106
+ if (node.callee.type === "MemberExpression" && isTestIdentifier(context, node.callee.object) && testHooks.has(getStringValue(node.callee.property))) {
107
+ return {
108
+ fn: node.arguments[0],
109
+ name: getStringValue(node.callee.property),
110
+ type: "hook"
111
+ };
112
+ }
113
+ if (isDescribeCall(node)) {
114
+ return {
115
+ name: getStringValue(node.callee),
116
+ type: "describe"
117
+ };
118
+ }
119
+ }
97
120
  var expectSubCommands = /* @__PURE__ */ new Set(["soft", "poll"]);
98
121
  function getExpectType(context, node) {
99
122
  const aliases = context.settings.playwright?.globalAliases?.expect ?? [];
@@ -129,6 +152,7 @@ function isPageMethod(node, name) {
129
152
  function isFunction(node) {
130
153
  return node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression";
131
154
  }
155
+ var equalityMatchers = /* @__PURE__ */ new Set(["toBe", "toEqual", "toStrictEqual"]);
132
156
 
133
157
  // src/rules/expect-expect.ts
134
158
  function isAssertionCall(context, node, assertFunctionNames) {
@@ -192,6 +216,69 @@ var expect_expect_default = {
192
216
  }
193
217
  };
194
218
 
219
+ // src/rules/max-expects.ts
220
+ var max_expects_default = {
221
+ create(context) {
222
+ const options = {
223
+ max: 5,
224
+ ...context.options?.[0] ?? {}
225
+ };
226
+ let count = 0;
227
+ const maybeResetCount = (node) => {
228
+ const parent = getParent(node);
229
+ const isTestFn = parent?.type !== "CallExpression" || isTestCall(context, parent);
230
+ if (isTestFn) {
231
+ count = 0;
232
+ }
233
+ };
234
+ return {
235
+ ArrowFunctionExpression: maybeResetCount,
236
+ "ArrowFunctionExpression:exit": maybeResetCount,
237
+ CallExpression(node) {
238
+ if (!getExpectType(context, node))
239
+ return;
240
+ count += 1;
241
+ if (count > options.max) {
242
+ context.report({
243
+ data: {
244
+ count: count.toString(),
245
+ max: options.max.toString()
246
+ },
247
+ messageId: "exceededMaxAssertion",
248
+ node
249
+ });
250
+ }
251
+ },
252
+ FunctionExpression: maybeResetCount,
253
+ "FunctionExpression:exit": maybeResetCount
254
+ };
255
+ },
256
+ meta: {
257
+ docs: {
258
+ category: "Best Practices",
259
+ description: "Enforces a maximum number assertion calls in a test body",
260
+ recommended: false,
261
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/max-expects.md"
262
+ },
263
+ messages: {
264
+ exceededMaxAssertion: "Too many assertion calls ({{ count }}) - maximum allowed is {{ max }}"
265
+ },
266
+ schema: [
267
+ {
268
+ additionalProperties: false,
269
+ properties: {
270
+ max: {
271
+ minimum: 1,
272
+ type: "integer"
273
+ }
274
+ },
275
+ type: "object"
276
+ }
277
+ ],
278
+ type: "suggestion"
279
+ }
280
+ };
281
+
195
282
  // src/rules/max-nested-describe.ts
196
283
  var max_nested_describe_default = {
197
284
  create(context) {
@@ -399,6 +486,132 @@ var missing_playwright_await_default = {
399
486
  }
400
487
  };
401
488
 
489
+ // src/rules/no-commented-out-tests.ts
490
+ function hasTests(context, node) {
491
+ const testNames = getTestNames(context);
492
+ const names = testNames.join("|");
493
+ const regex = new RegExp(
494
+ `^\\s*(${names}|describe)(\\.\\w+|\\[['"]\\w+['"]\\])?\\s*\\(`,
495
+ "mu"
496
+ );
497
+ return regex.test(node.value);
498
+ }
499
+ var no_commented_out_tests_default = {
500
+ create(context) {
501
+ function checkNode(node) {
502
+ if (!hasTests(context, node))
503
+ return;
504
+ context.report({
505
+ messageId: "commentedTests",
506
+ node
507
+ });
508
+ }
509
+ return {
510
+ Program() {
511
+ context.sourceCode.getAllComments().forEach(checkNode);
512
+ }
513
+ };
514
+ },
515
+ meta: {
516
+ docs: {
517
+ category: "Best Practices",
518
+ description: "Disallow commented out tests",
519
+ recommended: true,
520
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-commented-out-tests.md"
521
+ },
522
+ messages: {
523
+ commentedTests: "Some tests seem to be commented"
524
+ },
525
+ type: "problem"
526
+ }
527
+ };
528
+
529
+ // src/rules/no-conditional-expect.ts
530
+ var isCatchCall = (node) => node.callee.type === "MemberExpression" && isPropertyAccessor(node.callee, "catch");
531
+ var getTestCallExpressionsFromDeclaredVariables = (context, declaredVariables) => {
532
+ return declaredVariables.reduce(
533
+ (acc, { references }) => [
534
+ ...acc,
535
+ ...references.map(({ identifier }) => getParent(identifier)).filter(
536
+ (node) => node?.type === "CallExpression" && isTestCall(context, node)
537
+ )
538
+ ],
539
+ []
540
+ );
541
+ };
542
+ var no_conditional_expect_default = {
543
+ create(context) {
544
+ let conditionalDepth = 0;
545
+ let inTestCase = false;
546
+ let inPromiseCatch = false;
547
+ const increaseConditionalDepth = () => inTestCase && conditionalDepth++;
548
+ const decreaseConditionalDepth = () => inTestCase && conditionalDepth--;
549
+ return {
550
+ CallExpression(node) {
551
+ if (isTestCall(context, node)) {
552
+ inTestCase = true;
553
+ }
554
+ if (isCatchCall(node)) {
555
+ inPromiseCatch = true;
556
+ }
557
+ const expectType = getExpectType(context, node);
558
+ if (inTestCase && expectType && conditionalDepth > 0) {
559
+ context.report({
560
+ messageId: "conditionalExpect",
561
+ node
562
+ });
563
+ }
564
+ if (inPromiseCatch && expectType) {
565
+ context.report({
566
+ messageId: "conditionalExpect",
567
+ node
568
+ });
569
+ }
570
+ },
571
+ "CallExpression:exit"(node) {
572
+ if (isTestCall(context, node)) {
573
+ inTestCase = false;
574
+ }
575
+ if (isCatchCall(node)) {
576
+ inPromiseCatch = false;
577
+ }
578
+ },
579
+ CatchClause: increaseConditionalDepth,
580
+ "CatchClause:exit": decreaseConditionalDepth,
581
+ ConditionalExpression: increaseConditionalDepth,
582
+ "ConditionalExpression:exit": decreaseConditionalDepth,
583
+ FunctionDeclaration(node) {
584
+ const declaredVariables = context.sourceCode.getDeclaredVariables(node);
585
+ const testCallExpressions = getTestCallExpressionsFromDeclaredVariables(
586
+ context,
587
+ declaredVariables
588
+ );
589
+ if (testCallExpressions.length > 0) {
590
+ inTestCase = true;
591
+ }
592
+ },
593
+ IfStatement: increaseConditionalDepth,
594
+ "IfStatement:exit": decreaseConditionalDepth,
595
+ LogicalExpression: increaseConditionalDepth,
596
+ "LogicalExpression:exit": decreaseConditionalDepth,
597
+ SwitchStatement: increaseConditionalDepth,
598
+ "SwitchStatement:exit": decreaseConditionalDepth
599
+ };
600
+ },
601
+ meta: {
602
+ docs: {
603
+ category: "Best Practices",
604
+ description: "Disallow calling `expect` conditionally",
605
+ recommended: true,
606
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-conditional-expect.md"
607
+ },
608
+ messages: {
609
+ conditionalExpect: "Avoid calling `expect` conditionally`"
610
+ },
611
+ type: "problem"
612
+ }
613
+ };
614
+
402
615
  // src/rules/no-conditional-in-test.ts
403
616
  var no_conditional_in_test_default = {
404
617
  create(context) {
@@ -430,6 +643,51 @@ var no_conditional_in_test_default = {
430
643
  }
431
644
  };
432
645
 
646
+ // src/rules/no-duplicate-hooks.ts
647
+ var no_duplicate_hooks_default = {
648
+ create(context) {
649
+ const hookContexts = [{}];
650
+ return {
651
+ CallExpression(node) {
652
+ if (isDescribeCall(node)) {
653
+ hookContexts.push({});
654
+ }
655
+ if (!isTestHook(context, node)) {
656
+ return;
657
+ }
658
+ const currentLayer = hookContexts[hookContexts.length - 1];
659
+ const name = node.callee.type === "MemberExpression" ? getStringValue(node.callee.property) : "";
660
+ currentLayer[name] || (currentLayer[name] = 0);
661
+ currentLayer[name] += 1;
662
+ if (currentLayer[name] > 1) {
663
+ context.report({
664
+ data: { hook: name },
665
+ messageId: "noDuplicateHook",
666
+ node
667
+ });
668
+ }
669
+ },
670
+ "CallExpression:exit"(node) {
671
+ if (isDescribeCall(node)) {
672
+ hookContexts.pop();
673
+ }
674
+ }
675
+ };
676
+ },
677
+ meta: {
678
+ docs: {
679
+ category: "Best Practices",
680
+ description: "Disallow duplicate setup and teardown hooks",
681
+ recommended: false,
682
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-duplicate-hooks.md"
683
+ },
684
+ messages: {
685
+ noDuplicateHook: "Duplicate {{ hook }} in describe block"
686
+ },
687
+ type: "suggestion"
688
+ }
689
+ };
690
+
433
691
  // src/rules/no-element-handle.ts
434
692
  function getPropertyRange(node) {
435
693
  return node.type === "Identifier" ? node.range : [node.range[0] + 1, node.range[1] - 1];
@@ -630,6 +888,52 @@ var no_get_by_title_default = {
630
888
  }
631
889
  };
632
890
 
891
+ // src/rules/no-hooks.ts
892
+ var no_hooks_default = {
893
+ create(context) {
894
+ const options = {
895
+ allow: [],
896
+ ...context.options?.[0] ?? {}
897
+ };
898
+ return {
899
+ CallExpression(node) {
900
+ const call = parseFnCall(context, node);
901
+ if (call?.type === "hook" && !options.allow.includes(call.name)) {
902
+ context.report({
903
+ data: { hookName: call.name },
904
+ messageId: "unexpectedHook",
905
+ node
906
+ });
907
+ }
908
+ }
909
+ };
910
+ },
911
+ meta: {
912
+ docs: {
913
+ category: "Best Practices",
914
+ description: "Disallow setup and teardown hooks",
915
+ recommended: false,
916
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-hooks.md"
917
+ },
918
+ messages: {
919
+ unexpectedHook: "Unexpected '{{ hookName }}' hook"
920
+ },
921
+ schema: [
922
+ {
923
+ additionalProperties: false,
924
+ properties: {
925
+ allow: {
926
+ contains: ["beforeAll", "beforeEach", "afterAll", "afterEach"],
927
+ type: "array"
928
+ }
929
+ },
930
+ type: "object"
931
+ }
932
+ ],
933
+ type: "suggestion"
934
+ }
935
+ };
936
+
633
937
  // src/rules/no-nested-step.ts
634
938
  function isStepCall(node) {
635
939
  const inner = node.type === "CallExpression" ? node.callee : node;
@@ -997,6 +1301,95 @@ var no_skipped_test_default = {
997
1301
  }
998
1302
  };
999
1303
 
1304
+ // src/rules/no-standalone-expect.ts
1305
+ var getBlockType = (statement) => {
1306
+ const func = getParent(statement);
1307
+ if (!func) {
1308
+ throw new Error(
1309
+ `Unexpected BlockStatement. No parent defined. - please file a github issue at https://github.com/playwright-community/eslint-plugin-playwright`
1310
+ );
1311
+ }
1312
+ if (func.type === "FunctionDeclaration") {
1313
+ return "function";
1314
+ }
1315
+ if (isFunction(func) && func.parent) {
1316
+ const expr = func.parent;
1317
+ if (expr.type === "VariableDeclarator" || expr.type === "MethodDefinition") {
1318
+ return "function";
1319
+ }
1320
+ if (expr.type === "CallExpression" && isDescribeCall(expr)) {
1321
+ return "describe";
1322
+ }
1323
+ }
1324
+ return null;
1325
+ };
1326
+ var no_standalone_expect_default = {
1327
+ create(context) {
1328
+ const callStack = [];
1329
+ return {
1330
+ ArrowFunctionExpression(node) {
1331
+ if (node.parent?.type !== "CallExpression") {
1332
+ callStack.push("arrow");
1333
+ }
1334
+ },
1335
+ "ArrowFunctionExpression:exit"() {
1336
+ if (callStack[callStack.length - 1] === "arrow") {
1337
+ callStack.pop();
1338
+ }
1339
+ },
1340
+ BlockStatement(statement) {
1341
+ const blockType = getBlockType(statement);
1342
+ if (blockType) {
1343
+ callStack.push(blockType);
1344
+ }
1345
+ },
1346
+ "BlockStatement:exit"(statement) {
1347
+ if (callStack[callStack.length - 1] === getBlockType(statement)) {
1348
+ callStack.pop();
1349
+ }
1350
+ },
1351
+ CallExpression(node) {
1352
+ if (getExpectType(context, node)) {
1353
+ const parent = callStack.at(-1);
1354
+ if (!parent || parent === "describe") {
1355
+ const root = findParent(node, "CallExpression");
1356
+ context.report({
1357
+ messageId: "unexpectedExpect",
1358
+ node: root ?? node
1359
+ });
1360
+ }
1361
+ return;
1362
+ }
1363
+ if (isTestCall(context, node)) {
1364
+ callStack.push("test");
1365
+ }
1366
+ if (node.callee.type === "TaggedTemplateExpression") {
1367
+ callStack.push("template");
1368
+ }
1369
+ },
1370
+ "CallExpression:exit"(node) {
1371
+ const top = callStack[callStack.length - 1];
1372
+ if (top === "test" && isTestCall(context, node) && node.callee.type !== "MemberExpression" || top === "template" && node.callee.type === "TaggedTemplateExpression") {
1373
+ callStack.pop();
1374
+ }
1375
+ }
1376
+ };
1377
+ },
1378
+ meta: {
1379
+ docs: {
1380
+ category: "Best Practices",
1381
+ description: "Disallow using `expect` outside of `test` blocks",
1382
+ recommended: false,
1383
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-standalone-expect.md"
1384
+ },
1385
+ fixable: "code",
1386
+ messages: {
1387
+ unexpectedExpect: "Expect must be inside of a test block"
1388
+ },
1389
+ type: "suggestion"
1390
+ }
1391
+ };
1392
+
1000
1393
  // src/utils/misc.ts
1001
1394
  var getAmountData = (amount) => ({
1002
1395
  amount: amount.toString(),
@@ -1319,6 +1712,258 @@ var no_wait_for_timeout_default = {
1319
1712
  }
1320
1713
  };
1321
1714
 
1715
+ // src/rules/prefer-comparison-matcher.ts
1716
+ var isString = (node) => {
1717
+ return isStringLiteral(node) || node.type === "TemplateLiteral";
1718
+ };
1719
+ var isComparingToString = (expression) => {
1720
+ return isString(expression.left) || isString(expression.right);
1721
+ };
1722
+ var invertedOperators = {
1723
+ "<": ">=",
1724
+ "<=": ">",
1725
+ ">": "<=",
1726
+ ">=": "<"
1727
+ };
1728
+ var operatorMatcher = {
1729
+ "<": "toBeLessThan",
1730
+ "<=": "toBeLessThanOrEqual",
1731
+ ">": "toBeGreaterThan",
1732
+ ">=": "toBeGreaterThanOrEqual"
1733
+ };
1734
+ var determineMatcher = (operator, negated) => {
1735
+ const op = negated ? invertedOperators[operator] : operator;
1736
+ return operatorMatcher[op] ?? null;
1737
+ };
1738
+ var prefer_comparison_matcher_default = {
1739
+ create(context) {
1740
+ return {
1741
+ CallExpression(node) {
1742
+ const expectCall = parseExpectCall(context, node);
1743
+ if (!expectCall || expectCall.args.length === 0)
1744
+ return;
1745
+ const { args, matcher } = expectCall;
1746
+ const [comparison] = node.arguments;
1747
+ const expectCallEnd = node.range[1];
1748
+ const [matcherArg] = args;
1749
+ if (comparison?.type !== "BinaryExpression" || isComparingToString(comparison) || !equalityMatchers.has(getStringValue(matcher)) || !isBooleanLiteral(matcherArg)) {
1750
+ return;
1751
+ }
1752
+ const hasNot = expectCall.modifiers.some(
1753
+ (node2) => getStringValue(node2) === "not"
1754
+ );
1755
+ const preferredMatcher = determineMatcher(
1756
+ comparison.operator,
1757
+ getRawValue(matcherArg) === hasNot.toString()
1758
+ );
1759
+ if (!preferredMatcher) {
1760
+ return;
1761
+ }
1762
+ context.report({
1763
+ data: { preferredMatcher },
1764
+ fix(fixer) {
1765
+ const [modifier] = expectCall.modifiers;
1766
+ const modifierText = modifier && getStringValue(modifier) !== "not" ? `.${getStringValue(modifier)}` : "";
1767
+ return [
1768
+ // Replace the comparison argument with the left-hand side of the comparison
1769
+ fixer.replaceText(
1770
+ comparison,
1771
+ context.sourceCode.getText(comparison.left)
1772
+ ),
1773
+ // Replace the current matcher & modifier with the preferred matcher
1774
+ fixer.replaceTextRange(
1775
+ [expectCallEnd, getParent(matcher).range[1]],
1776
+ `${modifierText}.${preferredMatcher}`
1777
+ ),
1778
+ // Replace the matcher argument with the right-hand side of the comparison
1779
+ fixer.replaceText(
1780
+ matcherArg,
1781
+ context.sourceCode.getText(comparison.right)
1782
+ )
1783
+ ];
1784
+ },
1785
+ messageId: "useToBeComparison",
1786
+ node: matcher
1787
+ });
1788
+ }
1789
+ };
1790
+ },
1791
+ meta: {
1792
+ docs: {
1793
+ category: "Best Practices",
1794
+ description: "Suggest using the built-in comparison matchers",
1795
+ recommended: false
1796
+ },
1797
+ fixable: "code",
1798
+ messages: {
1799
+ useToBeComparison: "Prefer using `{{ preferredMatcher }}` instead"
1800
+ },
1801
+ type: "suggestion"
1802
+ }
1803
+ };
1804
+
1805
+ // src/rules/prefer-equality-matcher.ts
1806
+ var prefer_equality_matcher_default = {
1807
+ create(context) {
1808
+ return {
1809
+ CallExpression(node) {
1810
+ const expectCall = parseExpectCall(context, node);
1811
+ if (!expectCall || expectCall.args.length === 0)
1812
+ return;
1813
+ const { args, matcher } = expectCall;
1814
+ const [comparison] = node.arguments;
1815
+ const expectCallEnd = node.range[1];
1816
+ const [matcherArg] = args;
1817
+ if (comparison?.type !== "BinaryExpression" || comparison.operator !== "===" && comparison.operator !== "!==" || !equalityMatchers.has(getStringValue(matcher)) || !isBooleanLiteral(matcherArg)) {
1818
+ return;
1819
+ }
1820
+ const matcherValue = getRawValue(matcherArg) === "true";
1821
+ const [modifier] = expectCall.modifiers;
1822
+ const hasNot = expectCall.modifiers.some(
1823
+ (node2) => getStringValue(node2) === "not"
1824
+ );
1825
+ const addNotModifier = (comparison.operator === "!==" ? !matcherValue : matcherValue) === hasNot;
1826
+ context.report({
1827
+ messageId: "useEqualityMatcher",
1828
+ node: matcher,
1829
+ suggest: [...equalityMatchers.keys()].map((equalityMatcher) => ({
1830
+ data: { matcher: equalityMatcher },
1831
+ fix(fixer) {
1832
+ let modifierText = modifier && getStringValue(modifier) !== "not" ? `.${getStringValue(modifier)}` : "";
1833
+ if (addNotModifier) {
1834
+ modifierText += `.not`;
1835
+ }
1836
+ return [
1837
+ // replace the comparison argument with the left-hand side of the comparison
1838
+ fixer.replaceText(
1839
+ comparison,
1840
+ context.sourceCode.getText(comparison.left)
1841
+ ),
1842
+ // replace the current matcher & modifier with the preferred matcher
1843
+ fixer.replaceTextRange(
1844
+ [expectCallEnd, getParent(matcher).range[1]],
1845
+ `${modifierText}.${equalityMatcher}`
1846
+ ),
1847
+ // replace the matcher argument with the right-hand side of the comparison
1848
+ fixer.replaceText(
1849
+ matcherArg,
1850
+ context.sourceCode.getText(comparison.right)
1851
+ )
1852
+ ];
1853
+ },
1854
+ messageId: "suggestEqualityMatcher"
1855
+ }))
1856
+ });
1857
+ }
1858
+ };
1859
+ },
1860
+ meta: {
1861
+ docs: {
1862
+ category: "Best Practices",
1863
+ description: "Suggest using the built-in equality matchers",
1864
+ recommended: false,
1865
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-equality-matcher.md"
1866
+ },
1867
+ hasSuggestions: true,
1868
+ messages: {
1869
+ suggestEqualityMatcher: "Use `{{ matcher }}`",
1870
+ useEqualityMatcher: "Prefer using one of the equality matchers instead"
1871
+ },
1872
+ type: "suggestion"
1873
+ }
1874
+ };
1875
+
1876
+ // src/rules/prefer-hooks-in-order.ts
1877
+ var HooksOrder = ["beforeAll", "beforeEach", "afterEach", "afterAll"];
1878
+ var prefer_hooks_in_order_default = {
1879
+ create(context) {
1880
+ let previousHookIndex = -1;
1881
+ let inHook = false;
1882
+ return {
1883
+ CallExpression(node) {
1884
+ if (inHook)
1885
+ return;
1886
+ if (!isTestHook(context, node)) {
1887
+ previousHookIndex = -1;
1888
+ return;
1889
+ }
1890
+ inHook = true;
1891
+ const currentHook = node.callee.type === "MemberExpression" ? getStringValue(node.callee.property) : "";
1892
+ const currentHookIndex = HooksOrder.indexOf(currentHook);
1893
+ if (currentHookIndex < previousHookIndex) {
1894
+ return context.report({
1895
+ data: {
1896
+ currentHook,
1897
+ previousHook: HooksOrder[previousHookIndex]
1898
+ },
1899
+ messageId: "reorderHooks",
1900
+ node
1901
+ });
1902
+ }
1903
+ previousHookIndex = currentHookIndex;
1904
+ },
1905
+ "CallExpression:exit"(node) {
1906
+ if (isTestHook(context, node)) {
1907
+ inHook = false;
1908
+ return;
1909
+ }
1910
+ if (inHook) {
1911
+ return;
1912
+ }
1913
+ previousHookIndex = -1;
1914
+ }
1915
+ };
1916
+ },
1917
+ meta: {
1918
+ docs: {
1919
+ category: "Best Practices",
1920
+ description: "Prefer having hooks in a consistent order",
1921
+ recommended: false,
1922
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-hooks-in-order.md"
1923
+ },
1924
+ messages: {
1925
+ reorderHooks: "`{{ currentHook }}` hooks should be before any `{{ previousHook }}` hooks"
1926
+ },
1927
+ type: "suggestion"
1928
+ }
1929
+ };
1930
+
1931
+ // src/rules/prefer-hooks-on-top.ts
1932
+ var prefer_hooks_on_top_default = {
1933
+ create(context) {
1934
+ const stack = [false];
1935
+ return {
1936
+ CallExpression(node) {
1937
+ if (isTestCall(context, node)) {
1938
+ stack[stack.length - 1] = true;
1939
+ }
1940
+ if (stack.at(-1) && isTestHook(context, node)) {
1941
+ context.report({
1942
+ messageId: "noHookOnTop",
1943
+ node
1944
+ });
1945
+ }
1946
+ stack.push(false);
1947
+ },
1948
+ "CallExpression:exit"() {
1949
+ stack.pop();
1950
+ }
1951
+ };
1952
+ },
1953
+ meta: {
1954
+ docs: {
1955
+ category: "Best Practices",
1956
+ description: "Suggest having hooks before any test cases",
1957
+ recommended: false,
1958
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-hooks-on-top.md"
1959
+ },
1960
+ messages: {
1961
+ noHookOnTop: "Hooks should come before test cases"
1962
+ },
1963
+ type: "suggestion"
1964
+ }
1965
+ };
1966
+
1322
1967
  // src/rules/prefer-lowercase-title.ts
1323
1968
  var prefer_lowercase_title_default = {
1324
1969
  create(context) {
@@ -1506,9 +2151,8 @@ var prefer_to_be_default = {
1506
2151
  notModifier
1507
2152
  );
1508
2153
  }
1509
- const argumentMatchers = ["toBe", "toEqual", "toStrictEqual"];
1510
2154
  const firstArg = expectCall.args[0];
1511
- if (!argumentMatchers.includes(expectCall.matcherName) || !firstArg) {
2155
+ if (!equalityMatchers.has(expectCall.matcherName) || !firstArg) {
1512
2156
  return;
1513
2157
  }
1514
2158
  if (firstArg.type === "Literal" && firstArg.value === null) {
@@ -1548,7 +2192,6 @@ var prefer_to_be_default = {
1548
2192
  };
1549
2193
 
1550
2194
  // src/rules/prefer-to-contain.ts
1551
- var matchers = /* @__PURE__ */ new Set(["toBe", "toEqual", "toStrictEqual"]);
1552
2195
  var isFixableIncludesCallExpression = (node) => node.type === "CallExpression" && node.callee.type === "MemberExpression" && isPropertyAccessor(node.callee, "includes") && node.arguments.length === 1 && node.arguments[0].type !== "SpreadElement";
1553
2196
  var prefer_to_contain_default = {
1554
2197
  create(context) {
@@ -1560,7 +2203,7 @@ var prefer_to_contain_default = {
1560
2203
  const { args, matcher, matcherName } = expectCall;
1561
2204
  const [includesCall] = node.arguments;
1562
2205
  const [matcherArg] = args;
1563
- if (!includesCall || matcherArg.type === "SpreadElement" || !matchers.has(matcherName) || !isBooleanLiteral(matcherArg) || !isFixableIncludesCallExpression(includesCall)) {
2206
+ if (!includesCall || matcherArg.type === "SpreadElement" || !equalityMatchers.has(matcherName) || !isBooleanLiteral(matcherArg) || !isFixableIncludesCallExpression(includesCall)) {
1564
2207
  return;
1565
2208
  }
1566
2209
  const notModifier = expectCall.modifiers.find(
@@ -1618,13 +2261,12 @@ var prefer_to_contain_default = {
1618
2261
  };
1619
2262
 
1620
2263
  // src/rules/prefer-to-have-count.ts
1621
- var matchers2 = /* @__PURE__ */ new Set(["toBe", "toEqual", "toStrictEqual"]);
1622
2264
  var prefer_to_have_count_default = {
1623
2265
  create(context) {
1624
2266
  return {
1625
2267
  CallExpression(node) {
1626
2268
  const expectCall = parseExpectCall(context, node);
1627
- if (!expectCall || !matchers2.has(expectCall.matcherName)) {
2269
+ if (!expectCall || !equalityMatchers.has(expectCall.matcherName)) {
1628
2270
  return;
1629
2271
  }
1630
2272
  const [argument] = node.arguments;
@@ -1674,13 +2316,12 @@ var prefer_to_have_count_default = {
1674
2316
  };
1675
2317
 
1676
2318
  // src/rules/prefer-to-have-length.ts
1677
- var lengthMatchers = /* @__PURE__ */ new Set(["toBe", "toEqual", "toStrictEqual"]);
1678
2319
  var prefer_to_have_length_default = {
1679
2320
  create(context) {
1680
2321
  return {
1681
2322
  CallExpression(node) {
1682
2323
  const expectCall = parseExpectCall(context, node);
1683
- if (!expectCall || !lengthMatchers.has(expectCall.matcherName)) {
2324
+ if (!expectCall || !equalityMatchers.has(expectCall.matcherName)) {
1684
2325
  return;
1685
2326
  }
1686
2327
  const [argument] = node.arguments;
@@ -1875,6 +2516,86 @@ var prefer_web_first_assertions_default = {
1875
2516
  }
1876
2517
  };
1877
2518
 
2519
+ // src/rules/require-hook.ts
2520
+ var isNullOrUndefined = (node) => {
2521
+ return node.type === "Literal" && node.value === null || isIdentifier(node, "undefined");
2522
+ };
2523
+ var shouldBeInHook = (context, node, allowedFunctionCalls = []) => {
2524
+ switch (node.type) {
2525
+ case "ExpressionStatement":
2526
+ return shouldBeInHook(context, node.expression, allowedFunctionCalls);
2527
+ case "CallExpression":
2528
+ return !(parseFnCall(context, node) || allowedFunctionCalls.includes(getStringValue(node.callee)));
2529
+ case "VariableDeclaration": {
2530
+ if (node.kind === "const") {
2531
+ return false;
2532
+ }
2533
+ return node.declarations.some(
2534
+ ({ init }) => init != null && !isNullOrUndefined(init)
2535
+ );
2536
+ }
2537
+ default:
2538
+ return false;
2539
+ }
2540
+ };
2541
+ var require_hook_default = {
2542
+ create(context) {
2543
+ const options = {
2544
+ allowedFunctionCalls: [],
2545
+ ...context.options?.[0] ?? {}
2546
+ };
2547
+ const checkBlockBody = (body) => {
2548
+ for (const statement of body) {
2549
+ if (shouldBeInHook(context, statement, options.allowedFunctionCalls)) {
2550
+ context.report({
2551
+ messageId: "useHook",
2552
+ node: statement
2553
+ });
2554
+ }
2555
+ }
2556
+ };
2557
+ return {
2558
+ CallExpression(node) {
2559
+ if (!isDescribeCall(node) || node.arguments.length < 2) {
2560
+ return;
2561
+ }
2562
+ const [, testFn] = node.arguments;
2563
+ if (!isFunction(testFn) || testFn.body.type !== "BlockStatement") {
2564
+ return;
2565
+ }
2566
+ checkBlockBody(testFn.body.body);
2567
+ },
2568
+ Program(program) {
2569
+ checkBlockBody(program.body);
2570
+ }
2571
+ };
2572
+ },
2573
+ meta: {
2574
+ docs: {
2575
+ category: "Best Practices",
2576
+ description: "Require setup and teardown code to be within a hook",
2577
+ recommended: false,
2578
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/require-hook.md"
2579
+ },
2580
+ messages: {
2581
+ useHook: "This should be done within a hook"
2582
+ },
2583
+ schema: [
2584
+ {
2585
+ additionalProperties: false,
2586
+ properties: {
2587
+ allowedFunctionCalls: {
2588
+ items: { type: "string" },
2589
+ type: "array"
2590
+ }
2591
+ },
2592
+ type: "object"
2593
+ }
2594
+ ],
2595
+ type: "suggestion"
2596
+ }
2597
+ };
2598
+
1878
2599
  // src/rules/require-soft-assertions.ts
1879
2600
  var require_soft_assertions_default = {
1880
2601
  create(context) {
@@ -2071,17 +2792,17 @@ var compileMatcherPattern = (matcherMaybeWithMessage) => {
2071
2792
  const [matcher, message] = Array.isArray(matcherMaybeWithMessage) ? matcherMaybeWithMessage : [matcherMaybeWithMessage];
2072
2793
  return [new RegExp(matcher, "u"), message];
2073
2794
  };
2074
- var compileMatcherPatterns = (matchers3) => {
2075
- if (typeof matchers3 === "string" || Array.isArray(matchers3)) {
2076
- const compiledMatcher = compileMatcherPattern(matchers3);
2795
+ var compileMatcherPatterns = (matchers) => {
2796
+ if (typeof matchers === "string" || Array.isArray(matchers)) {
2797
+ const compiledMatcher = compileMatcherPattern(matchers);
2077
2798
  return {
2078
2799
  describe: compiledMatcher,
2079
2800
  test: compiledMatcher
2080
2801
  };
2081
2802
  }
2082
2803
  return {
2083
- describe: matchers3.describe ? compileMatcherPattern(matchers3.describe) : null,
2084
- test: matchers3.test ? compileMatcherPattern(matchers3.test) : null
2804
+ describe: matchers.describe ? compileMatcherPattern(matchers.describe) : null,
2805
+ test: matchers.test ? compileMatcherPattern(matchers.test) : null
2085
2806
  };
2086
2807
  };
2087
2808
  var MatcherAndMessageSchema = {
@@ -2273,14 +2994,19 @@ var index = {
2273
2994
  configs: {},
2274
2995
  rules: {
2275
2996
  "expect-expect": expect_expect_default,
2997
+ "max-expects": max_expects_default,
2276
2998
  "max-nested-describe": max_nested_describe_default,
2277
2999
  "missing-playwright-await": missing_playwright_await_default,
3000
+ "no-commented-out-tests": no_commented_out_tests_default,
3001
+ "no-conditional-expect": no_conditional_expect_default,
2278
3002
  "no-conditional-in-test": no_conditional_in_test_default,
3003
+ "no-duplicate-hooks": no_duplicate_hooks_default,
2279
3004
  "no-element-handle": no_element_handle_default,
2280
3005
  "no-eval": no_eval_default,
2281
3006
  "no-focused-test": no_focused_test_default,
2282
3007
  "no-force-option": no_force_option_default,
2283
3008
  "no-get-by-title": no_get_by_title_default,
3009
+ "no-hooks": no_hooks_default,
2284
3010
  "no-nested-step": no_nested_step_default,
2285
3011
  "no-networkidle": no_networkidle_default,
2286
3012
  "no-nth-methods": no_nth_methods_default,
@@ -2288,11 +3014,16 @@ var index = {
2288
3014
  "no-raw-locators": no_raw_locators_default,
2289
3015
  "no-restricted-matchers": no_restricted_matchers_default,
2290
3016
  "no-skipped-test": no_skipped_test_default,
3017
+ "no-standalone-expect": no_standalone_expect_default,
2291
3018
  "no-unsafe-references": no_unsafe_references_default,
2292
3019
  "no-useless-await": no_useless_await_default,
2293
3020
  "no-useless-not": no_useless_not_default,
2294
3021
  "no-wait-for-selector": no_wait_for_selector_default,
2295
3022
  "no-wait-for-timeout": no_wait_for_timeout_default,
3023
+ "prefer-comparison-matcher": prefer_comparison_matcher_default,
3024
+ "prefer-equality-matcher": prefer_equality_matcher_default,
3025
+ "prefer-hooks-in-order": prefer_hooks_in_order_default,
3026
+ "prefer-hooks-on-top": prefer_hooks_on_top_default,
2296
3027
  "prefer-lowercase-title": prefer_lowercase_title_default,
2297
3028
  "prefer-strict-equal": prefer_strict_equal_default,
2298
3029
  "prefer-to-be": prefer_to_be_default,
@@ -2300,6 +3031,7 @@ var index = {
2300
3031
  "prefer-to-have-count": prefer_to_have_count_default,
2301
3032
  "prefer-to-have-length": prefer_to_have_length_default,
2302
3033
  "prefer-web-first-assertions": prefer_web_first_assertions_default,
3034
+ "require-hook": require_hook_default,
2303
3035
  "require-soft-assertions": require_soft_assertions_default,
2304
3036
  "require-top-level-describe": require_top_level_describe_default,
2305
3037
  "valid-expect": valid_expect_default,
@@ -2312,6 +3044,7 @@ var sharedConfig = {
2312
3044
  "playwright/expect-expect": "warn",
2313
3045
  "playwright/max-nested-describe": "warn",
2314
3046
  "playwright/missing-playwright-await": "error",
3047
+ "playwright/no-conditional-expect": "warn",
2315
3048
  "playwright/no-conditional-in-test": "warn",
2316
3049
  "playwright/no-element-handle": "warn",
2317
3050
  "playwright/no-eval": "warn",
@@ -2321,6 +3054,7 @@ var sharedConfig = {
2321
3054
  "playwright/no-networkidle": "error",
2322
3055
  "playwright/no-page-pause": "warn",
2323
3056
  "playwright/no-skipped-test": "warn",
3057
+ "playwright/no-standalone-expect": "error",
2324
3058
  "playwright/no-unsafe-references": "error",
2325
3059
  "playwright/no-useless-await": "warn",
2326
3060
  "playwright/no-useless-not": "warn",