eslint-plugin-playwright 0.22.2 → 1.0.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/README.md CHANGED
@@ -91,45 +91,19 @@ export default [
91
91
  }
92
92
  ```
93
93
 
94
- ## Global Settings
94
+ ### Aliased Playwright Globals
95
95
 
96
- The plugin reads global settings from your ESLint configuration's shared data
97
- under the `playwright` key. It supports the following settings:
98
-
99
- - `additionalAssertFunctionNames`: an array of function names to treat as
100
- assertion functions for the case of rules like `expect-expect`, which enforces
101
- the presence of at least one assertion per test case. This allows such rules
102
- to recognise custom assertion functions as valid assertions. The global
103
- setting applies to all modules. The
104
- [`expect-expect` rule accepts an option by the same name](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/expect-expect.md#additionalassertfunctionnames)
105
- to enable per-module configuration (.e.g, for module-specific custom assert
106
- functions).
107
-
108
- You can configure these settings like so:
109
-
110
- [Flat config](https://eslint.org/docs/latest/use/configure/configuration-files-new)
111
- (**eslint.config.js**)
112
-
113
- ```javascript
114
- export default [
115
- {
116
- settings: {
117
- playwright: {
118
- additionalAssertFunctionNames: ['assertCustomCondition'],
119
- },
120
- },
121
- },
122
- ];
123
- ```
124
-
125
- [Legacy config](https://eslint.org/docs/latest/use/configure/configuration-files)
126
- (**.eslintrc**)
96
+ If you import Playwright globals (e.g. `test`, `expect`) with a custom name, you
97
+ can configure this plugin to be aware of these additional names.
127
98
 
128
99
  ```json
129
100
  {
130
101
  "settings": {
131
102
  "playwright": {
132
- "additionalAssertFunctionNames": ["assertCustomCondition"]
103
+ "globalAliases": {
104
+ "test": ["myTest"],
105
+ "expect": ["myExpect"]
106
+ }
133
107
  }
134
108
  }
135
109
  }
@@ -150,20 +124,21 @@ command line option.\
150
124
  | ✔ | 🔧 | | [missing-playwright-await](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/missing-playwright-await.md) | Enforce Playwright APIs to be awaited |
151
125
  | ✔ | | | [no-conditional-in-test](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-conditional-in-test.md) | Disallow conditional logic in tests |
152
126
  | ✔ | | 💡 | [no-element-handle](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-element-handle.md) | Disallow usage of element handles |
153
- | ✔ | | | [no-eval](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-eval.md) | Disallow usage of `page.$eval` and `page.$$eval` |
127
+ | ✔ | | | [no-eval](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-eval.md) | Disallow usage of `page.$eval()` and `page.$$eval()` |
154
128
  | ✔ | | 💡 | [no-focused-test](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-focused-test.md) | Disallow usage of `.only` annotation |
155
129
  | ✔ | | | [no-force-option](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-force-option.md) | Disallow usage of the `{ force: true }` option |
156
130
  | ✔ | | | [no-nested-step](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-nested-step.md) | Disallow nested `test.step()` methods |
157
131
  | ✔ | | | [no-networkidle](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-networkidle.md) | Disallow usage of the `networkidle` option |
158
132
  | | | | [no-nth-methods](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-nth-methods.md) | Disallow usage of `first()`, `last()`, and `nth()` methods |
159
- | ✔ | | | [no-page-pause](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-page-pause.md) | Disallow using `page.pause` |
133
+ | ✔ | | | [no-page-pause](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-page-pause.md) | Disallow using `page.pause()` |
134
+ | | 🔧 | | [no-get-by-title](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-get-by-title.md) | Disallow using `getByTitle()` |
160
135
  | | | | [no-raw-locators](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-raw-locators.md) | Disallow using raw locators |
161
136
  | ✔ | 🔧 | | [no-useless-await](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-useless-await.md) | Disallow unnecessary `await`s for Playwright methods |
162
137
  | | | | [no-restricted-matchers](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-restricted-matchers.md) | Disallow specific matchers & modifiers |
163
138
  | ✔ | | 💡 | [no-skipped-test](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-skipped-test.md) | Disallow usage of the `.skip` annotation |
164
139
  | ✔ | 🔧 | | [no-useless-not](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-useless-not.md) | Disallow usage of `not` matchers when a specific matcher exists |
165
- | ✔ | | 💡 | [no-wait-for-selector](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-wait-for-selector.md) | Disallow usage of `page.waitForSelector` |
166
- | ✔ | | 💡 | [no-wait-for-timeout](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-wait-for-timeout.md) | Disallow usage of `page.waitForTimeout` |
140
+ | ✔ | | 💡 | [no-wait-for-selector](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-wait-for-selector.md) | Disallow usage of `page.waitForSelector()` |
141
+ | ✔ | | 💡 | [no-wait-for-timeout](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-wait-for-timeout.md) | Disallow usage of `page.waitForTimeout()` |
167
142
  | | | 💡 | [prefer-strict-equal](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-strict-equal.md) | Suggest using `toStrictEqual()` |
168
143
  | | 🔧 | | [prefer-lowercase-title](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-lowercase-title.md) | Enforce lowercase test names |
169
144
  | | 🔧 | | [prefer-to-be](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-to-be.md) | Suggest using `toBe()` |
