Skip to content

Commit 91200b7

Browse files
renatoagdsgndelia
andauthored
feat: add no-side-effects-wait-for rule (#196)
* test: add scenarios for no-side-effects-wait-for * feat: add no-side-effects-wait-for rule * feat: add no-side-effects-wait-for in index * test: add more valid scenarios in no-side-effects-wait-for * docs: include no-side-effects-wait-for * fix: typo in no-side-effects-wait-for doc Co-authored-by: Gonzalo D'Elia <[email protected]> * fix: remove extra code in examples * refactor: use some instead filter in no-side-effects-wait-for * feat: check if no-side-effects-wait-for is called inside tests * refactor: use util for import check at no-side-effects-wait-for * test: valid scenario for no TL wait for import at no-side-effects * refactor: usage of correct user event methods Co-authored-by: Gonzalo D'Elia <[email protected]>
1 parent 8ad0184 commit 91200b7

File tree

5 files changed

+370
-0
lines changed

5 files changed

+370
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ To enable this configuration use the `extends` property in your
139139
| [no-multiple-assertions-wait-for](docs/rules/no-multiple-assertions-wait-for.md) | Disallow the use of multiple expect inside `waitFor` | | |
140140
| [no-node-access](docs/rules/no-node-access.md) | Disallow direct Node access | ![angular-badge][] ![react-badge][] ![vue-badge][] | |
141141
| [no-promise-in-fire-event](docs/rules/no-promise-in-fire-event.md) | Disallow the use of promises passed to a `fireEvent` method | | |
142+
| [no-side-effects-wait-for](docs/rules/no-side-effects-wait-for.md) | Disallow the use of side effects inside `waitFor` | | |
142143
| [no-wait-for-empty-callback](docs/rules/no-wait-for-empty-callback.md) | Disallow empty callbacks for `waitFor` and `waitForElementToBeRemoved` | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
143144
| [prefer-explicit-assert](docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than just `getBy*` queries | | |
144145
| [prefer-find-by](docs/rules/prefer-find-by.md) | Suggest using `findBy*` methods instead of the `waitFor` + `getBy` queries | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] |
+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Side effects inside `waitFor` are not preferred (no-side-effects-wait-for)
2+
3+
## Rule Details
4+
5+
This rule aims to avoid the usage of side effects actions (`fireEvent` or `userEvent`) inside `waitFor`.
6+
Since `waitFor` is intended for things that have a non-deterministic amount of time between the action you performed and the assertion passing,
7+
the callback can be called (or checked for errors) a non-deterministic number of times and frequency.
8+
This will make your side-effect run multiple times.
9+
10+
Example of **incorrect** code for this rule:
11+
12+
```js
13+
await waitFor(() => {
14+
fireEvent.keyDown(input, { key: 'ArrowDown' });
15+
expect(b).toEqual('b');
16+
});
17+
18+
// or
19+
await waitFor(function() {
20+
fireEvent.keyDown(input, { key: 'ArrowDown' });
21+
expect(b).toEqual('b');
22+
});
23+
24+
// or
25+
await waitFor(() => {
26+
userEvent.click(button);
27+
expect(b).toEqual('b');
28+
});
29+
30+
// or
31+
await waitFor(function() {
32+
userEvent.click(button);
33+
expect(b).toEqual('b');
34+
});
35+
};
36+
```
37+
38+
Examples of **correct** code for this rule:
39+
40+
```js
41+
fireEvent.keyDown(input, { key: 'ArrowDown' });
42+
await waitFor(() => {
43+
expect(b).toEqual('b');
44+
});
45+
46+
// or
47+
fireEvent.keyDown(input, { key: 'ArrowDown' });
48+
await waitFor(function() {
49+
expect(b).toEqual('b');
50+
});
51+
52+
// or
53+
userEvent.click(button);
54+
await waitFor(() => {
55+
expect(b).toEqual('b');
56+
});
57+
58+
// or
59+
userEvent.click(button);
60+
await waitFor(function() {
61+
expect(b).toEqual('b');
62+
});
63+
};
64+
```
65+
66+
## Further Reading
67+
68+
- [about `waitFor`](https://testing-library.com/docs/dom-testing-library/api-async#waitfor)
69+
- [about `userEvent`](https://github.com/testing-library/user-event)
70+
- [about `fireEvent`](https://testing-library.com/docs/dom-testing-library/api-events)
71+
- [inspiration for this rule](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library#performing-side-effects-in-waitfor)

lib/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import preferUserEvent from './rules/prefer-user-event';
1717
import preferWaitFor from './rules/prefer-wait-for';
1818
import noMultipleAssertionsWaitFor from './rules/no-multiple-assertions-wait-for';
1919
import preferFindBy from './rules/prefer-find-by';
20+
import noSideEffectsWaitFor from './rules/no-side-effects-wait-for';
2021
import renderResultNamingConvention from './rules/render-result-naming-convention';
2122

2223
const rules = {
@@ -32,6 +33,7 @@ const rules = {
3233
'no-multiple-assertions-wait-for': noMultipleAssertionsWaitFor,
3334
'no-node-access': noNodeAccess,
3435
'no-promise-in-fire-event': noPromiseInFireEvent,
36+
'no-side-effects-wait-for': noSideEffectsWaitFor,
3537
'no-wait-for-empty-callback': noWaitForEmptyCallback,
3638
'prefer-explicit-assert': preferExplicitAssert,
3739
'prefer-find-by': preferFindBy,

lib/rules/no-side-effects-wait-for.ts

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils'
2+
import { getDocsUrl, hasTestingLibraryImportModule } from '../utils'
3+
import { isBlockStatement, findClosestCallNode, isMemberExpression, isCallExpression, isIdentifier } from '../node-utils'
4+
5+
export const RULE_NAME = 'no-side-effects-wait-for';
6+
7+
const WAIT_EXPRESSION_QUERY =
8+
'CallExpression[callee.name=/^(waitFor)$/]';
9+
10+
const SIDE_EFFECTS: Array<string> = ['fireEvent', 'userEvent']
11+
12+
export type MessageIds = 'noSideEffectsWaitFor';
13+
type Options = [];
14+
15+
export default ESLintUtils.RuleCreator(getDocsUrl)<Options, MessageIds>({
16+
name: RULE_NAME,
17+
meta: {
18+
type: 'suggestion',
19+
docs: {
20+
description:
21+
"It's preferred to avoid side effects in `waitFor`",
22+
category: 'Best Practices',
23+
recommended: false,
24+
},
25+
messages: {
26+
noSideEffectsWaitFor: 'Avoid using side effects within `waitFor` callback',
27+
},
28+
fixable: null,
29+
schema: [],
30+
},
31+
defaultOptions: [],
32+
create: function(context) {
33+
let isImportingTestingLibrary = false;
34+
35+
function reportSideEffects(
36+
node: TSESTree.BlockStatement
37+
) {
38+
const hasSideEffects = (body: Array<TSESTree.Node>): boolean =>
39+
body.some((node: TSESTree.ExpressionStatement) => {
40+
if (
41+
isCallExpression(node.expression) &&
42+
isMemberExpression(node.expression.callee) &&
43+
isIdentifier(node.expression.callee.object)
44+
) {
45+
const object: TSESTree.Identifier = node.expression.callee.object
46+
const identifierName: string = object.name
47+
return SIDE_EFFECTS.includes(identifierName)
48+
} else {
49+
return false
50+
}
51+
})
52+
53+
if (isImportingTestingLibrary && isBlockStatement(node) && hasSideEffects(node.body)) {
54+
context.report({
55+
node,
56+
loc: node.loc.start,
57+
messageId: 'noSideEffectsWaitFor',
58+
});
59+
}
60+
}
61+
62+
return {
63+
[`${WAIT_EXPRESSION_QUERY} > ArrowFunctionExpression > BlockStatement`]: reportSideEffects,
64+
[`${WAIT_EXPRESSION_QUERY} > FunctionExpression > BlockStatement`]: reportSideEffects,
65+
ImportDeclaration(node: TSESTree.ImportDeclaration) {
66+
isImportingTestingLibrary = hasTestingLibraryImportModule(node);
67+
}
68+
};
69+
}
70+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
import { createRuleTester } from '../test-utils';
2+
import rule, { RULE_NAME } from '../../../lib/rules/no-side-effects-wait-for';
3+
4+
const ruleTester = createRuleTester({
5+
ecmaFeatures: {
6+
jsx: true,
7+
},
8+
});
9+
10+
ruleTester.run(RULE_NAME, rule, {
11+
valid: [
12+
{
13+
code: `
14+
import { waitFor } from '@testing-library/react';
15+
await waitFor(() => expect(a).toEqual('a'))
16+
`,
17+
},
18+
{
19+
code: `
20+
import { waitFor } from '@testing-library/react';
21+
await waitFor(function() {
22+
expect(a).toEqual('a')
23+
})
24+
`,
25+
},
26+
{
27+
code: `
28+
import { waitFor } from '@testing-library/react';
29+
await waitFor(() => {
30+
console.log('testing-library')
31+
expect(b).toEqual('b')
32+
})
33+
`,
34+
},
35+
{
36+
code: `
37+
import { waitFor } from '@testing-library/react';
38+
await waitFor(function() {
39+
console.log('testing-library')
40+
expect(b).toEqual('b')
41+
})
42+
`,
43+
},
44+
{
45+
code: `
46+
import { waitFor } from '@testing-library/react';
47+
await waitFor(() => {})
48+
`,
49+
},
50+
{
51+
code: `
52+
import { waitFor } from '@testing-library/react';
53+
await waitFor(function() {})
54+
`,
55+
},
56+
{
57+
code: `
58+
import { waitFor } from '@testing-library/react';
59+
await waitFor(() => {
60+
// testing
61+
})
62+
`,
63+
},
64+
{
65+
code: `
66+
import { waitFor } from '@testing-library/react';
67+
await waitFor(function() {
68+
// testing
69+
})
70+
`,
71+
},
72+
{
73+
code: `
74+
import { waitFor } from '@testing-library/react';
75+
fireEvent.keyDown(input, {key: 'ArrowDown'})
76+
await waitFor(() => {
77+
expect(b).toEqual('b')
78+
})
79+
`
80+
}, {
81+
code: `
82+
import { waitFor } from '@testing-library/react';
83+
fireEvent.keyDown(input, {key: 'ArrowDown'})
84+
await waitFor(function() {
85+
expect(b).toEqual('b')
86+
})
87+
`
88+
}, {
89+
code: `
90+
import { waitFor } from '@testing-library/react';
91+
userEvent.click(button)
92+
await waitFor(function() {
93+
expect(b).toEqual('b')
94+
})
95+
`
96+
}, {
97+
code: `
98+
import { waitFor } from 'react';
99+
await waitFor(function() {
100+
fireEvent.keyDown(input, {key: 'ArrowDown'})
101+
expect(b).toEqual('b')
102+
})
103+
`
104+
}
105+
],
106+
invalid: [
107+
// fireEvent
108+
{
109+
code: `
110+
import { waitFor } from '@testing-library/react';
111+
await waitFor(() => {
112+
fireEvent.keyDown(input, {key: 'ArrowDown'})
113+
})
114+
`,
115+
errors: [{ messageId: 'noSideEffectsWaitFor' }]
116+
},
117+
{
118+
code: `
119+
import { waitFor } from '@testing-library/react';
120+
await waitFor(() => {
121+
expect(b).toEqual('b')
122+
fireEvent.keyDown(input, {key: 'ArrowDown'})
123+
})
124+
`,
125+
errors: [{ messageId: 'noSideEffectsWaitFor' }]
126+
},
127+
{
128+
code: `
129+
import { waitFor } from '@testing-library/react';
130+
await waitFor(() => {
131+
fireEvent.keyDown(input, {key: 'ArrowDown'})
132+
expect(b).toEqual('b')
133+
})
134+
`,
135+
errors: [{ messageId: 'noSideEffectsWaitFor' }]
136+
},
137+
{
138+
code: `
139+
import { waitFor } from '@testing-library/react';
140+
await waitFor(function() {
141+
fireEvent.keyDown(input, {key: 'ArrowDown'})
142+
})
143+
`,
144+
errors: [{ messageId: 'noSideEffectsWaitFor' }]
145+
},
146+
{
147+
code: `
148+
import { waitFor } from '@testing-library/react';
149+
await waitFor(function() {
150+
expect(b).toEqual('b')
151+
fireEvent.keyDown(input, {key: 'ArrowDown'})
152+
})
153+
`,
154+
errors: [{ messageId: 'noSideEffectsWaitFor' }]
155+
},
156+
{
157+
code: `
158+
import { waitFor } from '@testing-library/react';
159+
await waitFor(function() {
160+
fireEvent.keyDown(input, {key: 'ArrowDown'})
161+
expect(b).toEqual('b')
162+
})
163+
`,
164+
errors: [{ messageId: 'noSideEffectsWaitFor' }]
165+
},
166+
// userEvent
167+
{
168+
code: `
169+
import { waitFor } from '@testing-library/react';
170+
await waitFor(() => {
171+
userEvent.click(button)
172+
})
173+
`,
174+
errors: [{ messageId: 'noSideEffectsWaitFor' }]
175+
},
176+
{
177+
code: `
178+
import { waitFor } from '@testing-library/react';
179+
await waitFor(() => {
180+
expect(b).toEqual('b')
181+
userEvent.click(button)
182+
})
183+
`,
184+
errors: [{ messageId: 'noSideEffectsWaitFor' }]
185+
},
186+
{
187+
code: `
188+
import { waitFor } from '@testing-library/react';
189+
await waitFor(() => {
190+
userEvent.click(button)
191+
expect(b).toEqual('b')
192+
})
193+
`,
194+
errors: [{ messageId: 'noSideEffectsWaitFor' }]
195+
},
196+
{
197+
code: `
198+
import { waitFor } from '@testing-library/react';
199+
await waitFor(function() {
200+
userEvent.click(button)
201+
})
202+
`,
203+
errors: [{ messageId: 'noSideEffectsWaitFor' }]
204+
},
205+
{
206+
code: `
207+
import { waitFor } from '@testing-library/react';
208+
await waitFor(function() {
209+
expect(b).toEqual('b')
210+
userEvent.click(button)
211+
})
212+
`,
213+
errors: [{ messageId: 'noSideEffectsWaitFor' }]
214+
},
215+
{
216+
code: `
217+
import { waitFor } from '@testing-library/react';
218+
await waitFor(function() {
219+
userEvent.click(button)
220+
expect(b).toEqual('b')
221+
})
222+
`,
223+
errors: [{ messageId: 'noSideEffectsWaitFor' }]
224+
}
225+
]
226+
})

0 commit comments

Comments
 (0)