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.mjs CHANGED
@@ -33,9 +33,12 @@ function isStringNode(node) {
33
33
  function isPropertyAccessor(node, name) {
34
34
  return getStringValue(node.property) === name;
35
35
  }
36
- function isTestIdentifier(context, node) {
36
+ function getTestNames(context) {
37
37
  const aliases = context.settings.playwright?.globalAliases?.test ?? [];
38
- const testNames = ["test", ...aliases];
38
+ return ["test", ...aliases];
39
+ }
40
+ function isTestIdentifier(context, node) {
41
+ const testNames = getTestNames(context);
39
42
  const regex = new RegExp(`^(${testNames.join("|")})$`);
40
43
  return isIdentifier(node, regex) || node.type === "MemberExpression" && isIdentifier(node.object, regex);
41
44
  }
@@ -58,13 +61,33 @@ function findParent(node, type) {
58
61
  return node.parent.type === type ? node.parent : findParent(node.parent, type);
59
62
  }
60
63
  function isTestCall(context, node, modifiers) {
61
- 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(
62
- node.arguments[1].type
63
- );
64
+ 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]);
64
65
  }
65
66
  function isTestHook(context, node) {
66
67
  return node.callee.type === "MemberExpression" && isTestIdentifier(context, node.callee.object) && testHooks.has(getStringValue(node.callee.property));
67
68
  }
