diff --git a/docs/rules/require-hook.md b/docs/rules/require-hook.md index 350f5b5ef..41b83bc7f 100644 --- a/docs/rules/require-hook.md +++ b/docs/rules/require-hook.md @@ -148,3 +148,40 @@ afterEach(() => { clearCityDatabase(); }); ``` + +## Options + +If there are methods that you want to call outside of hooks and tests, you can +mark them as allowed using the `allowedFunctionCalls` option. + +```json +{ + "jest/require-hook": [ + "error", + { + "allowedFunctionCalls": ["enableAutoDestroy"] + } + ] +} +``` + +Examples of **correct** code when using +`{ "allowedFunctionCalls": ["enableAutoDestroy"] }` option: + +```js +/* eslint jest/require-hook: ["error", { "allowedFunctionCalls": ["enableAutoDestroy"] }] */ + +import { enableAutoDestroy, mount } from '@vue/test-utils'; +import { initDatabase, tearDownDatabase } from './databaseUtils'; + +enableAutoDestroy(afterEach); + +beforeEach(initDatabase); +afterEach(tearDownDatabase); + +describe('Foo', () => { + test('always returns 42', () => { + expect(global.getAnswer()).toBe(42); + }); +}); +``` diff --git a/src/rules/__tests__/require-hook.test.ts b/src/rules/__tests__/require-hook.test.ts index 5c6f83ec3..cf071419a 100644 --- a/src/rules/__tests__/require-hook.test.ts +++ b/src/rules/__tests__/require-hook.test.ts @@ -152,6 +152,18 @@ ruleTester.run('require-hook', rule, { }); }); `, + { + code: dedent` + enableAutoDestroy(afterEach); + + describe('some tests', () => { + it('is false', () => { + expect(true).toBe(true); + }); + }); + `, + options: [{ allowedFunctionCalls: ['enableAutoDestroy'] }], + }, ], invalid: [ { @@ -374,6 +386,25 @@ ruleTester.run('require-hook', rule, { }, ], }, + { + code: dedent` + enableAutoDestroy(afterEach); + + describe('some tests', () => { + it('is false', () => { + expect(true).toBe(true); + }); + }); + `, + options: [{ allowedFunctionCalls: ['someOtherName'] }], + errors: [ + { + messageId: 'useHook', + line: 1, + column: 1, + }, + ], + }, ], }); diff --git a/src/rules/require-hook.ts b/src/rules/require-hook.ts index 179e554a8..8ade1bf34 100644 --- a/src/rules/require-hook.ts +++ b/src/rules/require-hook.ts @@ -27,12 +27,18 @@ const isNullOrUndefined = (node: TSESTree.Expression): boolean => { ); }; -const shouldBeInHook = (node: TSESTree.Node): boolean => { +const shouldBeInHook = ( + node: TSESTree.Node, + allowedFunctionCalls: readonly string[] = [], +): boolean => { switch (node.type) { case AST_NODE_TYPES.ExpressionStatement: - return shouldBeInHook(node.expression); + return shouldBeInHook(node.expression, allowedFunctionCalls); case AST_NODE_TYPES.CallExpression: - return !isJestFnCall(node); + return !( + isJestFnCall(node) || + allowedFunctionCalls.includes(getNodeName(node) as string) + ); case AST_NODE_TYPES.VariableDeclaration: { if (node.kind === 'const') { return false; @@ -48,7 +54,10 @@ const shouldBeInHook = (node: TSESTree.Node): boolean => { } }; -export default createRule({ +export default createRule< + [{ allowedFunctionCalls?: readonly string[] }], + 'useHook' +>({ name: __filename, meta: { docs: { @@ -60,13 +69,30 @@ export default createRule({ useHook: 'This should be done within a hook', }, type: 'suggestion', - schema: [], + schema: [ + { + type: 'object', + properties: { + allowedFunctionCalls: { + type: 'array', + items: { type: 'string' }, + }, + }, + additionalProperties: false, + }, + ], }, - defaultOptions: [], + defaultOptions: [ + { + allowedFunctionCalls: [], + }, + ], create(context) { + const { allowedFunctionCalls } = context.options[0] ?? {}; + const checkBlockBody = (body: TSESTree.BlockStatement['body']) => { for (const statement of body) { - if (shouldBeInHook(statement)) { + if (shouldBeInHook(statement, allowedFunctionCalls)) { context.report({ node: statement, messageId: 'useHook',