eslint-plugin-playwright 1.1.2 → 1.2.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,9 +61,7 @@ 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));
@@ -186,6 +187,76 @@ var init_expect_expect = __esm({
186
187
  }
187
188
  });
188
189
 
190
+ // src/rules/max-expects.ts
191
+ var max_expects_default;
192
+ var init_max_expects = __esm({
193
+ "src/rules/max-expects.ts"() {
194
+ "use strict";
195
+ init_ast();
196
+ max_expects_default = {
197
+ create(context) {
198
+ const options = {
199
+ max: 5,
200
+ ...context.options?.[0] ?? {}
201
+ };
202
+ let count = 0;
203
+ const maybeResetCount = (node) => {
204
+ const parent = getParent(node);
205
+ const isTestFn = parent?.type !== "CallExpression" || isTestCall(context, parent);
206
+ if (isTestFn) {
207
+ count = 0;
208
+ }
209
+ };
210
+ return {
211
+ ArrowFunctionExpression: maybeResetCount,
212
+ "ArrowFunctionExpression:exit": maybeResetCount,
213
+ CallExpression(node) {
214
+ if (!getExpectType(context, node))
215
+ return;
216
+ count += 1;
217
+ if (count > options.max) {
218
+ context.report({
219
+ data: {
220
+ count: count.toString(),
221
+ max: options.max.toString()
222
+ },
223
+ messageId: "exceededMaxAssertion",
224
+ node
225
+ });
226
+ }
227
+ },
228
+ FunctionExpression: maybeResetCount,
229
+ "FunctionExpression:exit": maybeResetCount
230
+ };
231
+ },
232
+ meta: {
233
+ docs: {
234
+ category: "Best Practices",
235
+ description: "Enforces a maximum number assertion calls in a test body",
236
+ recommended: false,
237
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/max-expects.md"
238
+ },
239
+ messages: {
240
+ exceededMaxAssertion: "Too many assertion calls ({{ count }}) - maximum allowed is {{ max }}"
241
+ },
242
+ schema: [
243
+ {
244
+ additionalProperties: false,
245
+ properties: {
246
+ max: {
247
+ minimum: 1,
248
+ type: "integer"
249
+ }
250
+ },
251
+ type: "object"
252
+ }
253
+ ],
254
+ type: "suggestion"
255
+ }
256
+ };
257
+ }
258
+ });
259
+
189
260
  // src/rules/max-nested-describe.ts
190
261
  var max_nested_describe_default;