package/dist/index.d.mts CHANGED
@@ -18,6 +18,7 @@ declare const _default: {
18
18
  'no-eval': eslint.Rule.RuleModule;
19
19
  'no-focused-test': eslint.Rule.RuleModule;
20
20
  'no-force-option': eslint.Rule.RuleModule;
21
+ 'no-get-by-title': eslint.Rule.RuleModule;
21
22
  'no-nested-step': eslint.Rule.RuleModule;
22
23
  'no-networkidle': eslint.Rule.RuleModule;
23
24
  'no-nth-methods': eslint.Rule.RuleModule;
@@ -67,6 +68,7 @@ declare const _default: {
67
68
  'no-eval': eslint.Rule.RuleModule;
68
69
  'no-focused-test': eslint.Rule.RuleModule;
69
70
  'no-force-option': eslint.Rule.RuleModule;
71
+ 'no-get-by-title': eslint.Rule.RuleModule;
70
72
  'no-nested-step': eslint.Rule.RuleModule;
71
73
  'no-networkidle': eslint.Rule.RuleModule;
72
74
  'no-nth-methods': eslint.Rule.RuleModule;
@@ -203,6 +205,7 @@ declare const _default: {
203
205
  'no-eval': eslint.Rule.RuleModule;
204
206
  'no-focused-test': eslint.Rule.RuleModule;
205
207
  'no-force-option': eslint.Rule.RuleModule;
208
+ 'no-get-by-title': eslint.Rule.RuleModule;
206
209
  'no-nested-step': eslint.Rule.RuleModule;
207
210
  'no-networkidle': eslint.Rule.RuleModule;
208
211
  'no-nth-methods': eslint.Rule.RuleModule;
package/dist/index.d.ts CHANGED
@@ -18,6 +18,7 @@ declare const _default: {
18
18
  'no-eval': eslint.Rule.RuleModule;
19
19
  'no-focused-test': eslint.Rule.RuleModule;
20
20
  'no-force-option': eslint.Rule.RuleModule;
21
+ 'no-get-by-title': eslint.Rule.RuleModule;
21
22
  'no-nested-step': eslint.Rule.RuleModule;
22
23
  'no-networkidle': eslint.Rule.RuleModule;
23
24
  'no-nth-methods': eslint.Rule.RuleModule;
@@ -67,6 +68,7 @@ declare const _default: {
67
68
  'no-eval': eslint.Rule.RuleModule;
68
69
  'no-focused-test': eslint.Rule.RuleModule;
69
70
  'no-force-option': eslint.Rule.RuleModule;
71
+ 'no-get-by-title': eslint.Rule.RuleModule;
70
72
  'no-nested-step': eslint.Rule.RuleModule;
71
73
  'no-networkidle': eslint.Rule.RuleModule;
72
74
  'no-nth-methods': eslint.Rule.RuleModule;
@@ -203,6 +205,7 @@ declare const _default: {
203
205
  'no-eval': eslint.Rule.RuleModule;
204
206
  'no-focused-test': eslint.Rule.RuleModule;
205
207
  'no-force-option': eslint.Rule.RuleModule;
208
+ 'no-get-by-title': eslint.Rule.RuleModule;
206
209
  'no-nested-step': eslint.Rule.RuleModule;
207
210
  'no-networkidle': eslint.Rule.RuleModule;
208
211
  'no-nth-methods': eslint.Rule.RuleModule;
package/dist/index.js CHANGED
@@ -54,8 +54,11 @@ function isStringNode(node) {
54
54
  function isPropertyAccessor(node, name) {
55
55
  return getStringValue(node.property) === name;
56
56
  }
57
- function isTestIdentifier(node) {
58
- return isIdentifier(node, "test") || node.type === "MemberExpression" && isIdentifier(node.object, "test");
57
+ function isTestIdentifier(context, node) {
58
+ const aliases = context.settings.playwright?.globalAliases?.test ?? [];
59
+ const testNames = ["test", ...aliases];
60
+ const regex = new RegExp(`^(${testNames.join("|")})$`);
61
+ return isIdentifier(node, regex) || node.type === "MemberExpression" && isIdentifier(node.object, regex);
59
62
  }
60
63
  var describeProperties = /* @__PURE__ */ new Set([
61
64
  "parallel",
@@ -79,27 +82,31 @@ function findParent(node, type) {
79
82
  return;
80
83
  return node.parent.type === type ? node.parent : findParent(node.parent, type);
81
84
  }
82
- function isTestCall(node, modifiers) {
83
- return isTestIdentifier(node.callee) && !isDescribeCall(node) && (node.callee.type !== "MemberExpression" || !modifiers || modifiers?.includes(getStringValue(node.callee.property))) && node.arguments.length === 2 && ["ArrowFunctionExpression", "FunctionExpression"].includes(
85
+ function isTestCall(context, node, modifiers) {
86
+ 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(
84
87
  node.arguments[1].type
85
88
  );
86
89
  }
87
90
  var testHooks = /* @__PURE__ */ new Set(["afterAll", "afterEach", "beforeAll", "beforeEach"]);
88
- function isTestHook(node) {
89
- return node.callee.type === "MemberExpression" && isIdentifier(node.callee.object, "test") && testHooks.has(getStringValue(node.callee.property));
91
+ function isTestHook(context, node) {
92
+ return node.callee.type === "MemberExpression" && isTestIdentifier(context, node.callee.object) && testHooks.has(getStringValue(node.callee.property));
90
93
  }
91
94
  var expectSubCommands = /* @__PURE__ */ new Set(["soft", "poll"]);
92
- function getExpectType(node) {
93
- if (isIdentifier(node.callee, /(^expect|Expect)$/)) {
95
+ function getExpectType(context, node) {
96
+ const aliases = context.settings.playwright?.globalAliases?.expect ?? [];
97
+ const expectNames = ["expect", ...aliases];
98
+ const regex = new RegExp(`(^(${expectNames.join("|")})|Expect)$`);
99
+ if (isIdentifier(node.callee, regex)) {
94
100
  return "standalone";
95
101
  }
96
- if (node.callee.type === "MemberExpression" && isIdentifier(node.callee.object, "expect")) {
102
+ if (node.callee.type === "MemberExpression" && // TODO: Maybe
103
+ isIdentifier(node.callee.object, "expect")) {
97
104
  const type = getStringValue(node.callee.property);
98
105
  return expectSubCommands.has(type) ? type : void 0;
99
106
  }
100
107
  }
101
- function isExpectCall(node) {
102
- return !!getExpectType(node);
108
+ function isExpectCall(context, node) {
109
+ return !!getExpectType(context, node);
103
110
  }
104
111
  function getMatchers(node, chain = []) {
105
112
  if (node.parent.type === "MemberExpression" && node.parent.object === node) {
@@ -117,26 +124,18 @@ function isPageMethod(node, name) {
117
124
  return node.callee.type === "MemberExpression" && dig(node.callee.object, /(^(page|frame)|(Page|Frame)$)/) && isPropertyAccessor(node.callee, name);
118
125
  }
119
126
 
120
- // src/utils/misc.ts
121
- var getAmountData = (amount) => ({
122
- amount: amount.toString(),
123
- s: amount === 1 ? "" : "s"
124
- });
125
- function getAdditionalAssertFunctionNames(context) {
126
- const globalSettings = context.settings.playwright?.additionalAssertFunctionNames ?? [];
127
- const ruleSettings = context.options[0]?.additionalAssertFunctionNames ?? [];
128
- return [...globalSettings, ...ruleSettings];
129
- }
130
-
131
127
  // src/rules/expect-expect.ts
132
- function isAssertionCall(node, additionalAssertFunctionNames) {
133
- return isExpectCall(node) || additionalAssertFunctionNames.find((name) => dig(node.callee, name));
128
+ function isAssertionCall(context, node, assertFunctionNames) {
129
+ return isExpectCall(context, node) || assertFunctionNames.find((name) => dig(node.callee, name));
134
130
  }
135
131
  var expect_expect_default = {
136
132
  create(context) {
133
+ const options = {
134
+ assertFunctionNames: [],
135
+ ...context.options?.[0] ?? {}
136
+ };
137
137
  const sourceCode = context.sourceCode ?? context.getSourceCode();
138
138
  const unchecked = [];
139
- const additionalAssertFunctionNames = getAdditionalAssertFunctionNames(context);
140
139
  function checkExpressions(nodes) {
141
140
  for (const node of nodes) {
142
141
  const index2 = node.type === "CallExpression" ? unchecked.indexOf(node) : -1;
@@ -148,9 +147,9 @@ var expect_expect_default = {
148
147
  }
149
148
  return {
150
149
  CallExpression(node) {
151
- if (isTestCall(node, ["fixme", "only", "skip"])) {
150
+ if (isTestCall(context, node, ["fixme", "only", "skip"])) {
152
151
  unchecked.push(node);
153
- } else if (isAssertionCall(node, additionalAssertFunctionNames)) {
152
+ } else if (isAssertionCall(context, node, options.assertFunctionNames)) {
154
153
  const ancestors = sourceCode.getAncestors ? sourceCode.getAncestors(node) : context.getAncestors();
155
154
  checkExpressions(ancestors);
156
155
  }
@@ -176,7 +175,7 @@ var expect_expect_default = {
176
175
  {
177
176
  additionalProperties: false,
178
177
  properties: {
179
- additionalAssertFunctionNames: {
178
+ assertFunctionNames: {
180
179
  items: [{ type: "string" }],
181
180
  type: "array"
182
181
  }
@@ -302,11 +301,11 @@ var playwrightTestMatchers = [
302
301
  "toBeAttached",
303
302
  "toBeInViewport"
304
303
  ];
305
- function getCallType(node, awaitableMatchers) {
304
+ function getCallType(context, node, awaitableMatchers) {
306
305
  if (node.callee.type === "MemberExpression" && isIdentifier(node.callee.object, "test") && isPropertyAccessor(node.callee, "step")) {
307
306
  return { messageId: "testStep", node };
308
307
  }
309
- const expectType = getExpectType(node);
308
+ const expectType = getExpectType(context, node);
310
309
  if (!expectType)
311
310
  return;
312
311
  const [lastMatcher] = getMatchers(node).slice(-1);
@@ -355,7 +354,7 @@ var missing_playwright_await_default = {
355
354
  }
356
355
  return {
357
356
  CallExpression(node) {
358
- const result = getCallType(node, awaitableMatchers);
357
+ const result = getCallType(context, node, awaitableMatchers);
359
358
  const isValid = result ? checkValidity(result.node) : false;
360
359
  if (result && !isValid) {
361
360
  context.report({
@@ -401,7 +400,7 @@ var no_conditional_in_test_default = {
401
400
  create(context) {
402
401
  function checkConditional(node) {
403
402
  const call = findParent(node, "CallExpression");
404
- if (call && isTestCall(call)) {
403
+ if (call && isTestCall(context, call)) {
405
404
  context.report({ messageId: "conditionalInTest", node });
406
405
  }
407
406
  }
@@ -519,7 +518,7 @@ var no_focused_test_default = {
519
518
  create(context) {
520
519
  return {
521
520
  CallExpression(node) {
522
- if ((isTestCall(node) || isDescribeCall(node)) && node.callee.type === "MemberExpression" && isPropertyAccessor(node.callee, "only")) {
521
+ if ((isTestCall(context, node) || isDescribeCall(node)) && node.callee.type === "MemberExpression" && isPropertyAccessor(node.callee, "only")) {
523
522
  const { callee } = node;
524
523
  context.report({
525
524
  messageId: "noFocusedTest",
@@ -602,6 +601,31 @@ var no_force_option_default = {
602
601
  }
603
602
  };
604
603
 
604
+ // src/rules/no-get-by-title.ts
605
+ var no_get_by_title_default = {
606
+ create(context) {
607
+ return {
608
+ CallExpression(node) {
609
+ if (isPageMethod(node, "getByTitle")) {
610
+ context.report({ messageId: "noGetByTitle", node });
611
+ }
612
+ }
613
+ };
614
+ },
615
+ meta: {
616
+ docs: {
617
+ category: "Best Practices",
618
+ description: "Disallows the usage of getByTitle()",
619
+ recommended: false,
620
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-get-by-title.md"
621
+ },
622
+ messages: {
623
+ noGetByTitle: "The HTML title attribute is not an accessible name. Prefer getByRole() or getByLabelText() instead."
624
+ },
625
+ type: "suggestion"
626
+ }
627
+ };
628
+
605
629
  // src/rules/no-nested-step.ts
606
630
  function isStepCall(node) {
607
631
  const inner = node.type === "CallExpression" ? node.callee : node;
@@ -824,8 +848,8 @@ function getExpectArguments(node) {
824
848
  const grandparent = node.parent.parent;
825
849
  return grandparent.type === "CallExpression" ? grandparent.arguments : [];
826
850
  }
827
- function parseExpectCall(node) {
828
- if (!isExpectCall(node)) {
851
+ function parseExpectCall(context, node) {
852
+ if (!isExpectCall(context, node)) {
829
853
  return;
830
854
  }
831
855
  const members = getMatchers(node);
@@ -856,7 +880,7 @@ var no_restricted_matchers_default = {
856
880
  const restrictedChains = context.options?.[0] ?? {};
857
881
  return {
858
882
  CallExpression(node) {
859
- const expectCall = parseExpectCall(node);
883
+ const expectCall = parseExpectCall(context, node);
860
884
  if (!expectCall)
861
885
  return;
862
886
  Object.entries(restrictedChains).map(([restriction, message]) => {
@@ -917,8 +941,8 @@ var no_skipped_test_default = {
917
941
  const options = context.options[0] || {};
918
942
  const allowConditional = !!options.allowConditional;
919
943
  const { callee } = node;
920
- if ((isTestIdentifier(callee) || isDescribeCall(node)) && callee.type === "MemberExpression" && isPropertyAccessor(callee, "skip")) {
921
- const isHook = isTestCall(node) || isDescribeCall(node);
944
+ if ((isTestIdentifier(context, callee) || isDescribeCall(node)) && callee.type === "MemberExpression" && isPropertyAccessor(callee, "skip")) {
945
+ const isHook = isTestCall(context, node) || isDescribeCall(node);
922
946
  if (!isHook && allowConditional && node.arguments.length) {
923
947
  return;
924
948
  }
@@ -1072,7 +1096,7 @@ var no_useless_not_default = {
1072
1096
  create(context) {
1073
1097
  return {
1074
1098
  CallExpression(node) {
1075
- const expectCall = parseExpectCall(node);
1099
+ const expectCall = parseExpectCall(context, node);
1076
1100
  if (!expectCall)
1077
1101
  return;
1078
1102
  const notModifier = expectCall.modifiers.find(
@@ -1204,7 +1228,7 @@ var prefer_lowercase_title_default = {
1204
1228
  let describeCount = 0;
1205
1229
  return {
1206
1230
  CallExpression(node) {
1207
- const method = isDescribeCall(node) ? "test.describe" : isTestCall(node) ? "test" : null;
1231
+ const method = isDescribeCall(node) ? "test.describe" : isTestCall(context, node) ? "test" : null;
1208
1232
  if (method === "test.describe") {
1209
1233
  describeCount++;
1210
1234
  if (ignoreTopLevelDescribe && describeCount === 1) {
@@ -1290,7 +1314,7 @@ var prefer_strict_equal_default = {
1290
1314
  create(context) {
1291
1315
  return {
1292
1316
  CallExpression(node) {
1293
- const expectCall = parseExpectCall(node);
1317
+ const expectCall = parseExpectCall(context, node);
1294
1318
  if (expectCall?.matcherName === "toEqual") {
1295
1319
  context.report({
1296
1320
  messageId: "useToStrictEqual",
@@ -1364,7 +1388,7 @@ var prefer_to_be_default = {
1364
1388
  create(context) {
1365
1389
  return {
1366
1390
  CallExpression(node) {
1367
- const expectCall = parseExpectCall(node);
1391
+ const expectCall = parseExpectCall(context, node);
1368
1392
  if (!expectCall)
1369
1393
  return;
1370
1394
  const notMatchers = ["toBeUndefined", "toBeDefined"];
@@ -1427,7 +1451,7 @@ var prefer_to_contain_default = {
1427
1451
  create(context) {
1428
1452
  return {
1429
1453
  CallExpression(node) {
1430
- const expectCall = parseExpectCall(node);
1454
+ const expectCall = parseExpectCall(context, node);
1431
1455
  if (!expectCall || expectCall.args.length === 0)
1432
1456
  return;
1433
1457
  const { args, matcher, matcherName } = expectCall;
@@ -1497,7 +1521,7 @@ var prefer_to_have_count_default = {
1497
1521
  create(context) {
1498
1522
  return {
1499
1523
  CallExpression(node) {
1500
- const expectCall = parseExpectCall(node);
1524
+ const expectCall = parseExpectCall(context, node);
1501
1525
  if (!expectCall || !matchers2.has(expectCall.matcherName)) {
1502
1526
  return;
1503
1527
  }
@@ -1553,7 +1577,7 @@ var prefer_to_have_length_default = {
1553
1577
  create(context) {
1554
1578
  return {
1555
1579
  CallExpression(node) {
1556
- const expectCall = parseExpectCall(node);
1580
+ const expectCall = parseExpectCall(context, node);
1557
1581
  if (!expectCall || !lengthMatchers.has(expectCall.matcherName)) {
1558
1582
  return;
1559
1583
  }
@@ -1641,7 +1665,7 @@ var prefer_web_first_assertions_default = {
1641
1665
  create(context) {
1642
1666
  return {
1643
1667
  CallExpression(node) {
1644
- const expectCall = parseExpectCall(node);
1668
+ const expectCall = parseExpectCall(context, node);
1645
1669
  if (!expectCall)
1646
1670
  return;
1647
1671
  const [arg] = node.arguments;
@@ -1742,7 +1766,7 @@ var require_soft_assertions_default = {
1742
1766
  create(context) {
1743
1767
  return {
1744
1768
  CallExpression(node) {
1745
- if (getExpectType(node) === "standalone") {
1769
+ if (getExpectType(context, node) === "standalone") {
1746
1770
  context.report({
1747
1771
  fix: (fixer) => fixer.insertTextAfter(node.callee, ".soft"),
1748
1772
  messageId: "requireSoft",
@@ -1767,6 +1791,12 @@ var require_soft_assertions_default = {
1767
1791
  }
1768
1792
  };
1769
1793
 
1794
+ // src/utils/misc.ts
1795
+ var getAmountData = (amount) => ({
1796
+ amount: amount.toString(),
1797
+ s: amount === 1 ? "" : "s"
1798
+ });
1799
+
1770
1800
  // src/rules/require-top-level-describe.ts
1771
1801
  var require_top_level_describe_default = {
1772
1802
  create(context) {
@@ -1791,9 +1821,9 @@ var require_top_level_describe_default = {
1791
1821
  }
1792
1822
  }
1793
1823
  } else if (!describeCount) {
1794
- if (isTestCall(node)) {
1824
+ if (isTestCall(context, node)) {
1795
1825
  context.report({ messageId: "unexpectedTest", node: node.callee });
1796
- } else if (isTestHook(node)) {
1826
+ } else if (isTestHook(context, node)) {
1797
1827
  context.report({ messageId: "unexpectedHook", node: node.callee });
1798
1828
  }
1799
1829
  }
@@ -1854,9 +1884,9 @@ var valid_expect_default = {
1854
1884
  const maxArgs = Math.max(options.minArgs, options.maxArgs);
1855
1885
  return {
1856
1886
  CallExpression(node) {
1857
- if (!isExpectCall(node))
1887
+ if (!isExpectCall(context, node))
1858
1888
  return;
1859
- const expectCall = parseExpectCall(node);
1889
+ const expectCall = parseExpectCall(context, node);
1860
1890
  if (!expectCall) {
1861
1891
  context.report({ messageId: "matcherNotFound", node });
1862
1892
  } else {
@@ -1973,7 +2003,7 @@ var valid_title_default = {
1973
2003
  return {
1974
2004
  CallExpression(node) {
1975
2005
  const isDescribe = isDescribeCall(node);
1976
- const isTest = isTestCall(node);
2006
+ const isTest = isTestCall(context, node);
1977
2007
  if (!isDescribe && !isTest) {
1978
2008
  return;
1979
2009
  }
@@ -2142,6 +2172,7 @@ var index = {
2142
2172
  "no-eval": no_eval_default,
2143
2173
  "no-focused-test": no_focused_test_default,
2144
2174
  "no-force-option": no_force_option_default,
2175
+ "no-get-by-title": no_get_by_title_default,
2145
2176
  "no-nested-step": no_nested_step_default,
2146
2177
  "no-networkidle": no_networkidle_default,
2147
2178
  "no-nth-methods": no_nth_methods_default,
package/dist/index.mjs CHANGED
@@ -33,8 +33,11 @@ function isStringNode(node) {
33
33
  function isPropertyAccessor(node, name) {
34
34
  return getStringValue(node.property) === name;
35
35
  }
36
- function isTestIdentifier(node) {
37
- return isIdentifier(node, "test") || node.type === "MemberExpression" && isIdentifier(node.object, "test");
36
+ function isTestIdentifier(context, node) {
37
+ const aliases = context.settings.playwright?.globalAliases?.test ?? [];
38
+ const testNames = ["test", ...aliases];
39
+ const regex = new RegExp(`^(${testNames.join("|")})$`);
40
+ return isIdentifier(node, regex) || node.type === "MemberExpression" && isIdentifier(node.object, regex);
38
41
  }
39
42
  function isDescribeCall(node) {
40
43
  const inner = node.type === "CallExpression" ? node.callee : node;
@@ -51,25 +54,29 @@ function findParent(node, type) {
51
54
  return;
52
55
  return node.parent.type === type ? node.parent : findParent(node.parent, type);
53
56
  }
54
- function isTestCall(node, modifiers) {
55
- return isTestIdentifier(node.callee) && !isDescribeCall(node) && (node.callee.type !== "MemberExpression" || !modifiers || modifiers?.includes(getStringValue(node.callee.property))) && node.arguments.length === 2 && ["ArrowFunctionExpression", "FunctionExpression"].includes(
57
+ function isTestCall(context, node, modifiers) {
58
+ 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(
56
59
  node.arguments[1].type
57
60
  );
58
61
  }
59
- function isTestHook(node) {
60
- return node.callee.type === "MemberExpression" && isIdentifier(node.callee.object, "test") && testHooks.has(getStringValue(node.callee.property));
62
+ function isTestHook(context, node) {
63
+ return node.callee.type === "MemberExpression" && isTestIdentifier(context, node.callee.object) && testHooks.has(getStringValue(node.callee.property));
61
64
  }
62
- function getExpectType(node) {
63
- if (isIdentifier(node.callee, /(^expect|Expect)$/)) {
65
+ function getExpectType(context, node) {
66
+ const aliases = context.settings.playwright?.globalAliases?.expect ?? [];
67
+ const expectNames = ["expect", ...aliases];
68
+ const regex = new RegExp(`(^(${expectNames.join("|")})|Expect)$`);
69
+ if (isIdentifier(node.callee, regex)) {
64
70
  return "standalone";
65
71
  }
66
- if (node.callee.type === "MemberExpression" && isIdentifier(node.callee.object, "expect")) {
72
+ if (node.callee.type === "MemberExpression" && // TODO: Maybe
73
+ isIdentifier(node.callee.object, "expect")) {
67
74
  const type = getStringValue(node.callee.property);
68
75
  return expectSubCommands.has(type) ? type : void 0;
69
76
  }
70
77
  }
71
- function isExpectCall(node) {
72
- return !!getExpectType(node);
78
+ function isExpectCall(context, node) {
79
+ return !!getExpectType(context, node);
73
80
  }
74
81
  function getMatchers(node, chain = []) {
75
82
  if (node.parent.type === "MemberExpression" && node.parent.object === node) {
@@ -104,38 +111,23 @@ var init_ast = __esm({
104
111
  }
105
112
  });
106
113
 
107
- // src/utils/misc.ts
108
- function getAdditionalAssertFunctionNames(context) {
109
- const globalSettings = context.settings.playwright?.additionalAssertFunctionNames ?? [];
110
- const ruleSettings = context.options[0]?.additionalAssertFunctionNames ?? [];
111
- return [...globalSettings, ...ruleSettings];
112
- }
113
- var getAmountData;
114
- var init_misc = __esm({
115
- "src/utils/misc.ts"() {
116
- "use strict";
117
- getAmountData = (amount) => ({
118
- amount: amount.toString(),
119
- s: amount === 1 ? "" : "s"
120
- });
121
- }
122
- });
123
-
124
114
  // src/rules/expect-expect.ts
125
- function isAssertionCall(node, additionalAssertFunctionNames) {
126
- return isExpectCall(node) || additionalAssertFunctionNames.find((name) => dig(node.callee, name));
115
+ function isAssertionCall(context, node, assertFunctionNames) {
116
+ return isExpectCall(context, node) || assertFunctionNames.find((name) => dig(node.callee, name));
127
117
  }
128
118
  var expect_expect_default;
129
119
  var init_expect_expect = __esm({
130
120
  "src/rules/expect-expect.ts"() {
131
121
  "use strict";
132
122
  init_ast();
133
- init_misc();
134
123
  expect_expect_default = {
135
124
  create(context) {
125
+ const options = {
126
+ assertFunctionNames: [],
127
+ ...context.options?.[0] ?? {}
128
+ };
136
129
  const sourceCode = context.sourceCode ?? context.getSourceCode();
137
130
  const unchecked = [];
138
- const additionalAssertFunctionNames = getAdditionalAssertFunctionNames(context);
139
131
  function checkExpressions(nodes) {
140
132
  for (const node of nodes) {
141
133
  const index = node.type === "CallExpression" ? unchecked.indexOf(node) : -1;
@@ -147,9 +139,9 @@ var init_expect_expect = __esm({
147
139
  }
148
140
  return {
149
141
  CallExpression(node) {
150
- if (isTestCall(node, ["fixme", "only", "skip"])) {
142
+ if (isTestCall(context, node, ["fixme", "only", "skip"])) {
151
143
  unchecked.push(node);
152
- } else if (isAssertionCall(node, additionalAssertFunctionNames)) {
144
+ } else if (isAssertionCall(context, node, options.assertFunctionNames)) {
153
145
  const ancestors = sourceCode.getAncestors ? sourceCode.getAncestors(node) : context.getAncestors();
154
146
  checkExpressions(ancestors);
155
147
  }
@@ -175,7 +167,7 @@ var init_expect_expect = __esm({
175
167
  {
176
168
  additionalProperties: false,
177
169
  properties: {
178
- additionalAssertFunctionNames: {
170
+ assertFunctionNames: {
179
171
  items: [{ type: "string" }],
180
172
  type: "array"
181
173
  }
@@ -258,11 +250,11 @@ var init_max_nested_describe = __esm({
258
250
  });
259
251
 
260
252
  // src/rules/missing-playwright-await.ts
261
- function getCallType(node, awaitableMatchers) {
253
+ function getCallType(context, node, awaitableMatchers) {
262
254
  if (node.callee.type === "MemberExpression" && isIdentifier(node.callee.object, "test") && isPropertyAccessor(node.callee, "step")) {
263
255
  return { messageId: "testStep", node };
264
256
  }
265
- const expectType = getExpectType(node);
257
+ const expectType = getExpectType(context, node);
266
258
  if (!expectType)
267
259
  return;
268
260
  const [lastMatcher] = getMatchers(node).slice(-1);
@@ -368,7 +360,7 @@ var init_missing_playwright_await = __esm({
368
360
  }
369
361
  return {
370
362
  CallExpression(node) {
371
- const result = getCallType(node, awaitableMatchers);
363
+ const result = getCallType(context, node, awaitableMatchers);
372
364
  const isValid = result ? checkValidity(result.node) : false;
373
365
  if (result && !isValid) {
374
366
  context.report({
@@ -421,7 +413,7 @@ var init_no_conditional_in_test = __esm({
421
413
  create(context) {
422
414
  function checkConditional(node) {
423
415
  const call = findParent(node, "CallExpression");
424
- if (call && isTestCall(call)) {
416
+ if (call && isTestCall(context, call)) {
425
417
  context.report({ messageId: "conditionalInTest", node });
426
418
  }
427
419
  }
@@ -560,7 +552,7 @@ var init_no_focused_test = __esm({
560
552
  create(context) {
561
553
  return {
562
554
  CallExpression(node) {
563
- if ((isTestCall(node) || isDescribeCall(node)) && node.callee.type === "MemberExpression" && isPropertyAccessor(node.callee, "only")) {
555
+ if ((isTestCall(context, node) || isDescribeCall(node)) && node.callee.type === "MemberExpression" && isPropertyAccessor(node.callee, "only")) {
564
556
  const { callee } = node;
565
557
  context.report({
566
558
  messageId: "noFocusedTest",
@@ -652,6 +644,38 @@ var init_no_force_option = __esm({
652
644
  }
653
645
  });
654
646
 
647
+ // src/rules/no-get-by-title.ts
648
+ var no_get_by_title_default;
649
+ var init_no_get_by_title = __esm({
650
+ "src/rules/no-get-by-title.ts"() {
651
+ "use strict";
652
+ init_ast();
653
+ no_get_by_title_default = {
654
+ create(context) {
655
+ return {
656
+ CallExpression(node) {
657
+ if (isPageMethod(node, "getByTitle")) {
658
+ context.report({ messageId: "noGetByTitle", node });
659
+ }
660
+ }
661
+ };
662
+ },
663
+ meta: {
664
+ docs: {
665
+ category: "Best Practices",
666
+ description: "Disallows the usage of getByTitle()",
667
+ recommended: false,
668
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-get-by-title.md"
669
+ },
670
+ messages: {
671
+ noGetByTitle: "The HTML title attribute is not an accessible name. Prefer getByRole() or getByLabelText() instead."
672
+ },
673
+ type: "suggestion"
674
+ }
675
+ };
676
+ }
677
+ });
678
+
655
679
  // src/rules/no-nested-step.ts
656
680
  function isStepCall(node) {
657
681
  const inner = node.type === "CallExpression" ? node.callee : node;
@@ -908,8 +932,8 @@ function getExpectArguments(node) {
908
932
  const grandparent = node.parent.parent;
909
933
  return grandparent.type === "CallExpression" ? grandparent.arguments : [];
910
934
  }
911
- function parseExpectCall(node) {
912
- if (!isExpectCall(node)) {
935
+ function parseExpectCall(context, node) {
936
+ if (!isExpectCall(context, node)) {
913
937
  return;
914
938
  }
915
939
  const members = getMatchers(node);
@@ -954,7 +978,7 @@ var init_no_restricted_matchers = __esm({
954
978
  const restrictedChains = context.options?.[0] ?? {};
955
979
  return {
956
980
  CallExpression(node) {
957
- const expectCall = parseExpectCall(node);
981
+ const expectCall = parseExpectCall(context, node);
958
982
  if (!expectCall)
959
983
  return;
960
984
  Object.entries(restrictedChains).map(([restriction, message]) => {
@@ -1022,8 +1046,8 @@ var init_no_skipped_test = __esm({
1022
1046
  const options = context.options[0] || {};
1023
1047
  const allowConditional = !!options.allowConditional;
1024
1048
  const { callee } = node;
1025
- if ((isTestIdentifier(callee) || isDescribeCall(node)) && callee.type === "MemberExpression" && isPropertyAccessor(callee, "skip")) {
1026
- const isHook = isTestCall(node) || isDescribeCall(node);
1049
+ if ((isTestIdentifier(context, callee) || isDescribeCall(node)) && callee.type === "MemberExpression" && isPropertyAccessor(callee, "skip")) {
1050
+ const isHook = isTestCall(context, node) || isDescribeCall(node);
1027
1051
  if (!isHook && allowConditional && node.arguments.length) {
1028
1052
  return;
1029
1053
  }
@@ -1199,7 +1223,7 @@ var init_no_useless_not = __esm({
1199
1223
  create(context) {
1200
1224
  return {
1201
1225
  CallExpression(node) {
1202
- const expectCall = parseExpectCall(node);
1226
+ const expectCall = parseExpectCall(context, node);
1203
1227
  if (!expectCall)
1204
1228
  return;
1205
1229
  const notModifier = expectCall.modifiers.find(
@@ -1352,7 +1376,7 @@ var init_prefer_lowercase_title = __esm({
1352
1376
  let describeCount = 0;
1353
1377
  return {
1354
1378
  CallExpression(node) {
1355
- const method = isDescribeCall(node) ? "test.describe" : isTestCall(node) ? "test" : null;
1379
+ const method = isDescribeCall(node) ? "test.describe" : isTestCall(context, node) ? "test" : null;
1356
1380
  if (method === "test.describe") {
1357
1381
  describeCount++;
1358
1382
  if (ignoreTopLevelDescribe && describeCount === 1) {
@@ -1446,7 +1470,7 @@ var init_prefer_strict_equal = __esm({
1446
1470
  create(context) {
1447
1471
  return {
1448
1472
  CallExpression(node) {
1449
- const expectCall = parseExpectCall(node);
1473
+ const expectCall = parseExpectCall(context, node);
1450
1474
  if (expectCall?.matcherName === "toEqual") {
1451
1475
  context.report({
1452
1476
  messageId: "useToStrictEqual",
@@ -1529,7 +1553,7 @@ var init_prefer_to_be = __esm({
1529
1553
  create(context) {
1530
1554
  return {
1531
1555
  CallExpression(node) {
1532
- const expectCall = parseExpectCall(node);
1556
+ const expectCall = parseExpectCall(context, node);
1533
1557
  if (!expectCall)
1534
1558
  return;
1535
1559
  const notMatchers = ["toBeUndefined", "toBeDefined"];
@@ -1600,7 +1624,7 @@ var init_prefer_to_contain = __esm({
1600
1624
  create(context) {
1601
1625
  return {
1602
1626
  CallExpression(node) {
1603
- const expectCall = parseExpectCall(node);
1627
+ const expectCall = parseExpectCall(context, node);
1604
1628
  if (!expectCall || expectCall.args.length === 0)
1605
1629
  return;
1606
1630
  const { args, matcher, matcherName } = expectCall;
@@ -1679,7 +1703,7 @@ var init_prefer_to_have_count = __esm({
1679
1703
  create(context) {
1680
1704
  return {
1681
1705
  CallExpression(node) {
1682
- const expectCall = parseExpectCall(node);
1706
+ const expectCall = parseExpectCall(context, node);
1683
1707
  if (!expectCall || !matchers2.has(expectCall.matcherName)) {
1684
1708
  return;
1685
1709
  }
@@ -1744,7 +1768,7 @@ var init_prefer_to_have_length = __esm({
1744
1768
  create(context) {
1745
1769
  return {
1746
1770
  CallExpression(node) {
1747
- const expectCall = parseExpectCall(node);
1771
+ const expectCall = parseExpectCall(context, node);
1748
1772
  if (!expectCall || !lengthMatchers.has(expectCall.matcherName)) {
1749
1773
  return;
1750
1774
  }
@@ -1840,7 +1864,7 @@ var init_prefer_web_first_assertions = __esm({
1840
1864
  create(context) {
1841
1865
  return {
1842
1866
  CallExpression(node) {
1843
- const expectCall = parseExpectCall(node);
1867
+ const expectCall = parseExpectCall(context, node);
1844
1868
  if (!expectCall)
1845
1869
  return;
1846
1870
  const [arg] = node.arguments;
@@ -1948,7 +1972,7 @@ var init_require_soft_assertions = __esm({
1948
1972
  create(context) {
1949
1973
  return {
1950
1974
  CallExpression(node) {
1951
- if (getExpectType(node) === "standalone") {
1975
+ if (getExpectType(context, node) === "standalone") {
1952
1976
  context.report({
1953
1977
  fix: (fixer) => fixer.insertTextAfter(node.callee, ".soft"),
1954
1978
  messageId: "requireSoft",
@@ -1975,6 +1999,18 @@ var init_require_soft_assertions = __esm({
1975
1999
  }
1976
2000
  });
1977
2001
 
2002
+ // src/utils/misc.ts
2003
+ var getAmountData;
2004
+ var init_misc = __esm({
2005
+ "src/utils/misc.ts"() {
2006
+ "use strict";
2007
+ getAmountData = (amount) => ({
2008
+ amount: amount.toString(),
2009
+ s: amount === 1 ? "" : "s"
2010
+ });
2011
+ }
2012
+ });
2013
+
1978
2014
  // src/rules/require-top-level-describe.ts
1979
2015
  var require_top_level_describe_default;
1980
2016
  var init_require_top_level_describe = __esm({
@@ -2005,9 +2041,9 @@ var init_require_top_level_describe = __esm({
2005
2041
  }
2006
2042
  }
2007
2043
  } else if (!describeCount) {
2008
- if (isTestCall(node)) {
2044
+ if (isTestCall(context, node)) {
2009
2045
  context.report({ messageId: "unexpectedTest", node: node.callee });
2010
- } else if (isTestHook(node)) {
2046
+ } else if (isTestHook(context, node)) {
2011
2047
  context.report({ messageId: "unexpectedHook", node: node.callee });
2012
2048
  }
2013
2049
  }
@@ -2077,9 +2113,9 @@ var init_valid_expect = __esm({
2077
2113
  const maxArgs = Math.max(options.minArgs, options.maxArgs);
2078
2114
  return {
2079
2115
  CallExpression(node) {
2080
- if (!isExpectCall(node))
2116
+ if (!isExpectCall(context, node))
2081
2117
  return;
2082
- const expectCall = parseExpectCall(node);
2118
+ const expectCall = parseExpectCall(context, node);
2083
2119
  if (!expectCall) {
2084
2120
  context.report({ messageId: "matcherNotFound", node });
2085
2121
  } else {
@@ -2203,7 +2239,7 @@ var init_valid_title = __esm({
2203
2239
  return {
2204
2240
  CallExpression(node) {
2205
2241
  const isDescribe = isDescribeCall(node);
2206
- const isTest = isTestCall(node);
2242
+ const isTest = isTestCall(context, node);
2207
2243
  if (!isDescribe && !isTest) {
2208
2244
  return;
2209
2245
  }
@@ -2374,6 +2410,7 @@ var require_src = __commonJS({
2374
2410
  init_no_eval();
2375
2411
  init_no_focused_test();
2376
2412
  init_no_force_option();
2413
+ init_no_get_by_title();
2377
2414
  init_no_nested_step();
2378
2415
  init_no_networkidle();
2379
2416
  init_no_nth_methods();
@@ -2407,6 +2444,7 @@ var require_src = __commonJS({
2407
2444
  "no-eval": no_eval_default,
2408
2445
  "no-focused-test": no_focused_test_default,
2409
2446
  "no-force-option": no_force_option_default,
2447
+ "no-get-by-title": no_get_by_title_default,
2410
2448
  "no-nested-step": no_nested_step_default,
2411
2449
  "no-networkidle": no_networkidle_default,
2412
2450
  "no-nth-methods": no_nth_methods_default,
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": "0.22.2",
4
+ "version": "1.0.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",
@@ -35,9 +35,11 @@
35
35
  "ts": "tsc --noEmit"
36
36
  },
37
37
  "devDependencies": {
38
+ "@jest/globals": "^29.7.0",
38
39
  "@mskelton/eslint-config": "^8.4.0",
39
40
  "@types/eslint": "^8.44.3",
40
41
  "@types/estree": "^1.0.2",
42
+ "@types/node": "^20.11.17",
41
43
  "@typescript-eslint/eslint-plugin": "^6.7.3",
42
44
  "@typescript-eslint/parser": "^6.7.3",
43
45
  "dedent": "^1.5.1",