diff --git a/README.md b/README.md index 70c6156..0ce8fa0 100644 --- a/README.md +++ b/README.md @@ -107,30 +107,31 @@ See [Configuring Eslint](http://eslint.org/docs/user-guide/configuring) on [esli βœ… Set in the `recommended` [configuration](https://github.com/lo1tuma/eslint-plugin-mocha#configs).\ πŸ”§ Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix). -| NameΒ Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β  | Description | πŸ’Ό | ⚠️ | 🚫 | πŸ”§ | -| :----------------------------------------------------------------- | :---------------------------------------------------------------------- | :- | :- | :- | :- | -| [handle-done-callback](docs/rules/handle-done-callback.md) | Enforces handling of callbacks for async tests | βœ… | | | | -| [max-top-level-suites](docs/rules/max-top-level-suites.md) | Enforce the number of top-level suites in a single file | βœ… | | | | -| [no-async-describe](docs/rules/no-async-describe.md) | Disallow async functions passed to describe | βœ… | | | πŸ”§ | -| [no-empty-description](docs/rules/no-empty-description.md) | Disallow empty test descriptions | βœ… | | | | -| [no-exclusive-tests](docs/rules/no-exclusive-tests.md) | Disallow exclusive tests | | βœ… | | | -| [no-exports](docs/rules/no-exports.md) | Disallow exports from test files | βœ… | | | | -| [no-global-tests](docs/rules/no-global-tests.md) | Disallow global tests | βœ… | | | | -| [no-hooks](docs/rules/no-hooks.md) | Disallow hooks | | | βœ… | | -| [no-hooks-for-single-case](docs/rules/no-hooks-for-single-case.md) | Disallow hooks for a single test or test suite | | | βœ… | | -| [no-identical-title](docs/rules/no-identical-title.md) | Disallow identical titles | βœ… | | | | -| [no-mocha-arrows](docs/rules/no-mocha-arrows.md) | Disallow arrow functions as arguments to mocha functions | βœ… | | | πŸ”§ | -| [no-nested-tests](docs/rules/no-nested-tests.md) | Disallow tests to be nested within other tests | βœ… | | | | -| [no-pending-tests](docs/rules/no-pending-tests.md) | Disallow pending tests | | βœ… | | | -| [no-return-and-callback](docs/rules/no-return-and-callback.md) | Disallow returning in a test or hook function that uses a callback | βœ… | | | | -| [no-return-from-async](docs/rules/no-return-from-async.md) | Disallow returning from an async test or hook | | | βœ… | | -| [no-setup-in-describe](docs/rules/no-setup-in-describe.md) | Disallow setup in describe blocks | βœ… | | | | -| [no-sibling-hooks](docs/rules/no-sibling-hooks.md) | Disallow duplicate uses of a hook at the same level inside a describe | βœ… | | | | -| [no-skipped-tests](docs/rules/no-skipped-tests.md) | Disallow skipped tests | | βœ… | | | -| [no-synchronous-tests](docs/rules/no-synchronous-tests.md) | Disallow synchronous tests | | | βœ… | | -| [no-top-level-hooks](docs/rules/no-top-level-hooks.md) | Disallow top-level hooks | | βœ… | | | -| [prefer-arrow-callback](docs/rules/prefer-arrow-callback.md) | Require using arrow functions for callbacks | | | βœ… | πŸ”§ | -| [valid-suite-description](docs/rules/valid-suite-description.md) | Require suite descriptions to match a pre-configured regular expression | | | βœ… | | -| [valid-test-description](docs/rules/valid-test-description.md) | Require test descriptions to match a pre-configured regular expression | | | βœ… | | +| NameΒ Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β  | Description | πŸ’Ό | ⚠️ | 🚫 | πŸ”§ | +| :----------------------------------------------------------------------------------- | :---------------------------------------------------------------------- | :- | :- | :- | :- | +| [consistent-spacing-between-blocks](docs/rules/consistent-spacing-between-blocks.md) | Require consistent spacing between blocks | βœ… | | | πŸ”§ | +| [handle-done-callback](docs/rules/handle-done-callback.md) | Enforces handling of callbacks for async tests | βœ… | | | | +| [max-top-level-suites](docs/rules/max-top-level-suites.md) | Enforce the number of top-level suites in a single file | βœ… | | | | +| [no-async-describe](docs/rules/no-async-describe.md) | Disallow async functions passed to describe | βœ… | | | πŸ”§ | +| [no-empty-description](docs/rules/no-empty-description.md) | Disallow empty test descriptions | βœ… | | | | +| [no-exclusive-tests](docs/rules/no-exclusive-tests.md) | Disallow exclusive tests | | βœ… | | | +| [no-exports](docs/rules/no-exports.md) | Disallow exports from test files | βœ… | | | | +| [no-global-tests](docs/rules/no-global-tests.md) | Disallow global tests | βœ… | | | | +| [no-hooks](docs/rules/no-hooks.md) | Disallow hooks | | | βœ… | | +| [no-hooks-for-single-case](docs/rules/no-hooks-for-single-case.md) | Disallow hooks for a single test or test suite | | | βœ… | | +| [no-identical-title](docs/rules/no-identical-title.md) | Disallow identical titles | βœ… | | | | +| [no-mocha-arrows](docs/rules/no-mocha-arrows.md) | Disallow arrow functions as arguments to mocha functions | βœ… | | | πŸ”§ | +| [no-nested-tests](docs/rules/no-nested-tests.md) | Disallow tests to be nested within other tests | βœ… | | | | +| [no-pending-tests](docs/rules/no-pending-tests.md) | Disallow pending tests | | βœ… | | | +| [no-return-and-callback](docs/rules/no-return-and-callback.md) | Disallow returning in a test or hook function that uses a callback | βœ… | | | | +| [no-return-from-async](docs/rules/no-return-from-async.md) | Disallow returning from an async test or hook | | | βœ… | | +| [no-setup-in-describe](docs/rules/no-setup-in-describe.md) | Disallow setup in describe blocks | βœ… | | | | +| [no-sibling-hooks](docs/rules/no-sibling-hooks.md) | Disallow duplicate uses of a hook at the same level inside a describe | βœ… | | | | +| [no-skipped-tests](docs/rules/no-skipped-tests.md) | Disallow skipped tests | | βœ… | | | +| [no-synchronous-tests](docs/rules/no-synchronous-tests.md) | Disallow synchronous tests | | | βœ… | | +| [no-top-level-hooks](docs/rules/no-top-level-hooks.md) | Disallow top-level hooks | | βœ… | | | +| [prefer-arrow-callback](docs/rules/prefer-arrow-callback.md) | Require using arrow functions for callbacks | | | βœ… | πŸ”§ | +| [valid-suite-description](docs/rules/valid-suite-description.md) | Require suite descriptions to match a pre-configured regular expression | | | βœ… | | +| [valid-test-description](docs/rules/valid-test-description.md) | Require test descriptions to match a pre-configured regular expression | | | βœ… | | diff --git a/docs/rules/consistent-spacing-between-blocks.md b/docs/rules/consistent-spacing-between-blocks.md new file mode 100644 index 0000000..a48664c --- /dev/null +++ b/docs/rules/consistent-spacing-between-blocks.md @@ -0,0 +1,66 @@ +# Require consistent spacing between blocks (`mocha/consistent-spacing-between-blocks`) + +πŸ’Ό This rule is enabled in the βœ… `recommended` [config](https://github.com/lo1tuma/eslint-plugin-mocha#configs). + +πŸ”§ This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). + + + +Mocha testing framework provides a structured way of writing tests using functions like `describe`, `it`, `before`, `after`, `beforeEach`, and `afterEach`. As a convention, it is very common to add some spacing between these calls. It's unfortunately also quite common that this spacing is applied inconsistently. + +Example: + +```js +describe("MyComponent", function () { + beforeEach(function () { + // setup code + }); + it("should behave correctly", function () { + // test code + }); + afterEach(function () { + // teardown code + }); +}); +``` + +In this example, there are no line breaks between Mocha function calls, making the code harder to read. + +## Rule Details + +This rule enforces a line break between calls to Mocha functions (before, after, describe, it, beforeEach, afterEach) within describe blocks. + +The following patterns are considered errors: + +```javascript +describe("MyComponent", function () { + beforeEach(function () { + // setup code + }); + it("should behave correctly", function () { + // test code + }); +}); +``` + +These patterns would not be considered errors: + +```javascript +describe("MyComponent", function () { + beforeEach(function () { + // setup code + }); + + it("should behave correctly", function () { + // test code + }); + + afterEach(function () { + // teardown code + }); +}); +``` + +## When Not To Use It + +If you don't prefer this convention. diff --git a/index.js b/index.js index 2de93c6..eb85d22 100644 --- a/index.js +++ b/index.js @@ -24,7 +24,8 @@ module.exports = { 'prefer-arrow-callback': require('./lib/rules/prefer-arrow-callback'), 'valid-suite-description': require('./lib/rules/valid-suite-description'), 'valid-test-description': require('./lib/rules/valid-test-description'), - 'no-empty-description': require('./lib/rules/no-empty-description.js') + 'no-empty-description': require('./lib/rules/no-empty-description.js'), + 'consistent-spacing-between-blocks': require('./lib/rules/consistent-spacing-between-blocks.js') }, configs: { all: { @@ -53,7 +54,8 @@ module.exports = { 'mocha/prefer-arrow-callback': 'error', 'mocha/valid-suite-description': 'error', 'mocha/valid-test-description': 'error', - 'mocha/no-empty-description': 'error' + 'mocha/no-empty-description': 'error', + 'mocha/consistent-spacing-between-blocks': 'error' } }, @@ -83,7 +85,8 @@ module.exports = { 'mocha/prefer-arrow-callback': 'off', 'mocha/valid-suite-description': 'off', 'mocha/valid-test-description': 'off', - 'mocha/no-empty-description': 'error' + 'mocha/no-empty-description': 'error', + 'mocha/consistent-spacing-between-blocks': 'error' } } } diff --git a/lib/rules/consistent-spacing-between-blocks.js b/lib/rules/consistent-spacing-between-blocks.js new file mode 100644 index 0000000..24a1e19 --- /dev/null +++ b/lib/rules/consistent-spacing-between-blocks.js @@ -0,0 +1,74 @@ +'use strict'; + +/* eslint "complexity": [ "error", 6 ] */ + +exports.meta = { + type: 'suggestion', + fixable: 'whitespace', + schema: [], + docs: { + description: 'Require consistent spacing between blocks', + url: + 'https://github.com/lo1tuma/eslint-plugin-mocha/blob/master/docs/rules/' + + 'consistent-spacing-between-blocks.md' + } +}; + +// List of Mocha functions that should have a line break before them. +const MOCHA_FUNCTIONS = [ + 'before', + 'after', + 'describe', + 'it', + 'beforeEach', + 'afterEach' +]; + +// Avoids enforcing line breaks at the beginning of a block. +function isFirstStatementInScope(node) { + return node.parent.parent.body[0] === node.parent; +} + +// Ensure that the rule is applied only within the context of Mocha describe blocks. +function isInsideDescribeBlock(node) { + return ( + node.parent.type === 'ExpressionStatement' && + node.parent.parent.type === 'BlockStatement' && + (node.parent.parent.parent.type === 'ArrowFunctionExpression' || + node.parent.parent.parent.type === 'FunctionExpression') && + node.parent.parent.parent.parent.type === 'CallExpression' && + node.parent.parent.parent.parent.callee.name === 'describe' + ); +} + +exports.create = function (context) { + return { + CallExpression(node) { + if ( + !MOCHA_FUNCTIONS.includes(node.callee.name) || + !isInsideDescribeBlock(node) || + isFirstStatementInScope(node) + ) { + return; + } + + // Retrieves the token before the current node, skipping comments. + const beforeToken = context.getSourceCode().getTokenBefore(node); + + // And then count the number of lines between the two. + const linesBetween = node.loc.start.line - beforeToken.loc.end.line; + if (linesBetween < 2) { + context.report({ + node, + message: 'Expected line break before this statement.', + fix(fixer) { + return fixer.insertTextAfter( + beforeToken, + linesBetween === 0 ? '\n\n' : '\n' + ); + } + }); + } + } + }; +}; diff --git a/test/rules/consistent-spacing-between-blocks.js b/test/rules/consistent-spacing-between-blocks.js new file mode 100644 index 0000000..7309c47 --- /dev/null +++ b/test/rules/consistent-spacing-between-blocks.js @@ -0,0 +1,124 @@ +'use strict'; + +const { RuleTester } = require('eslint'); + +const rule = require('../../lib/rules/consistent-spacing-between-blocks.js'); + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020 } }); + +ruleTester.run('require-spacing-between-mocha-calls', rule, { + valid: [ + // Basic describe block + `describe('My Test', () => { + it('does something', () => {}); + });`, + + // Proper line break before each block within describe + `describe('My Test', () => { + it('performs action one', () => {}); + + it('performs action two', () => {}); + });`, + + // Nested describe blocks with proper spacing + `describe('Outer block', () => { + describe('Inner block', () => { + it('performs an action', () => {}); + }); + + afterEach(() => {}); + });`, + + // Describe block with comments + `describe('My Test With Comments', () => { + it('does something', () => {}); + + // Some comment + afterEach(() => {}); + });`, + + // Mocha functions outside of a describe block + `it('does something outside a describe block', () => {}); + afterEach(() => {});` + ], + + invalid: [ + // Missing line break between it and afterEach + { + code: `describe('My Test', function () { + it('does something', () => {}); + afterEach(() => {}); + });`, + output: `describe('My Test', function () { + it('does something', () => {}); + + afterEach(() => {}); + });`, + errors: [ + { + message: 'Expected line break before this statement.', + type: 'CallExpression' + } + ] + }, + + // Missing line break between beforeEach and it + { + code: `describe('My Test', () => { + beforeEach(() => {}); + it('does something', () => {}); + });`, + output: `describe('My Test', () => { + beforeEach(() => {}); + + it('does something', () => {}); + });`, + errors: [ + { + message: 'Expected line break before this statement.', + type: 'CallExpression' + } + ] + }, + + // Missing line break after a variable declaration + { + code: `describe('Variable declaration', () => { + const a = 1; + it('uses a variable', () => {}); + });`, + output: `describe('Variable declaration', () => { + const a = 1; + + it('uses a variable', () => {}); + });`, + errors: [ + { + message: 'Expected line break before this statement.', + type: 'CallExpression' + } + ] + }, + + // Blocks on the same line + { + code: + 'describe(\'Same line blocks\', () => {' + + 'it(\'block one\', () => {});' + + 'it(\'block two\', () => {});' + + '});', + output: + 'describe(\'Same line blocks\', () => {' + + 'it(\'block one\', () => {});' + + '\n\n' + + 'it(\'block two\', () => {});' + + '});', + errors: [ + { + message: 'Expected line break before this statement.', + type: 'CallExpression' + } + ] + } + ] +});