Skip to content

Commit d1f0388

Browse files
committed
Merge remote-tracking branch 'origin/master' into v4
# Conflicts: # README.md
2 parents 6abb4ee + e74c215 commit d1f0388

File tree

7 files changed

+322
-1
lines changed

7 files changed

+322
-1
lines changed

.all-contributorsrc

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,17 @@
369369
"platform",
370370
"maintenance"
371371
]
372+
},
373+
{
374+
"login": "J-Huang",
375+
"name": "Jian Huang",
376+
"avatar_url": "https://avatars0.githubusercontent.com/u/4263459?v=4",
377+
"profile": "https://github.com/J-Huang",
378+
"contributions": [
379+
"code",
380+
"test",
381+
"doc"
382+
]
372383
}
373384
],
374385
"contributorsPerLine": 7,

README.md

Lines changed: 3 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-34-orange.svg?style=flat-square)](#contributors-)
27+
[![All Contributors](https://img.shields.io/badge/all_contributors-35-orange.svg?style=flat-square)](#contributors-)
2828

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

@@ -131,6 +131,7 @@ To enable this configuration use the `extends` property in your
131131
| [await-async-utils](docs/rules/await-async-utils.md) | Enforce async utils to be awaited properly | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
132132
| [await-fire-event](docs/rules/await-fire-event.md) | Enforce async fire event methods to be awaited | ![vue-badge][] | |
133133
| [consistent-data-testid](docs/rules/consistent-data-testid.md) | Ensure `data-testid` values match a provided regex. | | |
134+
| [no-await-sync-events](docs/rules/no-await-sync-events.md) | Disallow unnecessary `await` for sync events | | |
134135
| [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][] | |
135136
| [no-container](docs/rules/no-container.md) | Disallow the use of `container` methods | ![angular-badge][] ![react-badge][] ![vue-badge][] | |
136137
| [no-debug](docs/rules/no-debug.md) | Disallow the use of `debug` | ![angular-badge][] ![react-badge][] ![vue-badge][] | |
@@ -223,6 +224,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
223224
<td align="center"><a href="https://twitter.com/Gpx"><img src="https://avatars0.githubusercontent.com/u/767959?v=4" width="100px;" alt=""/><br /><sub><b>Giorgio Polvara</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=Gpx" title="Code">💻</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=Gpx" title="Tests">⚠️</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=Gpx" title="Documentation">📖</a></td>
224225
<td align="center"><a href="https://github.com/jdanil"><img src="https://avatars0.githubusercontent.com/u/8342105?v=4" width="100px;" alt=""/><br /><sub><b>Josh David</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=jdanil" title="Documentation">📖</a></td>
225226
<td align="center"><a href="https://michaeldeboey.be"><img src="https://avatars3.githubusercontent.com/u/6643991?v=4" width="100px;" alt=""/><br /><sub><b>Michaël De Boey</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=MichaelDeBoey" title="Code">💻</a> <a href="#platform-MichaelDeBoey" title="Packaging/porting to new platform">📦</a> <a href="#maintenance-MichaelDeBoey" title="Maintenance">🚧</a></td>
227+
<td align="center"><a href="https://github.com/J-Huang"><img src="https://avatars0.githubusercontent.com/u/4263459?v=4" width="100px;" alt=""/><br /><sub><b>Jian Huang</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=J-Huang" title="Code">💻</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=J-Huang" title="Tests">⚠️</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=J-Huang" title="Documentation">📖</a></td>
226228
</tr>
227229
</table>
228230

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

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Disallow unnecessary `await` for sync events (no-await-sync-events)
2+
3+
Ensure that sync events are not awaited unnecessarily.
4+
5+
## Rule Details
6+
7+
Functions in the event object provided by Testing Library, including
8+
fireEvent and userEvent, do NOT return Promise, with an exception of
9+
`userEvent.type`, which delays the promise resolve only if [`delay`
10+
option](https://github.com/testing-library/user-event#typeelement-text-options) is specified.
11+
Some examples are:
12+
13+
- `fireEvent.click`
14+
- `fireEvent.select`
15+
- `userEvent.tab`
16+
- `userEvent.hover`
17+
18+
This rule aims to prevent users from waiting for those function calls.
19+
20+
Examples of **incorrect** code for this rule:
21+
22+
```js
23+
const foo = async () => {
24+
// ...
25+
await fireEvent.click(button);
26+
// ...
27+
};
28+
29+
const bar = () => {
30+
// ...
31+
await userEvent.tab();
32+
// ...
33+
};
34+
35+
const baz = () => {
36+
// ...
37+
await userEvent.type(textInput, 'abc');
38+
// ...
39+
};
40+
```
41+
42+
Examples of **correct** code for this rule:
43+
44+
```js
45+
const foo = () => {
46+
// ...
47+
fireEvent.click(button);
48+
// ...
49+
};
50+
51+
const bar = () => {
52+
// ...
53+
userEvent.tab();
54+
// ...
55+
};
56+
57+
const baz = () => {
58+
// await userEvent.type only with delay option
59+
await userEvent.type(textInput, 'abc', {delay: 1000});
60+
userEvent.type(textInput, '123');
61+
// ...
62+
};
63+
```
64+
65+
## Notes
66+
67+
There is another rule `await-fire-event`, which is only in Vue Testing
68+
Library. Please do not confuse with this rule.

lib/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import awaitAsyncQuery from './rules/await-async-query';
22
import awaitAsyncUtils from './rules/await-async-utils';
33
import awaitFireEvent from './rules/await-fire-event';
44
import consistentDataTestid from './rules/consistent-data-testid';
5+
import noAwaitSyncEvents from './rules/no-await-sync-events';
56
import noAwaitSyncQuery from './rules/no-await-sync-query';
67
import noContainer from './rules/no-container';
78
import noDebug from './rules/no-debug';
@@ -27,6 +28,7 @@ const rules = {
2728
'await-async-utils': awaitAsyncUtils,
2829
'await-fire-event': awaitFireEvent,
2930
'consistent-data-testid': consistentDataTestid,
31+
'no-await-sync-events': noAwaitSyncEvents,
3032
'no-await-sync-query': noAwaitSyncQuery,
3133
'no-container': noContainer,
3234
'no-debug': noDebug,

lib/rules/no-await-sync-events.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils';
2+
import { getDocsUrl, SYNC_EVENTS } from '../utils';
3+
import { isObjectExpression, isProperty, isIdentifier } from '../node-utils';
4+
export const RULE_NAME = 'no-await-sync-events';
5+
export type MessageIds = 'noAwaitSyncEvents';
6+
type Options = [];
7+
8+
const SYNC_EVENTS_REGEXP = new RegExp(`^(${SYNC_EVENTS.join('|')})$`);
9+
export default ESLintUtils.RuleCreator(getDocsUrl)<Options, MessageIds>({
10+
name: RULE_NAME,
11+
meta: {
12+
type: 'problem',
13+
docs: {
14+
description: 'Disallow unnecessary `await` for sync events',
15+
category: 'Best Practices',
16+
recommended: 'error',
17+
},
18+
messages: {
19+
noAwaitSyncEvents: '`{{ name }}` does not need `await` operator',
20+
},
21+
fixable: null,
22+
schema: [],
23+
},
24+
defaultOptions: [],
25+
26+
create(context) {
27+
// userEvent.type() is an exception, which returns a
28+
// Promise. But it is only necessary to wait when delay
29+
// option is specified. So this rule has a special exception
30+
// for the case await userEvent.type(element, 'abc', {delay: 1234})
31+
return {
32+
[`AwaitExpression > CallExpression > MemberExpression > Identifier[name=${SYNC_EVENTS_REGEXP}]`](
33+
node: TSESTree.Identifier
34+
) {
35+
const memberExpression = node.parent as TSESTree.MemberExpression;
36+
const methodNode = memberExpression.property as TSESTree.Identifier;
37+
const callExpression = memberExpression.parent as TSESTree.CallExpression;
38+
const withDelay =
39+
callExpression.arguments.length >= 3 &&
40+
isObjectExpression(callExpression.arguments[2]) &&
41+
callExpression.arguments[2].properties.some(
42+
(property) =>
43+
isProperty(property) &&
44+
isIdentifier(property.key) &&
45+
property.key.name === 'delay'
46+
);
47+
48+
if (
49+
!(
50+
node.name === 'userEvent' &&
51+
methodNode.name === 'type' &&
52+
withDelay
53+
)
54+
) {
55+
context.report({
56+
node: methodNode,
57+
messageId: 'noAwaitSyncEvents',
58+
data: {
59+
name: `${node.name}.${methodNode.name}`,
60+
},
61+
});
62+
}
63+
},
64+
};
65+
},
66+
});

lib/utils.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ const ASYNC_UTILS = [
7171
'waitForDomChange',
7272
];
7373

74+
const SYNC_EVENTS = ['fireEvent', 'userEvent'];
75+
7476
const TESTING_FRAMEWORK_SETUP_HOOKS = ['beforeEach', 'beforeAll'];
7577

7678
const PROPERTIES_RETURNING_NODES = [
@@ -122,6 +124,7 @@ export {
122124
ASYNC_QUERIES_COMBINATIONS,
123125
ALL_QUERIES_COMBINATIONS,
124126
ASYNC_UTILS,
127+
SYNC_EVENTS,
125128
TESTING_FRAMEWORK_SETUP_HOOKS,
126129
LIBRARY_MODULES,
127130
PROPERTIES_RETURNING_NODES,
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import { createRuleTester } from '../test-utils';
2+
import rule, { RULE_NAME } from '../../../lib/rules/no-await-sync-events';
3+
import { SYNC_EVENTS } from '../../../lib/utils';
4+
5+
const ruleTester = createRuleTester();
6+
7+
const fireEventFunctions = [
8+
'copy',
9+
'cut',
10+
'paste',
11+
'compositionEnd',
12+
'compositionStart',
13+
'compositionUpdate',
14+
'keyDown',
15+
'keyPress',
16+
'keyUp',
17+
'focus',
18+
'blur',
19+
'focusIn',
20+
'focusOut',
21+
'change',
22+
'input',
23+
'invalid',
24+
'submit',
25+
'reset',
26+
'click',
27+
'contextMenu',
28+
'dblClick',
29+
'drag',
30+
'dragEnd',
31+
'dragEnter',
32+
'dragExit',
33+
'dragLeave',
34+
'dragOver',
35+
'dragStart',
36+
'drop',
37+
'mouseDown',
38+
'mouseEnter',
39+
'mouseLeave',
40+
'mouseMove',
41+
'mouseOut',
42+
'mouseOver',
43+
'mouseUp',
44+
'popState',
45+
'select',
46+
'touchCancel',
47+
'touchEnd',
48+
'touchMove',
49+
'touchStart',
50+
'scroll',
51+
'wheel',
52+
'abort',
53+
'canPlay',
54+
'canPlayThrough',
55+
'durationChange',
56+
'emptied',
57+
'encrypted',
58+
'ended',
59+
'loadedData',
60+
'loadedMetadata',
61+
'loadStart',
62+
'pause',
63+
'play',
64+
'playing',
65+
'progress',
66+
'rateChange',
67+
'seeked',
68+
'seeking',
69+
'stalled',
70+
'suspend',
71+
'timeUpdate',
72+
'volumeChange',
73+
'waiting',
74+
'load',
75+
'error',
76+
'animationStart',
77+
'animationEnd',
78+
'animationIteration',
79+
'transitionEnd',
80+
'doubleClick',
81+
'pointerOver',
82+
'pointerEnter',
83+
'pointerDown',
84+
'pointerMove',
85+
'pointerUp',
86+
'pointerCancel',
87+
'pointerOut',
88+
'pointerLeave',
89+
'gotPointerCapture',
90+
'lostPointerCapture',
91+
];
92+
const userEventFunctions = [
93+
'clear',
94+
'click',
95+
'dblClick',
96+
'selectOptions',
97+
'deselectOptions',
98+
'upload',
99+
// 'type',
100+
'tab',
101+
'paste',
102+
'hover',
103+
'unhover',
104+
];
105+
let eventFunctions: string[] = [];
106+
SYNC_EVENTS.forEach((event) => {
107+
switch (event) {
108+
case 'fireEvent':
109+
eventFunctions = eventFunctions.concat(
110+
fireEventFunctions.map((f: string): string => `${event}.${f}`)
111+
);
112+
break;
113+
case 'userEvent':
114+
eventFunctions = eventFunctions.concat(
115+
userEventFunctions.map((f: string): string => `${event}.${f}`)
116+
);
117+
break;
118+
default:
119+
eventFunctions.push(`${event}.anyFunc`);
120+
}
121+
});
122+
123+
ruleTester.run(RULE_NAME, rule, {
124+
valid: [
125+
// sync events without await are valid
126+
// userEvent.type() is an exception
127+
...eventFunctions.map((func) => ({
128+
code: `() => {
129+
${func}('foo')
130+
}
131+
`,
132+
})),
133+
{
134+
code: `() => {
135+
userEvent.type('foo')
136+
}
137+
`,
138+
},
139+
{
140+
code: `() => {
141+
await userEvent.type('foo', 'bar', {delay: 1234})
142+
}
143+
`,
144+
},
145+
],
146+
147+
invalid: [
148+
// sync events with await operator are not valid
149+
...eventFunctions.map((func) => ({
150+
code: `
151+
import { fireEvent } from '@testing-library/framework';
152+
import userEvent from '@testing-library/user-event';
153+
test('should report sync event awaited', async() => {
154+
await ${func}('foo');
155+
});
156+
`,
157+
errors: [{ line: 5, messageId: 'noAwaitSyncEvents' }],
158+
})),
159+
{
160+
code: `
161+
import userEvent from '@testing-library/user-event';
162+
test('should report sync event awaited', async() => {
163+
await userEvent.type('foo', 'bar', {hello: 1234});
164+
});
165+
`,
166+
errors: [{ line: 4, messageId: 'noAwaitSyncEvents' }],
167+
},
168+
],
169+
});

0 commit comments

Comments
 (0)