Skip to content

Commit c33f78e

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

File tree

3 files changed

+284
-7
lines changed

3 files changed

+284
-7
lines changed

docs/rules/require-hook.md

+90
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,93 @@ 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+
For additional control you might need to reduce possible hooks which can be
174+
passed as an argument (by default it is `beforeAll`, `beforeEach`, `afterAll`, `afterEach`)
175+
176+
```json
177+
{
178+
"jest/require-hook": [
179+
"error",
180+
{
181+
"excludedFunctions": [
182+
"enableAutoDestroy"
183+
],
184+
"allowedHooks": ["beforeEach"]
185+
}
186+
]
187+
}
188+
```
189+
190+
191+
Examples of **incorrect** code for the `{ "excludedFunctions": ["expect"], "allowedHooks": ["beforeEach"] }`
192+
option:
193+
194+
```js
195+
/* eslint jest/require-hook: ["error", { "excludedFunctions": ["expect"], "allowedHooks": ["beforeEach"] }] */
196+
197+
import {
198+
enableAutoDestroy,
199+
resetAutoDestroyState,
200+
mount
201+
} from '@vue/test-utils';
202+
import initDatabase from './initDatabase';
203+
204+
enableAutoDestroy(afterEach);
205+
afterAll(resetAutoDestroyState); // this will throw a linting error
206+
initDatabase(); // this will too
207+
208+
describe('Foo', () => {
209+
test('always returns 42', () => {
210+
expect(global.getAnswer()).toBe(42);
211+
})
212+
})
213+
```
214+
215+
216+
Examples of **correct** code for the `{ "excludedFunctions": ["expect"], "allowedHooks": ["beforeEach", "afterAll"] }`
217+
option:
218+
219+
```js
220+
/* eslint jest/require-hook: ["error", { "excludedFunctions": ["expect"], "allowedHooks": ["beforeEach", "afterAll"] }] */
221+
222+
import {
223+
enableAutoDestroy,
224+
resetAutoDestroyState,
225+
mount
226+
} from '@vue/test-utils';
227+
import {initDatabase, tearDownDatabase} from './databaseUtils';
228+
229+
enableAutoDestroy(afterEach);
230+
afterAll(resetAutoDestroyState);
231+
232+
beforeEach(initDatabase);
233+
afterEach(tearDownDatabase);
234+
235+
describe('Foo', () => {
236+
test('always returns 42', () => {
237+
expect(global.getAnswer()).toBe(42);
238+
});
239+
});
240+
```

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

+126
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,54 @@ 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+
},
167+
{
168+
code: dedent`
169+
enableAutoDestroy(beforeEach);
170+
171+
describe('some tests', () => {
172+
it('is false', () => {
173+
expect(true).toBe(true);
174+
});
175+
});
176+
`,
177+
options: [{ excludedFunctions: ['enableAutoDestroy'] }],
178+
},
179+
{
180+
code: dedent`
181+
enableAutoDestroy(afterAll);
182+
183+
describe('some tests', () => {
184+
it('is false', () => {
185+
expect(true).toBe(true);
186+
});
187+
});
188+
`,
189+
options: [{ excludedFunctions: ['enableAutoDestroy'] }],
190+
},
191+
{
192+
code: dedent`
193+
enableAutoDestroy(beforeAll);
194+
195+
describe('some tests', () => {
196+
it('is false', () => {
197+
expect(true).toBe(true);
198+
});
199+
});
200+
`,
201+
options: [{ excludedFunctions: ['enableAutoDestroy'] }],
202+
},
155203
],
156204
invalid: [
157205
{
@@ -374,6 +422,84 @@ ruleTester.run('require-hook', rule, {
374422
},
375423
],
376424
},
425+
{
426+
code: dedent`
427+
enableAutoDestroy(afterEach);
428+
429+
describe('some tests', () => {
430+
it('is false', () => {
431+
expect(true).toBe(true);
432+
});
433+
});
434+
`,
435+
options: [{ excludedFunctions: ['someOtherName'] }],
436+
errors: [
437+
{
438+
messageId: 'useHook',
439+
line: 1,
440+
column: 1,
441+
},
442+
],
443+
},
444+
{
445+
code: dedent`
446+
someOtherName(afterUnknownHook);
447+
448+
describe('some tests', () => {
449+
it('is false', () => {
450+
expect(true).toBe(true);
451+
});
452+
});
453+
`,
454+
options: [{ excludedFunctions: ['someOtherName'] }],
455+
errors: [
456+
{
457+
messageId: 'useHook',
458+
line: 1,
459+
column: 1,
460+
},
461+
],
462+
},
463+
{
464+
code: dedent`
465+
someOtherName(afterEach);
466+
467+
describe('some tests', () => {
468+
it('is false', () => {
469+
expect(true).toBe(true);
470+
});
471+
});
472+
`,
473+
options: [
474+
{ excludedFunctions: ['someOtherName'], allowedHooks: ['beforeAll'] },
475+
],
476+
errors: [
477+
{
478+
messageId: 'useHook',
479+
line: 1,
480+
column: 1,
481+
},
482+
],
483+
},
484+
{
485+
code: dedent`
486+
enableAutoDestroy();
487+
488+
describe('some tests', () => {
489+
it('is false', () => {
490+
expect(true).toBe(true);
491+
});
492+
});
493+
`,
494+
options: [{ excludedFunctions: ['enableAutoDestroy'] }],
495+
errors: [
496+
{
497+
messageId: 'useHook',
498+
line: 1,
499+
column: 1,
500+
},
501+
],
502+
},
377503
],
378504
});
379505

