Skip to content

Commit 8405f54

Browse files
author
Andrey Nelyubin
committed
fix(require-hook): added optional settings
1 parent 7833de4 commit 8405f54

File tree

3 files changed

+146
-7
lines changed

3 files changed

+146
-7
lines changed

docs/rules/require-hook.md

+71
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,74 @@ afterEach(() => {
148148
clearCityDatabase();
149149
});
150150
```
151+
152+
## Options
153+
154+
Some test utils provides methods which takes hook as an argument
155+
and should be executed outside a hook.
156+
157+
For example https://vue-test-utils.vuejs.org/api/#enableautodestroy-hook
158+
which takes the hook as an argument. To exclude them you can update settings
159+
160+
```json
161+
{
162+
"jest/require-hook": [
163+
"error",
164+
{
165+
"excludedFunctions": [
166+
"enableAutoDestroy"
167+
]
168+
}
169+
]
170+
}
171+
```
172+
173+
Examples of **incorrect** code for the `{ "excludedFunctions": ["enableAutoDestroy"] }`
174+
option:
175+
176+
```js
177+
/* eslint jest/require-hook: ["error", { "excludedFunctions": ["enableAutoDestroy"] }] */
178+
179+
import {
180+
enableAutoDestroy,
181+
resetAutoDestroyState,
182+
mount
183+
} from '@vue/test-utils';
184+
import initDatabase from './initDatabase';
185+
186+
enableAutoDestroy(afterEach);
187+
initDatabase(); // this will throw a linting error
188+
189+
describe('Foo', () => {
190+
test('always returns 42', () => {
191+
expect(global.getAnswer()).toBe(42);
192+
})
193+
})
194+
```
195+
196+
197+
Examples of **correct** code for the `{ "excludedFunctions": ["enableAutoDestroy"] }`
198+
option:
199+
200+
```js
201+
/* eslint jest/require-hook: ["error", { "excludedFunctions": ["enableAutoDestroy"] }] */
202+
203+
import {
204+
enableAutoDestroy,
205+
resetAutoDestroyState,
206+
mount
207+
} from '@vue/test-utils';
208+
import {initDatabase, tearDownDatabase} from './databaseUtils';
209+
210+
enableAutoDestroy(afterEach);
211+
afterAll(resetAutoDestroyState);
212+
213+
beforeEach(initDatabase);
214+
afterEach(tearDownDatabase);
215+
216+
describe('Foo', () => {
217+
test('always returns 42', () => {
218+
expect(global.getAnswer()).toBe(42);
219+
});
220+
});
221+
```

src/rules/__tests__/require-hook.test.ts

+31
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,18 @@ ruleTester.run('require-hook', rule, {
152152
});
153153
});
154154
`,
155+
{
156+
code: dedent`
157+
enableAutoDestroy(afterEach);
158+
159+
describe('some tests', () => {
160+
it('is false', () => {
161+
expect(true).toBe(true);
162+
});
163+
});
164+
`,
165+
options: [{ excludedFunctions: ['enableAutoDestroy'] }],
166+
},
155167
],
156168
invalid: [
157169
{
@@ -374,6 +386,25 @@ ruleTester.run('require-hook', rule, {
374386
},
375387
],
376388
},
389+
{
390+
code: dedent`
391+
enableAutoDestroy(afterEach);
392+
393+
describe('some tests', () => {
394+
it('is false', () => {
395+
expect(true).toBe(true);
396+
});
397+
});
398+
`,
399+
options: [{ excludedFunctions: ['someOtherName'] }],
400+
errors: [
401+
{
402+
messageId: 'useHook',
403+
line: 1,
404+
column: 1,
405+
},
406+
],
407+
},
377408
],
378409
});
379410

src/rules/require-hook.ts

+44-7
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ import {
1212
isTestCaseCall,
1313
} from './utils';
1414

15+
interface RequireHooksOptions {
16+
excludedFunctions?: readonly string[];
17+
}
18+
1519
const isJestFnCall = (node: TSESTree.CallExpression): boolean => {
1620
if (isDescribeCall(node) || isTestCaseCall(node) || isHook(node)) {
1721
return true;
@@ -27,12 +31,28 @@ const isNullOrUndefined = (node: TSESTree.Expression): boolean => {
2731
);
2832
};
2933

30-
const shouldBeInHook = (node: TSESTree.Node): boolean => {
34+
const isExcludedFnCall = (
35+
node: TSESTree.CallExpression,
36+
options: RequireHooksOptions,
37+
): boolean => {
38+
const nodeName = getNodeName(node);
39+
40+
if (nodeName === null) {
41+
return false;
42+
}
43+
44+
return !!options.excludedFunctions?.includes(nodeName);
45+
};
46+
47+
const shouldBeInHook = (
48+
node: TSESTree.Node,
49+
options: RequireHooksOptions,
50+
): boolean => {
3151
switch (node.type) {
3252
case AST_NODE_TYPES.ExpressionStatement:
33-
return shouldBeInHook(node.expression);
53+
return shouldBeInHook(node.expression, options);
3454
case AST_NODE_TYPES.CallExpression:
35-
return !isJestFnCall(node);
55+
return !(isJestFnCall(node) || isExcludedFnCall(node, options));
3656
case AST_NODE_TYPES.VariableDeclaration: {
3757
if (node.kind === 'const') {
3858
return false;
@@ -48,7 +68,7 @@ const shouldBeInHook = (node: TSESTree.Node): boolean => {
4868
}
4969
};
5070

51-
export default createRule({
71+
export default createRule<[RequireHooksOptions], 'useHook'>({
5272
name: __filename,
5373
meta: {
5474
docs: {
@@ -60,13 +80,30 @@ export default createRule({
6080
useHook: 'This should be done within a hook',
6181
},
6282
type: 'suggestion',
63-
schema: [],
83+
schema: [
84+
{
85+
type: 'object',
86+
properties: {
87+
excludedFunctions: {
88+
type: 'array',
89+
items: { type: 'string' },
90+
},
91+
},
92+
additionalProperties: false,
93+
},
94+
],
6495
},
65-
defaultOptions: [],
96+
defaultOptions: [
97+
{
98+
excludedFunctions: [],
99+
},
100+
],
66101
create(context) {
102+
const { excludedFunctions = [] } = context.options[0] ?? {};
103+
67104
const checkBlockBody = (body: TSESTree.BlockStatement['body']) => {
68105
for (const statement of body) {
69-
if (shouldBeInHook(statement)) {
106+
if (shouldBeInHook(statement, { excludedFunctions })) {
70107
context.report({
71108
node: statement,
72109
messageId: 'useHook',

0 commit comments

Comments
 (0)