Skip to content

Commit ad3fc06

Browse files
Mark1626G-Rath
authored andcommitted
feat(rules): prefer-hooks-on-top (#425)
* feat(rules): prefer-hooks-on-top * chore: review changes * fix: failing test
1 parent 7017fc7 commit ad3fc06

File tree

5 files changed

+325
-1
lines changed

5 files changed

+325
-1
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ installations requiring long-term consistency.
136136
| [no-try-expect][] | Prevent `catch` assertions in tests | | |
137137
| [prefer-called-with][] | Suggest using `toBeCalledWith()` OR `toHaveBeenCalledWith()` | | |
138138
| [prefer-expect-assertions][] | Suggest using `expect.assertions()` OR `expect.hasAssertions()` | | |
139+
| [prefer-hooks-on-top][] | Suggest to have all hooks at top-level before tests | | |
139140
| [prefer-inline-snapshots][] | Suggest using `toMatchInlineSnapshot()` | | ![fixable-green][] |
140141
| [prefer-spy-on][] | Suggest using `jest.spyOn()` | | ![fixable-green][] |
141142
| [prefer-strict-equal][] | Suggest using `toStrictEqual()` | | ![fixable-green][] |
@@ -191,6 +192,7 @@ https://github.com/dangreenisrael/eslint-plugin-jest-formatting
191192
[prefer-called-with]: docs/rules/prefer-called-with.md
192193
[prefer-expect-assertions]: docs/rules/prefer-expect-assertions.md
193194
[prefer-inline-snapshots]: docs/rules/prefer-inline-snapshots.md
195+
[prefer-hooks-on-top]: docs/rules/prefer-hooks-on-top.md
194196
[prefer-spy-on]: docs/rules/prefer-spy-on.md
195197
[prefer-strict-equal]: docs/rules/prefer-strict-equal.md
196198
[prefer-to-be-null]: docs/rules/prefer-to-be-null.md

docs/rules/prefer-hooks-on-top.md

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Suggest to have all hooks at top-level before tests (prefer-hooks-on-top)
2+
3+
All hooks should be defined before the start of the tests
4+
5+
## Rule Details
6+
7+
Examples of **incorrect** code for this rule
8+
9+
```js
10+
/* eslint jest/prefer-hooks-on-top: "error" */
11+
12+
describe("foo" () => {
13+
beforeEach(() => {
14+
//some hook code
15+
});
16+
test("bar" () => {
17+
some_fn();
18+
});
19+
beforeAll(() => {
20+
//some hook code
21+
});
22+
test("bar" () => {
23+
some_fn();
24+
});
25+
});
26+
27+
// Nested describe scenario
28+
describe("foo" () => {
29+
beforeAll(() => {
30+
//some hook code
31+
});
32+
test("bar" () => {
33+
some_fn();
34+
});
35+
describe("inner_foo" () => {
36+
beforeEach(() => {
37+
//some hook code
38+
});
39+
test("inner bar" () => {
40+
some_fn();
41+
});
42+
test("inner bar" () => {
43+
some_fn();
44+
});
45+
beforeAll(() => {
46+
//some hook code
47+
});
48+
afterAll(() => {
49+
//some hook code
50+
});
51+
test("inner bar" () => {
52+
some_fn();
53+
});
54+
});
55+
});
56+
```
57+
58+
Examples of **correct** code for this rule
59+
60+
```js
61+
/* eslint jest/prefer-hooks-on-top: "error" */
62+
63+
describe("foo" () => {
64+
beforeEach(() => {
65+
//some hook code
66+
});
67+
68+
// Not affected by rule
69+
someSetup();
70+
71+
afterEach(() => {
72+
//some hook code
73+
});
74+
test("bar" () => {
75+
some_fn();
76+
});
77+
});
78+
79+
// Nested describe scenario
80+
describe("foo" () => {
81+
beforeEach(() => {
82+
//some hook code
83+
});
84+
test("bar" () => {
85+
some_fn();
86+
});
87+
describe("inner_foo" () => {
88+
beforeEach(() => {
89+
//some hook code
90+
});
91+
test("inner bar" () => {
92+
some_fn();
93+
});
94+
});
95+
});
96+
```

src/__tests__/rules.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { resolve } from 'path';
33
import plugin from '../';
44

55
const ruleNames = Object.keys(plugin.rules);
6-
const numberOfRules = 39;
6+
const numberOfRules = 40;
77

88
describe('rules', () => {
99
it('should have a corresponding doc for each rule', () => {
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import { TSESLint } from '@typescript-eslint/experimental-utils';
2+
import rule from '../prefer-hooks-on-top';
3+
4+
const ruleTester = new TSESLint.RuleTester({
5+
parserOptions: {
6+
ecmaVersion: 6,
7+
},
8+
});
9+
10+
ruleTester.run('basic describe block', rule, {
11+
valid: [
12+
`describe("foo", () => {
13+
beforeEach(() => {
14+
});
15+
someSetupFn();
16+
afterEach(() => {
17+
});
18+
test("bar", () => {
19+
some_fn();
20+
});
21+
});`,
22+
],
23+
invalid: [
24+
{
25+
code: `describe("foo", () => {
26+
beforeEach(() => {
27+
});
28+
test("bar", () => {
29+
some_fn();
30+
});
31+
beforeAll(() => {
32+
});
33+
test("bar", () => {
34+
some_fn();
35+
});
36+
});`,
37+
errors: [
38+
{
39+
messageId: 'noHookOnTop',
40+
column: 11,
41+
line: 7,
42+
},
43+
],
44+
},
45+
],
46+
});
47+
48+
ruleTester.run('multiple describe blocks', rule, {
49+
valid: [
50+
`describe.skip("foo", () => {
51+
beforeEach(() => {
52+
});
53+
beforeAll(() => {
54+
});
55+
test("bar", () => {
56+
some_fn();
57+
});
58+
});
59+
describe("foo", () => {
60+
beforeEach(() => {
61+
});
62+
test("bar", () => {
63+
some_fn();
64+
});
65+
});`,
66+
],
67+
68+
invalid: [
69+
{
70+
code: `describe.skip("foo", () => {
71+
beforeEach(() => {
72+
});
73+
test("bar", () => {
74+
some_fn();
75+
});
76+
beforeAll(() => {
77+
});
78+
test("bar", () => {
79+
some_fn();
80+
});
81+
});
82+
describe("foo", () => {
83+
beforeEach(() => {
84+
});
85+
beforeEach(() => {
86+
});
87+
beforeAll(() => {
88+
});
89+
test("bar", () => {
90+
some_fn();
91+
});
92+
});
93+
describe("foo", () => {
94+
test("bar", () => {
95+
some_fn();
96+
});
97+
beforeEach(() => {
98+
});
99+
beforeEach(() => {
100+
});
101+
beforeAll(() => {
102+
});
103+
});`,
104+
errors: [
105+
{
106+
messageId: 'noHookOnTop',
107+
column: 11,
108+
line: 7,
109+
},
110+
{
111+
messageId: 'noHookOnTop',
112+
column: 11,
113+
line: 28,
114+
},
115+
{
116+
messageId: 'noHookOnTop',
117+
column: 11,
118+
line: 30,
119+
},
120+
{
121+
messageId: 'noHookOnTop',
122+
column: 11,
123+
line: 32,
124+
},
125+
],
126+
},
127+
],
128+
});
129+
130+
ruleTester.run('nested describe blocks', rule, {
131+
valid: [
132+
`describe("foo", () => {
133+
beforeEach(() => {
134+
});
135+
test("bar", () => {
136+
some_fn();
137+
});
138+
describe("inner_foo" , () => {
139+
beforeEach(() => {
140+
});
141+
test("inner bar", () => {
142+
some_fn();
143+
});
144+
});
145+
});`,
146+
],
147+
148+
invalid: [
149+
{
150+
code: `describe("foo", () => {
151+
beforeAll(() => {
152+
});
153+
test("bar", () => {
154+
some_fn();
155+
});
156+
describe("inner_foo" , () => {
157+
beforeEach(() => {
158+
});
159+
test("inner bar", () => {
160+
some_fn();
161+
});
162+
test("inner bar", () => {
163+
some_fn();
164+
});
165+
beforeAll(() => {
166+
});
167+
afterAll(() => {
168+
});
169+
test("inner bar", () => {
170+
some_fn();
171+
});
172+
});
173+
});`,
174+
errors: [
175+
{
176+
messageId: 'noHookOnTop',
177+
column: 13,
178+
line: 16,
179+
},
180+
{
181+
messageId: 'noHookOnTop',
182+
column: 13,
183+
line: 18,
184+
},
185+
],
186+
},
187+
],
188+
});

src/rules/prefer-hooks-on-top.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { createRule, isHook, isTestCase } from './utils';
2+
3+
export default createRule({
4+
name: __filename,
5+
meta: {
6+
docs: {
7+
category: 'Best Practices',
8+
description: 'Suggest to have all hooks at top level',
9+
recommended: false,
10+
},
11+
messages: {
12+
noHookOnTop: 'Move all hooks before test cases',
13+
},
14+
schema: [],
15+
type: 'suggestion',
16+
},
17+
defaultOptions: [],
18+
create(context) {
19+
const hooksContext = [false];
20+
return {
21+
CallExpression(node) {
22+
if (!isHook(node) && isTestCase(node)) {
23+
hooksContext[hooksContext.length - 1] = true;
24+
}
25+
if (hooksContext[hooksContext.length - 1] && isHook(node)) {
26+
context.report({
27+
messageId: 'noHookOnTop',
28+
node,
29+
});
30+
}
31+
hooksContext.push(false);
32+
},
33+
'CallExpression:exit'() {
34+
hooksContext.pop();
35+
},
36+
};
37+
},
38+
});

0 commit comments

Comments
 (0)