191
262
  var init_max_nested_describe = __esm({
@@ -407,6 +478,146 @@ var init_missing_playwright_await = __esm({
407
478
  }
408
479
  });
409
480
 
481
+ // src/rules/no-commented-out-tests.ts
482
+ function hasTests(context, node) {
483
+ const testNames = getTestNames(context);
484
+ const names = testNames.join("|");
485
+ const regex = new RegExp(
486
+ `^\\s*(${names}|describe)(\\.\\w+|\\[['"]\\w+['"]\\])?\\s*\\(`,
487
+ "mu"
488
+ );
489
+ return regex.test(node.value);
490
+ }
491
+ var no_commented_out_tests_default;
492
+ var init_no_commented_out_tests = __esm({
493
+ "src/rules/no-commented-out-tests.ts"() {
494
+ "use strict";
495
+ init_ast();
496
+ no_commented_out_tests_default = {
497
+ create(context) {
498
+ function checkNode(node) {
499
+ if (!hasTests(context, node))
500
+ return;
501
+ context.report({
502
+ messageId: "commentedTests",
503
+ node
504
+ });
505
+ }
506
+ return {
507
+ Program() {
508
+ context.sourceCode.getAllComments().forEach(checkNode);
509
+ }
510
+ };
511
+ },
512
+ meta: {
513
+ docs: {
514
+ category: "Best Practices",
515
+ description: "Disallow commented out tests",
516
+ recommended: true,
517
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-commented-out-tests.md"
518
+ },
519
+ messages: {
520
+ commentedTests: "Some tests seem to be commented"
521
+ },
522
+ type: "problem"
523
+ }
524
+ };
525
+ }
526
+ });
527
+
528
+ // src/rules/no-conditional-expect.ts
529
+ var isCatchCall, getTestCallExpressionsFromDeclaredVariables, no_conditional_expect_default;
530
+ var init_no_conditional_expect = __esm({
531
+ "src/rules/no-conditional-expect.ts"() {
532
+ "use strict";
533
+ init_ast();
534
+ isCatchCall = (node) => node.callee.type === "MemberExpression" && isPropertyAccessor(node.callee, "catch");
535
+ getTestCallExpressionsFromDeclaredVariables = (context, declaredVariables) => {
536
+ return declaredVariables.reduce(
537
+ (acc, { references }) => [
538
+ ...acc,
539
+ ...references.map(({ identifier }) => getParent(identifier)).filter(
540
+ (node) => node?.type === "CallExpression" && isTestCall(context, node)
541
+ )
542
+ ],
543
+ []
544
+ );
545
+ };
546
+ no_conditional_expect_default = {
547
+ create(context) {
548
+ let conditionalDepth = 0;
549
+ let inTestCase = false;
550
+ let inPromiseCatch = false;
551
+ const increaseConditionalDepth = () => inTestCase && conditionalDepth++;
552
+ const decreaseConditionalDepth = () => inTestCase && conditionalDepth--;
553
+ return {
554
+ CallExpression(node) {
555
+ if (isTestCall(context, node)) {
556
+ inTestCase = true;
557
+ }
558
+ if (isCatchCall(node)) {
559
+ inPromiseCatch = true;
560
+ }
561
+ const expectType = getExpectType(context, node);
562
+ if (inTestCase && expectType && conditionalDepth > 0) {
563
+ context.report({
564
+ messageId: "conditionalExpect",
565
+ node
566
+ });
567
+ }
568
+ if (inPromiseCatch && expectType) {
569
+ context.report({
570
+ messageId: "conditionalExpect",
571
+ node
572
+ });
573
+ }
574
+ },
575
+ "CallExpression:exit"(node) {
576
+ if (isTestCall(context, node)) {
577
+ inTestCase = false;
578
+ }
579
+ if (isCatchCall(node)) {
580
+ inPromiseCatch = false;
581
+ }
582
+ },
583
+ CatchClause: increaseConditionalDepth,
584
+ "CatchClause:exit": decreaseConditionalDepth,
585
+ ConditionalExpression: increaseConditionalDepth,
586
+ "ConditionalExpression:exit": decreaseConditionalDepth,
587
+ FunctionDeclaration(node) {
588
+ const declaredVariables = context.sourceCode.getDeclaredVariables(node);
589
+ const testCallExpressions = getTestCallExpressionsFromDeclaredVariables(
590
+ context,
591
+ declaredVariables
592
+ );
593
+ if (testCallExpressions.length > 0) {
594
+ inTestCase = true;
595
+ }
596
+ },
597
+ IfStatement: increaseConditionalDepth,
598
+ "IfStatement:exit": decreaseConditionalDepth,
599
+ LogicalExpression: increaseConditionalDepth,
600
+ "LogicalExpression:exit": decreaseConditionalDepth,
601
+ SwitchStatement: increaseConditionalDepth,
602
+ "SwitchStatement:exit": decreaseConditionalDepth
603
+ };
604
+ },
605
+ meta: {
606
+ docs: {
607
+ category: "Best Practices",
608
+ description: "Disallow calling `expect` conditionally",
609
+ recommended: true,
610
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-conditional-expect.md"
611
+ },
612
+ messages: {
613
+ conditionalExpect: "Avoid calling `expect` conditionally`"
614
+ },
615
+ type: "problem"
616
+ }
617
+ };
618
+ }
619
+ });
620
+
410
621
  // src/rules/no-conditional-in-test.ts
411
622
  var no_conditional_in_test_default;
412
623
  var init_no_conditional_in_test = __esm({
@@ -445,6 +656,58 @@ var init_no_conditional_in_test = __esm({
445
656
  }
446
657
  });
447
658
 
659
+ // src/rules/no-duplicate-hooks.ts
660
+ var no_duplicate_hooks_default;
661
+ var init_no_duplicate_hooks = __esm({
662
+ "src/rules/no-duplicate-hooks.ts"() {
663
+ "use strict";
664
+ init_ast();
665
+ no_duplicate_hooks_default = {
666
+ create(context) {
667
+ const hookContexts = [{}];
668
+ return {
669
+ CallExpression(node) {
670
+ if (isDescribeCall(node)) {
671
+ hookContexts.push({});
672
+ }
673
+ if (!isTestHook(context, node)) {
674
+ return;
675
+ }
676
+ const currentLayer = hookContexts[hookContexts.length - 1];
677
+ const name = node.callee.type === "MemberExpression" ? getStringValue(node.callee.property) : "";
678
+ currentLayer[name] || (currentLayer[name] = 0);
679
+ currentLayer[name] += 1;
680
+ if (currentLayer[name] > 1) {
681
+ context.report({
682
+ data: { hook: name },
683
+ messageId: "noDuplicateHook",
684
+ node
685
+ });
686
+ }
687
+ },
688
+ "CallExpression:exit"(node) {
689
+ if (isDescribeCall(node)) {
690
+ hookContexts.pop();
691
+ }
692
+ }
693
+ };
694
+ },
695
+ meta: {
696
+ docs: {
697
+ category: "Best Practices",
698
+ description: "Disallow duplicate setup and teardown hooks",
699
+ recommended: false,
700
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-duplicate-hooks.md"
701
+ },
702
+ messages: {
703
+ noDuplicateHook: "Duplicate {{ hook }} in describe block"
704
+ },
705
+ type: "suggestion"
706
+ }
707
+ };
708
+ }
709
+ });
710
+
448
711
  // src/rules/no-element-handle.ts
449
712
  function getPropertyRange(node) {
450
713
  return node.type === "Identifier" ? node.range : [node.range[0] + 1, node.range[1] - 1];
@@ -1104,6 +1367,102 @@ var init_no_skipped_test = __esm({
1104
1367
  }
1105
1368
  });
1106
1369
 
1370
+ // src/rules/no-standalone-expect.ts
1371
+ var getBlockType, no_standalone_expect_default;
1372
+ var init_no_standalone_expect = __esm({
1373
+ "src/rules/no-standalone-expect.ts"() {
1374
+ "use strict";
1375
+ init_ast();
1376
+ getBlockType = (statement) => {
1377
+ const func = getParent(statement);
1378
+ if (!func) {
1379
+ throw new Error(
1380
+ `Unexpected BlockStatement. No parent defined. - please file a github issue at https://github.com/jest-community/eslint-plugin-jest`
1381
+ );
1382
+ }
1383
+ if (func.type === "FunctionDeclaration") {
1384
+ return "function";
1385
+ }
1386
+ if (isFunction(func) && func.parent) {
1387
+ const expr = func.parent;
1388
+ if (expr.type === "VariableDeclarator") {
1389
+ return "function";
1390
+ }
1391
+ if (expr.type === "CallExpression" && isDescribeCall(expr)) {
1392
+ return "describe";
1393
+ }
1394
+ }
1395
+ return null;
1396
+ };
1397
+ no_standalone_expect_default = {
1398
+ create(context) {
1399
+ const callStack = [];
1400
+ return {
1401
+ ArrowFunctionExpression(node) {
1402
+ if (node.parent?.type !== "CallExpression") {
1403
+ callStack.push("arrow");
1404
+ }
1405
+ },
1406
+ "ArrowFunctionExpression:exit"() {
1407
+ if (callStack[callStack.length - 1] === "arrow") {
1408
+ callStack.pop();
1409
+ }
1410
+ },
1411
+ BlockStatement(statement) {
1412
+ const blockType = getBlockType(statement);
1413
+ if (blockType) {
1414
+ callStack.push(blockType);
1415
+ }
1416
+ },
1417
+ "BlockStatement:exit"(statement) {
1418
+ if (callStack[callStack.length - 1] === getBlockType(statement)) {
1419
+ callStack.pop();
1420
+ }
1421
+ },
1422
+ CallExpression(node) {
1423
+ if (getExpectType(context, node)) {
1424
+ const parent = callStack.at(-1);
1425
+ if (!parent || parent === "describe") {
1426
+ const root = findParent(node, "CallExpression");
1427
+ context.report({
1428
+ messageId: "unexpectedExpect",
1429
+ node: root ?? node
1430
+ });
1431
+ }
1432
+ return;
1433
+ }
1434
+ if (isTestCall(context, node)) {
1435
+ callStack.push("test");
1436
+ }
1437
+ if (node.callee.type === "TaggedTemplateExpression") {
1438
+ callStack.push("template");
1439
+ }
1440
+ },
1441
+ "CallExpression:exit"(node) {
1442
+ const top = callStack[callStack.length - 1];
1443
+ if (top === "test" && isTestCall(context, node) && node.callee.type !== "MemberExpression" || top === "template" && node.callee.type === "TaggedTemplateExpression") {
1444
+ callStack.pop();
1445
+ }
1446
+ }
1447
+ };
1448
+ },
1449
+ meta: {
1450
+ docs: {
1451
+ category: "Best Practices",
1452
+ description: "Disallow using `expect` outside of `test` blocks",
1453
+ recommended: false,
1454
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-to-contain.md"
1455
+ },
1456
+ fixable: "code",
1457
+ messages: {
1458
+ unexpectedExpect: "Expect must be inside of a test block"
1459
+ },
1460
+ type: "suggestion"
1461
+ }
1462
+ };
1463
+ }
1464
+ });
1465
+
1107
1466
  // src/utils/misc.ts
1108
1467
  var getAmountData, truthy;
1109
1468
  var init_misc = __esm({
@@ -1476,6 +1835,111 @@ var init_no_wait_for_timeout = __esm({
1476
1835
  }
1477
1836
  });
1478
1837
 
1838
+ // src/rules/prefer-hooks-in-order.ts
1839
+ var HooksOrder, prefer_hooks_in_order_default;
1840
+ var init_prefer_hooks_in_order = __esm({
1841
+ "src/rules/prefer-hooks-in-order.ts"() {
1842
+ "use strict";
1843
+ init_ast();
1844
+ HooksOrder = ["beforeAll", "beforeEach", "afterEach", "afterAll"];
1845
+ prefer_hooks_in_order_default = {
1846
+ create(context) {
1847
+ let previousHookIndex = -1;
1848
+ let inHook = false;
1849
+ return {
1850
+ CallExpression(node) {
1851
+ if (inHook)
1852
+ return;
1853
+ if (!isTestHook(context, node)) {
1854
+ previousHookIndex = -1;
1855
+ return;
1856
+ }
1857
+ inHook = true;
1858
+ const currentHook = node.callee.type === "MemberExpression" ? getStringValue(node.callee.property) : "";
1859
+ const currentHookIndex = HooksOrder.indexOf(currentHook);
1860
+ if (currentHookIndex < previousHookIndex) {
1861
+ return context.report({
1862
+ data: {
1863
+ currentHook,
1864
+ previousHook: HooksOrder[previousHookIndex]
1865
+ },
1866
+ messageId: "reorderHooks",
1867
+ node
1868
+ });
1869
+ }
1870
+ previousHookIndex = currentHookIndex;
1871
+ },
1872
+ "CallExpression:exit"(node) {
1873
+ if (isTestHook(context, node)) {
1874
+ inHook = false;
1875
+ return;
1876
+ }
1877
+ if (inHook) {
1878
+ return;
1879
+ }
1880
+ previousHookIndex = -1;
1881
+ }
1882
+ };
1883
+ },
1884
+ meta: {
1885
+ docs: {
1886
+ category: "Best Practices",
1887
+ description: "Prefer having hooks in a consistent order",
1888
+ recommended: false,
1889
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-hooks-in-order.md"
1890
+ },
1891
+ messages: {
1892
+ reorderHooks: "`{{ currentHook }}` hooks should be before any `{{ previousHook }}` hooks"
1893
+ },
1894
+ type: "suggestion"
1895
+ }
1896
+ };
1897
+ }
1898
+ });
1899
+
1900
+ // src/rules/prefer-hooks-on-top.ts
1901
+ var prefer_hooks_on_top_default;
1902
+ var init_prefer_hooks_on_top = __esm({
1903
+ "src/rules/prefer-hooks-on-top.ts"() {
1904
+ "use strict";
1905
+ init_ast();
1906
+ prefer_hooks_on_top_default = {
1907
+ create(context) {
1908
+ const stack = [false];
1909
+ return {
1910
+ CallExpression(node) {
1911
+ if (isTestCall(context, node)) {
1912
+ stack[stack.length - 1] = true;
1913
+ }
1914
+ if (stack.at(-1) && isTestHook(context, node)) {
1915
+ context.report({
1916
+ messageId: "noHookOnTop",
1917
+ node
1918
+ });
1919
+ }
1920
+ stack.push(false);
1921
+ },
1922
+ "CallExpression:exit"() {
1923
+ stack.pop();
1924
+ }
1925
+ };
1926
+ },
1927
+ meta: {
1928
+ docs: {
1929
+ category: "Best Practices",
1930
+ description: "Suggest having hooks before any test cases",
1931
+ recommended: false,
1932
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-hooks-on-top.md"
1933
+ },
1934
+ messages: {
1935
+ noHookOnTop: "Hooks should come before test cases"
1936
+ },
1937
+ type: "suggestion"
1938
+ }
1939
+ };
1940
+ }
1941
+ });
1942
+
1479
1943
  // src/rules/prefer-lowercase-title.ts
1480
1944
  var prefer_lowercase_title_default;
1481
1945
  var init_prefer_lowercase_title = __esm({
@@ -2519,9 +2983,13 @@ import globals from "globals";
2519
2983
  var require_src = __commonJS({
2520
2984
  "src/index.ts"(exports, module) {
2521
2985
  init_expect_expect();
2986
+ init_max_expects();
2522
2987
  init_max_nested_describe();
2523
2988
  init_missing_playwright_await();
2989
+ init_no_commented_out_tests();
2990
+ init_no_conditional_expect();
2524
2991
  init_no_conditional_in_test();
2992
+ init_no_duplicate_hooks();
2525
2993
  init_no_element_handle();
2526
2994
  init_no_eval();
2527
2995
  init_no_focused_test();
@@ -2534,11 +3002,14 @@ var require_src = __commonJS({
2534
3002
  init_no_raw_locators();
2535
3003
  init_no_restricted_matchers();
2536
3004
  init_no_skipped_test();
3005
+ init_no_standalone_expect();
2537
3006
  init_no_unsafe_references();
2538
3007
  init_no_useless_await();
2539
3008
  init_no_useless_not();
2540
3009
  init_no_wait_for_selector();
2541
3010
  init_no_wait_for_timeout();
3011
+ init_prefer_hooks_in_order();
3012
+ init_prefer_hooks_on_top();
2542
3013
  init_prefer_lowercase_title();
2543
3014
  init_prefer_strict_equal();
2544
3015
  init_prefer_to_be();
@@ -2554,9 +3025,13 @@ var require_src = __commonJS({
2554
3025
  configs: {},
2555
3026
  rules: {
2556
3027
  "expect-expect": expect_expect_default,
3028
+ "max-expects": max_expects_default,
2557
3029
  "max-nested-describe": max_nested_describe_default,
2558
3030
  "missing-playwright-await": missing_playwright_await_default,
3031
+ "no-commented-out-tests": no_commented_out_tests_default,
3032
+ "no-conditional-expect": no_conditional_expect_default,
2559
3033
  "no-conditional-in-test": no_conditional_in_test_default,
3034
+ "no-duplicate-hooks": no_duplicate_hooks_default,
2560
3035
  "no-element-handle": no_element_handle_default,
2561
3036
  "no-eval": no_eval_default,
2562
3037
  "no-focused-test": no_focused_test_default,
@@ -2569,11 +3044,14 @@ var require_src = __commonJS({
2569
3044
  "no-raw-locators": no_raw_locators_default,
2570
3045
  "no-restricted-matchers": no_restricted_matchers_default,
2571
3046
  "no-skipped-test": no_skipped_test_default,
3047
+ "no-standalone-expect": no_standalone_expect_default,
2572
3048
  "no-unsafe-references": no_unsafe_references_default,
2573
3049
  "no-useless-await": no_useless_await_default,
2574
3050
  "no-useless-not": no_useless_not_default,
2575
3051
  "no-wait-for-selector": no_wait_for_selector_default,
2576
3052
  "no-wait-for-timeout": no_wait_for_timeout_default,
3053
+ "prefer-hooks-in-order": prefer_hooks_in_order_default,
3054
+ "prefer-hooks-on-top": prefer_hooks_on_top_default,
2577
3055
  "prefer-lowercase-title": prefer_lowercase_title_default,
2578
3056
  "prefer-strict-equal": prefer_strict_equal_default,
2579
3057
  "prefer-to-be": prefer_to_be_default,
@@ -2593,6 +3071,7 @@ var require_src = __commonJS({
2593
3071
  "playwright/expect-expect": "warn",
2594
3072
  "playwright/max-nested-describe": "warn",
2595
3073
  "playwright/missing-playwright-await": "error",
3074
+ "playwright/no-conditional-expect": "warn",
2596
3075
  "playwright/no-conditional-in-test": "warn",
2597
3076
  "playwright/no-element-handle": "warn",
2598
3077
  "playwright/no-eval": "warn",
@@ -2602,6 +3081,7 @@ var require_src = __commonJS({
2602
3081
  "playwright/no-networkidle": "error",
2603
3082
  "playwright/no-page-pause": "warn",
2604
3083
  "playwright/no-skipped-test": "warn",
3084
+ "playwright/no-standalone-expect": "error",
2605
3085
  "playwright/no-unsafe-references": "error",
2606
3086
  "playwright/no-useless-await": "warn",
2607
3087
  "playwright/no-useless-not": "warn",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "eslint-plugin-playwright",
3
3
  "description": "ESLint plugin for Playwright testing.",
4
- "version": "1.1.2",
4
+ "version": "1.2.0",
5
5
  "repository": "https://github.com/playwright-community/eslint-plugin-playwright",
6
6
  "author": "Mark Skelton <mark@mskelton.dev>",
7
7
  "packageManager": "pnpm@8.12.0",