Skip to content

Commit 6420867

Browse files
authored
feat: new rule await-async-utils (#69)
* docs(await-async-utils): rule details * feat: new rule await-async-utils * docs: update util comment * test(await-async-utils): add extra test * test(await-async-utils): increase coverage * refactor(await-async-utils): move function definition outside
1 parent 2b9122d commit 6420867

File tree

7 files changed

+332
-1
lines changed

7 files changed

+332
-1
lines changed

README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
2626

27-
[![All Contributors](https://img.shields.io/badge/all_contributors-4-orange.svg?style=flat-square)](#contributors)
27+
[![All Contributors](https://img.shields.io/badge/all_contributors-4-orange.svg?style=flat-square)](#contributors-)
2828

2929
<!-- ALL-CONTRIBUTORS-BADGE:END -->
3030

@@ -137,6 +137,7 @@ To enable this configuration use the `extends` property in your
137137
| Rule | Description | Configurations | Fixable |
138138
| -------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------- | ------------------ |
139139
| [await-async-query](docs/rules/await-async-query.md) | Enforce async queries to have proper `await` | ![recommended-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
140+
| [await-async-utils](docs/rules/await-async-utils.md) | Enforce async utils to be awaited properly | ![recommended-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
140141
| [await-fire-event](docs/rules/await-fire-event.md) | Enforce async fire event methods to be awaited | ![vue-badge][] | |
141142
| [no-await-sync-query](docs/rules/no-await-sync-query.md) | Disallow unnecessary `await` for sync queries | ![recommended-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
142143
| [no-debug](docs/rules/no-debug.md) | Disallow the use of `debug` | ![angular-badge][] ![react-badge][] ![vue-badge][] | |

docs/rules/await-async-utils.md

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Enforce async utils to be awaited properly (await-async-utils)
2+
3+
Ensure that promises returned by async utils are handled properly.
4+
5+
## Rule Details
6+
7+
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:
8+
9+
- `wait`
10+
- `waitForElement`
11+
- `waitForDomChange`
12+
- `waitForElementToBeRemoved`
13+
14+
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+
16+
Examples of **incorrect** code for this rule:
17+
18+
```js
19+
test('something incorrectly', async () => {
20+
// ...
21+
wait(() => getByLabelText('email'));
22+
23+
const [usernameElement, passwordElement] = waitForElement(
24+
() => [
25+
getByLabelText(container, 'username'),
26+
getByLabelText(container, 'password'),
27+
],
28+
{ container }
29+
);
30+
31+
waitForDomChange(() => {
32+
return { container };
33+
});
34+
35+
waitForElementToBeRemoved(() => document.querySelector('div.getOuttaHere'));
36+
// ...
37+
});
38+
```
39+
40+
Examples of **correct** code for this rule:
41+
42+
```js
43+
test('something correctly', async () => {
44+
// ...
45+
// `await` operator is correct
46+
await wait(() => getByLabelText('email'));
47+
48+
const [usernameElement, passwordElement] = await waitForElement(
49+
() => [
50+
getByLabelText(container, 'username'),
51+
getByLabelText(container, 'password'),
52+
],
53+
{ container }
54+
);
55+
56+
// `then` chained method is correct
57+
waitForDomChange(() => {
58+
return { container };
59+
})
60+
.then(() => console.log('DOM changed!'))
61+
.catch(err => console.log(`Error you need to deal with: ${err}`));
62+
63+
// return the promise within a function is correct too!
64+
const makeCustomWait = () =>
65+
waitForElementToBeRemoved(() => document.querySelector('div.getOuttaHere'));
66+
// ...
67+
});
68+
```
69+
70+
## Further Reading
71+
72+
- [Async Utilities](https://testing-library.com/docs/dom-testing-library/api-async)

lib/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
const rules = {
44
'await-async-query': require('./rules/await-async-query'),
5+
'await-async-utils': require('./rules/await-async-utils'),
56
'await-fire-event': require('./rules/await-fire-event'),
67
'consistent-data-testid': require('./rules/consistent-data-testid'),
78
'no-await-sync-query': require('./rules/no-await-sync-query'),
@@ -13,6 +14,7 @@ const rules = {
1314

1415
const recommendedRules = {
1516
'testing-library/await-async-query': 'error',
17+
'testing-library/await-async-utils': 'error',
1618
'testing-library/no-await-sync-query': 'error',
1719
};
1820

lib/rules/await-async-utils.js

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
'use strict';
2+
3+
const { getDocsUrl, ASYNC_UTILS } = require('../utils');
4+
5+
const VALID_PARENTS = [
6+
'AwaitExpression',
7+
'ArrowFunctionExpression',
8+
'ReturnStatement',
9+
];
10+
11+
const ASYNC_UTILS_REGEXP = new RegExp(`^(${ASYNC_UTILS.join('|')})$`);
12+
13+
module.exports = {
14+
meta: {
15+
type: 'problem',
16+
docs: {
17+
description: 'Enforce async utils to be awaited properly',
18+
category: 'Best Practices',
19+
recommended: true,
20+
url: getDocsUrl('await-async-utils'),
21+
},
22+
messages: {
23+
awaitAsyncUtil: 'Promise returned from `{{ name }}` must be handled',
24+
},
25+
fixable: null,
26+
schema: [],
27+
},
28+
29+
create: function(context) {
30+
const testingLibraryUtilUsage = [];
31+
return {
32+
[`CallExpression > Identifier[name=${ASYNC_UTILS_REGEXP}]`](node) {
33+
if (!isAwaited(node.parent.parent) && !isPromiseResolved(node)) {
34+
testingLibraryUtilUsage.push(node);
35+
}
36+
},
37+
'Program:exit'() {
38+
testingLibraryUtilUsage.forEach(node => {
39+
const variableDeclaratorParent = node.parent.parent;
40+
41+
const references =
42+
(variableDeclaratorParent.type === 'VariableDeclarator' &&
43+
context
44+
.getDeclaredVariables(variableDeclaratorParent)[0]
45+
.references.slice(1)) ||
46+
[];
47+
48+
if (
49+
references &&
50+
references.length === 0 &&
51+
!isAwaited(node.parent.parent) &&
52+
!isPromiseResolved(node)
53+
) {
54+
context.report({
55+
node,
56+
messageId: 'awaitAsyncUtil',
57+
data: {
58+
name: node.name,
59+
},
60+
});
61+
} else {
62+
for (const reference of references) {
63+
const referenceNode = reference.identifier;
64+
if (
65+
!isAwaited(referenceNode.parent) &&
66+
!isPromiseResolved(referenceNode)
67+
) {
68+
context.report({
69+
node,
70+
messageId: 'awaitAsyncUtil',
71+
data: {
72+
name: node.name,
73+
},
74+
});
75+
76+
break;
77+
}
78+
}
79+
}
80+
});
81+
},
82+
};
83+
},
84+
};
85+
86+
function isAwaited(node) {
87+
return VALID_PARENTS.includes(node.type);
88+
}
89+
90+
const hasAThenProperty = node =>
91+
node.type === 'MemberExpression' && node.property.name === 'then';
92+
93+
function isPromiseResolved(node) {
94+
const parent = node.parent;
95+
96+
// wait(...).then(...)
97+
if (parent.type === 'CallExpression') {
98+
return hasAThenProperty(parent.parent);
99+
}
100+
101+
// promise.then(...)
102+
return hasAThenProperty(parent);
103+
}

lib/utils.js

+8
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,13 @@ const ALL_QUERIES_COMBINATIONS = [
4848
ASYNC_QUERIES_COMBINATIONS,
4949
];
5050

51+
const ASYNC_UTILS = [
52+
'wait',
53+
'waitForElement',
54+
'waitForDomChange',
55+
'waitForElementToBeRemoved',
56+
];
57+
5158
module.exports = {
5259
getDocsUrl,
5360
SYNC_QUERIES_VARIANTS,
@@ -57,4 +64,5 @@ module.exports = {
5764
SYNC_QUERIES_COMBINATIONS,
5865
ASYNC_QUERIES_COMBINATIONS,
5966
ALL_QUERIES_COMBINATIONS,
67+
ASYNC_UTILS,
6068
};

tests/__snapshots__/index.test.js.snap

+4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Object {
77
],
88
"rules": Object {
99
"testing-library/await-async-query": "error",
10+
"testing-library/await-async-utils": "error",
1011
"testing-library/no-await-sync-query": "error",
1112
"testing-library/no-debug": "warn",
1213
"testing-library/no-dom-import": Array [
@@ -24,6 +25,7 @@ Object {
2425
],
2526
"rules": Object {
2627
"testing-library/await-async-query": "error",
28+
"testing-library/await-async-utils": "error",
2729
"testing-library/no-await-sync-query": "error",
2830
"testing-library/no-debug": "warn",
2931
"testing-library/no-dom-import": Array [
@@ -41,6 +43,7 @@ Object {
4143
],
4244
"rules": Object {
4345
"testing-library/await-async-query": "error",
46+
"testing-library/await-async-utils": "error",
4447
"testing-library/no-await-sync-query": "error",
4548
},
4649
}
@@ -53,6 +56,7 @@ Object {
5356
],
5457
"rules": Object {
5558
"testing-library/await-async-query": "error",
59+
"testing-library/await-async-utils": "error",
5660
"testing-library/await-fire-event": "error",
5761
"testing-library/no-await-sync-query": "error",
5862
"testing-library/no-debug": "warn",

0 commit comments

Comments
 (0)