eslint-plugin-playwright 0.9.0 → 0.10.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/README.md CHANGED
@@ -50,20 +50,20 @@ Identify false positives when async Playwright APIs are not properly awaited.
50
50
  Example of **incorrect** code for this rule:
51
51
 
52
52
  ```js
53
- expect(page).toMatchText("text");
53
+ expect(page).toMatchText('text');
54
54
 
55
- test.step("clicks the button", async () => {
56
- await page.click("button");
55
+ test.step('clicks the button', async () => {
56
+ await page.click('button');
57
57
  });
58
58
  ```
59
59
 
60
60
  Example of **correct** code for this rule:
61
61
 
62
62
  ```js
63
- await expect(page).toMatchText("text");
63
+ await expect(page).toMatchText('text');
64
64
 
65
- await test.step("clicks the button", async () => {
66
- await page.click("button");
65
+ await test.step('clicks the button', async () => {
66
+ await page.click('button');
67
67
  });
68
68
  ```
69
69
 
@@ -79,6 +79,7 @@ The rule accepts a non-required option which can be used to specify custom match
79
79
  ]
80
80
  }
81
81
  ```
82
+
82
83
  ### `no-page-pause`
83
84
 
84
85
  Prevent usage of `page.pause()`.
@@ -127,19 +128,23 @@ Disallow usage of `page.$eval` and `page.$$eval`.
127
128
  Examples of **incorrect** code for this rule:
128
129
 
129
130
  ```js
130
- const searchValue = await page.$eval('#search', el => el.value);
131
+ const searchValue = await page.$eval('#search', (el) => el.value);
131
132
 
132
- const divCounts = await page.$$eval('div', (divs, min) => divs.length >= min, 10);
133
+ const divCounts = await page.$$eval(
134
+ 'div',
135
+ (divs, min) => divs.length >= min,
136
+ 10
137
+ );
133
138
 
134
- await page.$eval('#search', el => el.value);
139
+ await page.$eval('#search', (el) => el.value);
135
140
 
136
- await page.$$eval('#search', el => el.value);
141
+ await page.$$eval('#search', (el) => el.value);
137
142
  ```
138
143
 
139
144
  Example of **correct** code for this rule:
140
145
 
141
146
  ```js
142
- await page.locator('button').evaluate(node => node.innerText);
147
+ await page.locator('button').evaluate((node) => node.innerText);
143
148
 
144
149
  await page.locator('div').evaluateAll((divs, min) => divs.length >= min, 10);
145
150
  ```
@@ -167,7 +172,6 @@ test.describe.serial.only('focus two tests in serial mode', () => {
167
172
  test('one', async ({ page }) => {});
168
173
  test('two', async ({ page }) => {});
169
174
  });
170
-
171
175
  ```
172
176
 
173
177
  Examples of **correct** code for this rule:
