diff --git a/README.md b/README.md index 269e908ec..c0f854db0 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,7 @@ for more information about extending configuration files. | [consistent-test-it](docs/rules/consistent-test-it.md) | Enforce consistent test or it keyword | | ![fixable](https://img.shields.io/badge/-fixable-green.svg) | | [no-disabled-tests](docs/rules/no-disabled-tests.md) | Disallow disabled tests | ![recommended](https://img.shields.io/badge/-recommended-lightgrey.svg) | | | [no-focused-tests](docs/rules/no-focused-tests.md) | Disallow focused tests | ![recommended](https://img.shields.io/badge/-recommended-lightgrey.svg) | | +| [no-hooks](docs/rules/no-hooks.md) | Disallow setup and teardown hooks | | | | [no-identical-title](docs/rules/no-identical-title.md) | Disallow identical titles | ![recommended](https://img.shields.io/badge/-recommended-lightgrey.svg) | | | [no-large-snapshots](docs/rules/no-large-snapshots.md) | Disallow large snapshots | | | | [prefer-to-have-length](docs/rules/prefer-to-have-length.md) | Suggest using `toHaveLength()` | ![recommended](https://img.shields.io/badge/-recommended-lightgrey.svg) | ![fixable](https://img.shields.io/badge/-fixable-green.svg) | diff --git a/docs/rules/no-hooks.md b/docs/rules/no-hooks.md new file mode 100644 index 000000000..35d8c8f2b --- /dev/null +++ b/docs/rules/no-hooks.md @@ -0,0 +1,14 @@ +# Disallow setup and teardown hooks (no-hooks) + +## Rule Details + +## Options + +### `allowed` + +## When Not To Use It + +## Further Reading + +* [Jest docs - Setup and Teardown](https://facebook.github.io/jest/docs/en/setup-teardown.html) +* [@jamiebuilds Twitter thread](https://twitter.com/jamiebuilds/status/954906997169664000) diff --git a/index.js b/index.js index 4d60cdf07..654a8aa11 100644 --- a/index.js +++ b/index.js @@ -3,6 +3,7 @@ const consistentTestIt = require('./rules/consistent-test-it'); const noDisabledTests = require('./rules/no-disabled-tests'); const noFocusedTests = require('./rules/no-focused-tests'); +const noHooks = require('./rules/no-hooks'); const noIdenticalTitle = require('./rules/no-identical-title'); const noLargeSnapshots = require('./rules/no-large-snapshots'); const preferToBeNull = require('./rules/prefer-to-be-null'); @@ -61,6 +62,7 @@ module.exports = { 'consistent-test-it': consistentTestIt, 'no-disabled-tests': noDisabledTests, 'no-focused-tests': noFocusedTests, + 'no-hooks': noHooks, 'no-identical-title': noIdenticalTitle, 'no-large-snapshots': noLargeSnapshots, 'prefer-to-be-null': preferToBeNull, diff --git a/rules/__tests__/no-hooks.test.js b/rules/__tests__/no-hooks.test.js new file mode 100644 index 000000000..b1229f49d --- /dev/null +++ b/rules/__tests__/no-hooks.test.js @@ -0,0 +1,49 @@ +'use strict'; + +const RuleTester = require('eslint').RuleTester; +const rules = require('../..').rules; +const ruleTester = new RuleTester({ + parserOptions: { + ecmaVersion: 6, + }, +}); + +const error = { + ruleId: 'no-hooks', + message: 'Unexpected setup/teardown hook', +}; + +ruleTester.run('no-hooks', rules['no-hooks'], { + valid: [ + 'test("foo")', + 'describe("foo", () => { it("bar") })', + 'test("foo", () => { expect(subject.beforeEach()).toBe(true) })', + { + code: 'afterEach(() => {}); afterAll(() => {});', + options: [{ allowed: ['afterEach', 'afterAll'] }], + }, + ], + invalid: [ + { + code: 'beforeAll(() => {})', + errors: [error], + }, + { + code: 'beforeEach(() => {})', + errors: [error], + }, + { + code: 'afterAll(() => {})', + errors: [error], + }, + { + code: 'afterEach(() => {})', + errors: [error], + }, + { + code: 'beforeEach(() => {}); afterEach(() => { jest.resetModules() });', + options: [{ allowed: ['afterEach'] }], + errors: [error], + }, + ], +}); diff --git a/rules/no-hooks.js b/rules/no-hooks.js new file mode 100644 index 000000000..b411bde67 --- /dev/null +++ b/rules/no-hooks.js @@ -0,0 +1,52 @@ +'use strict'; + +module.exports = { + meta: { + docs: { + url: + 'https://github.com/jest-community/eslint-plugin-jest/blob/master/docs/rules/no-hooks.md', + }, + }, + schema: [ + { + type: 'object', + properties: { + allowed: { + type: 'array', + contains: ['beforeAll', 'beforeEach', 'afterAll', 'afterEach'], + }, + }, + additionalProperties: false, + }, + ], + create(context) { + const testHookNames = Object.assign(Object.create(null), { + beforeAll: true, + beforeEach: true, + afterAll: true, + afterEach: true, + }); + + const allowed = (context.options[0] || { allowed: [] }).allowed.reduce( + (hashMap, value) => { + hashMap[value] = true; + return hashMap; + }, + Object.create(null) + ); + + const isHook = node => testHookNames[node.callee.name]; + const isWhitelisted = node => allowed[node.callee.name]; + + return { + CallExpression(node) { + if (isHook(node) && !isWhitelisted(node)) { + context.report({ + node, + message: 'Unexpected setup/teardown hook', + }); + } + }, + }; + }, +};