eslint-plugin-playwright 1.3.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.
Files changed (3) hide show
  1. package/dist/index.js +139 -27
  2. package/dist/index.mjs +148 -28
  3. package/package.json +5 -7
package/dist/index.js CHANGED
@@ -35,7 +35,7 @@ function getRawValue(node) {
35
35
  return node.type === "Literal" ? node.raw : void 0;
36
36
  }
37
37
  function isIdentifier(node, name) {
38
- return node.type === "Identifier" && (typeof name === "string" ? node.name === name : name.test(node.name));
38
+ return node.type === "Identifier" && (!name || (typeof name === "string" ? node.name === name : name.test(node.name)));
39
39
  }
40
40
  function isLiteral(node, type, value) {
41
41
  return node.type === "Literal" && (value === void 0 ? typeof node.value === type : node.value === value);
@@ -48,8 +48,8 @@ function isStringLiteral(node, value) {
48
48
  function isBooleanLiteral(node, value) {
49
49
  return isLiteral(node, "boolean", value);
50
50
  }
51
- function isStringNode(node) {
52
- return node && (isStringLiteral(node) || isTemplateLiteral(node));
51
+ function isStringNode(node, value) {
52
+ return node && (isStringLiteral(node, value) || isTemplateLiteral(node, value));
53
53
  }
54
54
  function isPropertyAccessor(node, name) {
55
55
  return getStringValue(node.property) === name;
@@ -91,32 +91,15 @@ function findParent(node, type) {
91
91
  function isTestCall(context, node, modifiers) {
92
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]);
93
93
  }
94
- var testHooks = /* @__PURE__ */ new Set(["afterAll", "afterEach", "beforeAll", "beforeEach"]);
94
+ var testHooks = /* @__PURE__ */ new Set([
95
+ "afterAll",
96
+ "afterEach",
97
+ "beforeAll",
98
+ "beforeEach"
99
+ ]);
95
100
  function isTestHook(context, node) {
96
101
  return node.callee.type === "MemberExpression" && isTestIdentifier(context, node.callee.object) && testHooks.has(getStringValue(node.callee.property));
97
102
  }
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
- }
120
103
  var expectSubCommands = /* @__PURE__ */ new Set(["soft", "poll"]);
121
104
  function getExpectType(context, node) {
122
105
  const aliases = context.settings.playwright?.globalAliases?.expect ?? [];
@@ -888,6 +871,132 @@ var no_get_by_title_default = {
888
871
  }
889
872
  };
890
873
 
874
+ // src/utils/parseFnCall.ts
875
+ var VALID_CHAINS = /* @__PURE__ */ new Set([
876
+ // Hooks
877
+ "afterAll",
878
+ "afterEach",
879
+ "beforeAll",
880
+ "beforeEach",
881
+ "test.afterAll",
882
+ "test.afterEach",
883
+ "test.beforeAll",
884
+ "test.beforeEach",
885
+ // Describe
886
+ "describe",
887
+ "describe.only",
888
+ "describe.skip",
889
+ "describe.fixme",
890
+ "describe.configure",
891
+ "test.describe",
892
+ "test.describe.only",
893
+ "test.describe.skip",
894
+ "test.describe.fixme",
895
+ "test.describe.configure",
896
+ // Test
897
+ "test",
898
+ "test.fail",
899
+ "text.fixme",
900
+ "test.only",
901
+ "test.skip",
902
+ "test.slow",
903
+ "test.step",
904
+ "test.use"
905
+ ]);
906
+ var joinChains = (a, b) => a && b ? [...a, ...b] : null;
907
+ var isSupportedAccessor = (node, value) => isIdentifier(node, value) || isStringNode(node, value);
908
+ function getNodeChain(node) {
909
+ if (isSupportedAccessor(node)) {
910
+ return [node];
911
+ }
912
+ switch (node.type) {
913
+ case "TaggedTemplateExpression":
914
+ return getNodeChain(node.tag);
915
+ case "MemberExpression":
916
+ return joinChains(getNodeChain(node.object), getNodeChain(node.property));
917
+ case "CallExpression":
918
+ return getNodeChain(node.callee);
919
+ }
920
+ return null;
921
+ }
922
+ var resolvePossibleAliasedGlobal = (context, global) => {
923
+ const globalAliases = context.settings.playwright?.globalAliases ?? {};
924
+ const alias = Object.entries(globalAliases).find(
925
+ ([, aliases]) => aliases.includes(global)
926
+ );
927
+ return alias?.[0] ?? null;
928
+ };
929
+ var resolveToPlaywrightFn = (context, accessor) => {
930
+ const ident = getStringValue(accessor);
931
+ return {
932
+ local: ident,
933
+ original: resolvePossibleAliasedGlobal(context, ident),
934
+ type: "global"
935
+ };
936
+ };
937
+ function determinePlaywrightFnType(name) {
938
+ if (name === "expect")
939
+ return "expect";
940
+ if (name === "describe")
941
+ return "describe";
942
+ if (name === "test")
943
+ return "test";
944
+ if (testHooks.has(name))
945
+ return "hook";
946
+ return "unknown";
947
+ }
948
+ function parseFnCall(context, node) {
949
+ const chain = getNodeChain(node);
950
+ if (!chain?.length) {
951
+ return null;
952
+ }
953
+ const [first, ...rest] = chain;
954
+ const resolved = resolveToPlaywrightFn(context, first);
955
+ if (!resolved)
956
+ return null;
957
+ let name = resolved.original ?? resolved.local;
958
+ const links = [name, ...rest.map((link) => getStringValue(link))];
959
+ if (name !== "expect" && !VALID_CHAINS.has(links.join("."))) {
960
+ return null;
961
+ }
962
+ if (name === "test" && links.length > 1) {
963
+ const nextLinkName = links[1];
964
+ const nextLinkType = determinePlaywrightFnType(nextLinkName);
965
+ if (nextLinkType !== "unknown") {
966
+ name = nextLinkName;
967
+ }
968
+ }
969
+ const parsedFnCall = {
970
+ head: { ...resolved, node: first },
971
+ // every member node must have a member expression as their parent
972
+ // in order to be part of the call chain we're parsing
973
+ members: rest,
974
+ name
975
+ };
976
+ const type = determinePlaywrightFnType(name);
977
+ if (type === "expect") {
978
+ return {
979
+ ...parsedFnCall,
980
+ args: [],
981
+ matcher: rest[rest.length - 1],
982
+ modifiers: rest.slice(0, rest.length - 1),
983
+ type
984
+ };
985
+ }
986
+ if (chain.slice(0, chain.length - 1).some((n) => getParent(n)?.type !== "MemberExpression")) {
987
+ return null;
988
+ }
989
+ const parent = getParent(node);
990
+ if (parent?.type === "CallExpression" || parent?.type === "MemberExpression") {
991
+ return null;
992
+ }
993
+ return { ...parsedFnCall, type };
994
+ }
995
+ var isTypeOfFnCall = (context, node, types) => {
996
+ const call = parseFnCall(context, node);
997
+ return call !== null && types.includes(call.type);
998
+ };
999
+
891
1000
  // src/rules/no-hooks.ts
892
1001
  var no_hooks_default = {
893
1002
  create(context) {
@@ -1363,6 +1472,9 @@ var no_standalone_expect_default = {
1363
1472
  if (isTestCall(context, node)) {
1364
1473
  callStack.push("test");
1365
1474
  }
1475
+ if (isTestHook(context, node)) {
1476
+ callStack.push("hook");
1477
+ }
1366
1478
  if (node.callee.type === "TaggedTemplateExpression") {
1367
1479
  callStack.push("template");
1368
1480
  }
@@ -2556,7 +2668,7 @@ var require_hook_default = {
2556
2668
  };
2557
2669
  return {
2558
2670
  CallExpression(node) {
2559
- if (!isDescribeCall(node) || node.arguments.length < 2) {
2671
+ if (!isTypeOfFnCall(context, node, ["describe"]) || node.arguments.length < 2) {
2560
2672
  return;
2561
2673
  }
2562
2674
  const [, testFn] = node.arguments;
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;
@@ -66,28 +66,6 @@ function isTestCall(context, node, modifiers) {
66
66
  function isTestHook(context, node) {
67
67
  return node.callee.type === "MemberExpression" && isTestIdentifier(context, node.callee.object) && testHooks.has(getStringValue(node.callee.property));
68
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
- }
91
69
  function getExpectType(context, node) {
92
70
  const aliases = context.settings.playwright?.globalAliases?.expect ?? [];
93
71
  const expectNames = ["expect", ...aliases];
@@ -135,7 +113,12 @@ var init_ast = __esm({
135
113
  "skip",
136
114
  "fixme"
137
115
  ]);
138
- testHooks = /* @__PURE__ */ new Set(["afterAll", "afterEach", "beforeAll", "beforeEach"]);
116
+ testHooks = /* @__PURE__ */ new Set([
117
+ "afterAll",
118
+ "afterEach",
119
+ "beforeAll",
120
+ "beforeEach"
121
+ ]);
139
122
  expectSubCommands = /* @__PURE__ */ new Set(["soft", "poll"]);
140
123
  equalityMatchers = /* @__PURE__ */ new Set(["toBe", "toEqual", "toStrictEqual"]);
141
124
  }
@@ -966,12 +949,145 @@ var init_no_get_by_title = __esm({
966
949
  }
967
950
  });
968
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
+
969
1085
  // src/rules/no-hooks.ts
970
1086
  var no_hooks_default;
971
1087
  var init_no_hooks = __esm({
972
1088
  "src/rules/no-hooks.ts"() {
973
1089
  "use strict";
974
- init_ast();
1090
+ init_parseFnCall();
975
1091
  no_hooks_default = {
976
1092
  create(context) {
977
1093
  const options = {
@@ -1510,6 +1626,9 @@ var init_no_standalone_expect = __esm({
1510
1626
  if (isTestCall(context, node)) {
1511
1627
  callStack.push("test");
1512
1628
  }
1629
+ if (isTestHook(context, node)) {
1630
+ callStack.push("hook");
1631
+ }
1513
1632
  if (node.callee.type === "TaggedTemplateExpression") {
1514
1633
  callStack.push("template");
1515
1634
  }
@@ -2809,6 +2928,7 @@ var init_require_hook = __esm({
2809
2928
  "src/rules/require-hook.ts"() {
2810
2929
  "use strict";
2811
2930
  init_ast();
2931
+ init_parseFnCall();
2812
2932
  isNullOrUndefined = (node) => {
2813
2933
  return node.type === "Literal" && node.value === null || isIdentifier(node, "undefined");
2814
2934
  };
@@ -2848,7 +2968,7 @@ var init_require_hook = __esm({
2848
2968
  };
2849
2969
  return {
2850
2970
  CallExpression(node) {
2851
- if (!isDescribeCall(node) || node.arguments.length < 2) {
2971
+ if (!isTypeOfFnCall(context, node, ["describe"]) || node.arguments.length < 2) {
2852
2972
  return;
2853
2973
  }
2854
2974
  const [, testFn] = node.arguments;
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.3.0",
4
+ "version": "1.3.1",
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",
@@ -34,12 +34,11 @@
34
34
  "lint": "eslint .",
35
35
  "format": "prettier --write .",
36
36
  "format:check": "prettier --check .",
37
- "test": "jest",
38
- "test:watch": "jest --watch --no-verbose",
37
+ "test": "vitest",
38
+ "test:watch": "vitest --reporter=dot",
39
39
  "ts": "tsc --noEmit"
40
40
  },
41
41
  "devDependencies": {
42
- "@jest/globals": "^29.7.0",
43
42
  "@mskelton/eslint-config": "^8.4.0",
44
43
  "@mskelton/semantic-release-config": "^1.0.1",
45
44
  "@types/eslint": "^8.44.3",
@@ -50,13 +49,12 @@
50
49
  "dedent": "^1.5.1",
51
50
  "eslint": "^8.50.0",
52
51
  "eslint-plugin-sort": "^2.10.0",
53
- "jest": "^29.7.0",
54
52
  "prettier": "^3.0.3",
55
53
  "prettier-plugin-jsdoc": "^1.3.0",
56
54
  "semantic-release": "^23.0.2",
57
- "ts-jest": "^29.1.1",
58
55
  "tsup": "^8.0.1",
59
- "typescript": "^5.2.2"
56
+ "typescript": "^5.2.2",
57
+ "vitest": "^1.3.1"
60
58
  },
61
59
  "peerDependencies": {
62
60
  "eslint": ">=8.40.0",