Skip to content

Commit 384654c

Browse files
aaron012G-Rath
andauthoredMay 28, 2022
feat: create prefer-hooks-in-order rule (#1098)
* feat: create `prefer-hooks-in-order` rule * wip * Implement prefer-hooks-in-order logic * Fix all rules tests * ci: run prettier and docs generation, fix test description * test: Add examples to the tests * test: Add some more complicated tests * ci: change isHook to isHookCall * feat: use early returns consistently Co-authored-by: Gareth Jones <jones258@gmail.com>
1 parent 7c28c6b commit 384654c

File tree

6 files changed

+904
-1
lines changed

6 files changed

+904
-1
lines changed
 

‎README.md

+1
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ installations requiring long-term consistency.
209209
| [prefer-equality-matcher](docs/rules/prefer-equality-matcher.md) | Suggest using the built-in equality matchers | | ![suggest][] |
210210
| [prefer-expect-assertions](docs/rules/prefer-expect-assertions.md) | Suggest using `expect.assertions()` OR `expect.hasAssertions()` | | ![suggest][] |
211211
| [prefer-expect-resolves](docs/rules/prefer-expect-resolves.md) | Prefer `await expect(...).resolves` over `expect(await ...)` syntax | | ![fixable][] |
212+
| [prefer-hooks-in-order](docs/rules/prefer-hooks-in-order.md) | Prefer having hooks in a consistent order | | |
212213
| [prefer-hooks-on-top](docs/rules/prefer-hooks-on-top.md) | Suggest having hooks before any test cases | | |
213214
| [prefer-lowercase-title](docs/rules/prefer-lowercase-title.md) | Enforce lowercase test names | | ![fixable][] |
214215
| [prefer-snapshot-hint](docs/rules/prefer-snapshot-hint.md) | Prefer including a hint with external snapshots | | |

‎docs/rules/prefer-hooks-in-order.md

+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# Prefer having hooks in a consistent order (`prefer-hooks-in-order`)
2+
3+
While hooks can be setup in any order, they're always called by `jest` in this
4+
specific order:
5+
6+
1. `beforeAll`
7+
1. `beforeEach`
8+
1. `afterEach`
9+
1. `afterAll`
10+
11+
This rule aims to make that more obvious by enforcing grouped hooks be setup in
12+
that order within tests.
13+
14+
## Rule Details
15+
16+
Examples of **incorrect** code for this rule
17+
18+
```js
19+
/* eslint jest/prefer-hooks-in-order: "error" */
20+
21+
describe('foo', () => {
22+
beforeEach(() => {
23+
seedMyDatabase();
24+
});
25+
26+
beforeAll(() => {
27+
createMyDatabase();
28+
});
29+
30+
it('accepts this input', () => {
31+
// ...
32+
});
33+
34+
it('returns that value', () => {
35+
// ...
36+
});
37+
38+
describe('when the database has specific values', () => {
39+
const specificValue = '...';
40+
41+
beforeEach(() => {
42+
seedMyDatabase(specificValue);
43+
});
44+
45+
it('accepts that input', () => {
46+
// ...
47+
});
48+
49+
it('throws an error', () => {
50+
// ...
51+
});
52+
53+
afterEach(() => {
54+
clearLogger();
55+
});
56+
beforeEach(() => {
57+
mockLogger();
58+
});
59+
60+
it('logs a message', () => {
61+
// ...
62+
});
63+
});
64+
65+
afterAll(() => {
66+
removeMyDatabase();
67+
});
68+
});
69+
```
70+
71+
Examples of **correct** code for this rule
72+
73+
```js
74+
/* eslint jest/prefer-hooks-in-order: "error" */
75+
76+
describe('foo', () => {
77+
beforeAll(() => {
78+
createMyDatabase();
79+
});
80+
81+
beforeEach(() => {
82+
seedMyDatabase();
83+
});
84+
85+
it('accepts this input', () => {
86+
// ...
87+
});
88+
89+
it('returns that value', () => {
90+
// ...
91+
});
92+
93+
describe('when the database has specific values', () => {
94+
const specificValue = '...';
95+
96+
beforeEach(() => {
97+
seedMyDatabase(specificValue);
98+
});
99+
100+
it('accepts that input', () => {
101+
// ...
102+
});
103+
104+
it('throws an error', () => {
105+
// ...
106+
});
107+
108+
beforeEach(() => {
109+
mockLogger();
110+
});
111+
112+
afterEach(() => {
113+
clearLogger();
114+
});
115+
116+
it('logs a message', () => {
117+
// ...
118+
});
119+
});
120+
121+
afterAll(() => {
122+
removeMyDatabase();
123+
});
124+
});
125+
```
126+
127+
## Also See
128+
129+
- [`prefer-hooks-on-top`](prefer-hooks-on-top.md)
130+
131+
## Further Reading
132+
133+
- [Order of execution of describe and test blocks](https://jestjs.io/docs/setup-teardown#order-of-execution-of-describe-and-test-blocks)

‎src/__tests__/__snapshots__/rules.test.ts.snap

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ Object {
3939
"jest/prefer-equality-matcher": "error",
4040
"jest/prefer-expect-assertions": "error",
4141
"jest/prefer-expect-resolves": "error",
42+
"jest/prefer-hooks-in-order": "error",
4243
"jest/prefer-hooks-on-top": "error",
4344
"jest/prefer-lowercase-title": "error",
4445
"jest/prefer-snapshot-hint": "error",

‎src/__tests__/rules.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { existsSync } from 'fs';
22
import { resolve } from 'path';
33
import plugin from '../';
44

5-
const numberOfRules = 47;
5+
const numberOfRules = 48;
66
const ruleNames = Object.keys(plugin.rules);
77
const deprecatedRules = Object.entries(plugin.rules)
88
.filter(([, rule]) => rule.meta.deprecated)

‎src/rules/__tests__/prefer-hooks-in-order.test.ts

+690
Large diffs are not rendered by default.

‎src/rules/prefer-hooks-in-order.ts

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { createRule, isHookCall } from './utils';
2+
3+
const HooksOrder = [
4+
'beforeAll',
5+
'beforeEach',
6+
'afterEach',
7+
'afterAll',
8+
] as const;
9+
10+
export default createRule({
11+
name: __filename,
12+
meta: {
13+
docs: {
14+
category: 'Best Practices',
15+
description: 'Prefer having hooks in a consistent order',
16+
recommended: false,
17+
},
18+
messages: {
19+
reorderHooks: `\`{{ currentHook }}\` hooks should be before any \`{{ previousHook }}\` hooks`,
20+
},
21+
schema: [],
22+
type: 'suggestion',
23+
},
24+
defaultOptions: [],
25+
create(context) {
26+
let previousHookIndex = -1;
27+
let inHook = false;
28+
29+
return {
30+
CallExpression(node) {
31+
if (inHook) {
32+
// Ignore everything that is passed into a hook
33+
return;
34+
}
35+
36+
if (!isHookCall(node, context.getScope())) {
37+
// Reset the previousHookIndex when encountering something different from a hook
38+
previousHookIndex = -1;
39+
40+
return;
41+
}
42+
43+
inHook = true;
44+
const currentHook = node.callee.name;
45+
const currentHookIndex = HooksOrder.indexOf(currentHook);
46+
47+
if (currentHookIndex < previousHookIndex) {
48+
context.report({
49+
messageId: 'reorderHooks',
50+
node,
51+
data: {
52+
previousHook: HooksOrder[previousHookIndex],
53+
currentHook,
54+
},
55+
});
56+
57+
return;
58+
}
59+
60+
previousHookIndex = currentHookIndex;
61+
},
62+
'CallExpression:exit'(node) {
63+
if (isHookCall(node, context.getScope())) {
64+
inHook = false;
65+
66+
return;
67+
}
68+
69+
if (inHook) {
70+
return;
71+
}
72+
73+
// Reset the previousHookIndex when encountering something different from a hook
74+
previousHookIndex = -1;
75+
},
76+
};
77+
},
78+
});

0 commit comments

Comments
 (0)
Please sign in to comment.