eslint-plugin-playwright 1.2.0 → 1.3.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/dist/index.mjs CHANGED
@@ -16,7 +16,7 @@ function getRawValue(node) {
16
16
  return node.type === "Literal" ? node.raw : void 0;
17
17
  }
18
18
  function isIdentifier(node, name) {
19
- return node.type === "Identifier" && (typeof name === "string" ? node.name === name : name.test(node.name));
19
+ return node.type === "Identifier" && (!name || (typeof name === "string" ? node.name === name : name.test(node.name)));
20
20
  }
21
21
  function isLiteral(node, type, value) {
22
22
  return node.type === "Literal" && (value === void 0 ? typeof node.value === type : node.value === value);
@@ -27,8 +27,8 @@ function isStringLiteral(node, value) {
27
27
  function isBooleanLiteral(node, value) {
28
28
  return isLiteral(node, "boolean", value);
29
29
  }
30
- function isStringNode(node) {
31
- return node && (isStringLiteral(node) || isTemplateLiteral(node));
30
+ function isStringNode(node, value) {
31
+ return node && (isStringLiteral(node, value) || isTemplateLiteral(node, value));
32
32
  }
33
33
  function isPropertyAccessor(node, name) {
34
34
  return getStringValue(node.property) === name;
@@ -100,7 +100,7 @@ function isPageMethod(node, name) {
100
100
  function isFunction(node) {
101
101
  return node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression";
102
102
  }
103
- var isTemplateLiteral, describeProperties, testHooks, expectSubCommands;
103
+ var isTemplateLiteral, describeProperties, testHooks, expectSubCommands, equalityMatchers;
104
104
  var init_ast = __esm({
105
105
  "src/utils/ast.ts"() {
106
106
  "use strict";
@@ -113,8 +113,14 @@ var init_ast = __esm({
113
113
  "skip",
114
114
  "fixme"
115
115
  ]);
116
- testHooks = /* @__PURE__ */ new Set(["afterAll", "afterEach", "beforeAll", "beforeEach"]);
116
+ testHooks = /* @__PURE__ */ new Set([
117
+ "afterAll",
118
+ "afterEach",
119
+ "beforeAll",
120
+ "beforeEach"
121
+ ]);
117
122
  expectSubCommands = /* @__PURE__ */ new Set(["soft", "poll"]);
123
+ equalityMatchers = /* @__PURE__ */ new Set(["toBe", "toEqual", "toStrictEqual"]);
118
124
  }
119
125
  });
120
126
 
@@ -943,6 +949,192 @@ var init_no_get_by_title = __esm({
943
949
  }
944
950
  });
945
951
 
952
+ // src/utils/parseFnCall.ts
953
+ function getNodeChain(node) {
954
+ if (isSupportedAccessor(node)) {
955
+ return [node];
956
+ }
957
+ switch (node.type) {
958
+ case "TaggedTemplateExpression":
959
+ return getNodeChain(node.tag);
960
+ case "MemberExpression":
961
+ return joinChains(getNodeChain(node.object), getNodeChain(node.property));
962
+ case "CallExpression":
963
+ return getNodeChain(node.callee);
964
+ }
965
+ return null;
966
+ }
967
+ function determinePlaywrightFnType(name) {
968
+ if (name === "expect")
969
+ return "expect";
970
+ if (name === "describe")
971
+ return "describe";
972
+ if (name === "test")
973
+ return "test";
974
+ if (testHooks.has(name))
975
+ return "hook";
976
+ return "unknown";
977
+ }
978
+ function parseFnCall(context, node) {
979
+ const chain = getNodeChain(node);
980
+ if (!chain?.length) {
981
+ return null;
982
+ }
983
+ const [first, ...rest] = chain;
984
+ const resolved = resolveToPlaywrightFn(context, first);
985
+ if (!resolved)
986
+ return null;
987
+ let name = resolved.original ?? resolved.local;
988
+ const links = [name, ...rest.map((link) => getStringValue(link))];
989
+ if (name !== "expect" && !VALID_CHAINS.has(links.join("."))) {
990
+ return null;
991
+ }
992
+ if (name === "test" && links.length > 1) {
993
+ const nextLinkName = links[1];
994
+ const nextLinkType = determinePlaywrightFnType(nextLinkName);
995
+ if (nextLinkType !== "unknown") {
996
+ name = nextLinkName;
997
+ }
998
+ }
999
+ const parsedFnCall = {
1000
+ head: { ...resolved, node: first },
1001
+ // every member node must have a member expression as their parent
1002
+ // in order to be part of the call chain we're parsing
1003
+ members: rest,
1004
+ name
1005
+ };
1006
+ const type = determinePlaywrightFnType(name);
1007
+ if (type === "expect") {
1008
+ return {
1009
+ ...parsedFnCall,
1010
+ args: [],
1011
+ matcher: rest[rest.length - 1],
1012
+ modifiers: rest.slice(0, rest.length - 1),
1013
+ type
1014
+ };
1015
+ }
1016
+ if (chain.slice(0, chain.length - 1).some((n) => getParent(n)?.type !== "MemberExpression")) {
1017
+ return null;
1018
+ }
1019
+ const parent = getParent(node);
1020
+ if (parent?.type === "CallExpression" || parent?.type === "MemberExpression") {
1021
+ return null;
1022
+ }
1023
+ return { ...parsedFnCall, type };
1024
+ }
1025
+ var VALID_CHAINS, joinChains, isSupportedAccessor, resolvePossibleAliasedGlobal, resolveToPlaywrightFn, isTypeOfFnCall;
1026
+ var init_parseFnCall = __esm({
1027
+ "src/utils/parseFnCall.ts"() {
1028
+ "use strict";
1029
+ init_ast();
1030
+ VALID_CHAINS = /* @__PURE__ */ new Set([
1031
+ // Hooks
1032
+ "afterAll",
1033
+ "afterEach",
1034
+ "beforeAll",
1035
+ "beforeEach",
1036
+ "test.afterAll",
1037
+ "test.afterEach",
1038
+ "test.beforeAll",
1039
+ "test.beforeEach",
1040
+ // Describe
1041
+ "describe",
1042
+ "describe.only",
1043
+ "describe.skip",
1044
+ "describe.fixme",
1045
+ "describe.configure",
1046
+ "test.describe",
1047
+ "test.describe.only",
1048
+ "test.describe.skip",
1049
+ "test.describe.fixme",
1050
+ "test.describe.configure",
1051
+ // Test
1052
+ "test",
1053
+ "test.fail",
1054
+ "text.fixme",
1055
+ "test.only",
1056
+ "test.skip",
1057
+ "test.slow",
1058
+ "test.step",
1059
+ "test.use"
1060
+ ]);
1061
+ joinChains = (a, b) => a && b ? [...a, ...b] : null;
1062
+ isSupportedAccessor = (node, value) => isIdentifier(node, value) || isStringNode(node, value);
1063
+ resolvePossibleAliasedGlobal = (context, global) => {
1064
+ const globalAliases = context.settings.playwright?.globalAliases ?? {};
1065
+ const alias = Object.entries(globalAliases).find(
1066
+ ([, aliases]) => aliases.includes(global)
1067
+ );
1068
+ return alias?.[0] ?? null;
1069
+ };
1070
+ resolveToPlaywrightFn = (context, accessor) => {
1071
+ const ident = getStringValue(accessor);
1072
+ return {
1073
+ local: ident,
1074
+ original: resolvePossibleAliasedGlobal(context, ident),
1075
+ type: "global"
1076
+ };
1077
+ };
1078
+ isTypeOfFnCall = (context, node, types) => {
1079
+ const call = parseFnCall(context, node);
1080
+ return call !== null && types.includes(call.type);
1081
+ };
1082
+ }
1083
+ });
1084
+
1085
+ // src/rules/no-hooks.ts
1086
+ var no_hooks_default;
1087
+ var init_no_hooks = __esm({
1088
+ "src/rules/no-hooks.ts"() {
1089
+ "use strict";
1090
+ init_parseFnCall();
1091
+ no_hooks_default = {
1092
+ create(context) {
1093
+ const options = {
1094
+ allow: [],
1095
+ ...context.options?.[0] ?? {}
1096
+ };
1097
+ return {
1098
+ CallExpression(node) {
1099
+ const call = parseFnCall(context, node);
1100
+ if (call?.type === "hook" && !options.allow.includes(call.name)) {
1101
+ context.report({
1102
+ data: { hookName: call.name },
1103
+ messageId: "unexpectedHook",
1104
+ node
1105
+ });
1106
+ }
1107
+ }
1108
+ };
1109
+ },
1110
+ meta: {
1111
+ docs: {
1112
+ category: "Best Practices",
1113
+ description: "Disallow setup and teardown hooks",
1114
+ recommended: false,
1115
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-hooks.md"
1116
+ },
1117
+ messages: {
1118
+ unexpectedHook: "Unexpected '{{ hookName }}' hook"
1119
+ },
1120
+ schema: [
1121
+ {
1122
+ additionalProperties: false,
1123
+ properties: {
1124
+ allow: {
1125
+ contains: ["beforeAll", "beforeEach", "afterAll", "afterEach"],
1126
+ type: "array"
1127
+ }
1128
+ },
1129
+ type: "object"
1130
+ }
1131
+ ],
1132
+ type: "suggestion"
1133
+ }
1134
+ };
1135
+ }
1136
+ });
1137
+
946
1138
  // src/rules/no-nested-step.ts
947
1139
  function isStepCall(node) {
948
1140
  const inner = node.type === "CallExpression" ? node.callee : node;
@@ -1377,7 +1569,7 @@ var init_no_standalone_expect = __esm({
1377
1569
  const func = getParent(statement);
1378
1570
  if (!func) {
1379
1571
  throw new Error(
1380
- `Unexpected BlockStatement. No parent defined. - please file a github issue at https://github.com/jest-community/eslint-plugin-jest`
1572
+ `Unexpected BlockStatement. No parent defined. - please file a github issue at https://github.com/playwright-community/eslint-plugin-playwright`
1381
1573
  );
1382
1574
  }
1383
1575
  if (func.type === "FunctionDeclaration") {
@@ -1385,7 +1577,7 @@ var init_no_standalone_expect = __esm({
1385
1577
  }
1386
1578
  if (isFunction(func) && func.parent) {
1387
1579
  const expr = func.parent;
1388
- if (expr.type === "VariableDeclarator") {
1580
+ if (expr.type === "VariableDeclarator" || expr.type === "MethodDefinition") {
1389
1581
  return "function";
1390
1582
  }
1391
1583
  if (expr.type === "CallExpression" && isDescribeCall(expr)) {
@@ -1434,6 +1626,9 @@ var init_no_standalone_expect = __esm({
1434
1626
  if (isTestCall(context, node)) {
1435
1627
  callStack.push("test");
1436
1628
  }
1629
+ if (isTestHook(context, node)) {
1630
+ callStack.push("hook");
1631
+ }
1437
1632
  if (node.callee.type === "TaggedTemplateExpression") {
1438
1633
  callStack.push("template");
1439
1634
  }
@@ -1451,7 +1646,7 @@ var init_no_standalone_expect = __esm({
1451
1646
  category: "Best Practices",
1452
1647
  description: "Disallow using `expect` outside of `test` blocks",
1453
1648
  recommended: false,
1454
- url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-to-contain.md"
1649
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-standalone-expect.md"
1455
1650
  },
1456
1651
  fixable: "code",
1457
1652
  messages: {
@@ -1835,6 +2030,183 @@ var init_no_wait_for_timeout = __esm({
1835
2030
  }
1836
2031
  });
1837
2032
 
2033
+ // src/rules/prefer-comparison-matcher.ts
2034
+ var isString, isComparingToString, invertedOperators, operatorMatcher, determineMatcher, prefer_comparison_matcher_default;
2035
+ var init_prefer_comparison_matcher = __esm({
2036
+ "src/rules/prefer-comparison-matcher.ts"() {
2037
+ "use strict";
2038
+ init_ast();
2039
+ init_parseExpectCall();
2040
+ isString = (node) => {
2041
+ return isStringLiteral(node) || node.type === "TemplateLiteral";
2042
+ };
2043
+ isComparingToString = (expression) => {
2044
+ return isString(expression.left) || isString(expression.right);
2045
+ };
2046
+ invertedOperators = {
2047
+ "<": ">=",
2048
+ "<=": ">",
2049
+ ">": "<=",
2050
+ ">=": "<"
2051
+ };
2052
+ operatorMatcher = {
2053
+ "<": "toBeLessThan",
2054
+ "<=": "toBeLessThanOrEqual",
2055
+ ">": "toBeGreaterThan",
2056
+ ">=": "toBeGreaterThanOrEqual"
2057
+ };
2058
+ determineMatcher = (operator, negated) => {
2059
+ const op = negated ? invertedOperators[operator] : operator;
2060
+ return operatorMatcher[op] ?? null;
2061
+ };
2062
+ prefer_comparison_matcher_default = {
2063
+ create(context) {
2064
+ return {
2065
+ CallExpression(node) {
2066
+ const expectCall = parseExpectCall(context, node);
2067
+ if (!expectCall || expectCall.args.length === 0)
2068
+ return;
2069
+ const { args, matcher } = expectCall;
2070
+ const [comparison] = node.arguments;
2071
+ const expectCallEnd = node.range[1];
2072
+ const [matcherArg] = args;
2073
+ if (comparison?.type !== "BinaryExpression" || isComparingToString(comparison) || !equalityMatchers.has(getStringValue(matcher)) || !isBooleanLiteral(matcherArg)) {
2074
+ return;
2075
+ }
2076
+ const hasNot = expectCall.modifiers.some(
2077
+ (node2) => getStringValue(node2) === "not"
2078
+ );
2079
+ const preferredMatcher = determineMatcher(
2080
+ comparison.operator,
2081
+ getRawValue(matcherArg) === hasNot.toString()
2082
+ );
2083
+ if (!preferredMatcher) {
2084
+ return;
2085
+ }
2086
+ context.report({
2087
+ data: { preferredMatcher },
2088
+ fix(fixer) {
2089
+ const [modifier] = expectCall.modifiers;
2090
+ const modifierText = modifier && getStringValue(modifier) !== "not" ? `.${getStringValue(modifier)}` : "";
2091
+ return [
2092
+ // Replace the comparison argument with the left-hand side of the comparison
2093
+ fixer.replaceText(
2094
+ comparison,
2095
+ context.sourceCode.getText(comparison.left)
2096
+ ),
2097
+ // Replace the current matcher & modifier with the preferred matcher
2098
+ fixer.replaceTextRange(
2099
+ [expectCallEnd, getParent(matcher).range[1]],
2100
+ `${modifierText}.${preferredMatcher}`
2101
+ ),
2102
+ // Replace the matcher argument with the right-hand side of the comparison
2103
+ fixer.replaceText(
2104
+ matcherArg,
2105
+ context.sourceCode.getText(comparison.right)
2106
+ )
2107
+ ];
2108
+ },
2109
+ messageId: "useToBeComparison",
2110
+ node: matcher
2111
+ });
2112
+ }
2113
+ };
2114
+ },
2115
+ meta: {
2116
+ docs: {
2117
+ category: "Best Practices",
2118
+ description: "Suggest using the built-in comparison matchers",
2119
+ recommended: false
2120
+ },
2121
+ fixable: "code",
2122
+ messages: {
2123
+ useToBeComparison: "Prefer using `{{ preferredMatcher }}` instead"
2124
+ },
2125
+ type: "suggestion"
2126
+ }
2127
+ };
2128
+ }
2129
+ });
2130
+
2131
+ // src/rules/prefer-equality-matcher.ts
2132
+ var prefer_equality_matcher_default;
2133
+ var init_prefer_equality_matcher = __esm({
2134
+ "src/rules/prefer-equality-matcher.ts"() {
2135
+ "use strict";
2136
+ init_ast();
2137
+ init_parseExpectCall();
2138
+ prefer_equality_matcher_default = {
2139
+ create(context) {
2140
+ return {
2141
+ CallExpression(node) {
2142
+ const expectCall = parseExpectCall(context, node);
2143
+ if (!expectCall || expectCall.args.length === 0)
2144
+ return;
2145
+ const { args, matcher } = expectCall;
2146
+ const [comparison] = node.arguments;
2147
+ const expectCallEnd = node.range[1];
2148
+ const [matcherArg] = args;
2149
+ if (comparison?.type !== "BinaryExpression" || comparison.operator !== "===" && comparison.operator !== "!==" || !equalityMatchers.has(getStringValue(matcher)) || !isBooleanLiteral(matcherArg)) {
2150
+ return;
2151
+ }
2152
+ const matcherValue = getRawValue(matcherArg) === "true";
2153
+ const [modifier] = expectCall.modifiers;
2154
+ const hasNot = expectCall.modifiers.some(
2155
+ (node2) => getStringValue(node2) === "not"
2156
+ );
2157
+ const addNotModifier = (comparison.operator === "!==" ? !matcherValue : matcherValue) === hasNot;
2158
+ context.report({
2159
+ messageId: "useEqualityMatcher",
2160
+ node: matcher,
2161
+ suggest: [...equalityMatchers.keys()].map((equalityMatcher) => ({
2162
+ data: { matcher: equalityMatcher },
2163
+ fix(fixer) {
2164
+ let modifierText = modifier && getStringValue(modifier) !== "not" ? `.${getStringValue(modifier)}` : "";
2165
+ if (addNotModifier) {
2166
+ modifierText += `.not`;
2167
+ }
2168
+ return [
2169
+ // replace the comparison argument with the left-hand side of the comparison
2170
+ fixer.replaceText(
2171
+ comparison,
2172
+ context.sourceCode.getText(comparison.left)
2173
+ ),
2174
+ // replace the current matcher & modifier with the preferred matcher
2175
+ fixer.replaceTextRange(
2176
+ [expectCallEnd, getParent(matcher).range[1]],
2177
+ `${modifierText}.${equalityMatcher}`
2178
+ ),
2179
+ // replace the matcher argument with the right-hand side of the comparison
2180
+ fixer.replaceText(
2181
+ matcherArg,
2182
+ context.sourceCode.getText(comparison.right)
2183
+ )
2184
+ ];
2185
+ },
2186
+ messageId: "suggestEqualityMatcher"
2187
+ }))
2188
+ });
2189
+ }
2190
+ };
2191
+ },
2192
+ meta: {
2193
+ docs: {
2194
+ category: "Best Practices",
2195
+ description: "Suggest using the built-in equality matchers",
2196
+ recommended: false,
2197
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-equality-matcher.md"
2198
+ },
2199
+ hasSuggestions: true,
2200
+ messages: {
2201
+ suggestEqualityMatcher: "Use `{{ matcher }}`",
2202
+ useEqualityMatcher: "Prefer using one of the equality matchers instead"
2203
+ },
2204
+ type: "suggestion"
2205
+ }
2206
+ };
2207
+ }
2208
+ });
2209
+
1838
2210
  // src/rules/prefer-hooks-in-order.ts
1839
2211
  var HooksOrder, prefer_hooks_in_order_default;
1840
2212
  var init_prefer_hooks_in_order = __esm({
@@ -2149,9 +2521,8 @@ var init_prefer_to_be = __esm({
2149
2521
  notModifier
2150
2522
  );
2151
2523
  }
2152
- const argumentMatchers = ["toBe", "toEqual", "toStrictEqual"];
2153
2524
  const firstArg = expectCall.args[0];
2154
- if (!argumentMatchers.includes(expectCall.matcherName) || !firstArg) {
2525
+ if (!equalityMatchers.has(expectCall.matcherName) || !firstArg) {
2155
2526
  return;
2156
2527
  }
2157
2528
  if (firstArg.type === "Literal" && firstArg.value === null) {
@@ -2193,13 +2564,12 @@ var init_prefer_to_be = __esm({
2193
2564
  });
2194
2565
 
2195
2566
  // src/rules/prefer-to-contain.ts
2196
- var matchers, isFixableIncludesCallExpression, prefer_to_contain_default;
2567
+ var isFixableIncludesCallExpression, prefer_to_contain_default;
2197
2568
  var init_prefer_to_contain = __esm({
2198
2569
  "src/rules/prefer-to-contain.ts"() {
2199
2570
  "use strict";
2200
2571
  init_ast();
2201
2572
  init_parseExpectCall();
2202
- matchers = /* @__PURE__ */ new Set(["toBe", "toEqual", "toStrictEqual"]);
2203
2573
  isFixableIncludesCallExpression = (node) => node.type === "CallExpression" && node.callee.type === "MemberExpression" && isPropertyAccessor(node.callee, "includes") && node.arguments.length === 1 && node.arguments[0].type !== "SpreadElement";
2204
2574
  prefer_to_contain_default = {
2205
2575
  create(context) {
@@ -2211,7 +2581,7 @@ var init_prefer_to_contain = __esm({
2211
2581
  const { args, matcher, matcherName } = expectCall;
2212
2582
  const [includesCall] = node.arguments;
2213
2583
  const [matcherArg] = args;
2214
- if (!includesCall || matcherArg.type === "SpreadElement" || !matchers.has(matcherName) || !isBooleanLiteral(matcherArg) || !isFixableIncludesCallExpression(includesCall)) {
2584
+ if (!includesCall || matcherArg.type === "SpreadElement" || !equalityMatchers.has(matcherName) || !isBooleanLiteral(matcherArg) || !isFixableIncludesCallExpression(includesCall)) {
2215
2585
  return;
2216
2586
  }
2217
2587
  const notModifier = expectCall.modifiers.find(
@@ -2271,20 +2641,19 @@ var init_prefer_to_contain = __esm({
2271
2641
  });
2272
2642
 
2273
2643
  // src/rules/prefer-to-have-count.ts
2274
- var matchers2, prefer_to_have_count_default;
2644
+ var prefer_to_have_count_default;
2275
2645
  var init_prefer_to_have_count = __esm({
2276
2646
  "src/rules/prefer-to-have-count.ts"() {
2277
2647
  "use strict";
2278
2648
  init_ast();
2279
2649
  init_fixer();
2280
2650
  init_parseExpectCall();
2281
- matchers2 = /* @__PURE__ */ new Set(["toBe", "toEqual", "toStrictEqual"]);
2282
2651
  prefer_to_have_count_default = {
2283
2652
  create(context) {
2284
2653
  return {
2285
2654
  CallExpression(node) {
2286
2655
  const expectCall = parseExpectCall(context, node);
2287
- if (!expectCall || !matchers2.has(expectCall.matcherName)) {
2656
+ if (!expectCall || !equalityMatchers.has(expectCall.matcherName)) {
2288
2657
  return;
2289
2658
  }
2290
2659
  const [argument] = node.arguments;
@@ -2336,20 +2705,19 @@ var init_prefer_to_have_count = __esm({
2336
2705
  });
2337
2706
 
2338
2707
  // src/rules/prefer-to-have-length.ts
2339
- var lengthMatchers, prefer_to_have_length_default;
2708
+ var prefer_to_have_length_default;
2340
2709
  var init_prefer_to_have_length = __esm({
2341
2710
  "src/rules/prefer-to-have-length.ts"() {
2342
2711
  "use strict";
2343
2712
  init_ast();
2344
2713
  init_fixer();
2345
2714
  init_parseExpectCall();
2346
- lengthMatchers = /* @__PURE__ */ new Set(["toBe", "toEqual", "toStrictEqual"]);
2347
2715
  prefer_to_have_length_default = {
2348
2716
  create(context) {
2349
2717
  return {
2350
2718
  CallExpression(node) {
2351
2719
  const expectCall = parseExpectCall(context, node);
2352
- if (!expectCall || !lengthMatchers.has(expectCall.matcherName)) {
2720
+ if (!expectCall || !equalityMatchers.has(expectCall.matcherName)) {
2353
2721
  return;
2354
2722
  }
2355
2723
  const [argument] = node.arguments;
@@ -2554,6 +2922,94 @@ var init_prefer_web_first_assertions = __esm({
2554
2922
  }
2555
2923
  });
2556
2924
 
2925
+ // src/rules/require-hook.ts
2926
+ var isNullOrUndefined, shouldBeInHook, require_hook_default;
2927
+ var init_require_hook = __esm({
2928
+ "src/rules/require-hook.ts"() {
2929
+ "use strict";
2930
+ init_ast();
2931
+ init_parseFnCall();
2932
+ isNullOrUndefined = (node) => {
2933
+ return node.type === "Literal" && node.value === null || isIdentifier(node, "undefined");
2934
+ };
2935
+ shouldBeInHook = (context, node, allowedFunctionCalls = []) => {
2936
+ switch (node.type) {
2937
+ case "ExpressionStatement":
2938
+ return shouldBeInHook(context, node.expression, allowedFunctionCalls);
2939
+ case "CallExpression":
2940
+ return !(parseFnCall(context, node) || allowedFunctionCalls.includes(getStringValue(node.callee)));
2941
+ case "VariableDeclaration": {
2942
+ if (node.kind === "const") {
2943
+ return false;
2944
+ }
2945
+ return node.declarations.some(
2946
+ ({ init }) => init != null && !isNullOrUndefined(init)
2947
+ );
2948
+ }
2949
+ default:
2950
+ return false;
2951
+ }
2952
+ };
2953
+ require_hook_default = {
2954
+ create(context) {
2955
+ const options = {
2956
+ allowedFunctionCalls: [],
2957
+ ...context.options?.[0] ?? {}
2958
+ };
2959
+ const checkBlockBody = (body) => {
2960
+ for (const statement of body) {
2961
+ if (shouldBeInHook(context, statement, options.allowedFunctionCalls)) {
2962
+ context.report({
2963
+ messageId: "useHook",
2964
+ node: statement
2965
+ });
2966
+ }
2967
+ }
2968
+ };
2969
+ return {
2970
+ CallExpression(node) {
2971
+ if (!isTypeOfFnCall(context, node, ["describe"]) || node.arguments.length < 2) {
2972
+ return;
2973
+ }
2974
+ const [, testFn] = node.arguments;
2975
+ if (!isFunction(testFn) || testFn.body.type !== "BlockStatement") {
2976
+ return;
2977
+ }
2978
+ checkBlockBody(testFn.body.body);
2979
+ },
2980
+ Program(program) {
2981
+ checkBlockBody(program.body);
2982
+ }
2983
+ };
2984
+ },
2985
+ meta: {
2986
+ docs: {
2987
+ category: "Best Practices",
2988
+ description: "Require setup and teardown code to be within a hook",
2989
+ recommended: false,
2990
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/require-hook.md"
2991
+ },
2992
+ messages: {
2993
+ useHook: "This should be done within a hook"
2994
+ },
2995
+ schema: [
2996
+ {
2997
+ additionalProperties: false,
2998
+ properties: {
2999
+ allowedFunctionCalls: {
3000
+ items: { type: "string" },
3001
+ type: "array"
3002
+ }
3003
+ },
3004
+ type: "object"
3005
+ }
3006
+ ],
3007
+ type: "suggestion"
3008
+ }
3009
+ };
3010
+ }
3011
+ });
3012
+
2557
3013
  // src/rules/require-soft-assertions.ts
2558
3014
  var require_soft_assertions_default;
2559
3015
  var init_require_soft_assertions = __esm({
@@ -2779,17 +3235,17 @@ var init_valid_title = __esm({
2779
3235
  const [matcher, message] = Array.isArray(matcherMaybeWithMessage) ? matcherMaybeWithMessage : [matcherMaybeWithMessage];
2780
3236
  return [new RegExp(matcher, "u"), message];
2781
3237
  };
2782
- compileMatcherPatterns = (matchers3) => {
2783
- if (typeof matchers3 === "string" || Array.isArray(matchers3)) {
2784
- const compiledMatcher = compileMatcherPattern(matchers3);
3238
+ compileMatcherPatterns = (matchers) => {
3239
+ if (typeof matchers === "string" || Array.isArray(matchers)) {
3240
+ const compiledMatcher = compileMatcherPattern(matchers);
2785
3241
  return {
2786
3242
  describe: compiledMatcher,
2787
3243
  test: compiledMatcher
2788
3244
  };
2789
3245
  }
2790
3246
  return {
2791
- describe: matchers3.describe ? compileMatcherPattern(matchers3.describe) : null,
2792
- test: matchers3.test ? compileMatcherPattern(matchers3.test) : null
3247
+ describe: matchers.describe ? compileMatcherPattern(matchers.describe) : null,
3248
+ test: matchers.test ? compileMatcherPattern(matchers.test) : null
2793
3249
  };
2794
3250
  };
2795
3251
  MatcherAndMessageSchema = {
@@ -2995,6 +3451,7 @@ var require_src = __commonJS({
2995
3451
  init_no_focused_test();
2996
3452
  init_no_force_option();
2997
3453
  init_no_get_by_title();
3454
+ init_no_hooks();
2998
3455
  init_no_nested_step();
2999
3456
  init_no_networkidle();
3000
3457
  init_no_nth_methods();
@@ -3008,6 +3465,8 @@ var require_src = __commonJS({
3008
3465
  init_no_useless_not();
3009
3466
  init_no_wait_for_selector();
3010
3467
  init_no_wait_for_timeout();
3468
+ init_prefer_comparison_matcher();
3469
+ init_prefer_equality_matcher();
3011
3470
  init_prefer_hooks_in_order();
3012
3471
  init_prefer_hooks_on_top();
3013
3472
  init_prefer_lowercase_title();
@@ -3017,6 +3476,7 @@ var require_src = __commonJS({
3017
3476
  init_prefer_to_have_count();
3018
3477
  init_prefer_to_have_length();
3019
3478
  init_prefer_web_first_assertions();
3479
+ init_require_hook();
3020
3480
  init_require_soft_assertions();
3021
3481
  init_require_top_level_describe();
3022
3482
  init_valid_expect();
@@ -3037,6 +3497,7 @@ var require_src = __commonJS({
3037
3497
  "no-focused-test": no_focused_test_default,
3038
3498
  "no-force-option": no_force_option_default,
3039
3499
  "no-get-by-title": no_get_by_title_default,
3500
+ "no-hooks": no_hooks_default,
3040
3501
  "no-nested-step": no_nested_step_default,
3041
3502
  "no-networkidle": no_networkidle_default,
3042
3503
  "no-nth-methods": no_nth_methods_default,
@@ -3050,6 +3511,8 @@ var require_src = __commonJS({
3050
3511
  "no-useless-not": no_useless_not_default,
3051
3512
  "no-wait-for-selector": no_wait_for_selector_default,
3052
3513
  "no-wait-for-timeout": no_wait_for_timeout_default,
3514
+ "prefer-comparison-matcher": prefer_comparison_matcher_default,
3515
+ "prefer-equality-matcher": prefer_equality_matcher_default,
3053
3516
  "prefer-hooks-in-order": prefer_hooks_in_order_default,
3054
3517
  "prefer-hooks-on-top": prefer_hooks_on_top_default,
3055
3518
  "prefer-lowercase-title": prefer_lowercase_title_default,
@@ -3059,6 +3522,7 @@ var require_src = __commonJS({
3059
3522
  "prefer-to-have-count": prefer_to_have_count_default,
3060
3523
  "prefer-to-have-length": prefer_to_have_length_default,
3061
3524
  "prefer-web-first-assertions": prefer_web_first_assertions_default,
3525
+ "require-hook": require_hook_default,
3062
3526
  "require-soft-assertions": require_soft_assertions_default,
3063
3527
  "require-top-level-describe": require_top_level_describe_default,
3064
3528
  "valid-expect": valid_expect_default,