diff --git a/README.md b/README.md index dae8f3a8..02407eff 100644 --- a/README.md +++ b/README.md @@ -116,56 +116,57 @@ CLI option\ 💡 Manually fixable by [editor suggestions](https://eslint.org/docs/latest/developer-guide/working-with-rules#providing-suggestions) -| Rule | Description | ✅ | 🔧 | 💡 | -| --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | :-: | :-: | :-: | -| [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 | ✅ | | | 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..061f5de6 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, @@ -111,6 +113,7 @@ const index = { const sharedConfig = { rules: { 'no-empty-pattern': 'off', + 'playwright/consistent-spacing-between-blocks': 'warn', 'playwright/expect-expect': 'warn', 'playwright/max-nested-describe': 'warn', 'playwright/missing-playwright-await': 'error', 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..e720e16b --- /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('should pass', () => {}); + test('should fail', async () => { + await test.step('should pass', () => {}); + // a comment + test.step('should fail', () => {}); + test.step('should fail', () => {}); + const foo = await test.step('should fail', () => {}); + foo = await test.step('should fail', () => {}); + }); + /** + * another comment + */ + test('should fail', () => {}); + `, + errors: [ + { 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` + test.beforeEach('should pass', () => {}); + + test('should fail', async () => { + await test.step('should pass', () => {}); + + // a comment + test.step('should fail', () => {}); + + test.step('should fail', () => {}); + + const foo = await test.step('should fail', () => {}); + + foo = await test.step('should fail', () => {}); + }); + + /** + * another comment + */ + test('should fail', () => {}); + `, + }, + ], + valid: [ + { + code: javascript` + test('should pass', () => {}); + + test('should pass', () => {}); + `, + name: 'blank line between simple test blocks', + }, + { + code: javascript` + test.beforeEach(() => {}); + + test.skip('should pass', () => {}); + `, + name: 'blank line between test modifiers', + }, + { + code: javascript` + test('should pass', async () => { + await test.step('should pass', () => {}); + + await test.step('should pass', () => {}); + }); + `, + name: 'blank line between nested steps in async test', + }, + { + code: javascript` + test('should pass', async () => { + await test.step('should pass', () => {}); + + // some comment + await test.step('should pass', () => {}); + }); + `, + name: 'nested steps with a line comment in between', + }, + { + code: javascript` + test('should pass', async () => { + await test.step('should pass', () => {}); + + /** + * another comment + */ + await test.step('should pass', () => {}); + }); + `, + name: 'nested steps with a block comment in between', + }, + { + code: javascript` + test('assign', async () => { + let foo = await test.step('should pass', () => {}); + + foo = await test.step('should pass', () => {}); + }); + `, + name: 'assignments initialized by test.step', + }, + { + code: javascript` + test('assign', async () => { + let { foo } = await test.step('should pass', () => {}); + + ({ foo } = await test.step('should pass', () => {})); + }); + `, + 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..d72656b3 --- /dev/null +++ b/src/rules/consistent-spacing-between-blocks.ts @@ -0,0 +1,181 @@ +import type { Rule } from 'eslint' +import type { CallExpression } from 'estree' +import { getParent } from '../utils/ast.js' +import { createRule } from '../utils/createRule.js' +import { parseFnCall } from '../utils/parseFnCall.js' + +export default createRule({ + create(context) { + const { sourceCode } = context + + function isPrecededByTokens(node: Rule.Node, testTokens: string[]) { + const tokenBefore = sourceCode.getTokenBefore(node) + return tokenBefore && testTokens.includes(tokenBefore.value) + } + + function isFirstNode(node: Rule.Node) { + const parent = getParent(node) + if (!parent) return true + + const parentType = parent.type + if ( + parentType === 'ExpressionStatement' || + parentType === 'VariableDeclaration' + ) { + const realParent = getParent(parent) + if ('body' in realParent && realParent.body) { + const { body } = realParent + return Array.isArray(body) ? body[0] === node : body === parent + } + return false + } + + if (parentType === 'IfStatement') { + return isPrecededByTokens(node, ['else', ')']) + } + + if (parentType === 'DoWhileStatement') { + return isPrecededByTokens(node, ['do']) + } + + if (parentType === 'SwitchCase') { + return isPrecededByTokens(node, [':']) + } + + if ('body' in parent && parent.body) { + const { body } = parent + return Array.isArray(body) ? body[0] === node : body === node + } + + return isPrecededByTokens(node, [')']) + } + + 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 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) + } + + 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: sourceCode.getText(nodeToCheck).split('\n')[0], + }, + 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: nodeToCheck, + }) + } + + return { + CallExpression(node) { + const call = parseFnCall(context, node) + if ( + call?.type === 'test' || + call?.type === 'hook' || + call?.type === 'step' + ) { + checkSpacing(node) + } + }, + } + }, + 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', + }, +})