Skip to content

Commit 11d67b2

Browse files
feat: add rule no-multiple-assertions-wait-for (#189)
* feat: add initial files for no-multiple-expect-wait-for rule * fix: add expect fields in test * feat: add no-multiple-assertion-wait-for logic * feat: add findClosestCalleName in node-utils * feat: add check for expect and rename file * docs: add no-multiple-assertions-wait-for rule doc * docs: add link for no-multiple-assertions-wait-for doc * docs: insert function example in no-multiple-assertions-wait-for * refactor: remove find closest call node from node-utils * fix: check expect based in total * docs: better english in no-multiple-assertions-wait-for rule details Co-authored-by: Tim Deschryver <[email protected]> * fix: use correct rule name in no-multiple-assertions-wait-for Co-authored-by: Tim Deschryver <[email protected]> * docs: improve docs for no-multiple-assertions-wait-for * fix: typo in no-multiple-assertions-wait-for * fix: better english in no-multiple-assertions-wait-for Co-authored-by: Tim Deschryver <[email protected]>
1 parent 9db40ee commit 11d67b2

File tree

5 files changed

+233
-0
lines changed

5 files changed

+233
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ To enable this configuration use the `extends` property in your
136136
| [no-debug](docs/rules/no-debug.md) | Disallow the use of `debug` | ![angular-badge][] ![react-badge][] ![vue-badge][] | |
137137
| [no-dom-import](docs/rules/no-dom-import.md) | Disallow importing from DOM Testing Library | ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] |
138138
| [no-manual-cleanup](docs/rules/no-manual-cleanup.md) | Disallow the use of `cleanup` | | |
139+
| [no-multiple-assertions-wait-for](docs/rules/no-multiple-assertions-wait-for.md) | Disallow the use of multiple expect inside `waitFor` | | |
139140
| [no-promise-in-fire-event](docs/rules/no-promise-in-fire-event.md) | Disallow the use of promises passed to a `fireEvent` method | | |
140141
| [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][] | |
141142
| [prefer-explicit-assert](docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than just `getBy*` queries | | |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Multiple assertions inside `waitFor` are not preferred (no-multiple-assertions-wait-for)
2+
3+
## Rule Details
4+
5+
This rule aims to ensure the correct usage of `expect` inside `waitFor`, in the way that they're intended to be used.
6+
When using multiples assertions inside `waitFor`, if one fails, you have to wait for a timeout before seeing it failing.
7+
Putting one assertion, you can both wait for the UI to settle to the state you want to assert on,
8+
and also fail faster if one of the assertions do end up failing
9+
10+
Example of **incorrect** code for this rule:
11+
12+
```js
13+
const foo = async () => {
14+
await waitFor(() => {
15+
expect(a).toEqual('a');
16+
expect(b).toEqual('b');
17+
});
18+
19+
// or
20+
await waitFor(function() {
21+
expect(a).toEqual('a')
22+
expect(b).toEqual('b');
23+
})
24+
};
25+
```
26+
27+
Examples of **correct** code for this rule:
28+
29+
```js
30+
const foo = async () => {
31+
await waitFor(() => expect(a).toEqual('a'));
32+
expect(b).toEqual('b');
33+
34+
// or
35+
await waitFor(function() {
36+
expect(a).toEqual('a')
37+
})
38+
expect(b).toEqual('b');
39+
40+
// it only detects expect
41+
// so this case doesn't generate warnings
42+
await waitFor(() => {
43+
fireEvent.keyDown(input, { key: 'ArrowDown' });
44+
expect(b).toEqual('b');
45+
});
46+
};
47+
```
48+
49+
## Further Reading
50+
51+
- [about `waitFor`](https://testing-library.com/docs/dom-testing-library/api-async#waitfor)
52+
- [inspiration for this rule](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library#having-multiple-assertions-in-a-single-waitfor-callback)

lib/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import preferExplicitAssert from './rules/prefer-explicit-assert';
1313
import preferPresenceQueries from './rules/prefer-presence-queries';
1414
import preferScreenQueries from './rules/prefer-screen-queries';
1515
import preferWaitFor from './rules/prefer-wait-for';
16+
import noMultipleAssertionsWaitFor from './rules/no-multiple-assertions-wait-for'
1617
import preferFindBy from './rules/prefer-find-by';
1718

1819
const rules = {
@@ -25,6 +26,7 @@ const rules = {
2526
'no-debug': noDebug,
2627
'no-dom-import': noDomImport,
2728
'no-manual-cleanup': noManualCleanup,
29+
'no-multiple-assertions-wait-for': noMultipleAssertionsWaitFor,
2830
'no-promise-in-fire-event': noPromiseInFireEvent,
2931
'no-wait-for-empty-callback': noWaitForEmptyCallback,
3032
'prefer-explicit-assert': preferExplicitAssert,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils'
2+
import { getDocsUrl } from '../utils'
3+
import { isBlockStatement, findClosestCallNode, isMemberExpression, isCallExpression, isIdentifier } from '../node-utils'
4+
5+
export const RULE_NAME = 'no-multiple-assertions-wait-for';
6+
7+
const WAIT_EXPRESSION_QUERY =
8+
'CallExpression[callee.name=/^(waitFor)$/]';
9+
10+
export type MessageIds = 'noMultipleAssertionWaitFor';
11+
type Options = [];
12+
13+
export default ESLintUtils.RuleCreator(getDocsUrl)<Options, MessageIds>({
14+
name: RULE_NAME,
15+
meta: {
16+
type: 'suggestion',
17+
docs: {
18+
description:
19+
"It's preferred to avoid multiple assertions in `waitFor`",
20+
category: 'Best Practices',
21+
recommended: false,
22+
},
23+
messages: {
24+
noMultipleAssertionWaitFor: 'Avoid using multiple assertions within `waitFor` callback',
25+
},
26+
fixable: null,
27+
schema: [],
28+
},
29+
defaultOptions: [],
30+
create: function(context) {
31+
function reportMultipleAssertion(
32+
node: TSESTree.BlockStatement
33+
) {
34+
const totalExpect = (body: Array<TSESTree.Node>): Array<TSESTree.Node> =>
35+
body.filter((node: TSESTree.ExpressionStatement) => {
36+
if (
37+
isCallExpression(node.expression) &&
38+
isMemberExpression(node.expression.callee) &&
39+
isCallExpression(node.expression.callee.object)
40+
) {
41+
const object: TSESTree.CallExpression = node.expression.callee.object
42+
const expressionName: string = isIdentifier(object.callee) && object.callee.name
43+
return expressionName === 'expect'
44+
} else {
45+
return false
46+
}
47+
})
48+
49+
if (isBlockStatement(node) && totalExpect(node.body).length > 1) {
50+
context.report({
51+
node,
52+
loc: node.loc.start,
53+
messageId: 'noMultipleAssertionWaitFor',
54+
});
55+
}
56+
}
57+
58+
return {
59+
[`${WAIT_EXPRESSION_QUERY} > ArrowFunctionExpression > BlockStatement`]: reportMultipleAssertion,
60+
[`${WAIT_EXPRESSION_QUERY} > FunctionExpression > BlockStatement`]: reportMultipleAssertion,
61+
};
62+
}
63+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { createRuleTester } from '../test-utils';
2+
import rule, { RULE_NAME } from '../../../lib/rules/no-multiple-assertions-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+
await waitFor(() => expect(a).toEqual('a'))
15+
`,
16+
},
17+
{
18+
code: `
19+
await waitFor(function() {
20+
expect(a).toEqual('a')
21+
})
22+
`,
23+
},
24+
// this needs to be check by other rule
25+
{
26+
code: `
27+
await waitFor(() => {
28+
fireEvent.keyDown(input, {key: 'ArrowDown'})
29+
expect(b).toEqual('b')
30+
})
31+
`,
32+
},
33+
{
34+
code: `
35+
await waitFor(function() {
36+
fireEvent.keyDown(input, {key: 'ArrowDown'})
37+
expect(b).toEqual('b')
38+
})
39+
`,
40+
},
41+
{
42+
code: `
43+
await waitFor(() => {
44+
console.log('testing-library')
45+
expect(b).toEqual('b')
46+
})
47+
`,
48+
},
49+
{
50+
code: `
51+
await waitFor(function() {
52+
console.log('testing-library')
53+
expect(b).toEqual('b')
54+
})
55+
`,
56+
},
57+
{
58+
code: `
59+
await waitFor(() => {})
60+
`,
61+
},
62+
{
63+
code: `
64+
await waitFor(function() {})
65+
`,
66+
},
67+
{
68+
code: `
69+
await waitFor(() => {
70+
// testing
71+
})
72+
`,
73+
},
74+
],
75+
invalid: [
76+
{
77+
code: `
78+
await waitFor(() => {
79+
expect(a).toEqual('a')
80+
expect(b).toEqual('b')
81+
})
82+
`,
83+
errors: [{ messageId: 'noMultipleAssertionWaitFor' }]
84+
},
85+
{
86+
code: `
87+
await waitFor(() => {
88+
expect(a).toEqual('a')
89+
console.log('testing-library')
90+
expect(b).toEqual('b')
91+
})
92+
`,
93+
errors: [{ messageId: 'noMultipleAssertionWaitFor' }]
94+
},
95+
{
96+
code: `
97+
await waitFor(function() {
98+
expect(a).toEqual('a')
99+
expect(b).toEqual('b')
100+
})
101+
`,
102+
errors: [{ messageId: 'noMultipleAssertionWaitFor' }]
103+
},
104+
{
105+
code: `
106+
await waitFor(function() {
107+
expect(a).toEqual('a')
108+
console.log('testing-library')
109+
expect(b).toEqual('b')
110+
})
111+
`,
112+
errors: [{ messageId: 'noMultipleAssertionWaitFor' }]
113+
}
114+
]
115+
})

0 commit comments

Comments
 (0)