src/rules/require-hook.ts

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

15+
interface RequireHooksOptions {
16+
excludedFunctions?: readonly string[];
17+
allowedHooks?: readonly string[];
18+
}
19+
20+
const isExcludedFnCall = (
21+
node: TSESTree.CallExpression,
22+
options: RequireHooksOptions,
23+
): boolean => {
24+
const [firstArgument] = node.arguments;
25+
26+
if (firstArgument === undefined) {
27+
return false;
28+
}
29+
30+
const nodeName = getNodeName(node);
31+
const argumentNodeName = getNodeName(firstArgument);
32+
33+
if (nodeName === null || argumentNodeName === null) {
34+
return false;
35+
}
36+
37+
return (
38+
!!options.excludedFunctions?.includes(nodeName) &&
39+
!!options.allowedHooks?.includes(argumentNodeName)
40+
);
41+
};
42+
1543
const isJestFnCall = (node: TSESTree.CallExpression): boolean => {
1644
if (isDescribeCall(node) || isTestCaseCall(node) || isHook(node)) {
1745
return true;
@@ -27,12 +55,15 @@ const isNullOrUndefined = (node: TSESTree.Expression): boolean => {
2755
);
2856
};
2957

30-
const shouldBeInHook = (node: TSESTree.Node): boolean => {
58+
const shouldBeInHook = (
59+
node: TSESTree.Node,
60+
options: RequireHooksOptions,
61+
): boolean => {
3162
switch (node.type) {
3263
case AST_NODE_TYPES.ExpressionStatement:
33-
return shouldBeInHook(node.expression);
64+
return shouldBeInHook(node.expression, options);
3465
case AST_NODE_TYPES.CallExpression:
35-
return !isJestFnCall(node);
66+
return !(isJestFnCall(node) || isExcludedFnCall(node, options));
3667
case AST_NODE_TYPES.VariableDeclaration: {
3768
if (node.kind === 'const') {
3869
return false;
@@ -48,7 +79,14 @@ const shouldBeInHook = (node: TSESTree.Node): boolean => {
4879
}
4980
};
5081

51-
export default createRule({
82+
const defaultAllowedHooks = [
83+
'beforeAll',
84+
'beforeEach',
85+
'afterAll',
86+
'afterEach',
87+
];
88+
89+
export default createRule<[RequireHooksOptions], 'useHook'>({
5290
name: __filename,
5391
meta: {
5492
docs: {
@@ -60,13 +98,36 @@ export default createRule({
6098
useHook: 'This should be done within a hook',
6199
},
62100
type: 'suggestion',
63-
schema: [],
101+
schema: [
102+
{
103+
type: 'object',
104+
properties: {
105+
excludedFunctions: {
106+
type: 'array',
107+
items: { type: 'string' },
108+
},
109+
allowedHooks: {
110+
type: 'array',
111+
items: { type: 'string' },
112+
default: defaultAllowedHooks,
113+
},
114+
},
115+
additionalProperties: false,
116+
},
117+
],
64118
},
65-
defaultOptions: [],
119+
defaultOptions: [
120+
{
121+
excludedFunctions: [],
122+
},
123+
],
66124
create(context) {
125+
const { allowedHooks = defaultAllowedHooks, excludedFunctions = [] } =
126+
context.options[0] ?? {};
127+
67128
const checkBlockBody = (body: TSESTree.BlockStatement['body']) => {
68129
for (const statement of body) {
69-
if (shouldBeInHook(statement)) {
130+
if (shouldBeInHook(statement, { allowedHooks, excludedFunctions })) {
70131
context.report({
71132
node: statement,
72133
messageId: 'useHook',

0 commit comments

Comments
 (0)