69
+ function parseFnCall(context, node) {
70
+ if (isTestCall(context, node)) {
71
+ return {
72
+ fn: node.arguments[1],
73
+ name: getStringValue(node.callee),
74
+ type: "test"
75
+ };
76
+ }
77
+ if (node.callee.type === "MemberExpression" && isTestIdentifier(context, node.callee.object) && testHooks.has(getStringValue(node.callee.property))) {
78
+ return {
79
+ fn: node.arguments[0],
80
+ name: getStringValue(node.callee.property),
81
+ type: "hook"
82
+ };
83
+ }
84
+ if (isDescribeCall(node)) {
85
+ return {
86
+ name: getStringValue(node.callee),
87
+ type: "describe"
88
+ };
89
+ }
90
+ }
68
91
  function getExpectType(context, node) {
69
92
  const aliases = context.settings.playwright?.globalAliases?.expect ?? [];
70
93
  const expectNames = ["expect", ...aliases];
@@ -99,7 +122,7 @@ function isPageMethod(node, name) {
99
122
  function isFunction(node) {
100
123
  return node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression";
101
124
  }
102
- var isTemplateLiteral, describeProperties, testHooks, expectSubCommands;
125
+ var isTemplateLiteral, describeProperties, testHooks, expectSubCommands, equalityMatchers;
103
126
  var init_ast = __esm({
104
127
  "src/utils/ast.ts"() {
105
128
  "use strict";
@@ -114,6 +137,7 @@ var init_ast = __esm({
114
137
  ]);
115
138
  testHooks = /* @__PURE__ */ new Set(["afterAll", "afterEach", "beforeAll", "beforeEach"]);
116
139
  expectSubCommands = /* @__PURE__ */ new Set(["soft", "poll"]);
140
+ equalityMatchers = /* @__PURE__ */ new Set(["toBe", "toEqual", "toStrictEqual"]);
117
141
  }
118
142
  });
119
143
 
@@ -186,6 +210,76 @@ var init_expect_expect = __esm({
186
210
  }
187
211
  });
188
212
 
213
+ // src/rules/max-expects.ts
214
+ var max_expects_default;
215
+ var init_max_expects = __esm({
216
+ "src/rules/max-expects.ts"() {
217
+ "use strict";
218
+ init_ast();
219
+ max_expects_default = {
220
+ create(context) {
221
+ const options = {
222
+ max: 5,
223
+ ...context.options?.[0] ?? {}
224
+ };
225
+ let count = 0;
226
+ const maybeResetCount = (node) => {
227
+ const parent = getParent(node);
228
+ const isTestFn = parent?.type !== "CallExpression" || isTestCall(context, parent);
229
+ if (isTestFn) {
230
+ count = 0;
231
+ }
232
+ };
233
+ return {
234
+ ArrowFunctionExpression: maybeResetCount,
235
+ "ArrowFunctionExpression:exit": maybeResetCount,
236
+ CallExpression(node) {
237
+ if (!getExpectType(context, node))
238
+ return;
239
+ count += 1;
240
+ if (count > options.max) {
241
+ context.report({
242
+ data: {
243
+ count: count.toString(),
244
+ max: options.max.toString()
245
+ },
246
+ messageId: "exceededMaxAssertion",
247
+ node
248
+ });
249
+ }
250
+ },
251
+ FunctionExpression: maybeResetCount,
252
+ "FunctionExpression:exit": maybeResetCount
253
+ };
254
+ },
255
+ meta: {
256
+ docs: {
257
+ category: "Best Practices",
258
+ description: "Enforces a maximum number assertion calls in a test body",
259
+ recommended: false,
260
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/max-expects.md"
261
+ },
262
+ messages: {
263
+ exceededMaxAssertion: "Too many assertion calls ({{ count }}) - maximum allowed is {{ max }}"
264
+ },
265
+ schema: [
266
+ {
267
+ additionalProperties: false,
268
+ properties: {
269
+ max: {
270
+ minimum: 1,
271
+ type: "integer"
272
+ }
273
+ },
274
+ type: "object"
275
+ }
276
+ ],
277
+ type: "suggestion"
278
+ }
279
+ };
280
+ }
281
+ });
282
+
189
283
  // src/rules/max-nested-describe.ts
190
284
  var max_nested_describe_default;
191
285
  var init_max_nested_describe = __esm({
@@ -407,6 +501,146 @@ var init_missing_playwright_await = __esm({
407
501
  }
408
502
  });
409
503
 
504
+ // src/rules/no-commented-out-tests.ts
505
+ function hasTests(context, node) {
506
+ const testNames = getTestNames(context);
507
+ const names = testNames.join("|");
508
+ const regex = new RegExp(
509
+ `^\\s*(${names}|describe)(\\.\\w+|\\[['"]\\w+['"]\\])?\\s*\\(`,
510
+ "mu"
511
+ );
512
+ return regex.test(node.value);
513
+ }
514
+ var no_commented_out_tests_default;
515
+ var init_no_commented_out_tests = __esm({
516
+ "src/rules/no-commented-out-tests.ts"() {
517
+ "use strict";
518
+ init_ast();
519
+ no_commented_out_tests_default = {
520
+ create(context) {
521
+ function checkNode(node) {
522
+ if (!hasTests(context, node))
523
+ return;
524
+ context.report({
525
+ messageId: "commentedTests",
526
+ node
527
+ });
528
+ }
529
+ return {
530
+ Program() {
531
+ context.sourceCode.getAllComments().forEach(checkNode);
532
+ }
533
+ };
534
+ },
535
+ meta: {
536
+ docs: {
537
+ category: "Best Practices",
538
+ description: "Disallow commented out tests",
539
+ recommended: true,
540
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-commented-out-tests.md"
541
+ },
542
+ messages: {
543
+ commentedTests: "Some tests seem to be commented"
544
+ },
545
+ type: "problem"
546
+ }
547
+ };
548
+ }
549
+ });
550
+
551
+ // src/rules/no-conditional-expect.ts
552
+ var isCatchCall, getTestCallExpressionsFromDeclaredVariables, no_conditional_expect_default;
553
+ var init_no_conditional_expect = __esm({
554
+ "src/rules/no-conditional-expect.ts"() {
555
+ "use strict";
556
+ init_ast();
557
+ isCatchCall = (node) => node.callee.type === "MemberExpression" && isPropertyAccessor(node.callee, "catch");
558
+ getTestCallExpressionsFromDeclaredVariables = (context, declaredVariables) => {
559
+ return declaredVariables.reduce(
560
+ (acc, { references }) => [
561
+ ...acc,
562
+ ...references.map(({ identifier }) => getParent(identifier)).filter(
563
+ (node) => node?.type === "CallExpression" && isTestCall(context, node)
564
+ )
565
+ ],
566
+ []
567
+ );
568
+ };
569
+ no_conditional_expect_default = {
570
+ create(context) {
571
+ let conditionalDepth = 0;
572
+ let inTestCase = false;
573
+ let inPromiseCatch = false;
574
+ const increaseConditionalDepth = () => inTestCase && conditionalDepth++;
575
+ const decreaseConditionalDepth = () => inTestCase && conditionalDepth--;
576
+ return {
577
+ CallExpression(node) {
578
+ if (isTestCall(context, node)) {
579
+ inTestCase = true;
580
+ }
581
+ if (isCatchCall(node)) {
582
+ inPromiseCatch = true;
583
+ }
584
+ const expectType = getExpectType(context, node);
585
+ if (inTestCase && expectType && conditionalDepth > 0) {
586
+ context.report({
587
+ messageId: "conditionalExpect",
588
+ node
589
+ });
590
+ }
591
+ if (inPromiseCatch && expectType) {
592
+ context.report({
593
+ messageId: "conditionalExpect",
594
+ node
595
+ });
596
+ }
597
+ },
598
+ "CallExpression:exit"(node) {
599
+ if (isTestCall(context, node)) {
600
+ inTestCase = false;
601
+ }
602
+ if (isCatchCall(node)) {
603
+ inPromiseCatch = false;
604
+ }
605
+ },
606
+ CatchClause: increaseConditionalDepth,
607
+ "CatchClause:exit": decreaseConditionalDepth,
608
+ ConditionalExpression: increaseConditionalDepth,
609
+ "ConditionalExpression:exit": decreaseConditionalDepth,
610
+ FunctionDeclaration(node) {
611
+ const declaredVariables = context.sourceCode.getDeclaredVariables(node);
612
+ const testCallExpressions = getTestCallExpressionsFromDeclaredVariables(
613
+ context,
614
+ declaredVariables
615
+ );
616
+ if (testCallExpressions.length > 0) {
617
+ inTestCase = true;
618
+ }
619
+ },
620
+ IfStatement: increaseConditionalDepth,
621
+ "IfStatement:exit": decreaseConditionalDepth,
622
+ LogicalExpression: increaseConditionalDepth,
623
+ "LogicalExpression:exit": decreaseConditionalDepth,
624
+ SwitchStatement: increaseConditionalDepth,
625
+ "SwitchStatement:exit": decreaseConditionalDepth
626
+ };
627
+ },
628
+ meta: {
629
+ docs: {
630
+ category: "Best Practices",
631
+ description: "Disallow calling `expect` conditionally",
632
+ recommended: true,
633
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-conditional-expect.md"
634
+ },
635
+ messages: {
636
+ conditionalExpect: "Avoid calling `expect` conditionally`"
637
+ },
638
+ type: "problem"
639
+ }
640
+ };
641
+ }
642
+ });
643
+
410
644
  // src/rules/no-conditional-in-test.ts
411
645
  var no_conditional_in_test_default;
412
646
  var init_no_conditional_in_test = __esm({
@@ -445,6 +679,58 @@ var init_no_conditional_in_test = __esm({
445
679
  }
446
680
  });
447
681
 
682
+ // src/rules/no-duplicate-hooks.ts
683
+ var no_duplicate_hooks_default;
684
+ var init_no_duplicate_hooks = __esm({
685
+ "src/rules/no-duplicate-hooks.ts"() {
686
+ "use strict";
687
+ init_ast();
688
+ no_duplicate_hooks_default = {
689
+ create(context) {
690
+ const hookContexts = [{}];
691
+ return {
692
+ CallExpression(node) {
693
+ if (isDescribeCall(node)) {
694
+ hookContexts.push({});
695
+ }
696
+ if (!isTestHook(context, node)) {
697
+ return;
698
+ }
699
+ const currentLayer = hookContexts[hookContexts.length - 1];
700
+ const name = node.callee.type === "MemberExpression" ? getStringValue(node.callee.property) : "";
701
+ currentLayer[name] || (currentLayer[name] = 0);
702
+ currentLayer[name] += 1;
703
+ if (currentLayer[name] > 1) {
704
+ context.report({
705
+ data: { hook: name },
706
+ messageId: "noDuplicateHook",
707
+ node
708
+ });
709
+ }
710
+ },
711
+ "CallExpression:exit"(node) {
712
+ if (isDescribeCall(node)) {
713
+ hookContexts.pop();
714
+ }
715
+ }
716
+ };
717
+ },
718
+ meta: {
719
+ docs: {
720
+ category: "Best Practices",
721
+ description: "Disallow duplicate setup and teardown hooks",
722
+ recommended: false,
723
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-duplicate-hooks.md"
724
+ },
725
+ messages: {
726
+ noDuplicateHook: "Duplicate {{ hook }} in describe block"
727
+ },
728
+ type: "suggestion"
729
+ }
730
+ };
731
+ }
732
+ });
733
+
448
734
  // src/rules/no-element-handle.ts
449
735
  function getPropertyRange(node) {
450
736
  return node.type === "Identifier" ? node.range : [node.range[0] + 1, node.range[1] - 1];
@@ -680,6 +966,59 @@ var init_no_get_by_title = __esm({
680
966
  }
681
967
  });
682
968
 
969
+ // src/rules/no-hooks.ts
970
+ var no_hooks_default;
971
+ var init_no_hooks = __esm({
972
+ "src/rules/no-hooks.ts"() {
973
+ "use strict";
974
+ init_ast();
975
+ no_hooks_default = {
976
+ create(context) {
977
+ const options = {
978
+ allow: [],
979
+ ...context.options?.[0] ?? {}
980
+ };
981
+ return {
982
+ CallExpression(node) {
983
+ const call = parseFnCall(context, node);
984
+ if (call?.type === "hook" && !options.allow.includes(call.name)) {
985
+ context.report({
986
+ data: { hookName: call.name },
987
+ messageId: "unexpectedHook",
988
+ node
989
+ });
990
+ }
991
+ }
992
+ };
993
+ },
994
+ meta: {
995
+ docs: {
996
+ category: "Best Practices",
997
+ description: "Disallow setup and teardown hooks",
998
+ recommended: false,
999
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-hooks.md"
1000
+ },
1001
+ messages: {
1002
+ unexpectedHook: "Unexpected '{{ hookName }}' hook"
1003
+ },
1004
+ schema: [
1005
+ {
1006
+ additionalProperties: false,
1007
+ properties: {
1008
+ allow: {
1009
+ contains: ["beforeAll", "beforeEach", "afterAll", "afterEach"],
1010
+ type: "array"
1011
+ }
1012
+ },
1013
+ type: "object"
1014
+ }
1015
+ ],
1016
+ type: "suggestion"
1017
+ }
1018
+ };
1019
+ }
1020
+ });
1021
+
683
1022
  // src/rules/no-nested-step.ts
684
1023
  function isStepCall(node) {
685
1024
  const inner = node.type === "CallExpression" ? node.callee : node;
@@ -1104,6 +1443,102 @@ var init_no_skipped_test = __esm({
1104
1443
  }
1105
1444
  });
1106
1445
 
1446
+ // src/rules/no-standalone-expect.ts
1447
+ var getBlockType, no_standalone_expect_default;
1448
+ var init_no_standalone_expect = __esm({
1449
+ "src/rules/no-standalone-expect.ts"() {
1450
+ "use strict";
1451
+ init_ast();
1452
+ getBlockType = (statement) => {
1453
+ const func = getParent(statement);
1454
+ if (!func) {
1455
+ throw new Error(
1456
+ `Unexpected BlockStatement. No parent defined. - please file a github issue at https://github.com/playwright-community/eslint-plugin-playwright`
1457
+ );
1458
+ }
1459
+ if (func.type === "FunctionDeclaration") {
1460
+ return "function";
1461
+ }
1462
+ if (isFunction(func) && func.parent) {
1463
+ const expr = func.parent;
1464
+ if (expr.type === "VariableDeclarator" || expr.type === "MethodDefinition") {
1465
+ return "function";
1466
+ }
1467
+ if (expr.type === "CallExpression" && isDescribeCall(expr)) {
1468
+ return "describe";
1469
+ }
1470
+ }
1471
+ return null;
1472
+ };
1473
+ no_standalone_expect_default = {
1474
+ create(context) {
1475
+ const callStack = [];
1476
+ return {
1477
+ ArrowFunctionExpression(node) {
1478
+ if (node.parent?.type !== "CallExpression") {
1479
+ callStack.push("arrow");
1480
+ }
1481
+ },
1482
+ "ArrowFunctionExpression:exit"() {
1483
+ if (callStack[callStack.length - 1] === "arrow") {
1484
+ callStack.pop();
1485
+ }
1486
+ },
1487
+ BlockStatement(statement) {
1488
+ const blockType = getBlockType(statement);
1489
+ if (blockType) {
1490
+ callStack.push(blockType);
1491
+ }
1492
+ },
1493
+ "BlockStatement:exit"(statement) {
1494
+ if (callStack[callStack.length - 1] === getBlockType(statement)) {
1495
+ callStack.pop();
1496
+ }
1497
+ },
1498
+ CallExpression(node) {
1499
+ if (getExpectType(context, node)) {
1500
+ const parent = callStack.at(-1);
1501
+ if (!parent || parent === "describe") {
1502
+ const root = findParent(node, "CallExpression");
1503
+ context.report({
1504
+ messageId: "unexpectedExpect",
1505
+ node: root ?? node
1506
+ });
1507
+ }
1508
+ return;
1509
+ }
1510
+ if (isTestCall(context, node)) {
1511
+ callStack.push("test");
1512
+ }
1513
+ if (node.callee.type === "TaggedTemplateExpression") {
1514
+ callStack.push("template");
1515
+ }
1516
+ },
1517
+ "CallExpression:exit"(node) {
1518
+ const top = callStack[callStack.length - 1];
1519
+ if (top === "test" && isTestCall(context, node) && node.callee.type !== "MemberExpression" || top === "template" && node.callee.type === "TaggedTemplateExpression") {
1520
+ callStack.pop();
1521
+ }
1522
+ }
1523
+ };
1524
+ },
1525
+ meta: {
1526
+ docs: {
1527
+ category: "Best Practices",
1528
+ description: "Disallow using `expect` outside of `test` blocks",
1529
+ recommended: false,
1530
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-standalone-expect.md"
1531
+ },
1532
+ fixable: "code",
1533
+ messages: {
1534
+ unexpectedExpect: "Expect must be inside of a test block"
1535
+ },
1536
+ type: "suggestion"
1537
+ }
1538
+ };
1539
+ }
1540
+ });
1541
+
1107
1542
  // src/utils/misc.ts
1108
1543
  var getAmountData, truthy;
1109
1544
  var init_misc = __esm({
@@ -1476,6 +1911,288 @@ var init_no_wait_for_timeout = __esm({
1476
1911
  }
1477
1912
  });
1478
1913
 
1914
+ // src/rules/prefer-comparison-matcher.ts
1915
+ var isString, isComparingToString, invertedOperators, operatorMatcher, determineMatcher, prefer_comparison_matcher_default;
1916
+ var init_prefer_comparison_matcher = __esm({
1917
+ "src/rules/prefer-comparison-matcher.ts"() {
1918
+ "use strict";
1919
+ init_ast();
1920
+ init_parseExpectCall();
1921
+ isString = (node) => {
1922
+ return isStringLiteral(node) || node.type === "TemplateLiteral";
1923
+ };
1924
+ isComparingToString = (expression) => {
1925
+ return isString(expression.left) || isString(expression.right);
1926
+ };
1927
+ invertedOperators = {
1928
+ "<": ">=",
1929
+ "<=": ">",
1930
+ ">": "<=",
1931
+ ">=": "<"
1932
+ };
1933
+ operatorMatcher = {
1934
+ "<": "toBeLessThan",
1935
+ "<=": "toBeLessThanOrEqual",
1936
+ ">": "toBeGreaterThan",
1937
+ ">=": "toBeGreaterThanOrEqual"
1938
+ };
1939
+ determineMatcher = (operator, negated) => {
1940
+ const op = negated ? invertedOperators[operator] : operator;
1941
+ return operatorMatcher[op] ?? null;
1942
+ };
1943
+ prefer_comparison_matcher_default = {
1944
+ create(context) {
1945
+ return {
1946
+ CallExpression(node) {
1947
+ const expectCall = parseExpectCall(context, node);
1948
+ if (!expectCall || expectCall.args.length === 0)
1949
+ return;
1950
+ const { args, matcher } = expectCall;
1951
+ const [comparison] = node.arguments;
1952
+ const expectCallEnd = node.range[1];
1953
+ const [matcherArg] = args;
1954
+ if (comparison?.type !== "BinaryExpression" || isComparingToString(comparison) || !equalityMatchers.has(getStringValue(matcher)) || !isBooleanLiteral(matcherArg)) {
1955
+ return;
1956
+ }
1957
+ const hasNot = expectCall.modifiers.some(
1958
+ (node2) => getStringValue(node2) === "not"
1959
+ );
1960
+ const preferredMatcher = determineMatcher(
1961
+ comparison.operator,
1962
+ getRawValue(matcherArg) === hasNot.toString()
1963
+ );
1964
+ if (!preferredMatcher) {
1965
+ return;
1966
+ }
1967
+ context.report({
1968
+ data: { preferredMatcher },
1969
+ fix(fixer) {
1970
+ const [modifier] = expectCall.modifiers;
1971
+ const modifierText = modifier && getStringValue(modifier) !== "not" ? `.${getStringValue(modifier)}` : "";
1972
+ return [
1973
+ // Replace the comparison argument with the left-hand side of the comparison
1974
+ fixer.replaceText(
1975
+ comparison,
1976
+ context.sourceCode.getText(comparison.left)
1977
+ ),
1978
+ // Replace the current matcher & modifier with the preferred matcher
1979
+ fixer.replaceTextRange(
1980
+ [expectCallEnd, getParent(matcher).range[1]],
1981
+ `${modifierText}.${preferredMatcher}`
1982
+ ),
1983
+ // Replace the matcher argument with the right-hand side of the comparison
1984
+ fixer.replaceText(
1985
+ matcherArg,
1986
+ context.sourceCode.getText(comparison.right)
1987
+ )
1988
+ ];
1989
+ },
1990
+ messageId: "useToBeComparison",
1991
+ node: matcher
1992
+ });
1993
+ }
1994
+ };
1995
+ },
1996
+ meta: {
1997
+ docs: {
1998
+ category: "Best Practices",
1999
+ description: "Suggest using the built-in comparison matchers",
2000
+ recommended: false
2001
+ },
2002
+ fixable: "code",
2003
+ messages: {
2004
+ useToBeComparison: "Prefer using `{{ preferredMatcher }}` instead"
2005
+ },
2006
+ type: "suggestion"
2007
+ }
2008
+ };
2009
+ }
2010
+ });
2011
+
2012
+ // src/rules/prefer-equality-matcher.ts
2013
+ var prefer_equality_matcher_default;
2014
+ var init_prefer_equality_matcher = __esm({
2015
+ "src/rules/prefer-equality-matcher.ts"() {
2016
+ "use strict";
2017
+ init_ast();
2018
+ init_parseExpectCall();
2019
+ prefer_equality_matcher_default = {
2020
+ create(context) {
2021
+ return {
2022
+ CallExpression(node) {
2023
+ const expectCall = parseExpectCall(context, node);
2024
+ if (!expectCall || expectCall.args.length === 0)
2025
+ return;
2026
+ const { args, matcher } = expectCall;
2027
+ const [comparison] = node.arguments;
2028
+ const expectCallEnd = node.range[1];
2029
+ const [matcherArg] = args;
2030
+ if (comparison?.type !== "BinaryExpression" || comparison.operator !== "===" && comparison.operator !== "!==" || !equalityMatchers.has(getStringValue(matcher)) || !isBooleanLiteral(matcherArg)) {
2031
+ return;
2032
+ }
2033
+ const matcherValue = getRawValue(matcherArg) === "true";
2034
+ const [modifier] = expectCall.modifiers;
2035
+ const hasNot = expectCall.modifiers.some(
2036
+ (node2) => getStringValue(node2) === "not"
2037
+ );
2038
+ const addNotModifier = (comparison.operator === "!==" ? !matcherValue : matcherValue) === hasNot;
2039
+ context.report({
2040
+ messageId: "useEqualityMatcher",
2041
+ node: matcher,
2042
+ suggest: [...equalityMatchers.keys()].map((equalityMatcher) => ({
2043
+ data: { matcher: equalityMatcher },
2044
+ fix(fixer) {
2045
+ let modifierText = modifier && getStringValue(modifier) !== "not" ? `.${getStringValue(modifier)}` : "";
2046
+ if (addNotModifier) {
2047
+ modifierText += `.not`;
2048
+ }
2049
+ return [
2050
+ // replace the comparison argument with the left-hand side of the comparison
2051
+ fixer.replaceText(
2052
+ comparison,
2053
+ context.sourceCode.getText(comparison.left)
2054
+ ),
2055
+ // replace the current matcher & modifier with the preferred matcher
2056
+ fixer.replaceTextRange(
2057
+ [expectCallEnd, getParent(matcher).range[1]],
2058
+ `${modifierText}.${equalityMatcher}`
2059
+ ),
2060
+ // replace the matcher argument with the right-hand side of the comparison
2061
+ fixer.replaceText(
2062
+ matcherArg,
2063
+ context.sourceCode.getText(comparison.right)
2064
+ )
2065
+ ];
2066
+ },
2067
+ messageId: "suggestEqualityMatcher"
2068
+ }))
2069
+ });
2070
+ }
2071
+ };
2072
+ },
2073
+ meta: {
2074
+ docs: {
2075
+ category: "Best Practices",
2076
+ description: "Suggest using the built-in equality matchers",
2077
+ recommended: false,
2078
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-equality-matcher.md"
2079
+ },
2080
+ hasSuggestions: true,
2081
+ messages: {
2082
+ suggestEqualityMatcher: "Use `{{ matcher }}`",
2083
+ useEqualityMatcher: "Prefer using one of the equality matchers instead"
2084
+ },
2085
+ type: "suggestion"
2086
+ }
2087
+ };
2088
+ }
2089
+ });
2090
+
2091
+ // src/rules/prefer-hooks-in-order.ts
2092
+ var HooksOrder, prefer_hooks_in_order_default;
2093
+ var init_prefer_hooks_in_order = __esm({
2094
+ "src/rules/prefer-hooks-in-order.ts"() {
2095
+ "use strict";
2096
+ init_ast();
2097
+ HooksOrder = ["beforeAll", "beforeEach", "afterEach", "afterAll"];
2098
+ prefer_hooks_in_order_default = {
2099
+ create(context) {
2100
+ let previousHookIndex = -1;
2101
+ let inHook = false;
2102
+ return {
2103
+ CallExpression(node) {
2104
+ if (inHook)
2105
+ return;
2106
+ if (!isTestHook(context, node)) {
2107
+ previousHookIndex = -1;
2108
+ return;
2109
+ }
2110
+ inHook = true;
2111
+ const currentHook = node.callee.type === "MemberExpression" ? getStringValue(node.callee.property) : "";
2112
+ const currentHookIndex = HooksOrder.indexOf(currentHook);
2113
+ if (currentHookIndex < previousHookIndex) {
2114
+ return context.report({
2115
+ data: {
2116
+ currentHook,
2117
+ previousHook: HooksOrder[previousHookIndex]
2118
+ },
2119
+ messageId: "reorderHooks",
2120
+ node
2121
+ });
2122
+ }
2123
+ previousHookIndex = currentHookIndex;
2124
+ },
2125
+ "CallExpression:exit"(node) {
2126
+ if (isTestHook(context, node)) {
2127
+ inHook = false;
2128
+ return;
2129
+ }
2130
+ if (inHook) {
2131
+ return;
2132
+ }
2133
+ previousHookIndex = -1;
2134
+ }
2135
+ };
2136
+ },
2137
+ meta: {
2138
+ docs: {
2139
+ category: "Best Practices",
2140
+ description: "Prefer having hooks in a consistent order",
2141
+ recommended: false,
2142
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-hooks-in-order.md"
2143
+ },
2144
+ messages: {
2145
+ reorderHooks: "`{{ currentHook }}` hooks should be before any `{{ previousHook }}` hooks"
2146
+ },
2147
+ type: "suggestion"
2148
+ }
2149
+ };
2150
+ }
2151
+ });
2152
+
2153
+ // src/rules/prefer-hooks-on-top.ts
2154
+ var prefer_hooks_on_top_default;
2155
+ var init_prefer_hooks_on_top = __esm({
2156
+ "src/rules/prefer-hooks-on-top.ts"() {
2157
+ "use strict";
2158
+ init_ast();
2159
+ prefer_hooks_on_top_default = {
2160
+ create(context) {
2161
+ const stack = [false];
2162
+ return {
2163
+ CallExpression(node) {
2164
+ if (isTestCall(context, node)) {
2165
+ stack[stack.length - 1] = true;
2166
+ }
2167
+ if (stack.at(-1) && isTestHook(context, node)) {
2168
+ context.report({
2169
+ messageId: "noHookOnTop",
2170
+ node
2171
+ });
2172
+ }
2173
+ stack.push(false);
2174
+ },
2175
+ "CallExpression:exit"() {
2176
+ stack.pop();
2177
+ }
2178
+ };
2179
+ },
2180
+ meta: {
2181
+ docs: {
2182
+ category: "Best Practices",
2183
+ description: "Suggest having hooks before any test cases",
2184
+ recommended: false,
2185
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-hooks-on-top.md"
2186
+ },
2187
+ messages: {
2188
+ noHookOnTop: "Hooks should come before test cases"
2189
+ },
2190
+ type: "suggestion"
2191
+ }
2192
+ };
2193
+ }
2194
+ });
2195
+
1479
2196
  // src/rules/prefer-lowercase-title.ts
1480
2197
  var prefer_lowercase_title_default;
1481
2198
  var init_prefer_lowercase_title = __esm({
@@ -1685,9 +2402,8 @@ var init_prefer_to_be = __esm({
1685
2402
  notModifier
1686
2403
  );
1687
2404
  }
1688
- const argumentMatchers = ["toBe", "toEqual", "toStrictEqual"];
1689
2405
  const firstArg = expectCall.args[0];
1690
- if (!argumentMatchers.includes(expectCall.matcherName) || !firstArg) {
2406
+ if (!equalityMatchers.has(expectCall.matcherName) || !firstArg) {
1691
2407
  return;
1692
2408
  }
1693
2409
  if (firstArg.type === "Literal" && firstArg.value === null) {
@@ -1729,13 +2445,12 @@ var init_prefer_to_be = __esm({
1729
2445
  });
1730
2446
 
1731
2447
  // src/rules/prefer-to-contain.ts
1732
- var matchers, isFixableIncludesCallExpression, prefer_to_contain_default;
2448
+ var isFixableIncludesCallExpression, prefer_to_contain_default;
1733
2449
  var init_prefer_to_contain = __esm({
1734
2450
  "src/rules/prefer-to-contain.ts"() {
1735
2451
  "use strict";
1736
2452
  init_ast();
1737
2453
  init_parseExpectCall();
1738
- matchers = /* @__PURE__ */ new Set(["toBe", "toEqual", "toStrictEqual"]);
1739
2454
  isFixableIncludesCallExpression = (node) => node.type === "CallExpression" && node.callee.type === "MemberExpression" && isPropertyAccessor(node.callee, "includes") && node.arguments.length === 1 && node.arguments[0].type !== "SpreadElement";
1740
2455
  prefer_to_contain_default = {
1741
2456
  create(context) {
@@ -1747,7 +2462,7 @@ var init_prefer_to_contain = __esm({
1747
2462
  const { args, matcher, matcherName } = expectCall;
1748
2463
  const [includesCall] = node.arguments;
1749
2464
  const [matcherArg] = args;
1750
- if (!includesCall || matcherArg.type === "SpreadElement" || !matchers.has(matcherName) || !isBooleanLiteral(matcherArg) || !isFixableIncludesCallExpression(includesCall)) {
2465
+ if (!includesCall || matcherArg.type === "SpreadElement" || !equalityMatchers.has(matcherName) || !isBooleanLiteral(matcherArg) || !isFixableIncludesCallExpression(includesCall)) {
1751
2466
  return;
1752
2467
  }
1753
2468
  const notModifier = expectCall.modifiers.find(
@@ -1807,20 +2522,19 @@ var init_prefer_to_contain = __esm({
1807
2522
  });
1808
2523
 
1809
2524
  // src/rules/prefer-to-have-count.ts
1810
- var matchers2, prefer_to_have_count_default;
2525
+ var prefer_to_have_count_default;
1811
2526
  var init_prefer_to_have_count = __esm({
1812
2527
  "src/rules/prefer-to-have-count.ts"() {
1813
2528
  "use strict";
1814
2529
  init_ast();
1815
2530
  init_fixer();
1816
2531
  init_parseExpectCall();
1817
- matchers2 = /* @__PURE__ */ new Set(["toBe", "toEqual", "toStrictEqual"]);
1818
2532
  prefer_to_have_count_default = {
1819
2533
  create(context) {
1820
2534
  return {
1821
2535
  CallExpression(node) {
1822
2536
  const expectCall = parseExpectCall(context, node);
1823
- if (!expectCall || !matchers2.has(expectCall.matcherName)) {
2537
+ if (!expectCall || !equalityMatchers.has(expectCall.matcherName)) {
1824
2538
  return;
1825
2539
  }
1826
2540
  const [argument] = node.arguments;
@@ -1872,20 +2586,19 @@ var init_prefer_to_have_count = __esm({
1872
2586
  });
1873
2587
 
1874
2588
  // src/rules/prefer-to-have-length.ts
1875
- var lengthMatchers, prefer_to_have_length_default;
2589
+ var prefer_to_have_length_default;
1876
2590
  var init_prefer_to_have_length = __esm({
1877
2591
  "src/rules/prefer-to-have-length.ts"() {
1878
2592
  "use strict";
1879
2593
  init_ast();
1880
2594
  init_fixer();
1881
2595
  init_parseExpectCall();
1882
- lengthMatchers = /* @__PURE__ */ new Set(["toBe", "toEqual", "toStrictEqual"]);
1883
2596
  prefer_to_have_length_default = {
1884
2597
  create(context) {
1885
2598
  return {
1886
2599
  CallExpression(node) {
1887
2600
  const expectCall = parseExpectCall(context, node);
1888
- if (!expectCall || !lengthMatchers.has(expectCall.matcherName)) {
2601
+ if (!expectCall || !equalityMatchers.has(expectCall.matcherName)) {
1889
2602
  return;
1890
2603
  }
1891
2604
  const [argument] = node.arguments;
@@ -2090,6 +2803,93 @@ var init_prefer_web_first_assertions = __esm({
2090
2803
  }
2091
2804
  });
2092
2805
 
2806
+ // src/rules/require-hook.ts
2807
+ var isNullOrUndefined, shouldBeInHook, require_hook_default;
2808
+ var init_require_hook = __esm({
2809
+ "src/rules/require-hook.ts"() {
2810
+ "use strict";
2811
+ init_ast();
2812
+ isNullOrUndefined = (node) => {
2813
+ return node.type === "Literal" && node.value === null || isIdentifier(node, "undefined");
2814
+ };
2815
+ shouldBeInHook = (context, node, allowedFunctionCalls = []) => {
2816
+ switch (node.type) {
2817
+ case "ExpressionStatement":
2818
+ return shouldBeInHook(context, node.expression, allowedFunctionCalls);
2819
+ case "CallExpression":
2820
+ return !(parseFnCall(context, node) || allowedFunctionCalls.includes(getStringValue(node.callee)));
2821
+ case "VariableDeclaration": {
2822
+ if (node.kind === "const") {
2823
+ return false;
2824
+ }
2825
+ return node.declarations.some(
2826
+ ({ init }) => init != null && !isNullOrUndefined(init)
2827
+ );
2828
+ }
2829
+ default:
2830
+ return false;
2831
+ }
2832
+ };
2833
+ require_hook_default = {
2834
+ create(context) {
2835
+ const options = {
2836
+ allowedFunctionCalls: [],
2837
+ ...context.options?.[0] ?? {}
2838
+ };
2839
+ const checkBlockBody = (body) => {
2840
+ for (const statement of body) {
2841
+ if (shouldBeInHook(context, statement, options.allowedFunctionCalls)) {
2842
+ context.report({
2843
+ messageId: "useHook",
2844
+ node: statement
2845
+ });
2846
+ }
2847
+ }
2848
+ };
2849
+ return {
2850
+ CallExpression(node) {
2851
+ if (!isDescribeCall(node) || node.arguments.length < 2) {
2852
+ return;
2853
+ }
2854
+ const [, testFn] = node.arguments;
2855
+ if (!isFunction(testFn) || testFn.body.type !== "BlockStatement") {
2856
+ return;
2857
+ }
2858
+ checkBlockBody(testFn.body.body);
2859
+ },
2860
+ Program(program) {
2861
+ checkBlockBody(program.body);
2862
+ }
2863
+ };
2864
+ },
2865
+ meta: {
2866
+ docs: {
2867
+ category: "Best Practices",
2868
+ description: "Require setup and teardown code to be within a hook",
2869
+ recommended: false,
2870
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/require-hook.md"
2871
+ },
2872
+ messages: {
2873
+ useHook: "This should be done within a hook"
2874
+ },
2875
+ schema: [
2876
+ {
2877
+ additionalProperties: false,
2878
+ properties: {
2879
+ allowedFunctionCalls: {
2880
+ items: { type: "string" },
2881
+ type: "array"
2882
+ }
2883
+ },
2884
+ type: "object"
2885
+ }
2886
+ ],
2887
+ type: "suggestion"
2888
+ }
2889
+ };
2890
+ }
2891
+ });
2892
+
2093
2893
  // src/rules/require-soft-assertions.ts
2094
2894
  var require_soft_assertions_default;
2095
2895
  var init_require_soft_assertions = __esm({
@@ -2315,17 +3115,17 @@ var init_valid_title = __esm({
2315
3115
  const [matcher, message] = Array.isArray(matcherMaybeWithMessage) ? matcherMaybeWithMessage : [matcherMaybeWithMessage];
2316
3116
  return [new RegExp(matcher, "u"), message];
2317
3117
  };
2318
- compileMatcherPatterns = (matchers3) => {
2319
- if (typeof matchers3 === "string" || Array.isArray(matchers3)) {
2320
- const compiledMatcher = compileMatcherPattern(matchers3);
3118
+ compileMatcherPatterns = (matchers) => {
3119
+ if (typeof matchers === "string" || Array.isArray(matchers)) {
3120
+ const compiledMatcher = compileMatcherPattern(matchers);
2321
3121
  return {
2322
3122
  describe: compiledMatcher,
2323
3123
  test: compiledMatcher
2324
3124
  };
2325
3125
  }
2326
3126
  return {
2327
- describe: matchers3.describe ? compileMatcherPattern(matchers3.describe) : null,
2328
- test: matchers3.test ? compileMatcherPattern(matchers3.test) : null
3127
+ describe: matchers.describe ? compileMatcherPattern(matchers.describe) : null,
3128
+ test: matchers.test ? compileMatcherPattern(matchers.test) : null
2329
3129
  };
2330
3130
  };
2331
3131
  MatcherAndMessageSchema = {
@@ -2519,14 +3319,19 @@ import globals from "globals";
2519
3319
  var require_src = __commonJS({
2520
3320
  "src/index.ts"(exports, module) {
2521
3321
  init_expect_expect();
3322
+ init_max_expects();
2522
3323
  init_max_nested_describe();
2523
3324
  init_missing_playwright_await();
3325
+ init_no_commented_out_tests();
3326
+ init_no_conditional_expect();
2524
3327
  init_no_conditional_in_test();
3328
+ init_no_duplicate_hooks();
2525
3329
  init_no_element_handle();
2526
3330
  init_no_eval();
2527
3331
  init_no_focused_test();
2528
3332
  init_no_force_option();
2529
3333
  init_no_get_by_title();
3334
+ init_no_hooks();
2530
3335
  init_no_nested_step();
2531
3336
  init_no_networkidle();
2532
3337
  init_no_nth_methods();
@@ -2534,11 +3339,16 @@ var require_src = __commonJS({
2534
3339
  init_no_raw_locators();
2535
3340
  init_no_restricted_matchers();
2536
3341
  init_no_skipped_test();
3342
+ init_no_standalone_expect();
2537
3343
  init_no_unsafe_references();
2538
3344
  init_no_useless_await();
2539
3345
  init_no_useless_not();
2540
3346
  init_no_wait_for_selector();
2541
3347
  init_no_wait_for_timeout();
3348
+ init_prefer_comparison_matcher();
3349
+ init_prefer_equality_matcher();
3350
+ init_prefer_hooks_in_order();
3351
+ init_prefer_hooks_on_top();
2542
3352
  init_prefer_lowercase_title();
2543
3353
  init_prefer_strict_equal();
2544
3354
  init_prefer_to_be();
@@ -2546,6 +3356,7 @@ var require_src = __commonJS({
2546
3356
  init_prefer_to_have_count();
2547
3357
  init_prefer_to_have_length();
2548
3358
  init_prefer_web_first_assertions();
3359
+ init_require_hook();
2549
3360
  init_require_soft_assertions();
2550
3361
  init_require_top_level_describe();
2551
3362
  init_valid_expect();
@@ -2554,14 +3365,19 @@ var require_src = __commonJS({
2554
3365
  configs: {},
2555
3366
  rules: {
2556
3367
  "expect-expect": expect_expect_default,
3368
+ "max-expects": max_expects_default,
2557
3369
  "max-nested-describe": max_nested_describe_default,
2558
3370
  "missing-playwright-await": missing_playwright_await_default,
3371
+ "no-commented-out-tests": no_commented_out_tests_default,
3372
+ "no-conditional-expect": no_conditional_expect_default,
2559
3373
  "no-conditional-in-test": no_conditional_in_test_default,
3374
+ "no-duplicate-hooks": no_duplicate_hooks_default,
2560
3375
  "no-element-handle": no_element_handle_default,
2561
3376
  "no-eval": no_eval_default,
2562
3377
  "no-focused-test": no_focused_test_default,
2563
3378
  "no-force-option": no_force_option_default,
2564
3379
  "no-get-by-title": no_get_by_title_default,
3380
+ "no-hooks": no_hooks_default,
2565
3381
  "no-nested-step": no_nested_step_default,
2566
3382
  "no-networkidle": no_networkidle_default,
2567
3383
  "no-nth-methods": no_nth_methods_default,
@@ -2569,11 +3385,16 @@ var require_src = __commonJS({
2569
3385
  "no-raw-locators": no_raw_locators_default,
2570
3386
  "no-restricted-matchers": no_restricted_matchers_default,
2571
3387
  "no-skipped-test": no_skipped_test_default,
3388
+ "no-standalone-expect": no_standalone_expect_default,
2572
3389
  "no-unsafe-references": no_unsafe_references_default,
2573
3390
  "no-useless-await": no_useless_await_default,
2574
3391
  "no-useless-not": no_useless_not_default,
2575
3392
  "no-wait-for-selector": no_wait_for_selector_default,
2576
3393
  "no-wait-for-timeout": no_wait_for_timeout_default,
3394
+ "prefer-comparison-matcher": prefer_comparison_matcher_default,
3395
+ "prefer-equality-matcher": prefer_equality_matcher_default,
3396
+ "prefer-hooks-in-order": prefer_hooks_in_order_default,
3397
+ "prefer-hooks-on-top": prefer_hooks_on_top_default,
2577
3398
  "prefer-lowercase-title": prefer_lowercase_title_default,
2578
3399
  "prefer-strict-equal": prefer_strict_equal_default,
2579
3400
  "prefer-to-be": prefer_to_be_default,
@@ -2581,6 +3402,7 @@ var require_src = __commonJS({
2581
3402
  "prefer-to-have-count": prefer_to_have_count_default,
2582
3403
  "prefer-to-have-length": prefer_to_have_length_default,
2583
3404
  "prefer-web-first-assertions": prefer_web_first_assertions_default,
3405
+ "require-hook": require_hook_default,
2584
3406
  "require-soft-assertions": require_soft_assertions_default,
2585
3407
  "require-top-level-describe": require_top_level_describe_default,
2586
3408
  "valid-expect": valid_expect_default,
@@ -2593,6 +3415,7 @@ var require_src = __commonJS({
2593
3415
  "playwright/expect-expect": "warn",
2594
3416
  "playwright/max-nested-describe": "warn",
2595
3417
  "playwright/missing-playwright-await": "error",
3418
+ "playwright/no-conditional-expect": "warn",
2596
3419
  "playwright/no-conditional-in-test": "warn",
2597
3420
  "playwright/no-element-handle": "warn",
2598
3421
  "playwright/no-eval": "warn",
@@ -2602,6 +3425,7 @@ var require_src = __commonJS({
2602
3425
  "playwright/no-networkidle": "error",
2603
3426
  "playwright/no-page-pause": "warn",
2604
3427
  "playwright/no-skipped-test": "warn",
3428
+ "playwright/no-standalone-expect": "error",
2605
3429
  "playwright/no-unsafe-references": "error",
2606
3430
  "playwright/no-useless-await": "warn",
2607
3431
  "playwright/no-useless-not": "warn",