Skip to content

Commit 9062040

Browse files
authored
refactor(await-async-utils): use custom rule creator (#263)
* refactor: extract utils for checking promise all methods * test(await-async-query): add cases for promise all and allSettled * docs(await-async-query): add cases for promise all and allSettled * refactor(await-async-utils): create rule with custom creator * refactor(await-async-utils): replace async utils regexp by method * refactor(await-async-utils): replace manual import checks by helper * refactor(await-async-utils): replace manual promise checks by util * refactor(await-async-utils): merge identifier and member expression nodes checks * test(await-async-query): check column on invalid cases * test(await-async-query): promise.allSettled cases * refactor(await-async-query): extract util to get innermost returning function name * feat(await-async-utils): report unhandled functions wrapping async utils * docs: minor improvements * test(await-async-utils): increase coverage * refactor: repurpose getInnermostReturningFunctionName to getInnermostReturningFunction
1 parent dcc0693 commit 9062040

8 files changed

+325
-162
lines changed

docs/rules/await-async-query.md

+16-2
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@ found. Those queries variants are:
1212
- `findAllBy*`
1313

1414
This rule aims to prevent users from forgetting to handle the returned
15-
promise from those async queries to be fulfilled, which could lead to
16-
errors in the tests. The promise will be considered as handled when:
15+
promise from those async queries, which could lead to
16+
problems in the tests. The promise will be considered as handled when:
1717

1818
- using the `await` operator
19+
- wrapped within `Promise.all` or `Promise.allSettled` methods
1920
- chaining the `then` method
2021
- chaining `resolves` or `rejects` from jest
2122
- it's returned from a function (in this case, that particular function will be analyzed by this rule too)
@@ -70,6 +71,19 @@ const findMyButton = () => findByText('my button');
7071
const someButton = await findMyButton();
7172
```
7273

74+
```js
75+
// several promises handled with `Promise.all` is correct
76+
await Promise.all([findByText('my button'), findByText('something else')]);
77+
```
78+
79+
```js
80+
// several promises handled `Promise.allSettled` is correct
81+
await Promise.allSettled([
82+
findByText('my button'),
83+
findByText('something else'),
84+
]);
85+
```
86+
7387
```js
7488
// using a resolves/rejects matcher is also correct
7589
expect(findByTestId('alert')).resolves.toBe('Success');

docs/rules/await-async-utils.md

+29-9
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,26 @@
1-
# Enforce async utils to be awaited properly (await-async-utils)
1+
# Enforce promises from async utils to be handled (await-async-utils)
22

33
Ensure that promises returned by async utils are handled properly.
44

55
## Rule Details
66

77
Testing library provides several utilities for dealing with asynchronous code. These are useful to wait for an element until certain criteria or situation happens. The available async utils are:
88

9-
- `waitFor` _(introduced in dom-testing-library v7)_
9+
- `waitFor` _(introduced since dom-testing-library v7)_
1010
- `waitForElementToBeRemoved`
11-
- `wait` _(**deprecated** in dom-testing-library v7)_
12-
- `waitForElement` _(**deprecated** in dom-testing-library v7)_
13-
- `waitForDomChange` _(**deprecated** in dom-testing-library v7)_
11+
- `wait` _(**deprecated** since dom-testing-library v7)_
12+
- `waitForElement` _(**deprecated** since dom-testing-library v7)_
13+
- `waitForDomChange` _(**deprecated** since dom-testing-library v7)_
1414

15-
This rule aims to prevent users from forgetting to handle the returned promise from those async utils, which could lead to unexpected errors in the tests execution. The promises can be handled by using either `await` operator or `then` method.
15+
This rule aims to prevent users from forgetting to handle the returned
16+
promise from async utils, which could lead to
17+
problems in the tests. The promise will be considered as handled when:
18+
19+
- using the `await` operator
20+
- wrapped within `Promise.all` or `Promise.allSettled` methods
21+
- chaining the `then` method
22+
- chaining `resolves` or `rejects` from jest
23+
- it's returned from a function (in this case, that particular function will be analyzed by this rule too)
1624

1725
Examples of **incorrect** code for this rule:
1826

@@ -32,6 +40,14 @@ test('something incorrectly', async () => {
3240
waitFor(() => {}, { timeout: 100 });
3341

3442
waitForElementToBeRemoved(() => document.querySelector('div.getOuttaHere'));
43+
44+
// wrap an async util within a function...
45+
const makeCustomWait = () => {
46+
return waitForElementToBeRemoved(() =>
47+
document.querySelector('div.getOuttaHere')
48+
);
49+
};
50+
makeCustomWait(); // ...but not handling promise from it is incorrect
3551
});
3652
```
3753

@@ -56,9 +72,13 @@ test('something correctly', async () => {
5672
.then(() => console.log('DOM changed!'))
5773
.catch((err) => console.log(`Error you need to deal with: ${err}`));
5874

59-
// return the promise within a function is correct too!
60-
const makeCustomWait = () =>
61-
waitForElementToBeRemoved(() => document.querySelector('div.getOuttaHere'));
75+
// wrap an async util within a function...
76+
const makeCustomWait = () => {
77+
return waitForElementToBeRemoved(() =>
78+
document.querySelector('div.getOuttaHere')
79+
);
80+
};
81+
await makeCustomWait(); // ...and handling promise from it is correct
6282

6383
// using Promise.all combining the methods
6484
await Promise.all([

lib/detect-testing-library-utils.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
isCallExpression,
1616
isObjectPattern,
1717
} from './node-utils';
18-
import { ABSENCE_MATCHERS, PRESENCE_MATCHERS } from './utils';
18+
import { ABSENCE_MATCHERS, ASYNC_UTILS, PRESENCE_MATCHERS } from './utils';
1919

2020
export type TestingLibrarySettings = {
2121
'testing-library/module'?: string;
@@ -53,6 +53,7 @@ export type DetectionHelpers = {
5353
isFindByQuery: (node: TSESTree.Identifier) => boolean;
5454
isSyncQuery: (node: TSESTree.Identifier) => boolean;
5555
isAsyncQuery: (node: TSESTree.Identifier) => boolean;
56+
isAsyncUtil: (node: TSESTree.Identifier) => boolean;
5657
isPresenceAssert: (node: TSESTree.MemberExpression) => boolean;
5758
isAbsenceAssert: (node: TSESTree.MemberExpression) => boolean;
5859
canReportErrors: () => boolean;
@@ -168,6 +169,13 @@ export function detectTestingLibraryUtils<
168169
return isFindByQuery(node);
169170
};
170171

172+
/**
173+
* Determines whether a given node is async util or not.
174+
*/
175+
const isAsyncUtil: DetectionHelpers['isAsyncUtil'] = (node) => {
176+
return ASYNC_UTILS.includes(node.name);
177+
};
178+
171179
/**
172180
* Determines whether a given MemberExpression node is a presence assert
173181
*
@@ -312,6 +320,7 @@ export function detectTestingLibraryUtils<
312320
isFindByQuery,
313321
isSyncQuery,
314322
isAsyncQuery,
323+
isAsyncUtil,
315324
isPresenceAssert,
316325
isAbsenceAssert,
317326
canReportErrors,

lib/node-utils.ts

+72
Original file line numberDiff line numberDiff line change
@@ -212,11 +212,45 @@ export function hasChainedThen(node: TSESTree.Node): boolean {
212212
return hasThenProperty(parent);
213213
}
214214

215+
export function isPromiseAll(node: TSESTree.CallExpression): boolean {
216+
return (
217+
isMemberExpression(node.callee) &&
218+
ASTUtils.isIdentifier(node.callee.object) &&
219+
node.callee.object.name === 'Promise' &&
220+
ASTUtils.isIdentifier(node.callee.property) &&
221+
node.callee.property.name === 'all'
222+
);
223+
}
224+
225+
export function isPromiseAllSettled(node: TSESTree.CallExpression): boolean {
226+
return (
227+
isMemberExpression(node.callee) &&
228+
ASTUtils.isIdentifier(node.callee.object) &&
229+
node.callee.object.name === 'Promise' &&
230+
ASTUtils.isIdentifier(node.callee.property) &&
231+
node.callee.property.name === 'allSettled'
232+
);
233+
}
234+
235+
export function isPromisesArrayResolved(node: TSESTree.Node): boolean {
236+
const parent = node.parent;
237+
238+
return (
239+
isCallExpression(parent) &&
240+
isArrayExpression(parent.parent) &&
241+
isCallExpression(parent.parent.parent) &&
242+
(isPromiseAll(parent.parent.parent) ||
243+
isPromiseAllSettled(parent.parent.parent))
244+
);
245+
}
246+
215247
/**
216248
* Determines whether an Identifier related to a promise is considered as handled.
217249
*
218250
* It will be considered as handled if:
219251
* - it belongs to the `await` expression
252+
* - it belongs to the `Promise.all` method
253+
* - it belongs to the `Promise.allSettled` method
220254
* - it's chained with the `then` method
221255
* - it's returned from a function
222256
* - has `resolves` or `rejects`
@@ -250,6 +284,10 @@ export function isPromiseHandled(nodeIdentifier: TSESTree.Identifier): boolean {
250284
if (hasChainedThen(node)) {
251285
return true;
252286
}
287+
288+
if (isPromisesArrayResolved(node)) {
289+
return true;
290+
}
253291
}
254292

255293
return false;
@@ -476,3 +514,37 @@ export function hasClosestExpectResolvesRejects(node: TSESTree.Node): boolean {
476514

477515
return hasClosestExpectResolvesRejects(node.parent);
478516
}
517+
518+
/**
519+
* Gets the Function node which returns the given Identifier.
520+
*/
521+
export function getInnermostReturningFunction(
522+
context: RuleContext<string, []>,
523+
node: TSESTree.Identifier
524+
):
525+
| TSESTree.FunctionDeclaration
526+
| TSESTree.FunctionExpression
527+
| TSESTree.ArrowFunctionExpression
528+
| undefined {
529+
const functionScope = getInnermostFunctionScope(context, node);
530+
531+
if (!functionScope) {
532+
return;
533+
}
534+
535+
const returnStatementNode = getFunctionReturnStatementNode(
536+
functionScope.block
537+
);
538+
539+
if (!returnStatementNode) {
540+
return;
541+
}
542+
543+
const returnStatementIdentifier = getIdentifierNode(returnStatementNode);
544+
545+
if (returnStatementIdentifier?.name !== node.name) {
546+
return;
547+
}
548+
549+
return functionScope.block;
550+
}

lib/rules/await-async-query.ts

+4-22
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@ import { ASTUtils, TSESTree } from '@typescript-eslint/experimental-utils';
22
import {
33
findClosestCallExpressionNode,
44
getFunctionName,
5-
getFunctionReturnStatementNode,
6-
getIdentifierNode,
7-
getInnermostFunctionScope,
5+
getInnermostReturningFunction,
86
getVariableReferences,
97
isPromiseHandled,
108
} from '../node-utils';
@@ -37,25 +35,9 @@ export default createTestingLibraryRule<Options, MessageIds>({
3735
const functionWrappersNames: string[] = [];
3836

3937
function detectAsyncQueryWrapper(node: TSESTree.Identifier) {
40-
const functionScope = getInnermostFunctionScope(context, node);
41-
42-
if (functionScope) {
43-
// save function wrapper calls rather than async calls to be reported later
44-
const returnStatementNode = getFunctionReturnStatementNode(
45-
functionScope.block
46-
);
47-
48-
if (!returnStatementNode) {
49-
return;
50-
}
51-
52-
const returnStatementIdentifier = getIdentifierNode(
53-
returnStatementNode
54-
);
55-
56-
if (returnStatementIdentifier?.name === node.name) {
57-
functionWrappersNames.push(getFunctionName(functionScope.block));
58-
}
38+
const innerFunction = getInnermostReturningFunction(context, node);
39+
if (innerFunction) {
40+
functionWrappersNames.push(getFunctionName(innerFunction));
5941
}
6042
}
6143

0 commit comments

Comments
 (0)