Skip to content

Commit

Permalink
Merge pull request #340 from bjornua/master
Browse files Browse the repository at this point in the history
Add `consistent-spacing-between-blocks` rule
  • Loading branch information
lo1tuma committed Jan 23, 2024
2 parents d95f2c3 + 06951af commit 7b87a5c
Show file tree
Hide file tree
Showing 5 changed files with 296 additions and 28 deletions.
51 changes: 26 additions & 25 deletions README.md
Expand Up @@ -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 | | || |

<!-- end auto-generated rules list -->
66 changes: 66 additions & 0 deletions 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).

<!-- end auto-generated rule header -->

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.
9 changes: 6 additions & 3 deletions index.js
Expand Up @@ -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: {
Expand Down Expand Up @@ -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'
}
},

Expand Down Expand Up @@ -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'
}
}
}
Expand Down
74 changes: 74 additions & 0 deletions 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'
);
}
});
}
}
};
};
124 changes: 124 additions & 0 deletions 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'
}
]
}
]
});

0 comments on commit 7b87a5c

Please sign in to comment.