Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 86c9452

Browse files
authoredOct 2, 2022
feat: rename await-fire-event to await-async-event and support user-event (#652)
1 parent d0d0df5 commit 86c9452

15 files changed

+1082
-540
lines changed
 

‎README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -206,9 +206,9 @@ To enable this configuration use the `extends` property in your
206206

207207
| Name | Description | 🔧 | Included in configurations |
208208
| ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------- | --- | ---------------------------------------------------------------------------------- |
209+
| [`await-async-event`](./docs/rules/await-async-event.md) | Enforce promises from async event methods are handled | | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] ![marko-badge][] |
209210
| [`await-async-query`](./docs/rules/await-async-query.md) | Enforce promises from async queries to be handled | | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] ![marko-badge][] |
210211
| [`await-async-utils`](./docs/rules/await-async-utils.md) | Enforce promises from async utils to be awaited properly | | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] ![marko-badge][] |
211-
| [`await-fire-event`](./docs/rules/await-fire-event.md) | Enforce promises from `fireEvent` methods to be handled | | ![vue-badge][] ![marko-badge][] |
212212
| [`consistent-data-testid`](./docs/rules/consistent-data-testid.md) | Ensures consistent usage of `data-testid` | | |
213213
| [`no-await-sync-events`](./docs/rules/no-await-sync-events.md) | Disallow unnecessary `await` for sync events | | |
214214
| [`no-await-sync-query`](./docs/rules/no-await-sync-query.md) | Disallow unnecessary `await` for sync queries | | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] ![marko-badge][] |

‎docs/rules/await-async-event.md

