From 93749737a8e2b2d93e166d8883691937ca1d2172 Mon Sep 17 00:00:00 2001 From: David Enke Date: Mon, 25 Aug 2025 22:38:57 +0200 Subject: [PATCH 1/9] feat: Add `consistent-spacing-between-blocks` rule --- README.md | 1 + .../consistent-spacing-between-blocks.md | 54 ++++++ src/index.ts | 2 + .../consistent-spacing-between-blocks.test.ts | 126 +++++++++++++ .../consistent-spacing-between-blocks.ts | 169 ++++++++++++++++++ src/utils/test-expression.ts | 83 +++++++++ 6 files changed, 435 insertions(+) create mode 100644 docs/rules/consistent-spacing-between-blocks.md create mode 100644 src/rules/consistent-spacing-between-blocks.test.ts create mode 100644 src/rules/consistent-spacing-between-blocks.ts create mode 100644 src/utils/test-expression.ts diff --git a/README.md b/README.md index dae8f3a8..18f7f5f8 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,7 @@ CLI option\ | Rule | Description | ✅ | 🔧 | 💡 | | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | :-: | :-: | :-: | +| [consistent-spacing-between-blocks](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/consistent-spacing-between-blocks.md) | Enforce consistent spacing between test blocks | ✅ | 🔧 | | | [expect-expect](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/expect-expect.md) | Enforce assertion to be made in a test body | ✅ | | | | [max-expects](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/max-expects.md) | Enforces a maximum number assertion calls in a test body | | | | | [max-nested-describe](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/max-nested-describe.md) | Enforces a maximum depth to nested describe calls | ✅ | | | diff --git a/docs/rules/consistent-spacing-between-blocks.md b/docs/rules/consistent-spacing-between-blocks.md new file mode 100644 index 00000000..cf00160b --- /dev/null +++ b/docs/rules/consistent-spacing-between-blocks.md @@ -0,0 +1,54 @@ +# Enforce consistent spacing between test blocks (`enforce-consistent-spacing-between-blocks`) + +Ensure that there is a consistent spacing between test blocks. + +## Rule Details + +Examples of **incorrect** code for this rule: + +```javascript +test('example 1', () => { + expect(true).toBe(true) +}) +test('example 2', () => { + expect(true).toBe(true) +}) +``` + +```javascript +test.beforeEach(() => {}) +test('example 3', () => { + await test.step('first', async () => { + expect(true).toBe(true) + }) + await test.step('second', async () => { + expect(true).toBe(true) + }) +}) +``` + +Examples of **correct** code for this rule: + +```javascript +test('example 1', () => { + expect(true).toBe(true) +}) + +test('example 2', () => { + expect(true).toBe(true) +}) +``` + +```javascript +test.beforeEach(() => {}) + +test('example 3', () => { + await test.step('first', async () => { + expect(true).toBe(true) + }) + + await test.step('second', async () => { + expect(true).toBe(true) + }) +}) +``` diff --git a/src/index.ts b/src/index.ts index 0101521a..88a373f9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ import globals from 'globals' +import consistentSpacingBetweenBlocks from './rules/consistent-spacing-between-blocks.js' import expectExpect from './rules/expect-expect.js' import maxExpects from './rules/max-expects.js' import maxNestedDescribe from './rules/max-nested-describe.js' @@ -54,6 +55,7 @@ import validTitle from './rules/valid-title.js' const index = { configs: {}, rules: { + 'consistent-spacing-between-blocks': consistentSpacingBetweenBlocks, 'expect-expect': expectExpect, 'max-expects': maxExpects, 'max-nested-describe': maxNestedDescribe, diff --git a/src/rules/consistent-spacing-between-blocks.test.ts b/src/rules/consistent-spacing-between-blocks.test.ts new file mode 100644 index 00000000..c65d5464 --- /dev/null +++ b/src/rules/consistent-spacing-between-blocks.test.ts @@ -0,0 +1,126 @@ +import { javascript, runRuleTester } from '../utils/rule-tester.js' +import rule from './consistent-spacing-between-blocks.js' + +runRuleTester('consistent-spacing-between-blocks', rule, { + invalid: [ + { + code: javascript` + test.beforeEach('Test 1', () => {}); + test('Test 2', async () => { + await test.step('Step 1', () => {}); + // a comment + test.step('Step 2', () => {}); + test.step('Step 3', () => {}); + const foo = await test.step('Step 4', () => {}); + foo = await test.step('Step 5', () => {}); + }); + /** + * another comment + */ + test('Test 6', () => {}); + `, + errors: [ + { messageId: 'missingWhitespace' }, + { messageId: 'missingWhitespace' }, + { messageId: 'missingWhitespace' }, + { messageId: 'missingWhitespace' }, + { messageId: 'missingWhitespace' }, + { messageId: 'missingWhitespace' }, + ], + name: 'missing blank lines before test blocks', + output: javascript` + test.beforeEach('Test 1', () => {}); + + test('Test 2', async () => { + await test.step('Step 1', () => {}); + + // a comment + test.step('Step 2', () => {}); + + test.step('Step 3', () => {}); + + const foo = await test.step('Step 4', () => {}); + + foo = await test.step('Step 5', () => {}); + }); + + /** + * another comment + */ + test('Test 6', () => {}); + `, + }, + ], + valid: [ + { + code: javascript` + test('Test 1', () => {}); + + test('Test 2', () => {}); + `, + name: 'blank line between simple test blocks', + }, + { + code: javascript` + test.beforeEach(() => {}); + + test.skip('Test 2', () => {}); + `, + name: 'blank line between test modifiers', + }, + { + code: javascript` + test('Test', async () => { + await test.step('Step 1', () => {}); + + await test.step('Step 2', () => {}); + }); + `, + name: 'blank line between nested steps in async test', + }, + { + code: javascript` + test('Test', async () => { + await test.step('Step 1', () => {}); + + // some comment + await test.step('Step 2', () => {}); + }); + `, + name: 'nested steps with a line comment in between', + }, + { + code: javascript` + test('Test', async () => { + await test.step('Step 1', () => {}); + + /** + * another comment + */ + await test.step('Step 2', () => {}); + }); + `, + name: 'nested steps with a block comment in between', + }, + { + code: javascript` + test('assign', async () => { + let foo = await test.step('Step 1', () => {}); + + foo = await test.step('Step 2', () => {}); + }); + `, + name: 'assignments initialized by test.step', + }, + { + code: javascript` + test('assign', async () => { + let { foo } = await test.step('Step 1', () => {}); + + ({ foo } = await test.step('Step 2', () => {})); + }); + `, + name: 'destructuring assignments initialized by test.step', + }, + ], +}) diff --git a/src/rules/consistent-spacing-between-blocks.ts b/src/rules/consistent-spacing-between-blocks.ts new file mode 100644 index 00000000..2c3dd247 --- /dev/null +++ b/src/rules/consistent-spacing-between-blocks.ts @@ -0,0 +1,169 @@ +import type { AST } from 'eslint' +import type { Comment, Expression, Node } from 'estree' +import { createRule } from '../utils/createRule.js' +import { isTestExpression, unwrapExpression } from '../utils/test-expression.js' + +/** + * An ESLint rule that ensures consistent spacing between test blocks (e.g. + * `test`, `test.step`, `test.beforeEach`, etc.). This rule helps improve the + * readability and maintainability of test code by ensuring that test blocks are + * clearly separated from each other. + */ +export default createRule({ + create(context) { + /** + * Recursively determines the previous token (if present) and, if necessary, + * a stand-in token to check spacing against. Therefore, the current start + * token can optionally be passed through and used as the comparison token. + * + * Returns the previous token that is not a comment or a grouping expression + * (`previous`), the first token to compare (`start`), and the actual token + * being examined (`origin`). + * + * If there is no previous token for the expression, `null` is returned for + * it. Ideally, the first comparable token is the same as the actual token. + * + * | 1 | test('foo', async () => { + * previous > | 2 | await test.step(...); + * | 3 | + * start > | 4 | // Erster Kommentar + * | 5 | // weiterer Kommentar + * origin > | 6 | await test.step(...); + */ + function getPreviousToken( + node: AST.Token | Node, + start?: AST.Token | Comment | Node, + ): { + /** The token actually being checked */ + origin: AST.Token | Node + + /** + * The previous token that is neither a comment nor a grouping expression, + * if present + */ + previous: AST.Token | null + + /** + * The first token used for comparison, e.g. the start of the test + * expression + */ + start: AST.Token | Comment | Node + } { + const current = start ?? node + const previous = context.sourceCode.getTokenBefore(current, { + includeComments: true, + }) + + // no predecessor present + if ( + previous === null || + previous === undefined || + previous.value === '{' + ) { + return { + origin: node, + previous: null, + start: current, + } + } + + // Recursively traverse comments and determine a stand-in + // and unwrap parenthesized expressions + if ( + previous.type === 'Line' || // line comment + previous.type === 'Block' || // block comment + previous.value === '(' // grouping operator + ) { + return getPreviousToken(node, previous) + } + + // Return result + return { + origin: node, + previous: previous as AST.Token, + start: current, + } + } + + /** + * Checks whether the spacing before the given test block meets + * expectations. Optionally an offset token can be provided to check + * against, for example in the case of an assignment. + * + * @param node - The node to be checked. + * @param offset - Optional offset token to check spacing against. + */ + function checkSpacing(node: Expression, offset?: AST.Token | Node) { + const { previous, start } = getPreviousToken(node, offset) + + // First expression or no previous token + if (previous === null) return + + // Ignore when there is one or more blank lines between + if (previous.loc.end.line < start.loc!.start.line - 1) { + return + } + + // Since the hint in the IDE may not appear on the affected test expression + // but possibly on the preceding comment, include the test expression in the message + const source = context.sourceCode.getText(unwrapExpression(node)) + + context.report({ + data: { source }, + fix(fixer) { + return fixer.insertTextAfter(previous, '\n') + }, + loc: { + end: { + column: start.loc!.start.column, + line: start.loc!.start.line, + }, + start: { + column: 0, + line: previous.loc.end.line + 1, + }, + }, + messageId: 'missingWhitespace', + node, + }) + } + + return { + // Checks call expressions that could be test steps, + // e.g. `test(...)`, `test.step(...)`, or `await test.step(...)`, but also `foo = test(...)` + ExpressionStatement(node) { + if (isTestExpression(context, node.expression)) { + checkSpacing(node.expression) + } + }, + // Checks declarations that might be initialized from return values of test steps, + // e.g. `let result = await test(...)` or `const result = await test.step(...)` + VariableDeclaration(node) { + node.declarations.forEach((declaration) => { + if (declaration.init && isTestExpression(context, declaration.init)) { + // When declaring a variable, our examined test expression is used for initialization. + // Therefore, to check spacing we use the keyword token (let, const, var) before it: + // 1 | const foo = test('foo', () => {}); + // 2 | ^ + const offset = context.sourceCode.getTokenBefore(declaration) + checkSpacing(declaration.init, offset ?? undefined) + } + }) + }, + } + }, + meta: { + docs: { + description: + 'Enforces a blank line between Playwright test blocks (e.g., test, test.step, test.beforeEach, etc.).', + recommended: true, + }, + fixable: 'whitespace', + messages: { + missingWhitespace: + "A blank line is required before the test block '{{source}}'.", + }, + schema: [], + type: 'layout', + }, +}) diff --git a/src/utils/test-expression.ts b/src/utils/test-expression.ts new file mode 100644 index 00000000..dd7192de --- /dev/null +++ b/src/utils/test-expression.ts @@ -0,0 +1,83 @@ +import type { Rule } from 'eslint' +import type { Expression } from 'estree' +import { parseFnCall } from '../utils/parseFnCall.js' + +/** + * Unwraps a given expression to get the actual expression being called. This is + * useful when checking the final expression specifically. + * + * This handles: + * + * - Assignment expressions like `result = test(...)` + * - Async calls like `await test(...)` + * - Chained function calls like `test.step().then(...)` + * + * @param node - The expression to unwrap + * @returns The unwrapped expression + */ +export function unwrapExpression(node: Expression): Expression { + // Resolve assignments to get the actual expression + if (node.type === 'AssignmentExpression') { + return unwrapExpression(node.right) + } + + // Resolve async await expressions + if (node.type === 'AwaitExpression') { + return unwrapExpression(node.argument) + } + + // Traverse chains recursively to find the actual call + if ( + node.type === 'CallExpression' && + node.callee.type === 'MemberExpression' && + node.callee.object.type === 'CallExpression' + ) { + return unwrapExpression(node.callee.object) + } + + return node +} + +/** + * Checks if a given expression is a test-related call. A test call is a call to + * `test(...)` or one of its methods like `test.step(...)`. + * + * If the expression is chained, the calls are recursively traced back to find + * the actual call. This also handles assignments and async calls with `await`. + * + * @param context - The ESLint rule context + * @param node - The expression to check + * @param methods - Optional list of specific methods to check for + * @returns Whether it's a test block call + */ +export function isTestExpression( + context: Rule.RuleContext, + node: Expression, + methods?: string[], +): boolean { + // Unwrap the actual expression to check the call + const unwrapped = unwrapExpression(node) + + // Must be a call expression to be a test call + if (unwrapped.type !== 'CallExpression') { + return false + } + + // Use the existing parseFnCall to identify test-related calls + const call = parseFnCall(context, unwrapped) + if (!call) { + return false + } + + // If specific methods are requested, check if it's one of them + if (methods !== undefined) { + return ( + call.type === 'step' || + call.type === 'hook' || + (call.type === 'test' && methods.includes('test')) + ) + } + + // Check if it's any test-related call + return ['test', 'step', 'hook'].includes(call.type) +} From 854a9dc77d38aa40014a25fd7a2ffd2dd2262730 Mon Sep 17 00:00:00 2001 From: David Enke Date: Wed, 1 Oct 2025 13:08:22 +0200 Subject: [PATCH 2/9] chore: remove comments as requested in review --- .../consistent-spacing-between-blocks.ts | 69 +------------------ 1 file changed, 3 insertions(+), 66 deletions(-) diff --git a/src/rules/consistent-spacing-between-blocks.ts b/src/rules/consistent-spacing-between-blocks.ts index 2c3dd247..fea99de4 100644 --- a/src/rules/consistent-spacing-between-blocks.ts +++ b/src/rules/consistent-spacing-between-blocks.ts @@ -3,50 +3,14 @@ import type { Comment, Expression, Node } from 'estree' import { createRule } from '../utils/createRule.js' import { isTestExpression, unwrapExpression } from '../utils/test-expression.js' -/** - * An ESLint rule that ensures consistent spacing between test blocks (e.g. - * `test`, `test.step`, `test.beforeEach`, etc.). This rule helps improve the - * readability and maintainability of test code by ensuring that test blocks are - * clearly separated from each other. - */ export default createRule({ create(context) { - /** - * Recursively determines the previous token (if present) and, if necessary, - * a stand-in token to check spacing against. Therefore, the current start - * token can optionally be passed through and used as the comparison token. - * - * Returns the previous token that is not a comment or a grouping expression - * (`previous`), the first token to compare (`start`), and the actual token - * being examined (`origin`). - * - * If there is no previous token for the expression, `null` is returned for - * it. Ideally, the first comparable token is the same as the actual token. - * - * | 1 | test('foo', async () => { - * previous > | 2 | await test.step(...); - * | 3 | - * start > | 4 | // Erster Kommentar - * | 5 | // weiterer Kommentar - * origin > | 6 | await test.step(...); - */ function getPreviousToken( node: AST.Token | Node, start?: AST.Token | Comment | Node, ): { - /** The token actually being checked */ origin: AST.Token | Node - - /** - * The previous token that is neither a comment nor a grouping expression, - * if present - */ previous: AST.Token | null - - /** - * The first token used for comparison, e.g. the start of the test - * expression - */ start: AST.Token | Comment | Node } { const current = start ?? node @@ -54,7 +18,6 @@ export default createRule({ includeComments: true, }) - // no predecessor present if ( previous === null || previous === undefined || @@ -67,17 +30,14 @@ export default createRule({ } } - // Recursively traverse comments and determine a stand-in - // and unwrap parenthesized expressions if ( - previous.type === 'Line' || // line comment - previous.type === 'Block' || // block comment - previous.value === '(' // grouping operator + previous.type === 'Line' || + previous.type === 'Block' || + previous.value === '(' ) { return getPreviousToken(node, previous) } - // Return result return { origin: node, previous: previous as AST.Token, @@ -85,29 +45,14 @@ export default createRule({ } } - /** - * Checks whether the spacing before the given test block meets - * expectations. Optionally an offset token can be provided to check - * against, for example in the case of an assignment. - * - * @param node - The node to be checked. - * @param offset - Optional offset token to check spacing against. - */ function checkSpacing(node: Expression, offset?: AST.Token | Node) { const { previous, start } = getPreviousToken(node, offset) - - // First expression or no previous token if (previous === null) return - - // Ignore when there is one or more blank lines between if (previous.loc.end.line < start.loc!.start.line - 1) { return } - // Since the hint in the IDE may not appear on the affected test expression - // but possibly on the preceding comment, include the test expression in the message const source = context.sourceCode.getText(unwrapExpression(node)) - context.report({ data: { source }, fix(fixer) { @@ -129,22 +74,14 @@ export default createRule({ } return { - // Checks call expressions that could be test steps, - // e.g. `test(...)`, `test.step(...)`, or `await test.step(...)`, but also `foo = test(...)` ExpressionStatement(node) { if (isTestExpression(context, node.expression)) { checkSpacing(node.expression) } }, - // Checks declarations that might be initialized from return values of test steps, - // e.g. `let result = await test(...)` or `const result = await test.step(...)` VariableDeclaration(node) { node.declarations.forEach((declaration) => { if (declaration.init && isTestExpression(context, declaration.init)) { - // When declaring a variable, our examined test expression is used for initialization. - // Therefore, to check spacing we use the keyword token (let, const, var) before it: - // 1 | const foo = test('foo', () => {}); - // 2 | ^ const offset = context.sourceCode.getTokenBefore(declaration) checkSpacing(declaration.init, offset ?? undefined) } From 500add1ce0ced331fb6de7a244145d7c892b0de8 Mon Sep 17 00:00:00 2001 From: David Enke Date: Wed, 1 Oct 2025 13:11:49 +0200 Subject: [PATCH 3/9] test: expect errors for line numbers --- src/rules/consistent-spacing-between-blocks.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/rules/consistent-spacing-between-blocks.test.ts b/src/rules/consistent-spacing-between-blocks.test.ts index c65d5464..58f6a507 100644 --- a/src/rules/consistent-spacing-between-blocks.test.ts +++ b/src/rules/consistent-spacing-between-blocks.test.ts @@ -20,12 +20,12 @@ runRuleTester('consistent-spacing-between-blocks', rule, { test('Test 6', () => {}); `, errors: [ - { messageId: 'missingWhitespace' }, - { messageId: 'missingWhitespace' }, - { messageId: 'missingWhitespace' }, - { messageId: 'missingWhitespace' }, - { messageId: 'missingWhitespace' }, - { messageId: 'missingWhitespace' }, + { line: 2, messageId: 'missingWhitespace' }, + { line: 4, messageId: 'missingWhitespace' }, + { line: 6, messageId: 'missingWhitespace' }, + { line: 7, messageId: 'missingWhitespace' }, + { line: 8, messageId: 'missingWhitespace' }, + { line: 10, messageId: 'missingWhitespace' }, ], name: 'missing blank lines before test blocks', output: javascript` From 66d0690075c064aaef99d4184e9d879418e2c5e7 Mon Sep 17 00:00:00 2001 From: David Enke Date: Wed, 1 Oct 2025 13:13:33 +0200 Subject: [PATCH 4/9] chore: add rule to recommended config but turn it off by default to be opt-in --- src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.ts b/src/index.ts index 88a373f9..6dab5f38 100644 --- a/src/index.ts +++ b/src/index.ts @@ -112,6 +112,7 @@ const index = { const sharedConfig = { rules: { + 'consistent-spacing-between-blocks': 'off', 'no-empty-pattern': 'off', 'playwright/expect-expect': 'warn', 'playwright/max-nested-describe': 'warn', From 15ae0dbbda7db5471154bb04d815ef094433c7ba Mon Sep 17 00:00:00 2001 From: David Enke Date: Wed, 1 Oct 2025 15:33:15 +0200 Subject: [PATCH 5/9] test: adopt wording of other tests --- .../consistent-spacing-between-blocks.test.ts | 64 +++++++++---------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/src/rules/consistent-spacing-between-blocks.test.ts b/src/rules/consistent-spacing-between-blocks.test.ts index 58f6a507..e720e16b 100644 --- a/src/rules/consistent-spacing-between-blocks.test.ts +++ b/src/rules/consistent-spacing-between-blocks.test.ts @@ -5,19 +5,19 @@ runRuleTester('consistent-spacing-between-blocks', rule, { invalid: [ { code: javascript` - test.beforeEach('Test 1', () => {}); - test('Test 2', async () => { - await test.step('Step 1', () => {}); + test.beforeEach('should pass', () => {}); + test('should fail', async () => { + await test.step('should pass', () => {}); // a comment - test.step('Step 2', () => {}); - test.step('Step 3', () => {}); - const foo = await test.step('Step 4', () => {}); - foo = await test.step('Step 5', () => {}); + test.step('should fail', () => {}); + test.step('should fail', () => {}); + const foo = await test.step('should fail', () => {}); + foo = await test.step('should fail', () => {}); }); /** * another comment */ - test('Test 6', () => {}); + test('should fail', () => {}); `, errors: [ { line: 2, messageId: 'missingWhitespace' }, @@ -29,34 +29,34 @@ runRuleTester('consistent-spacing-between-blocks', rule, { ], name: 'missing blank lines before test blocks', output: javascript` - test.beforeEach('Test 1', () => {}); + test.beforeEach('should pass', () => {}); - test('Test 2', async () => { - await test.step('Step 1', () => {}); + test('should fail', async () => { + await test.step('should pass', () => {}); // a comment - test.step('Step 2', () => {}); + test.step('should fail', () => {}); - test.step('Step 3', () => {}); + test.step('should fail', () => {}); - const foo = await test.step('Step 4', () => {}); + const foo = await test.step('should fail', () => {}); - foo = await test.step('Step 5', () => {}); + foo = await test.step('should fail', () => {}); }); /** * another comment */ - test('Test 6', () => {}); + test('should fail', () => {}); `, }, ], valid: [ { code: javascript` - test('Test 1', () => {}); + test('should pass', () => {}); - test('Test 2', () => {}); + test('should pass', () => {}); `, name: 'blank line between simple test blocks', }, @@ -64,40 +64,40 @@ runRuleTester('consistent-spacing-between-blocks', rule, { code: javascript` test.beforeEach(() => {}); - test.skip('Test 2', () => {}); + test.skip('should pass', () => {}); `, name: 'blank line between test modifiers', }, { code: javascript` - test('Test', async () => { - await test.step('Step 1', () => {}); + test('should pass', async () => { + await test.step('should pass', () => {}); - await test.step('Step 2', () => {}); + await test.step('should pass', () => {}); }); `, name: 'blank line between nested steps in async test', }, { code: javascript` - test('Test', async () => { - await test.step('Step 1', () => {}); + test('should pass', async () => { + await test.step('should pass', () => {}); // some comment - await test.step('Step 2', () => {}); + await test.step('should pass', () => {}); }); `, name: 'nested steps with a line comment in between', }, { code: javascript` - test('Test', async () => { - await test.step('Step 1', () => {}); + test('should pass', async () => { + await test.step('should pass', () => {}); /** * another comment */ - await test.step('Step 2', () => {}); + await test.step('should pass', () => {}); }); `, name: 'nested steps with a block comment in between', @@ -105,9 +105,9 @@ runRuleTester('consistent-spacing-between-blocks', rule, { { code: javascript` test('assign', async () => { - let foo = await test.step('Step 1', () => {}); + let foo = await test.step('should pass', () => {}); - foo = await test.step('Step 2', () => {}); + foo = await test.step('should pass', () => {}); }); `, name: 'assignments initialized by test.step', @@ -115,9 +115,9 @@ runRuleTester('consistent-spacing-between-blocks', rule, { { code: javascript` test('assign', async () => { - let { foo } = await test.step('Step 1', () => {}); + let { foo } = await test.step('should pass', () => {}); - ({ foo } = await test.step('Step 2', () => {})); + ({ foo } = await test.step('should pass', () => {})); }); `, name: 'destructuring assignments initialized by test.step', From b420456d70315bc729f762f1fbe72e8a0819ee7a Mon Sep 17 00:00:00 2001 From: David Enke Date: Wed, 1 Oct 2025 15:34:23 +0200 Subject: [PATCH 6/9] refactor: use existing utils directly and adopt logic from existing official rules, like [`newline-before-return`](https://github.com/eslint/eslint/blob/main/lib/rules/newline-before-return.js) --- .../consistent-spacing-between-blocks.ts | 199 ++++++++++++------ src/utils/test-expression.ts | 83 -------- 2 files changed, 137 insertions(+), 145 deletions(-) delete mode 100644 src/utils/test-expression.ts diff --git a/src/rules/consistent-spacing-between-blocks.ts b/src/rules/consistent-spacing-between-blocks.ts index fea99de4..52f84069 100644 --- a/src/rules/consistent-spacing-between-blocks.ts +++ b/src/rules/consistent-spacing-between-blocks.ts @@ -1,92 +1,167 @@ -import type { AST } from 'eslint' -import type { Comment, Expression, Node } from 'estree' +import type { Rule } from 'eslint' +import type { CallExpression } from 'estree' +import { getParent } from '../utils/ast.js' import { createRule } from '../utils/createRule.js' -import { isTestExpression, unwrapExpression } from '../utils/test-expression.js' +import { parseFnCall } from '../utils/parseFnCall.js' export default createRule({ create(context) { - function getPreviousToken( - node: AST.Token | Node, - start?: AST.Token | Comment | Node, - ): { - origin: AST.Token | Node - previous: AST.Token | null - start: AST.Token | Comment | Node - } { - const current = start ?? node - const previous = context.sourceCode.getTokenBefore(current, { - includeComments: true, - }) + const { sourceCode } = context + + function isPrecededByTokens(node: Rule.Node, testTokens: string[]) { + const tokenBefore = sourceCode.getTokenBefore(node) + return tokenBefore && testTokens.includes(tokenBefore.value as string) + } + function isFirstNode(node: Rule.Node) { + const parent = getParent(node) + if (!parent) return true + + const parentType = parent.type if ( - previous === null || - previous === undefined || - previous.value === '{' + parentType === 'ExpressionStatement' || + parentType === 'VariableDeclaration' ) { - return { - origin: node, - previous: null, - start: current, + const realParent = getParent(parent) + if ('body' in realParent && realParent.body) { + const body = realParent.body as unknown + return Array.isArray(body) ? body[0] === node : body === parent } + return false } - if ( - previous.type === 'Line' || - previous.type === 'Block' || - previous.value === '(' - ) { - return getPreviousToken(node, previous) + if (parentType === 'IfStatement') { + return isPrecededByTokens(node as any, ['else', ')']) } - return { - origin: node, - previous: previous as AST.Token, - start: current, + if (parentType === 'DoWhileStatement') { + return isPrecededByTokens(node as any, ['do']) } + + if (parentType === 'SwitchCase') { + return isPrecededByTokens(node as any, [':']) + } + + if ('body' in parent && parent.body) { + const body = parent.body as unknown + return Array.isArray(body) ? body[0] === node : body === node + } + + return isPrecededByTokens(node as any, [')']) + } + + function calcCommentLines(node: Rule.Node, lineNumTokenBefore: number) { + const comments = sourceCode.getCommentsBefore(node) + let numLinesComments = 0 + + if (!comments.length) { + return numLinesComments + } + + comments.forEach((comment) => { + numLinesComments++ + + if (comment.type === 'Block') { + numLinesComments += comment.loc!.end.line - comment.loc!.start.line + } + + // avoid counting lines with inline comments twice + if (comment.loc!.start.line === lineNumTokenBefore) { + numLinesComments-- + } + + if (comment.loc!.end.line === node.loc!.start.line) { + numLinesComments-- + } + }) + + return numLinesComments + } + + function hasNewlineBefore(node: Rule.Node) { + const tokenBefore = sourceCode.getTokenBefore(node) + const lineNumTokenBefore = !tokenBefore ? 0 : tokenBefore.loc.end.line + const lineNumNode = node.loc!.start.line + const commentLines = calcCommentLines(node, lineNumTokenBefore) + + return lineNumNode - lineNumTokenBefore - commentLines > 1 } - function checkSpacing(node: Expression, offset?: AST.Token | Node) { - const { previous, start } = getPreviousToken(node, offset) - if (previous === null) return - if (previous.loc.end.line < start.loc!.start.line - 1) { - return + function getRealNodeToCheck( + node: CallExpression & Rule.NodeParentExtension, + ) { + const parent = getParent(node) + if (!parent) return node + + if (parent.type === 'ExpressionStatement') { + return parent + } + if (parent.type === 'AwaitExpression') { + const awaitParent = getParent(parent) + return awaitParent.type === 'ExpressionStatement' + ? awaitParent + : getParent(awaitParent) + } + if ( + parent.type === 'VariableDeclarator' || + parent.type === 'AssignmentExpression' + ) { + return getParent(parent) } - const source = context.sourceCode.getText(unwrapExpression(node)) + return node + } + + function checkSpacing(node: CallExpression & Rule.NodeParentExtension) { + const nodeToCheck = getRealNodeToCheck(node) + + if (isFirstNode(nodeToCheck)) return + if (hasNewlineBefore(nodeToCheck)) return + + const leadingComments = sourceCode.getCommentsBefore(nodeToCheck) + const firstComment = leadingComments[0] + const reportLoc = firstComment?.loc ?? nodeToCheck.loc + context.report({ - data: { source }, - fix(fixer) { - return fixer.insertTextAfter(previous, '\n') + data: { + source: sourceCode.getText(nodeToCheck).split('\n')[0], }, - loc: { - end: { - column: start.loc!.start.column, - line: start.loc!.start.line, - }, - start: { - column: 0, - line: previous.loc.end.line + 1, - }, + fix(fixer) { + const tokenBefore = sourceCode.getTokenBefore(nodeToCheck) + if (!tokenBefore) return null + + const newlines = + nodeToCheck.loc?.start.line === tokenBefore.loc.end.line + ? '\n\n' + : '\n' + const targetNode = firstComment ?? nodeToCheck + const nodeStart = targetNode.range?.[0] ?? 0 + const textBeforeNode = sourceCode.text.substring(0, nodeStart) + const lastNewlineIndex = textBeforeNode.lastIndexOf('\n') + const insertPosition = lastNewlineIndex + 1 + + return fixer.insertTextBeforeRange( + [insertPosition, nodeStart], + newlines, + ) }, + loc: reportLoc!, messageId: 'missingWhitespace', - node, + node: nodeToCheck, }) } return { - ExpressionStatement(node) { - if (isTestExpression(context, node.expression)) { - checkSpacing(node.expression) + CallExpression(node) { + const call = parseFnCall(context, node) + if ( + call?.type === 'test' || + call?.type === 'hook' || + call?.type === 'step' + ) { + checkSpacing(node) } }, - VariableDeclaration(node) { - node.declarations.forEach((declaration) => { - if (declaration.init && isTestExpression(context, declaration.init)) { - const offset = context.sourceCode.getTokenBefore(declaration) - checkSpacing(declaration.init, offset ?? undefined) - } - }) - }, } }, meta: { diff --git a/src/utils/test-expression.ts b/src/utils/test-expression.ts deleted file mode 100644 index dd7192de..00000000 --- a/src/utils/test-expression.ts +++ /dev/null @@ -1,83 +0,0 @@ -import type { Rule } from 'eslint' -import type { Expression } from 'estree' -import { parseFnCall } from '../utils/parseFnCall.js' - -/** - * Unwraps a given expression to get the actual expression being called. This is - * useful when checking the final expression specifically. - * - * This handles: - * - * - Assignment expressions like `result = test(...)` - * - Async calls like `await test(...)` - * - Chained function calls like `test.step().then(...)` - * - * @param node - The expression to unwrap - * @returns The unwrapped expression - */ -export function unwrapExpression(node: Expression): Expression { - // Resolve assignments to get the actual expression - if (node.type === 'AssignmentExpression') { - return unwrapExpression(node.right) - } - - // Resolve async await expressions - if (node.type === 'AwaitExpression') { - return unwrapExpression(node.argument) - } - - // Traverse chains recursively to find the actual call - if ( - node.type === 'CallExpression' && - node.callee.type === 'MemberExpression' && - node.callee.object.type === 'CallExpression' - ) { - return unwrapExpression(node.callee.object) - } - - return node -} - -/** - * Checks if a given expression is a test-related call. A test call is a call to - * `test(...)` or one of its methods like `test.step(...)`. - * - * If the expression is chained, the calls are recursively traced back to find - * the actual call. This also handles assignments and async calls with `await`. - * - * @param context - The ESLint rule context - * @param node - The expression to check - * @param methods - Optional list of specific methods to check for - * @returns Whether it's a test block call - */ -export function isTestExpression( - context: Rule.RuleContext, - node: Expression, - methods?: string[], -): boolean { - // Unwrap the actual expression to check the call - const unwrapped = unwrapExpression(node) - - // Must be a call expression to be a test call - if (unwrapped.type !== 'CallExpression') { - return false - } - - // Use the existing parseFnCall to identify test-related calls - const call = parseFnCall(context, unwrapped) - if (!call) { - return false - } - - // If specific methods are requested, check if it's one of them - if (methods !== undefined) { - return ( - call.type === 'step' || - call.type === 'hook' || - (call.type === 'test' && methods.includes('test')) - ) - } - - // Check if it's any test-related call - return ['test', 'step', 'hook'].includes(call.type) -} From 65cf800a75b7e198c4461f6c3814c06ea345c969 Mon Sep 17 00:00:00 2001 From: David Enke Date: Wed, 1 Oct 2025 15:35:55 +0200 Subject: [PATCH 7/9] docs: format readme --- README.md | 108 +++++++++++++++++++++++++++--------------------------- 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 18f7f5f8..02407eff 100644 --- a/README.md +++ b/README.md @@ -116,57 +116,57 @@ CLI option\ 💡 Manually fixable by [editor suggestions](https://eslint.org/docs/latest/developer-guide/working-with-rules#providing-suggestions) -| Rule | Description | ✅ | 🔧 | 💡 | -| --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | :-: | :-: | :-: | -| [consistent-spacing-between-blocks](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/consistent-spacing-between-blocks.md) | Enforce consistent spacing between test blocks | ✅ | 🔧 | | -| [expect-expect](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/expect-expect.md) | Enforce assertion to be made in a test body | ✅ | | | -| [max-expects](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/max-expects.md) | Enforces a maximum number assertion calls in a test body | | | | -| [max-nested-describe](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/max-nested-describe.md) | Enforces a maximum depth to nested describe calls | ✅ | | | -| [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 | ✅ | 🔧 | | -| [no-commented-out-tests](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-commented-out-tests.md) | Disallow commented out tests | | | | -| [no-conditional-expect](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-conditional-expect.md) | Disallow calling `expect` conditionally | ✅ | | | -| [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 | ✅ | | | -| [no-duplicate-hooks](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-duplicate-hooks.md) | Disallow duplicate setup and teardown hooks | | | | -| [no-element-handle](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-element-handle.md) | Disallow usage of element handles | ✅ | | 💡 | -| [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()` | ✅ | | | -| [no-focused-test](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-focused-test.md) | Disallow usage of `.only` annotation | ✅ | | 💡 | -| [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 | ✅ | | | -| [no-get-by-title](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-get-by-title.md) | Disallow using `getByTitle()` | | 🔧 | | -| [no-hooks](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-hooks.md) | Disallow setup and teardown hooks | | | | -| [no-nested-step](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-nested-step.md) | Disallow nested `test.step()` methods | ✅ | | | -| [no-networkidle](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-networkidle.md) | Disallow usage of the `networkidle` option | ✅ | | | -| [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 | | | | -| [no-page-pause](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-page-pause.md) | Disallow using `page.pause()` | ✅ | | | -| [no-raw-locators](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-raw-locators.md) | Disallow using raw locators | | | | -| [no-restricted-matchers](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-restricted-matchers.md) | Disallow specific matchers & modifiers | | | | -| [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 | ✅ | | 💡 | -| [no-slowed-test](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-slowed-test.md) | Disallow usage of the `.slow` annotation | | | 💡 | -| [no-standalone-expect](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-standalone-expect.md) | Disallow using expect outside of `test` blocks | ✅ | | | -| [no-unsafe-references](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-unsafe-references.md) | Prevent unsafe variable references in `page.evaluate()` | ✅ | 🔧 | | -| [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 | ✅ | 🔧 | | -| [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 | ✅ | 🔧 | | -| [no-wait-for-navigation](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-wait-for-navigation.md) | Disallow usage of `page.waitForNavigation()` | ✅ | | 💡 | -| [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()` | ✅ | | 💡 | -| [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()` | ✅ | | 💡 | -| [prefer-comparison-matcher](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-comparison-matcher.md) | Suggest using the built-in comparison matchers | | 🔧 | | -| [prefer-equality-matcher](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-equality-matcher.md) | Suggest using the built-in equality matchers | | | 💡 | -| [prefer-hooks-in-order](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-hooks-in-order.md) | Prefer having hooks in a consistent order | | | | -| [prefer-hooks-on-top](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-hooks-on-top.md) | Suggest having hooks before any test cases | | | | -| [prefer-lowercase-title](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-lowercase-title.md) | Enforce lowercase test names | | 🔧 | | -| [prefer-native-locators](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-native-locators.md) | Suggest built-in locators over `page.locator()` | | 🔧 | | -| [prefer-locator](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-locator.md) | Suggest locators over page methods | | | | -| [prefer-strict-equal](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-strict-equal.md) | Suggest using `toStrictEqual()` | | | 💡 | -| [prefer-to-be](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-to-be.md) | Suggest using `toBe()` | | 🔧 | | -| [prefer-to-contain](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-to-contain.md) | Suggest using `toContain()` | | 🔧 | | -| [prefer-to-have-count](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-to-have-count.md) | Suggest using `toHaveCount()` | | 🔧 | | -| [prefer-to-have-length](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-to-have-length.md) | Suggest using `toHaveLength()` | | 🔧 | | -| [prefer-web-first-assertions](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-web-first-assertions.md) | Suggest using web first assertions | ✅ | 🔧 | | -| [require-hook](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/require-hook.md) | Require setup and teardown code to be within a hook | | | | -| [require-soft-assertions](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/require-soft-assertions.md) | Require assertions to use `expect.soft()` | | 🔧 | | -| [require-to-throw-message](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/require-to-throw-message.md) | Require a message for `toThrow()` | | | | -| [require-top-level-describe](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/require-top-level-describe.md) | Require test cases and hooks to be inside a `test.describe` block | | | | -| [valid-describe-callback](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/valid-describe-callback.md) | Enforce valid `describe()` callback | ✅ | | | -| [valid-expect-in-promise](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/valid-expect-in-promise.md) | Require promises that have expectations in their chain to be valid | ✅ | | | -| [valid-expect](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/valid-expect.md) | Enforce valid `expect()` usage | ✅ | | | -| [valid-title](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/valid-title.md) | Enforce valid titles | ✅ | 🔧 | | -| [valid-test-tags](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/valid-test-tags.md) | Enforce valid tag format in test blocks | ✅ | | | +| Rule | Description | ✅ | 🔧 | 💡 | +| --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | :-: | :-: | :-: | +| [consistent-spacing-between-blocks](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/consistent-spacing-between-blocks.md) | Enforce consistent spacing between test blocks | ✅ | 🔧 | | +| [expect-expect](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/expect-expect.md) | Enforce assertion to be made in a test body | ✅ | | | +| [max-expects](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/max-expects.md) | Enforces a maximum number assertion calls in a test body | | | | +| [max-nested-describe](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/max-nested-describe.md) | Enforces a maximum depth to nested describe calls | ✅ | | | +| [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 | ✅ | 🔧 | | +| [no-commented-out-tests](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-commented-out-tests.md) | Disallow commented out tests | | | | +| [no-conditional-expect](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-conditional-expect.md) | Disallow calling `expect` conditionally | ✅ | | | +| [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 | ✅ | | | +| [no-duplicate-hooks](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-duplicate-hooks.md) | Disallow duplicate setup and teardown hooks | | | | +| [no-element-handle](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-element-handle.md) | Disallow usage of element handles | ✅ | | 💡 | +| [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()` | ✅ | | | +| [no-focused-test](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-focused-test.md) | Disallow usage of `.only` annotation | ✅ | | 💡 | +| [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 | ✅ | | | +| [no-get-by-title](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-get-by-title.md) | Disallow using `getByTitle()` | | 🔧 | | +| [no-hooks](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-hooks.md) | Disallow setup and teardown hooks | | | | +| [no-nested-step](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-nested-step.md) | Disallow nested `test.step()` methods | ✅ | | | +| [no-networkidle](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-networkidle.md) | Disallow usage of the `networkidle` option | ✅ | | | +| [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 | | | | +| [no-page-pause](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-page-pause.md) | Disallow using `page.pause()` | ✅ | | | +| [no-raw-locators](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-raw-locators.md) | Disallow using raw locators | | | | +| [no-restricted-matchers](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-restricted-matchers.md) | Disallow specific matchers & modifiers | | | | +| [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 | ✅ | | 💡 | +| [no-slowed-test](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-slowed-test.md) | Disallow usage of the `.slow` annotation | | | 💡 | +| [no-standalone-expect](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-standalone-expect.md) | Disallow using expect outside of `test` blocks | ✅ | | | +| [no-unsafe-references](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-unsafe-references.md) | Prevent unsafe variable references in `page.evaluate()` | ✅ | 🔧 | | +| [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 | ✅ | 🔧 | | +| [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 | ✅ | 🔧 | | +| [no-wait-for-navigation](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-wait-for-navigation.md) | Disallow usage of `page.waitForNavigation()` | ✅ | | 💡 | +| [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()` | ✅ | | 💡 | +| [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()` | ✅ | | 💡 | +| [prefer-comparison-matcher](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-comparison-matcher.md) | Suggest using the built-in comparison matchers | | 🔧 | | +| [prefer-equality-matcher](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-equality-matcher.md) | Suggest using the built-in equality matchers | | | 💡 | +| [prefer-hooks-in-order](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-hooks-in-order.md) | Prefer having hooks in a consistent order | | | | +| [prefer-hooks-on-top](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-hooks-on-top.md) | Suggest having hooks before any test cases | | | | +| [prefer-lowercase-title](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-lowercase-title.md) | Enforce lowercase test names | | 🔧 | | +| [prefer-native-locators](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-native-locators.md) | Suggest built-in locators over `page.locator()` | | 🔧 | | +| [prefer-locator](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-locator.md) | Suggest locators over page methods | | | | +| [prefer-strict-equal](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-strict-equal.md) | Suggest using `toStrictEqual()` | | | 💡 | +| [prefer-to-be](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-to-be.md) | Suggest using `toBe()` | | 🔧 | | +| [prefer-to-contain](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-to-contain.md) | Suggest using `toContain()` | | 🔧 | | +| [prefer-to-have-count](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-to-have-count.md) | Suggest using `toHaveCount()` | | 🔧 | | +| [prefer-to-have-length](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-to-have-length.md) | Suggest using `toHaveLength()` | | 🔧 | | +| [prefer-web-first-assertions](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-web-first-assertions.md) | Suggest using web first assertions | ✅ | 🔧 | | +| [require-hook](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/require-hook.md) | Require setup and teardown code to be within a hook | | | | +| [require-soft-assertions](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/require-soft-assertions.md) | Require assertions to use `expect.soft()` | | 🔧 | | +| [require-to-throw-message](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/require-to-throw-message.md) | Require a message for `toThrow()` | | | | +| [require-top-level-describe](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/require-top-level-describe.md) | Require test cases and hooks to be inside a `test.describe` block | | | | +| [valid-describe-callback](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/valid-describe-callback.md) | Enforce valid `describe()` callback | ✅ | | | +| [valid-expect-in-promise](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/valid-expect-in-promise.md) | Require promises that have expectations in their chain to be valid | ✅ | | | +| [valid-expect](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/valid-expect.md) | Enforce valid `expect()` usage | ✅ | | | +| [valid-title](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/valid-title.md) | Enforce valid titles | ✅ | 🔧 | | +| [valid-test-tags](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/valid-test-tags.md) | Enforce valid tag format in test blocks | ✅ | | | From b3373f0c3e79712f269b257f0bbb041725bbdbe1 Mon Sep 17 00:00:00 2001 From: David Enke Date: Wed, 1 Oct 2025 15:43:14 +0200 Subject: [PATCH 8/9] chore: add as recommended with warn --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 6dab5f38..061f5de6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -112,8 +112,8 @@ const index = { const sharedConfig = { rules: { - 'consistent-spacing-between-blocks': 'off', 'no-empty-pattern': 'off', + 'playwright/consistent-spacing-between-blocks': 'warn', 'playwright/expect-expect': 'warn', 'playwright/max-nested-describe': 'warn', 'playwright/missing-playwright-await': 'error', From 5f4cbd0cf87e6e8b7158a6c9acc63a01b3b7843a Mon Sep 17 00:00:00 2001 From: David Enke Date: Thu, 2 Oct 2025 08:32:00 +0200 Subject: [PATCH 9/9] chore: remove unnecessary type casts --- src/rules/consistent-spacing-between-blocks.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/rules/consistent-spacing-between-blocks.ts b/src/rules/consistent-spacing-between-blocks.ts index 52f84069..d72656b3 100644 --- a/src/rules/consistent-spacing-between-blocks.ts +++ b/src/rules/consistent-spacing-between-blocks.ts @@ -10,7 +10,7 @@ export default createRule({ function isPrecededByTokens(node: Rule.Node, testTokens: string[]) { const tokenBefore = sourceCode.getTokenBefore(node) - return tokenBefore && testTokens.includes(tokenBefore.value as string) + return tokenBefore && testTokens.includes(tokenBefore.value) } function isFirstNode(node: Rule.Node) { @@ -24,30 +24,30 @@ export default createRule({ ) { const realParent = getParent(parent) if ('body' in realParent && realParent.body) { - const body = realParent.body as unknown + const { body } = realParent return Array.isArray(body) ? body[0] === node : body === parent } return false } if (parentType === 'IfStatement') { - return isPrecededByTokens(node as any, ['else', ')']) + return isPrecededByTokens(node, ['else', ')']) } if (parentType === 'DoWhileStatement') { - return isPrecededByTokens(node as any, ['do']) + return isPrecededByTokens(node, ['do']) } if (parentType === 'SwitchCase') { - return isPrecededByTokens(node as any, [':']) + return isPrecededByTokens(node, [':']) } if ('body' in parent && parent.body) { - const body = parent.body as unknown + const { body } = parent return Array.isArray(body) ? body[0] === node : body === node } - return isPrecededByTokens(node as any, [')']) + return isPrecededByTokens(node, [')']) } function calcCommentLines(node: Rule.Node, lineNumTokenBefore: number) {