Skip to content

Commit 5caa05c

Browse files
committed
feat: new rule await-async-utils
1 parent 34dfde6 commit 5caa05c

File tree

7 files changed

+269
-12
lines changed

7 files changed

+269
-12
lines changed

README.md

Lines changed: 2 additions & 1 deletion
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

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,19 @@ Examples of **incorrect** code for this rule:
1919
test('something incorrectly', async () => {
2020
// ...
2121
wait(() => getByLabelText('email'));
22-
22+
2323
const [usernameElement, passwordElement] = waitForElement(
2424
() => [
2525
getByLabelText(container, 'username'),
2626
getByLabelText(container, 'password'),
2727
],
2828
{ container }
2929
);
30-
31-
waitForDomChange(() => { return { container } });
32-
30+
31+
waitForDomChange(() => {
32+
return { container };
33+
});
34+
3335
waitForElementToBeRemoved(() => document.querySelector('div.getOuttaHere'));
3436
// ...
3537
});
@@ -40,23 +42,27 @@ Examples of **correct** code for this rule:
4042
```js
4143
test('something correctly', async () => {
4244
// ...
45+
// `await` operator is correct
4346
await wait(() => getByLabelText('email'));
44-
47+
4548
const [usernameElement, passwordElement] = await waitForElement(
4649
() => [
4750
getByLabelText(container, 'username'),
4851
getByLabelText(container, 'password'),
4952
],
5053
{ container }
5154
);
52-
53-
waitForDomChange(() => { return { container } })
55+
56+
// `then` chained method is correct
57+
waitForDomChange(() => {
58+
return { container };
59+
})
5460
.then(() => console.log('DOM changed!'))
5561
.catch(err => console.log(`Error you need to deal with: ${err}`));
56-
57-
waitForElementToBeRemoved(
58-
() => document.querySelector('div.getOuttaHere')
59-
).then(() => console.log('Element no longer in DOM'));
62+
63+
// return the promise within a function is correct too!
64+
const makeCustomWait = () =>
65+
waitForElementToBeRemoved(() => document.querySelector('div.getOuttaHere'));
6066
// ...
6167
});
6268
```

lib/index.js

Lines changed: 2 additions & 0 deletions
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

Lines changed: 103 additions & 0 deletions
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+
function isPromiseResolved(node) {
91+
const parent = node.parent;
92+
93+
const hasAThenProperty = node =>
94+
node.type === 'MemberExpression' && node.property.name === 'then';
95+
96+
// findByText("foo").then(...)
97+
if (parent.type === 'CallExpression') {
98+
return hasAThenProperty(parent.parent);
99+
}
100+
101+
// promise.then(...)
102+
return hasAThenProperty(parent);
103+
}

lib/utils.js

Lines changed: 8 additions & 0 deletions
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

Lines changed: 4 additions & 0 deletions
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",

tests/lib/rules/await-async-utils.js

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
'use strict';
2+
3+
const rule = require('../../../lib/rules/await-async-utils');
4+
const { ASYNC_UTILS } = require('../../../lib/utils');
5+
const RuleTester = require('eslint').RuleTester;
6+
7+
const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2018 } });
8+
9+
ruleTester.run('await-async-utils', rule, {
10+
valid: [
11+
...ASYNC_UTILS.map(asyncUtil => ({
12+
code: `
13+
test('${asyncUtil} util directly waited with await operator is valid', async () => {
14+
doSomethingElse();
15+
await ${asyncUtil}(() => getByLabelText('email'));
16+
});
17+
`,
18+
})),
19+
20+
...ASYNC_UTILS.map(asyncUtil => ({
21+
code: `
22+
test('${asyncUtil} util promise saved in var and waited with await operator is valid', async () => {
23+
doSomethingElse();
24+
const aPromise = ${asyncUtil}(() => getByLabelText('email'));
25+
await aPromise;
26+
});
27+
`,
28+
})),
29+
30+
...ASYNC_UTILS.map(asyncUtil => ({
31+
code: `
32+
test('${asyncUtil} util directly chained with then is valid', () => {
33+
doSomethingElse();
34+
${asyncUtil}(() => getByLabelText('email')).then(() => { console.log('done') });
35+
});
36+
`,
37+
})),
38+
39+
...ASYNC_UTILS.map(asyncUtil => ({
40+
code: `
41+
test('${asyncUtil} util promise saved in var and chained with then is valid', () => {
42+
doSomethingElse();
43+
const aPromise = ${asyncUtil}(() => getByLabelText('email'));
44+
aPromise.then(() => { console.log('done') });
45+
});
46+
`,
47+
})),
48+
49+
...ASYNC_UTILS.map(asyncUtil => ({
50+
code: `
51+
test('${asyncUtil} util directly returned in arrow function is valid', async () => {
52+
const makeCustomWait = () =>
53+
${asyncUtil}(() =>
54+
document.querySelector('div.getOuttaHere')
55+
);
56+
});
57+
`,
58+
})),
59+
60+
...ASYNC_UTILS.map(asyncUtil => ({
61+
code: `
62+
test('${asyncUtil} util explicitly returned in arrow function is valid', async () => {
63+
const makeCustomWait = () => {
64+
return ${asyncUtil}(() =>
65+
document.querySelector('div.getOuttaHere')
66+
);
67+
};
68+
});
69+
`,
70+
})),
71+
72+
...ASYNC_UTILS.map(asyncUtil => ({
73+
code: `
74+
test('${asyncUtil} util returned in regular function is valid', async () => {
75+
function makeCustomWait() {
76+
return ${asyncUtil}(() =>
77+
document.querySelector('div.getOuttaHere')
78+
);
79+
}
80+
});
81+
`,
82+
})),
83+
84+
...ASYNC_UTILS.map(asyncUtil => ({
85+
code: `
86+
test('${asyncUtil} util promise saved in var and returned in function is valid', async () => {
87+
const makeCustomWait = () => {
88+
const aPromise = ${asyncUtil}(() =>
89+
document.querySelector('div.getOuttaHere')
90+
);
91+
92+
doSomethingElse();
93+
94+
return aPromise;
95+
};
96+
});
97+
`,
98+
})),
99+
],
100+
invalid: [
101+
...ASYNC_UTILS.map(asyncUtil => ({
102+
code: `
103+
test('${asyncUtil} util not waited', () => {
104+
doSomethingElse();
105+
${asyncUtil}(() => getByLabelText('email'));
106+
});
107+
`,
108+
errors: [{ line: 4, messageId: 'awaitAsyncUtil' }],
109+
})),
110+
...ASYNC_UTILS.map(asyncUtil => ({
111+
code: `
112+
test('${asyncUtil} util promise saved not waited', () => {
113+
doSomethingElse();
114+
const aPromise = ${asyncUtil}(() => getByLabelText('email'));
115+
});
116+
`,
117+
errors: [{ line: 4, column: 28, messageId: 'awaitAsyncUtil' }],
118+
})),
119+
...ASYNC_UTILS.map(asyncUtil => ({
120+
code: `
121+
test('several ${asyncUtil} utils not waited', () => {
122+
${asyncUtil}(() => getByLabelText('username'));
123+
doSomethingElse();
124+
${asyncUtil}(() => getByLabelText('email'));
125+
});
126+
`,
127+
errors: [
128+
{ line: 3, messageId: 'awaitAsyncUtil' },
129+
{ line: 5, messageId: 'awaitAsyncUtil' },
130+
],
131+
})),
132+
],
133+
});

0 commit comments

Comments
 (0)