+143
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
# Enforce promises from async event methods are handled (`testing-library/await-async-event`)
2+
3+
Ensure that promises returned by `userEvent` (v14+) async methods or `fireEvent` (only Vue and Marko) async methods are handled properly.
4+
5+
## Rule Details
6+
7+
This rule aims to prevent users from forgetting to handle promise returned from async event
8+
methods.
9+
10+
> ⚠️ `fireEvent` methods are async only on following Testing Library packages:
11+
>
12+
> - `@testing-library/vue` (supported by this plugin)
13+
> - `@testing-library/svelte` (not supported yet by this plugin)
14+
> - `@marko/testing-library` (supported by this plugin)
15+
16+
Examples of **incorrect** code for this rule:
17+
18+
```js
19+
fireEvent.click(getByText('Click me'));
20+
21+
fireEvent.focus(getByLabelText('username'));
22+
fireEvent.blur(getByLabelText('username'));
23+
24+
// wrap a fireEvent method within a function...
25+
function triggerEvent() {
26+
return fireEvent.click(button);
27+
}
28+
triggerEvent(); // ...but not handling promise from it is incorrect too
29+
```
30+
31+
```js
32+
userEvent.click(getByText('Click me'));
33+
userEvent.tripleClick(getByText('Click me'));
34+
userEvent.keyboard('foo');
35+
36+
// wrap a userEvent method within a function...
37+
function triggerEvent() {
38+
return userEvent.click(button);
39+
}
40+
triggerEvent(); // ...but not handling promise from it is incorrect too
41+
```
42+
43+
Examples of **correct** code for this rule:
44+
45+
```js
46+
// `await` operator is correct
47+
await fireEvent.focus(getByLabelText('username'));
48+
await fireEvent.blur(getByLabelText('username'));
49+
50+
// `then` method is correct
51+
fireEvent.click(getByText('Click me')).then(() => {
52+
// ...
53+
});
54+
55+
// return the promise within a function is correct too!
56+
const clickMeArrowFn = () => fireEvent.click(getByText('Click me'));
57+
58+
// wrap a fireEvent method within a function...
59+
function triggerEvent() {
60+
return fireEvent.click(button);
61+
}
62+
await triggerEvent(); // ...and handling promise from it is correct also
63+
64+
// using `Promise.all` or `Promise.allSettled` with an array of promises is valid
65+
await Promise.all([
66+
fireEvent.focus(getByLabelText('username')),
67+
fireEvent.blur(getByLabelText('username')),
68+
]);
69+
```
70+
71+
```js
72+
// `await` operator is correct
73+
await userEvent.click(getByText('Click me'));
74+
await userEvent.tripleClick(getByText('Click me'));
75+
76+
// `then` method is correct
77+
userEvent.keyboard('foo').then(() => {
78+
// ...
79+
});
80+
81+
// return the promise within a function is correct too!
82+
const clickMeArrowFn = () => userEvent.click(getByText('Click me'));
83+
84+
// wrap a userEvent method within a function...
85+
function triggerEvent() {
86+
return userEvent.click(button);
87+
}
88+
await triggerEvent(); // ...and handling promise from it is correct also
89+
90+
// using `Promise.all` or `Promise.allSettled` with an array of promises is valid
91+
await Promise.all([
92+
userEvent.click(getByText('Click me'));
93+
userEvent.tripleClick(getByText('Click me'));
94+
]);
95+
```
96+
97+
## Options
98+
99+
- `eventModule`: `string` or `string[]`. Which event module should be linted for async event methods. Defaults to `userEvent` which should be used after v14. `fireEvent` should only be used with frameworks that have async fire event methods.
100+
101+
## Example
102+
103+
```json
104+
{
105+
"testing-library/await-async-event": [
106+
2,
107+
{
108+
"eventModule": "userEvent"
109+
}
110+
]
111+
}
112+
```
113+
114+
```json
115+
{
116+
"testing-library/await-async-event": [
117+
2,
118+
{
119+
"eventModule": "fireEvent"
120+
}
121+
]
122+
}
123+
```
124+
125+
```json
126+
{
127+
"testing-library/await-async-event": [
128+
2,
129+
{
130+
"eventModule": ["fireEvent", "userEvent"]
131+
}
132+
]
133+
}
134+
```
135+
136+
## When Not To Use It
137+
138+
- `userEvent` is below v14, before all event methods are async
139+
- `fireEvent` methods are sync for most Testing Library packages. If you are not using Testing Library package with async events, you shouldn't use this rule.
140+
141+
## Further Reading
142+
143+
- [Vue Testing Library fireEvent](https://testing-library.com/docs/vue-testing-library/api#fireevent)

‎docs/rules/await-fire-event.md

-66
This file was deleted.

‎docs/rules/no-await-sync-events.md

+1-2
Original file line numberDiff line numberDiff line change
@@ -105,5 +105,4 @@ Example:
105105
## Notes
106106

107107
- Since `user-event` v14 all its methods are async, so you should disable reporting them by setting the `eventModules` to just `"fire-event"` so `user-event` methods are not reported.
108-
- There is another rule `await-fire-event`, which is only in Vue Testing
109-
Library. Please do not confuse with this rule.
108+
- There is another rule `await-async-event`, which is for awaiting async events for `user-event` v14 or `fire-event` only in Vue Testing Library. Please do not confuse with this rule.

‎lib/configs/angular.ts

+4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
export = {
66
plugins: ['testing-library'],
77
rules: {
8+
'testing-library/await-async-event': [
9+
'error',
10+
{ eventModule: 'userEvent' },
11+
],
812
'testing-library/await-async-query': 'error',
913
'testing-library/await-async-utils': 'error',
1014
'testing-library/no-await-sync-query': 'error',

‎lib/configs/dom.ts

+4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
export = {
66
plugins: ['testing-library'],
77
rules: {
8+
'testing-library/await-async-event': [
9+
'error',
10+
{ eventModule: 'userEvent' },
11+
],
812
'testing-library/await-async-query': 'error',
913
'testing-library/await-async-utils': 'error',
1014
'testing-library/no-await-sync-query': 'error',

‎lib/configs/marko.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55
export = {
66
plugins: ['testing-library'],
77
rules: {
8+
'testing-library/await-async-event': [
9+
'error',
10+
{ eventModule: ['fireEvent', 'userEvent'] },
11+
],
812
'testing-library/await-async-query': 'error',
913
'testing-library/await-async-utils': 'error',
10-
'testing-library/await-fire-event': 'error',
1114
'testing-library/no-await-sync-query': 'error',
1215
'testing-library/no-container': 'error',
1316
'testing-library/no-debugging-utils': 'error',

‎lib/configs/react.ts

+4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
export = {
66
plugins: ['testing-library'],
77
rules: {
8+
'testing-library/await-async-event': [
9+
'error',
10+
{ eventModule: 'userEvent' },
11+
],
812
'testing-library/await-async-query': 'error',
913
'testing-library/await-async-utils': 'error',
1014
'testing-library/no-await-sync-query': 'error',

‎lib/configs/vue.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55
export = {
66
plugins: ['testing-library'],
77
rules: {
8+
'testing-library/await-async-event': [
9+
'error',
10+
{ eventModule: ['fireEvent', 'userEvent'] },
11+
],
812
'testing-library/await-async-query': 'error',
913
'testing-library/await-async-utils': 'error',
10-
'testing-library/await-fire-event': 'error',
1114
'testing-library/no-await-sync-query': 'error',
1215
'testing-library/no-container': 'error',
1316
'testing-library/no-debugging-utils': 'error',

‎lib/node-utils/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ export function isPromiseHandled(nodeIdentifier: TSESTree.Identifier): boolean {
234234
}
235235

236236
export function getVariableReferences(
237-
context: TSESLint.RuleContext<string, []>,
237+
context: TSESLint.RuleContext<string, unknown[]>,
238238
node: TSESTree.Node
239239
): TSESLint.Scope.Reference[] {
240240
if (ASTUtils.isVariableDeclarator(node)) {

‎lib/rules/await-async-event.ts

+163
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import { ASTUtils, TSESTree } from '@typescript-eslint/utils';
2+
3+
import { createTestingLibraryRule } from '../create-testing-library-rule';
4+
import {
5+
findClosestCallExpressionNode,
6+
getFunctionName,
7+
getInnermostReturningFunction,
8+
getVariableReferences,
9+
isPromiseHandled,
10+
} from '../node-utils';
11+
import { EVENTS_SIMULATORS } from '../utils';
12+
13+
export const RULE_NAME = 'await-async-event';
14+
export type MessageIds = 'awaitAsyncEvent' | 'awaitAsyncEventWrapper';
15+
const FIRE_EVENT_NAME = 'fireEvent';
16+
const USER_EVENT_NAME = 'userEvent';
17+
type EventModules = typeof EVENTS_SIMULATORS[number];
18+
export type Options = [
19+
{
20+
eventModule: EventModules | EventModules[];
21+
}
22+
];
23+
24+
export default createTestingLibraryRule<Options, MessageIds>({
25+
name: RULE_NAME,
26+
meta: {
27+
type: 'problem',
28+
docs: {
29+
description: 'Enforce promises from async event methods are handled',
30+
recommendedConfig: {
31+
dom: ['error', { eventModule: 'userEvent' }],
32+
angular: ['error', { eventModule: 'userEvent' }],
33+
react: ['error', { eventModule: 'userEvent' }],
34+
vue: ['error', { eventModule: ['fireEvent', 'userEvent'] }],
35+
marko: ['error', { eventModule: ['fireEvent', 'userEvent'] }],
36+
},
37+
},
38+
messages: {
39+
awaitAsyncEvent:
40+
'Promise returned from async event method `{{ name }}` must be handled',
41+
awaitAsyncEventWrapper:
42+
'Promise returned from `{{ name }}` wrapper over async event method must be handled',
43+
},
44+
schema: [
45+
{
46+
type: 'object',
47+
default: {},
48+
additionalProperties: false,
49+
properties: {
50+
eventModule: {
51+
default: USER_EVENT_NAME,
52+
oneOf: [
53+
{
54+
type: 'string',
55+
enum: EVENTS_SIMULATORS,
56+
},
57+
{
58+
type: 'array',
59+
items: {
60+
type: 'string',
61+
enum: EVENTS_SIMULATORS,
62+
},
63+
},
64+
],
65+
},
66+
},
67+
},
68+
],
69+
},
70+
defaultOptions: [
71+
{
72+
eventModule: USER_EVENT_NAME,
73+
},
74+
],
75+
76+
create(context, [options], helpers) {
77+
const functionWrappersNames: string[] = [];
78+
79+
function reportUnhandledNode(
80+
node: TSESTree.Identifier,
81+
closestCallExpressionNode: TSESTree.CallExpression,
82+
messageId: MessageIds = 'awaitAsyncEvent'
83+
): void {
84+
if (!isPromiseHandled(node)) {
85+
context.report({
86+
node: closestCallExpressionNode.callee,
87+
messageId,
88+
data: { name: node.name },
89+
});
90+
}
91+
}
92+
93+
function detectEventMethodWrapper(node: TSESTree.Identifier): void {
94+
const innerFunction = getInnermostReturningFunction(context, node);
95+
96+
if (innerFunction) {
97+
functionWrappersNames.push(getFunctionName(innerFunction));
98+
}
99+
}
100+
101+
const eventModules =
102+
typeof options.eventModule === 'string'
103+
? [options.eventModule]
104+
: options.eventModule;
105+
const isFireEventEnabled = eventModules.includes(FIRE_EVENT_NAME);
106+
const isUserEventEnabled = eventModules.includes(USER_EVENT_NAME);
107+
108+
return {
109+
'CallExpression Identifier'(node: TSESTree.Identifier) {
110+
if (
111+
(isFireEventEnabled && helpers.isFireEventMethod(node)) ||
112+
(isUserEventEnabled && helpers.isUserEventMethod(node))
113+
) {
114+
detectEventMethodWrapper(node);
115+
116+
const closestCallExpression = findClosestCallExpressionNode(
117+
node,
118+
true
119+
);
120+
121+
if (!closestCallExpression || !closestCallExpression.parent) {
122+
return;
123+
}
124+
125+
const references = getVariableReferences(
126+
context,
127+
closestCallExpression.parent
128+
);
129+
130+
if (references.length === 0) {
131+
reportUnhandledNode(node, closestCallExpression);
132+
} else {
133+
for (const reference of references) {
134+
if (ASTUtils.isIdentifier(reference.identifier)) {
135+
reportUnhandledNode(
136+
reference.identifier,
137+
closestCallExpression
138+
);
139+
}
140+
}
141+
}
142+
} else if (functionWrappersNames.includes(node.name)) {
143+
// report promise returned from function wrapping fire event method
144+
// previously detected
145+
const closestCallExpression = findClosestCallExpressionNode(
146+
node,
147+
true
148+
);
149+
150+
if (!closestCallExpression) {
151+
return;
152+
}
153+
154+
reportUnhandledNode(
155+
node,
156+
closestCallExpression,
157+
'awaitAsyncEventWrapper'
158+
);
159+
}
160+
},
161+
};
162+
},
163+
});

‎lib/rules/await-fire-event.ts

-113
This file was deleted.

‎tests/__snapshots__/index.test.ts.snap

+36-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ Object {
77
"testing-library",
88
],
99
"rules": Object {
10+
"testing-library/await-async-event": Array [
11+
"error",
12+
Object {
13+
"eventModule": "userEvent",
14+
},
15+
],
1016
"testing-library/await-async-query": "error",
1117
"testing-library/await-async-utils": "error",
1218
"testing-library/no-await-sync-query": "error",
@@ -36,6 +42,12 @@ Object {
3642
"testing-library",
3743
],
3844
"rules": Object {
45+
"testing-library/await-async-event": Array [
46+
"error",
47+
Object {
48+
"eventModule": "userEvent",
49+
},
50+
],
3951
"testing-library/await-async-query": "error",
4052
"testing-library/await-async-utils": "error",
4153
"testing-library/no-await-sync-query": "error",
@@ -56,9 +68,17 @@ Object {
5668
"testing-library",
5769
],
5870
"rules": Object {
71+
"testing-library/await-async-event": Array [
72+
"error",
73+
Object {
74+
"eventModule": Array [
75+
"fireEvent",
76+
"userEvent",
77+
],
78+
},
79+
],
5980
"testing-library/await-async-query": "error",
6081
"testing-library/await-async-utils": "error",
61-
"testing-library/await-fire-event": "error",
6282
"testing-library/no-await-sync-query": "error",
6383
"testing-library/no-container": "error",
6484
"testing-library/no-debugging-utils": "error",
@@ -87,6 +107,12 @@ Object {
87107
"testing-library",
88108
],
89109
"rules": Object {
110+
"testing-library/await-async-event": Array [
111+
"error",
112+
Object {
113+
"eventModule": "userEvent",
114+
},
115+
],
90116
"testing-library/await-async-query": "error",
91117
"testing-library/await-async-utils": "error",
92118
"testing-library/no-await-sync-query": "error",
@@ -118,9 +144,17 @@ Object {
118144
"testing-library",
119145
],
120146
"rules": Object {
147+
"testing-library/await-async-event": Array [
148+
"error",
149+
Object {
150+
"eventModule": Array [
151+
"fireEvent",
152+
"userEvent",
153+
],
154+
},
155+
],
121156
"testing-library/await-async-query": "error",
122157
"testing-library/await-async-utils": "error",
123-
"testing-library/await-fire-event": "error",
124158
"testing-library/no-await-sync-query": "error",
125159
"testing-library/no-container": "error",
126160
"testing-library/no-debugging-utils": "error",

‎tests/lib/rules/await-async-event.test.ts

+717
Large diffs are not rendered by default.

‎tests/lib/rules/await-fire-event.test.ts

-353
This file was deleted.

0 commit comments

Comments
 (0)