diff --git a/README.md b/README.md index c222070..342f6c8 100644 --- a/README.md +++ b/README.md @@ -116,57 +116,58 @@ 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-unused-locators](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-unused-locators.md) | Disallow usage of page locators that are not used | ✅ | | | -| [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-unused-locators](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-unused-locators.md) | Disallow usage of page locators that are not used | ✅ | | | +| [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 0000000..cf00160 --- /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 5bca74c..490e6bc 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' @@ -55,6 +56,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, @@ -113,6 +115,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 0000000..608a5e4 --- /dev/null +++ b/src/rules/consistent-spacing-between-blocks.test.ts @@ -0,0 +1,127 @@ +import dedent from 'dedent' +import { runRuleTester } from '../utils/rule-tester.js' +import rule from './consistent-spacing-between-blocks.js' + +runRuleTester('consistent-spacing-between-blocks', rule, { + invalid: [ + { + code: dedent` + 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: dedent` + 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: dedent` + test('should pass', () => {}); + + test('should pass', () => {}); + `, + name: 'blank line between simple test blocks', + }, + { + code: dedent` + test.beforeEach(() => {}); + + test.skip('should pass', () => {}); + `, + name: 'blank line between test modifiers', + }, + { + code: dedent` + test('should pass', async () => { + await test.step('should pass', () => {}); + + await test.step('should pass', () => {}); + }); + `, + name: 'blank line between nested steps in async test', + }, + { + code: dedent` + 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: dedent` + 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: dedent` + test('assign', async () => { + let foo = await test.step('should pass', () => {}); + + foo = await test.step('should pass', () => {}); + }); + `, + name: 'assignments initialized by test.step', + }, + { + code: dedent` + 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 0000000..3a77afe --- /dev/null +++ b/src/rules/consistent-spacing-between-blocks.ts @@ -0,0 +1,94 @@ +import type { Rule } from 'eslint' +import type { BlockStatement, CallExpression, Program } 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 isBlockOrProgram( + node: Rule.Node, + ): node is + | (BlockStatement & Rule.NodeParentExtension) + | (Program & Rule.NodeParentExtension) { + return node.type === 'BlockStatement' || node.type === 'Program' + } + + function getStatementNode(node: Rule.Node): Rule.Node { + const parent = getParent(node) + if (!parent) return node + if (isBlockOrProgram(parent)) return node + return getStatementNode(parent) + } + + function isFirstStatementInBlock(node: Rule.Node): boolean { + const parent = getParent(node) + if (!parent) return true + if (isBlockOrProgram(parent)) return parent.body[0] === node + return false + } + + function checkSpacing(node: CallExpression & Rule.NodeParentExtension) { + const statementNode = getStatementNode(node) + + if (isFirstStatementInBlock(statementNode)) return + + const comments = sourceCode.getCommentsBefore(statementNode) + const nodeToCheck = comments.length > 0 ? comments[0] : statementNode + const lineNumber = nodeToCheck.loc!.start.line + + if (lineNumber === 1) return + + const lines = sourceCode.lines + const previousLine = lines[lineNumber - 2] + + if (previousLine.trim() === '') return + + context.report({ + data: { + source: sourceCode.getText(statementNode).split('\n')[0], + }, + fix(fixer) { + const nodeStart = nodeToCheck.range![0] + const textBefore = sourceCode.text.substring(0, nodeStart) + const lastNewlineIndex = textBefore.lastIndexOf('\n') + const lineStart = lastNewlineIndex + 1 + + return fixer.insertTextBeforeRange([lineStart, nodeStart], '\n') + }, + loc: nodeToCheck.loc!, + messageId: 'missingWhitespace', + node: statementNode, + }) + } + + 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', + }, +})