Skip to content

Commit 0a02e06

Browse files
authored
Merge pull request #225 from playwright-community/prefer-hooks-in-order
Add `prefer-hooks-in-order` rule
2 parents 39610ae + a42513c commit 0a02e06

6 files changed

+961
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ command line option.\
142142
|| 🔧 | | [no-useless-not](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-useless-not.md) | Disallow usage of `not` matchers when a specific matcher exists |
143143
|| | 💡 | [no-wait-for-selector](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-wait-for-selector.md) | Disallow usage of `page.waitForSelector()` |
144144
|| | 💡 | [no-wait-for-timeout](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-wait-for-timeout.md) | Disallow usage of `page.waitForTimeout()` |
145+
| | | | [prefer-hooks-in-order](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-hooks-in-order.md) | Prefer having hooks in a consistent order |
145146
| | | | [prefer-hooks-on-top](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-hooks-on-top.md) | Suggest having hooks before any test cases |
146147
| | | 💡 | [prefer-strict-equal](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-strict-equal.md) | Suggest using `toStrictEqual()` |
147148
| | 🔧 | | [prefer-lowercase-title](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-lowercase-title.md) | Enforce lowercase test names |

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

+129
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
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 `playwright` in
4+
this 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 playwright/prefer-hooks-in-order: "error" */
20+
21+
test.describe('foo', () => {
22+
test.beforeEach(() => {
23+
seedMyDatabase();
24+
});
25+
26+
test.beforeAll(() => {
27+
createMyDatabase();
28+
});
29+
30+
test('accepts this input', () => {
31+
// ...
32+
});
33+
34+
test('returns that value', () => {
35+
// ...
36+
});
37+
38+
test.describe('when the database has specific values', () => {
39+
const specificValue = '...';
40+
41+
test.beforeEach(() => {
42+
seedMyDatabase(specificValue);
43+
});
44+
45+
test('accepts that input', () => {
46+
// ...
47+
});
48+
49+
test('throws an error', () => {
50+
// ...
51+
});
52+
53+
test.afterEach(() => {
54+
clearLogger();
55+
});
56+
test.beforeEach(() => {
57+
mockLogger();
58+
});
59+
60+
test('logs a message', () => {
61+
// ...
62+
});
63+
});
64+
65+
test.afterAll(() => {
66+
removeMyDatabase();
67+
});
68+
});
69+
```
70+
71+
Examples of **correct** code for this rule
72+
73+
```js
74+
/* eslint playwright/prefer-hooks-in-order: "error" */
75+
76+
test.describe('foo', () => {
77+
test.beforeAll(() => {
78+
createMyDatabase();
79+
});
80+
81+
test.beforeEach(() => {
82+
seedMyDatabase();
83+
});
84+
85+
test('accepts this input', () => {
86+
// ...
87+
});
88+
89+
test('returns that value', () => {
90+
// ...
91+
});
92+
93+
test.describe('when the database has specific values', () => {
94+
const specificValue = '...';
95+
96+
test.beforeEach(() => {
97+
seedMyDatabase(specificValue);
98+
});
99+
100+
test('accepts that input', () => {
101+
// ...
102+
});
103+
104+
test('throws an error', () => {
105+
// ...
106+
});
107+
108+
test.beforeEach(() => {
109+
mockLogger();
110+
});
111+
112+
test.afterEach(() => {
113+
clearLogger();
114+
});
115+
116+
test('logs a message', () => {
117+
// ...
118+
});
119+
});
120+
121+
test.afterAll(() => {
122+
removeMyDatabase();
123+
});
124+
});
125+
```
126+
127+
## Also See
128+
129+
- [`prefer-hooks-on-top`](prefer-hooks-on-top.md)

src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import noUselessAwait from './rules/no-useless-await';
2222
import noUselessNot from './rules/no-useless-not';
2323
import noWaitForSelector from './rules/no-wait-for-selector';
2424
import noWaitForTimeout from './rules/no-wait-for-timeout';
25+
import preferHooksInOrder from './rules/prefer-hooks-in-order';
2526
import preferHooksOnTop from './rules/prefer-hooks-on-top';
2627
import preferLowercaseTitle from './rules/prefer-lowercase-title';
2728
import preferStrictEqual from './rules/prefer-strict-equal';
@@ -61,6 +62,7 @@ const index = {
6162
'no-useless-not': noUselessNot,
6263
'no-wait-for-selector': noWaitForSelector,
6364
'no-wait-for-timeout': noWaitForTimeout,
65+
'prefer-hooks-in-order': preferHooksInOrder,
6466
'prefer-hooks-on-top': preferHooksOnTop,
6567
'prefer-lowercase-title': preferLowercaseTitle,
6668
'prefer-strict-equal': preferStrictEqual,

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

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

0 commit comments

Comments
 (0)