@@ -233,7 +237,6 @@ test.describe('skip test inside describe', () => {
233
237
  test.describe('skip test conditionally', async ({ browserName }) => {
234
238
  test.skip(browserName === 'firefox', 'Working on it');
235
239
  });
236
-
237
240
  ```
238
241
 
239
242
  Examples of **correct** code for this rule:
@@ -270,3 +273,126 @@ await page.locator('check').check();
270
273
 
271
274
  await page.locator('input').fill('something');
272
275
  ```
276
+
277
+ ### `max-nested-describe`
278
+
279
+ Enforces a maximum depth to nested `.describe()` calls. Useful for improving readability and parallelization of tests.
280
+
281
+ Uses a default max depth option of `{ "max": 5 }`.
282
+
283
+ Examples of **incorrect** code for this rule (using defaults):
284
+
285
+ ```js
286
+ test.describe('level 1', () => {
287
+ test.describe('level 2', () => {
288
+ test.describe('level 3', () => {
289
+ test.describe('level 4', () => {
290
+ test.describe('level 5', () => {
291
+ test.describe('level 6', () => {
292
+ test('this test', async ({ page }) => {});
293
+ test('that test', async ({ page }) => {});
294
+ });
295
+ });
296
+ });
297
+ });
298
+ });
299
+ });
300
+ ```
301
+
302
+ Examples of **correct** code for this rule (using defaults):
303
+
304
+ ```js
305
+ test.describe('first level', () => {
306
+ test.describe('second level', () => {
307
+ test('this test', async ({ page }) => {});
308
+ test('that test', async ({ page }) => {});
309
+ });
310
+ });
311
+ ```
312
+
313
+ #### Options
314
+
315
+ The rule accepts a non-required option to override the default maximum nested describe depth (5).
316
+
317
+ ```json
318
+ {
319
+ "playwright/max-nested-describe": ["error", { "max": 3 }]
320
+ }
321
+ ```
322
+
323
+ ### `no-conditional-in-test`
324
+
325
+ Disallow conditional statements such as `if`, `switch`, and ternary expressions within tests.
326
+
327
+ Examples of **incorrect** code for this rule:
328
+
329
+ ```js
330
+ test('foo', async ({ page }) => {
331
+ if (someCondition) {
332
+ bar();
333
+ }
334
+ });
335
+
336
+ test('bar', async ({ page }) => {
337
+ switch (mode) {
338
+ case 'single':
339
+ generateOne();
340
+ break;
341
+ case 'double':
342
+ generateTwo();
343
+ break;
344
+ case 'multiple':
345
+ generateMany();
346
+ break;
347
+ }
348
+
349
+ await expect(page.locator('.my-image').count()).toBeGreaterThan(0);
350
+ });
351
+
352
+ test('baz', async ({ page }) => {
353
+ const hotkey =
354
+ process.platform === 'linux' ? ['Control', 'Alt', 'f'] : ['Alt', 'f'];
355
+ await Promise.all(hotkey.map((x) => page.keyboard.down(x)));
356
+
357
+ expect(actionIsPerformed()).toBe(true);
358
+ });
359
+ ```
360
+
361
+ Examples of **correct** code for this rule:
362
+
363
+ ```js
364
+ test.describe('my tests', () => {
365
+ if (someCondition) {
366
+ test('foo', async ({ page }) => {
367
+ bar();
368
+ });
369
+ }
370
+ });
371
+
372
+ beforeEach(() => {
373
+ switch (mode) {
374
+ case 'single':
375
+ generateOne();
376
+ break;
377
+ case 'double':
378
+ generateTwo();
379
+ break;
380
+ case 'multiple':
381
+ generateMany();
382
+ break;
383
+ }
384
+ });
385
+
386
+ test('bar', async ({ page }) => {
387
+ await expect(page.locator('.my-image').count()).toBeGreaterThan(0);
388
+ });
389
+
390
+ const hotkey =
391
+ process.platform === 'linux' ? ['Control', 'Alt', 'f'] : ['Alt', 'f'];
392
+
393
+ test('baz', async ({ page }) => {
394
+ await Promise.all(hotkey.map((x) => page.keyboard.down(x)));
395
+
396
+ expect(actionIsPerformed()).toBe(true);
397
+ });
398
+ ```
package/lib/index.js CHANGED
@@ -1,50 +1,54 @@
1
- const missingPlaywrightAwait = require("./rules/missing-playwright-await");
2
- const noPagePause = require("./rules/no-page-pause");
3
- const noElementHandle = require("./rules/no-element-handle");
4
- const noEval = require("./rules/no-eval");
5
- const noFocusedTest = require("./rules/no-focused-test");
6
- const noSkippedTest = require("./rules/no-skipped-test");
7
- const noWaitForTimeout = require("./rules/no-wait-for-timeout");
8
- const noForceOption = require("./rules/no-force-option");
1
+ const missingPlaywrightAwait = require('./rules/missing-playwright-await');
2
+ const noPagePause = require('./rules/no-page-pause');
3
+ const noElementHandle = require('./rules/no-element-handle');
4
+ const noEval = require('./rules/no-eval');
5
+ const noFocusedTest = require('./rules/no-focused-test');
6
+ const noSkippedTest = require('./rules/no-skipped-test');
7
+ const noWaitForTimeout = require('./rules/no-wait-for-timeout');
8
+ const noForceOption = require('./rules/no-force-option');
9
+ const maxNestedDescribe = require('./rules/max-nested-describe');
10
+ const noConditionalInTest = require('./rules/no-conditional-in-test');
9
11
 
10
12
  module.exports = {
11
13
  configs: {
12
- "playwright-test": {
13
- plugins: ["playwright"],
14
+ 'playwright-test': {
15
+ plugins: ['playwright'],
14
16
  env: {
15
- "shared-node-browser": true,
17
+ 'shared-node-browser': true,
16
18
  },
17
19
  rules: {
18
- "no-empty-pattern": "off",
19
- "playwright/missing-playwright-await": "error",
20
- "playwright/no-page-pause": "warn",
21
- "playwright/no-element-handle": "warn",
22
- "playwright/no-eval": "warn",
23
- "playwright/no-focused-test": "error",
24
- "playwright/no-skipped-test": "warn",
25
- "playwright/no-wait-for-timeout": "warn",
26
- "playwright/no-force-option": "warn",
20
+ 'no-empty-pattern': 'off',
21
+ 'playwright/missing-playwright-await': 'error',
22
+ 'playwright/no-page-pause': 'warn',
23
+ 'playwright/no-element-handle': 'warn',
24
+ 'playwright/no-eval': 'warn',
25
+ 'playwright/no-focused-test': 'error',
26
+ 'playwright/no-skipped-test': 'warn',
27
+ 'playwright/no-wait-for-timeout': 'warn',
28
+ 'playwright/no-force-option': 'warn',
29
+ 'playwright/max-nested-describe': 'warn',
30
+ 'playwright/no-conditional-in-test': 'warn',
27
31
  },
28
32
  },
29
- "jest-playwright": {
30
- plugins: ["jest", "playwright"],
33
+ 'jest-playwright': {
34
+ plugins: ['jest', 'playwright'],
31
35
  env: {
32
- "shared-node-browser": true,
36
+ 'shared-node-browser': true,
33
37
  jest: true,
34
38
  },
35
39
  rules: {
36
- "playwright/missing-playwright-await": "error",
37
- "playwright/no-page-pause": "warn",
38
- "jest/no-standalone-expect": [
39
- "error",
40
+ 'playwright/missing-playwright-await': 'error',
41
+ 'playwright/no-page-pause': 'warn',
42
+ 'jest/no-standalone-expect': [
43
+ 'error',
40
44
  {
41
45
  additionalTestBlockFunctions: [
42
- "test.jestPlaywrightDebug",
43
- "it.jestPlaywrightDebug",
44
- "test.jestPlaywrightSkip",
45
- "it.jestPlaywrightSkip",
46
- "test.jestPlaywrightConfig",
47
- "it.jestPlaywrightConfig",
46
+ 'test.jestPlaywrightDebug',
47
+ 'it.jestPlaywrightDebug',
48
+ 'test.jestPlaywrightSkip',
49
+ 'it.jestPlaywrightSkip',
50
+ 'test.jestPlaywrightConfig',
51
+ 'it.jestPlaywrightConfig',
48
52
  ],
49
53
  },
50
54
  ],
@@ -60,13 +64,15 @@ module.exports = {
60
64
  },
61
65
  },
62
66
  rules: {
63
- "missing-playwright-await": missingPlaywrightAwait,
64
- "no-page-pause": noPagePause,
65
- "no-element-handle": noElementHandle,
66
- "no-eval": noEval,
67
- "no-focused-test": noFocusedTest,
68
- "no-skipped-test": noSkippedTest,
69
- "no-wait-for-timeout": noWaitForTimeout,
70
- "no-force-option": noForceOption,
67
+ 'missing-playwright-await': missingPlaywrightAwait,
68
+ 'no-page-pause': noPagePause,
69
+ 'no-element-handle': noElementHandle,
70
+ 'no-eval': noEval,
71
+ 'no-focused-test': noFocusedTest,
72
+ 'no-skipped-test': noSkippedTest,
73
+ 'no-wait-for-timeout': noWaitForTimeout,
74
+ 'no-force-option': noForceOption,
75
+ 'max-nested-describe': maxNestedDescribe,
76
+ 'no-conditional-in-test': noConditionalInTest,
71
77
  },
72
78
  };
@@ -0,0 +1,70 @@
1
+ const { isCallExpression, isDescribeCall } = require('../utils/ast');
2
+
3
+ /** @type {import('eslint').Rule.RuleModule} */
4
+ module.exports = {
5
+ create(context) {
6
+ const { options } = context;
7
+ const defaultOptions = { max: 5 };
8
+ const { max } = options[0] || defaultOptions;
9
+ const describeCallbackStack = [];
10
+
11
+ function pushDescribeCallback(node) {
12
+ const { parent } = node;
13
+
14
+ if (!isCallExpression(parent) || !isDescribeCall(parent)) {
15
+ return;
16
+ }
17
+
18
+ describeCallbackStack.push(0);
19
+
20
+ if (describeCallbackStack.length > max) {
21
+ context.report({
22
+ node: parent,
23
+ messageId: 'exceededMaxDepth',
24
+ data: { depth: describeCallbackStack.length, max },
25
+ });
26
+ }
27
+ }
28
+
29
+ function popDescribeCallback(node) {
30
+ const { parent } = node;
31
+
32
+ if (isCallExpression(parent) && isDescribeCall(parent)) {
33
+ describeCallbackStack.pop();
34
+ }
35
+ }
36
+
37
+ return {
38
+ FunctionExpression: pushDescribeCallback,
39
+ 'FunctionExpression:exit': popDescribeCallback,
40
+ ArrowFunctionExpression: pushDescribeCallback,
41
+ 'ArrowFunctionExpression:exit': popDescribeCallback,
42
+ };
43
+ },
44
+ meta: {
45
+ docs: {
46
+ category: 'Best Practices',
47
+ description: 'Enforces a maximum depth to nested describe calls',
48
+ recommended: false,
49
+ url: 'https://github.com/playwright-community/eslint-plugin-playwright#max-nested-describe',
50
+ },
51
+ messages: {
52
+ exceededMaxDepth:
53
+ 'Maximum describe call depth exceeded ({{ depth }}). Maximum allowed is {{ max }}.',
54
+ },
55
+ type: 'suggestion',
56
+ fixable: 'code',
57
+ schema: [
58
+ {
59
+ type: 'object',
60
+ properties: {
61
+ max: {
62
+ type: 'integer',
63
+ minimum: 0,
64
+ },
65
+ },
66
+ additionalProperties: false,
67
+ },
68
+ ],
69
+ },
70
+ };
@@ -1,68 +1,71 @@
1
1
  function getMemberPartName(node, part) {
2
- return node[part].type === "Identifier" ? node[part].name : undefined;
2
+ return node[part].type === 'Identifier' ? node[part].name : undefined;
3
3
  }
4
4
 
5
5
  function getMemberExpressionNode(node, matchers) {
6
- const propertyName = getMemberPartName(node, "property");
6
+ const propertyName = getMemberPartName(node, 'property');
7
7
 
8
- if (getMemberPartName(node, "object") === "test") {
9
- return propertyName === "step" ? { node, type: "testStep" } : undefined;
8
+ if (getMemberPartName(node, 'object') === 'test') {
9
+ return propertyName === 'step' ? { node, type: 'testStep' } : undefined;
10
10
  }
11
11
 
12
- return matchers.has(propertyName) ? { node, type: "expect" } : undefined;
12
+ return matchers.has(propertyName) ? { node, type: 'expect' } : undefined;
13
13
  }
14
14
 
15
- function isValid(node) {
16
- const grandparentType =
17
- node.parent && node.parent.parent && node.parent.parent.type;
15
+ const validTypes = new Set([
16
+ 'AwaitExpression',
17
+ 'ReturnStatement',
18
+ 'ArrowFunctionExpression',
19
+ ]);
18
20
 
21
+ function isValid(node) {
19
22
  return (
20
- grandparentType === "AwaitExpression" ||
21
- grandparentType === "ReturnStatement" ||
22
- grandparentType === "ArrowFunctionExpression"
23
+ validTypes.has(node.parent?.type) ||
24
+ validTypes.has(node.parent?.parent?.type)
23
25
  );
24
26
  }
25
27
 
26
28
  const expectPlaywrightMatchers = [
27
- "toBeChecked",
28
- "toBeDisabled",
29
- "toBeEnabled",
30
- "toEqualText", // deprecated
31
- "toEqualUrl",
32
- "toEqualValue",
33
- "toHaveFocus",
34
- "toHaveSelector",
35
- "toHaveSelectorCount",
36
- "toHaveText", // deprecated
37
- "toMatchAttribute",
38
- "toMatchComputedStyle",
39
- "toMatchText",
40
- "toMatchTitle",
41
- "toMatchURL",
42
- "toMatchValue",
29
+ 'toBeChecked',
30
+ 'toBeDisabled',
31
+ 'toBeEnabled',
32
+ 'toEqualText', // deprecated
33
+ 'toEqualUrl',
34
+ 'toEqualValue',
35
+ 'toHaveFocus',
36
+ 'toHaveSelector',
37
+ 'toHaveSelectorCount',
38
+ 'toHaveText', // deprecated
39
+ 'toMatchAttribute',
40
+ 'toMatchComputedStyle',
41
+ 'toMatchText',
42
+ 'toMatchTitle',
43
+ 'toMatchURL',
44
+ 'toMatchValue',
43
45
  ];
44
46
 
45
47
  const playwrightTestMatchers = [
46
- "toBeChecked",
47
- "toBeDisabled",
48
- "toBeEditable",
49
- "toBeEmpty",
50
- "toBeEnabled",
51
- "toBeFocused",
52
- "toBeHidden",
53
- "toBeVisible",
54
- "toContainText",
55
- "toHaveAttribute",
56
- "toHaveClass",
57
- "toHaveCount",
58
- "toHaveCSS",
59
- "toHaveId",
60
- "toHaveJSProperty",
61
- "toBeOK",
62
- "toHaveText",
63
- "toHaveTitle",
64
- "toHaveURL",
65
- "toHaveValue",
48
+ 'toBeChecked',
49
+ 'toBeDisabled',
50
+ 'toBeEditable',
51
+ 'toBeEmpty',
52
+ 'toBeEnabled',
53
+ 'toBeFocused',
54
+ 'toBeHidden',
55
+ 'toBeVisible',
56
+ 'toContainText',
57
+ 'toHaveAttribute',
58
+ 'toHaveClass',
59
+ 'toHaveCount',
60
+ 'toHaveCSS',
61
+ 'toHaveId',
62
+ 'toHaveJSProperty',
63
+ 'toBeOK',
64
+ 'toHaveScreenshot',
65
+ 'toHaveText',
66
+ 'toHaveTitle',
67
+ 'toHaveURL',
68
+ 'toHaveValue',
66
69
  ];
67
70
 
68
71
  module.exports = {
@@ -81,7 +84,7 @@ module.exports = {
81
84
 
82
85
  if (result && !isValid(result.node)) {
83
86
  context.report({
84
- fix: (fixer) => fixer.insertTextBefore(result.node, "await "),
87
+ fix: (fixer) => fixer.insertTextBefore(result.node, 'await '),
85
88
  messageId: result.type,
86
89
  node: result.node,
87
90
  });
@@ -91,11 +94,11 @@ module.exports = {
91
94
  },
92
95
  meta: {
93
96
  docs: {
94
- category: "Possible Errors",
97
+ category: 'Possible Errors',
95
98
  description: `Identify false positives when async Playwright APIs are not properly awaited.`,
96
99
  recommended: true,
97
100
  },
98
- fixable: "code",
101
+ fixable: 'code',
99
102
  messages: {
100
103
  expect: "'expect' matchers must be awaited or returned.",
101
104
  testStep: "'test.step' must be awaited or returned.",
@@ -105,13 +108,13 @@ module.exports = {
105
108
  additionalProperties: false,
106
109
  properties: {
107
110
  customMatchers: {
108
- items: { type: "string" },
109
- type: "array",
111
+ items: { type: 'string' },
112
+ type: 'array',
110
113
  },
111
114
  },
112
- type: "object",
115
+ type: 'object',
113
116
  },
114
117
  ],
115
- type: "problem",
118
+ type: 'problem',
116
119
  },
117
120
  };
@@ -0,0 +1,61 @@
1
+ const {
2
+ isTestIdentifier,
3
+ isDescribeCall,
4
+ isHookCall,
5
+ } = require('../utils/ast');
6
+
7
+ /** @type {import('eslint').Rule.RuleModule} */
8
+ module.exports = {
9
+ create(context) {
10
+ let inTestCase = false;
11
+
12
+ function maybeReportConditional(node) {
13
+ if (inTestCase) {
14
+ context.report({
15
+ messageId: 'conditionalInTest',
16
+ node,
17
+ });
18
+ }
19
+ }
20
+
21
+ return {
22
+ CallExpression(node) {
23
+ const { callee } = node;
24
+ if (
25
+ isTestIdentifier(callee) &&
26
+ !isDescribeCall(node) &&
27
+ !isHookCall(node)
28
+ ) {
29
+ inTestCase = true;
30
+ }
31
+ },
32
+ 'CallExpression:exit'(node) {
33
+ const { callee } = node;
34
+ if (
35
+ isTestIdentifier(callee) &&
36
+ !isDescribeCall(node) &&
37
+ !isHookCall(node)
38
+ ) {
39
+ inTestCase = false;
40
+ }
41
+ },
42
+ IfStatement: maybeReportConditional,
43
+ SwitchStatement: maybeReportConditional,
44
+ ConditionalExpression: maybeReportConditional,
45
+ LogicalExpression: maybeReportConditional,
46
+ };
47
+ },
48
+ meta: {
49
+ docs: {
50
+ category: 'Best Practices',
51
+ description: 'Disallow conditional logic in tests',
52
+ recommended: false,
53
+ url: 'https://github.com/playwright-community/eslint-plugin-playwright#no-conditional-in-test',
54
+ },
55
+ messages: {
56
+ conditionalInTest: 'Avoid having conditionals in tests',
57
+ },
58
+ type: 'problem',
59
+ schema: [],
60
+ },
61
+ };
@@ -1,9 +1,10 @@
1
1
  const { isObject, isCalleeProperty } = require('../utils/ast');
2
2
 
3
3
  function getRange(node) {
4
- const start = node.parent && node.parent.type === 'AwaitExpression'
5
- ? node.parent.range[0]
6
- : node.callee.object.range[0];
4
+ const start =
5
+ node.parent && node.parent.type === 'AwaitExpression'
6
+ ? node.parent.range[0]
7
+ : node.callee.object.range[0];
7
8
 
8
9
  return [start, node.callee.property.range[1]];
9
10
  }
@@ -12,7 +13,10 @@ module.exports = {
12
13
  create(context) {
13
14
  return {
14
15
  CallExpression(node) {
15
- if (isObject(node, 'page') && (isCalleeProperty(node, '$') || isCalleeProperty(node, '$$'))) {
16
+ if (
17
+ isObject(node, 'page') &&
18
+ (isCalleeProperty(node, '$') || isCalleeProperty(node, '$$'))
19
+ ) {
16
20
  context.report({
17
21
  messageId: 'noElementHandle',
18
22
  suggest: [
@@ -20,7 +24,8 @@ module.exports = {
20
24
  messageId: isCalleeProperty(node, '$')
21
25
  ? 'replaceElementHandleWithLocator'
22
26
  : 'replaceElementHandlesWithLocator',
23
- fix: (fixer) => fixer.replaceTextRange(getRange(node), 'page.locator'),
27
+ fix: (fixer) =>
28
+ fixer.replaceTextRange(getRange(node), 'page.locator'),
24
29
  },
25
30
  ],
26
31
  node,
@@ -32,7 +37,8 @@ module.exports = {
32
37
  meta: {
33
38
  docs: {
34
39
  category: 'Possible Errors',
35
- description: 'The use of ElementHandle is discouraged, use Locator instead',
40
+ description:
41
+ 'The use of ElementHandle is discouraged, use Locator instead',
36
42
  recommended: true,
37
43
  url: 'https://github.com/playwright-community/eslint-plugin-playwright#no-element-handle',
38
44
  },
@@ -4,8 +4,14 @@ module.exports = {
4
4
  create(context) {
5
5
  return {
6
6
  CallExpression(node) {
7
- if (isObject(node, 'page') && (isCalleeProperty(node, '$eval') || isCalleeProperty(node, '$$eval'))) {
8
- context.report({ messageId: isCalleeProperty(node, '$eval') ? 'noEval' : 'noEvalAll', node });
7
+ if (
8
+ isObject(node, 'page') &&
9
+ (isCalleeProperty(node, '$eval') || isCalleeProperty(node, '$$eval'))
10
+ ) {
11
+ context.report({
12
+ messageId: isCalleeProperty(node, '$eval') ? 'noEval' : 'noEvalAll',
13
+ node,
14
+ });
9
15
  }
10
16
  },
11
17
  };
@@ -16,14 +16,21 @@ module.exports = {
16
16
  create(context) {
17
17
  return {
18
18
  MemberExpression(node) {
19
- if ((isTestIdentifier(node) || isTestGroup(node)) && hasAnnotation(node, 'only')) {
19
+ if (
20
+ (isTestIdentifier(node) || isTestGroup(node)) &&
21
+ hasAnnotation(node, 'only')
22
+ ) {
20
23
  context.report({
21
24
  messageId: 'noFocusedTest',
22
25
  suggest: [
23
26
  {
24
27
  messageId: 'removeFocusedTestAnnotation',
25
28
  // - 1 to remove the `.only` annotation with dot notation
26
- fix: (fixer) => fixer.removeRange([node.property.range[0] - 1, node.property.range[1]]),
29
+ fix: (fixer) =>
30
+ fixer.removeRange([
31
+ node.property.range[0] - 1,
32
+ node.property.range[1],
33
+ ]),
27
34
  },
28
35
  ],
29
36
  node,
@@ -6,7 +6,10 @@ function isForceOptionEnabled({ parent }) {
6
6
  parent.arguments.some(
7
7
  (argument) =>
8
8
  argument.type === 'ObjectExpression' &&
9
- argument.properties.some(({ key, value }) => key && key.name === 'force' && value && value.value === true)
9
+ argument.properties.some(
10
+ ({ key, value }) =>
11
+ key && key.name === 'force' && value && value.value === true
12
+ )
10
13
  )
11
14
  );
12
15
  }
@@ -31,7 +34,11 @@ module.exports = {
31
34
  create(context) {
32
35
  return {
33
36
  MemberExpression(node) {
34
- if (node.property && methodsWithForceOption.has(node.property.name) && isForceOptionEnabled(node)) {
37
+ if (
38
+ node.property &&
39
+ methodsWithForceOption.has(node.property.name) &&
40
+ isForceOptionEnabled(node)
41
+ ) {
35
42
  context.report({ messageId: 'noForceOption', node });
36
43
  }
37
44
  },
@@ -5,20 +5,20 @@ module.exports = {
5
5
  return {
6
6
  CallExpression(node) {
7
7
  if (isObject(node, 'page') && isCalleeProperty(node, 'pause')) {
8
- context.report({ messageId: "noPagePause", node });
8
+ context.report({ messageId: 'noPagePause', node });
9
9
  }
10
10
  },
11
11
  };
12
12
  },
13
13
  meta: {
14
14
  docs: {
15
- category: "Possible Errors",
16
- description: "Prevent usage of page.pause()",
15
+ category: 'Possible Errors',
16
+ description: 'Prevent usage of page.pause()',
17
17
  recommended: true,
18
18
  },
19
19
  messages: {
20
- noPagePause: "Unexpected use of page.pause().",
20
+ noPagePause: 'Unexpected use of page.pause().',
21
21
  },
22
- type: "problem",
22
+ type: 'problem',
23
23
  },
24
24
  };
@@ -24,9 +24,12 @@ function getSkipRange(node) {
24
24
 
25
25
  const isStandaloneSkip =
26
26
  !node.parent.arguments.length ||
27
- ((isBinaryExpression(first) || isBooleanLiteral(first)) && isStringLiteral(second));
27
+ ((isBinaryExpression(first) || isBooleanLiteral(first)) &&
28
+ isStringLiteral(second));
28
29
 
29
- return isStandaloneSkip ? node.parent.parent.range : [node.property.range[0] - 1, node.property.range[1]];
30
+ return isStandaloneSkip
31
+ ? node.parent.parent.range
32
+ : [node.property.range[0] - 1, node.property.range[1]];
30
33
  }
31
34
 
32
35
  /** @type {import('eslint').Rule.RuleModule} */
@@ -34,7 +37,10 @@ module.exports = {
34
37
  create(context) {
35
38
  return {
36
39
  MemberExpression(node) {
37
- if ((isTestIdentifier(node) || isObjectProperty(node, 'describe')) && hasAnnotation(node, 'skip')) {
40
+ if (
41
+ (isTestIdentifier(node) || isObjectProperty(node, 'describe')) &&
42
+ hasAnnotation(node, 'skip')
43
+ ) {
38
44
  context.report({
39
45
  messageId: 'noSkippedTest',
40
46
  suggest: [
@@ -5,7 +5,10 @@ module.exports = {
5
5
  create(context) {
6
6
  return {
7
7
  CallExpression(node) {
8
- if (isObject(node, 'page') && isCalleeProperty(node, 'waitForTimeout')) {
8
+ if (
9
+ isObject(node, 'page') &&
10
+ isCalleeProperty(node, 'waitForTimeout')
11
+ ) {
9
12
  context.report({
10
13
  messageId: 'noWaitForTimeout',
11
14
  suggest: [
@@ -13,7 +16,9 @@ module.exports = {
13
16
  messageId: 'removeWaitForTimeout',
14
17
  fix: (fixer) =>
15
18
  fixer.remove(
16
- node.parent && node.parent.type !== 'AwaitExpression' ? node.parent : node.parent.parent
19
+ node.parent && node.parent.type !== 'AwaitExpression'
20
+ ? node.parent
21
+ : node.parent.parent
17
22
  ),
18
23
  },
19
24
  ],
package/lib/utils/ast.js CHANGED
@@ -16,8 +16,12 @@ function isCalleeProperty({ callee }, name) {
16
16
  );
17
17
  }
18
18
 
19
+ function isTestObject({ object }) {
20
+ return object && isIdentifier(object, 'test');
21
+ }
22
+
19
23
  function isTestIdentifier(node) {
20
- return node.object && node.object.type === 'Identifier' && node.object.name === 'test';
24
+ return isIdentifier(node, 'test') || isTestObject(node);
21
25
  }
22
26
 
23
27
  function hasAnnotation(node, annotation) {
@@ -37,8 +41,13 @@ function isObjectProperty({ object }, name) {
37
41
  );
38
42
  }
39
43
 
40
- function isStringLiteral(node) {
41
- return node && node.type === 'Literal' && typeof node.value === 'string';
44
+ function isStringLiteral(node, value) {
45
+ return (
46
+ node &&
47
+ node.type === 'Literal' &&
48
+ typeof node.value === 'string' &&
49
+ (value === undefined || node.value === value)
50
+ );
42
51
  }
43
52
 
44
53
  function isBooleanLiteral(node) {
@@ -49,6 +58,66 @@ function isBinaryExpression(node) {
49
58
  return node && node.type === 'BinaryExpression';
50
59
  }
51
60
 
61
+ function isCallExpression(node) {
62
+ return node && node.type === 'CallExpression';
63
+ }
64
+
65
+ function isMemberExpression(node) {
66
+ return node && node.type === 'MemberExpression';
67
+ }
68
+
69
+ function isIdentifier(node, name) {
70
+ return (
71
+ node &&
72
+ node.type === 'Identifier' &&
73
+ (name === undefined || node.name === name)
74
+ );
75
+ }
76
+
77
+ function isDescribeAlias(node) {
78
+ return isIdentifier(node, 'describe');
79
+ }
80
+
81
+ function isDescribeProperty(node) {
82
+ const describeProperties = ['parallel', 'serial', 'only', 'skip'];
83
+ return describeProperties.some((prop) => isIdentifier(node, prop));
84
+ }
85
+
86
+ function isDescribeCall(node) {
87
+ if (isDescribeAlias(node.callee)) {
88
+ return true;
89
+ }
90
+
91
+ const callee =
92
+ node.callee.type === 'TaggedTemplateExpression'
93
+ ? node.callee.tag
94
+ : node.callee.type === 'CallExpression'
95
+ ? node.callee.callee
96
+ : node.callee;
97
+
98
+ if (callee.type === 'MemberExpression' && isDescribeAlias(callee.property)) {
99
+ return true;
100
+ }
101
+
102
+ if (
103
+ callee.type === 'MemberExpression' &&
104
+ isDescribeProperty(callee.property)
105
+ ) {
106
+ return callee.object.type === 'MemberExpression'
107
+ ? callee.object.object.type === 'MemberExpression'
108
+ ? isDescribeAlias(callee.object.object.property)
109
+ : isDescribeAlias(callee.object.property)
110
+ : isDescribeAlias(callee.property) || isDescribeAlias(callee.object);
111
+ }
112
+
113
+ return false;
114
+ }
115
+
116
+ function isHookCall(node) {
117
+ const hooks = ['beforeAll', 'beforeEach', 'afterAll', 'afterEach'];
118
+ return hooks.some((hook) => isCalleeProperty(node, hook));
119
+ }
120
+
52
121
  module.exports = {
53
122
  isObject,
54
123
  isCalleeProperty,
@@ -58,4 +127,8 @@ module.exports = {
58
127
  isStringLiteral,
59
128
  isBooleanLiteral,
60
129
  isBinaryExpression,
130
+ isCallExpression,
131
+ isMemberExpression,
132
+ isDescribeCall,
133
+ isHookCall,
61
134
  };
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.9.0",
4
+ "version": "0.10.0",
5
5
  "main": "lib/index.js",
6
6
  "repository": "https://github.com/playwright-community/eslint-plugin-playwright",
7
7
  "author": "Max Schmitt <max@schmitt.mx>",
@@ -10,11 +10,13 @@
10
10
  "lib"
11
11
  ],
12
12
  "scripts": {
13
+ "format": "prettier --write .",
13
14
  "test": "jest"
14
15
  },
15
16
  "devDependencies": {
16
17
  "eslint": "^8.4.1",
17
- "jest": "^27.4.5"
18
+ "jest": "^27.4.5",
19
+ "prettier": "^2.7.1"
18
20
  },
19
21
  "peerDependencies": {
20
22
  "eslint": ">=7",