Skip to content

Commit a6eea4d

Browse files
authored
refactor(no-await-sync-events): migrate to v4 (#302)
* docs: update rule description * test: improve current cases * refactor: use new rule creator * feat: avoid reporting type and keyboard with 0 delay * refactor: use new helpers for detection * test: split fire and user events cases * test: improve errors location asserts * feat: detect user-event import properly * test: add cases for increasing coverage up to 100% * test: assert error message data
1 parent 9386773 commit a6eea4d

File tree

7 files changed

+431
-149
lines changed

7 files changed

+431
-149
lines changed

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

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
# Disallow unnecessary `await` for sync events (no-await-sync-events)
22

3-
Ensure that sync events are not awaited unnecessarily.
3+
Ensure that sync simulated events are not awaited unnecessarily.
44

55
## Rule Details
66

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`
7+
Methods for simulating events in Testing Library ecosystem -`fireEvent` and `userEvent`-
8+
do NOT return any Promise, with an exception of
9+
`userEvent.type` and `userEvent.keyboard`, which delays the promise resolve only if [`delay`
1010
option](https://github.com/testing-library/user-event#typeelement-text-options) is specified.
11-
Some examples are:
11+
12+
Some examples of simulating events not returning any Promise are:
1213

1314
- `fireEvent.click`
1415
- `fireEvent.select`
@@ -26,15 +27,16 @@ const foo = async () => {
2627
// ...
2728
};
2829

29-
const bar = () => {
30+
const bar = async () => {
3031
// ...
3132
await userEvent.tab();
3233
// ...
3334
};
3435

35-
const baz = () => {
36+
const baz = async () => {
3637
// ...
3738
await userEvent.type(textInput, 'abc');
39+
await userEvent.keyboard('abc');
3840
// ...
3941
};
4042
```
@@ -54,10 +56,14 @@ const bar = () => {
5456
// ...
5557
};
5658

57-
const baz = () => {
59+
const baz = async () => {
5860
// await userEvent.type only with delay option
59-
await userEvent.type(textInput, 'abc', {delay: 1000});
61+
await userEvent.type(textInput, 'abc', { delay: 1000 });
6062
userEvent.type(textInput, '123');
63+
64+
// same for userEvent.keyboard
65+
await userEvent.keyboard(textInput, 'abc', { delay: 1000 });
66+
userEvent.keyboard('123');
6167
// ...
6268
};
6369
```

lib/detect-testing-library-utils.ts

Lines changed: 135 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
hasImportMatch,
1313
ImportModuleNode,
1414
isImportDeclaration,
15+
isImportDefaultSpecifier,
1516
isImportNamespaceSpecifier,
1617
isImportSpecifier,
1718
isLiteral,
@@ -20,9 +21,9 @@ import {
2021
} from './node-utils';
2122
import {
2223
ABSENCE_MATCHERS,
24+
ALL_QUERIES_COMBINATIONS,
2325
ASYNC_UTILS,
2426
PRESENCE_MATCHERS,
25-
ALL_QUERIES_COMBINATIONS,
2627
} from './utils';
2728

2829
export type TestingLibrarySettings = {
@@ -67,6 +68,7 @@ type IsAsyncUtilFn = (
6768
validNames?: readonly typeof ASYNC_UTILS[number][]
6869
) => boolean;
6970
type IsFireEventMethodFn = (node: TSESTree.Identifier) => boolean;
71+
type IsUserEventMethodFn = (node: TSESTree.Identifier) => boolean;
7072
type IsRenderUtilFn = (node: TSESTree.Identifier) => boolean;
7173
type IsRenderVariableDeclaratorFn = (
7274
node: TSESTree.VariableDeclarator
@@ -99,6 +101,7 @@ export interface DetectionHelpers {
99101
isFireEventUtil: (node: TSESTree.Identifier) => boolean;
100102
isUserEventUtil: (node: TSESTree.Identifier) => boolean;
101103
isFireEventMethod: IsFireEventMethodFn;
104+
isUserEventMethod: IsUserEventMethodFn;
102105
isRenderUtil: IsRenderUtilFn;
103106
isRenderVariableDeclarator: IsRenderVariableDeclaratorFn;
104107
isDebugUtil: IsDebugUtilFn;
@@ -109,6 +112,9 @@ export interface DetectionHelpers {
109112
isNodeComingFromTestingLibrary: IsNodeComingFromTestingLibraryFn;
110113
}
111114

115+
const USER_EVENT_PACKAGE = '@testing-library/user-event';
116+
const FIRE_EVENT_NAME = 'fireEvent';
117+
const USER_EVENT_NAME = 'userEvent';
112118
const RENDER_NAME = 'render';
113119

114120
/**
@@ -125,6 +131,7 @@ export function detectTestingLibraryUtils<
125131
): TSESLint.RuleListener => {
126132
let importedTestingLibraryNode: ImportModuleNode | null = null;
127133
let importedCustomModuleNode: ImportModuleNode | null = null;
134+
let importedUserEventLibraryNode: ImportModuleNode | null = null;
128135

129136
// Init options based on shared ESLint settings
130137
const customModule = context.settings['testing-library/utils-module'];
@@ -174,69 +181,6 @@ export function detectTestingLibraryUtils<
174181
return isNodeComingFromTestingLibrary(referenceNodeIdentifier);
175182
}
176183

177-
/**
178-
* Determines whether a given node is a simulate event util related to
179-
* Testing Library or not.
180-
*
181-
* In order to determine this, the node must match:
182-
* - indicated simulate event name: fireEvent or userEvent
183-
* - imported from valid Testing Library module (depends on Aggressive
184-
* Reporting)
185-
*
186-
*/
187-
function isTestingLibrarySimulateEventUtil(
188-
node: TSESTree.Identifier,
189-
utilName: 'fireEvent' | 'userEvent'
190-
): boolean {
191-
const simulateEventUtil = findImportedUtilSpecifier(utilName);
192-
let simulateEventUtilName: string | undefined;
193-
194-
if (simulateEventUtil) {
195-
simulateEventUtilName = ASTUtils.isIdentifier(simulateEventUtil)
196-
? simulateEventUtil.name
197-
: simulateEventUtil.local.name;
198-
} else if (isAggressiveModuleReportingEnabled()) {
199-
simulateEventUtilName = utilName;
200-
}
201-
202-
if (!simulateEventUtilName) {
203-
return false;
204-
}
205-
206-
const parentMemberExpression:
207-
| TSESTree.MemberExpression
208-
| undefined = isMemberExpression(node.parent) ? node.parent : undefined;
209-
210-
if (!parentMemberExpression) {
211-
return false;
212-
}
213-
214-
// make sure that given node it's not fireEvent/userEvent object itself
215-
if (
216-
[simulateEventUtilName, utilName].includes(node.name) ||
217-
(ASTUtils.isIdentifier(parentMemberExpression.object) &&
218-
parentMemberExpression.object.name === node.name)
219-
) {
220-
return false;
221-
}
222-
223-
// check fireEvent.click()/userEvent.click() usage
224-
const regularCall =
225-
ASTUtils.isIdentifier(parentMemberExpression.object) &&
226-
parentMemberExpression.object.name === simulateEventUtilName;
227-
228-
// check testingLibraryUtils.fireEvent.click() or
229-
// testingLibraryUtils.userEvent.click() usage
230-
const wildcardCall =
231-
isMemberExpression(parentMemberExpression.object) &&
232-
ASTUtils.isIdentifier(parentMemberExpression.object.object) &&
233-
parentMemberExpression.object.object.name === simulateEventUtilName &&
234-
ASTUtils.isIdentifier(parentMemberExpression.object.property) &&
235-
parentMemberExpression.object.property.name === utilName;
236-
237-
return regularCall || wildcardCall;
238-
}
239-
240184
/**
241185
* Determines whether aggressive module reporting is enabled or not.
242186
*
@@ -403,7 +347,90 @@ export function detectTestingLibraryUtils<
403347
* Determines whether a given node is fireEvent method or not
404348
*/
405349
const isFireEventMethod: IsFireEventMethodFn = (node) => {
406-
return isTestingLibrarySimulateEventUtil(node, 'fireEvent');
350+
const fireEventUtil = findImportedUtilSpecifier(FIRE_EVENT_NAME);
351+
let fireEventUtilName: string | undefined;
352+
353+
if (fireEventUtil) {
354+
fireEventUtilName = ASTUtils.isIdentifier(fireEventUtil)
355+
? fireEventUtil.name
356+
: fireEventUtil.local.name;
357+
} else if (isAggressiveModuleReportingEnabled()) {
358+
fireEventUtilName = FIRE_EVENT_NAME;
359+
}
360+
361+
if (!fireEventUtilName) {
362+
return false;
363+
}
364+
365+
const parentMemberExpression:
366+
| TSESTree.MemberExpression
367+
| undefined = isMemberExpression(node.parent) ? node.parent : undefined;
368+
369+
if (!parentMemberExpression) {
370+
return false;
371+
}
372+
373+
// make sure that given node it's not fireEvent object itself
374+
if (
375+
[fireEventUtilName, FIRE_EVENT_NAME].includes(node.name) ||
376+
(ASTUtils.isIdentifier(parentMemberExpression.object) &&
377+
parentMemberExpression.object.name === node.name)
378+
) {
379+
return false;
380+
}
381+
382+
// check fireEvent.click() usage
383+
const regularCall =
384+
ASTUtils.isIdentifier(parentMemberExpression.object) &&
385+
parentMemberExpression.object.name === fireEventUtilName;
386+
387+
// check testingLibraryUtils.fireEvent.click() usage
388+
const wildcardCall =
389+
isMemberExpression(parentMemberExpression.object) &&
390+
ASTUtils.isIdentifier(parentMemberExpression.object.object) &&
391+
parentMemberExpression.object.object.name === fireEventUtilName &&
392+
ASTUtils.isIdentifier(parentMemberExpression.object.property) &&
393+
parentMemberExpression.object.property.name === FIRE_EVENT_NAME;
394+
395+
return regularCall || wildcardCall;
396+
};
397+
398+
const isUserEventMethod: IsUserEventMethodFn = (node) => {
399+
const userEvent = findImportedUserEventSpecifier();
400+
let userEventName: string | undefined;
401+
402+
if (userEvent) {
403+
userEventName = userEvent.name;
404+
} else if (isAggressiveModuleReportingEnabled()) {
405+
userEventName = USER_EVENT_NAME;
406+
}
407+
408+
if (!userEventName) {
409+
return false;
410+
}
411+
412+
const parentMemberExpression:
413+
| TSESTree.MemberExpression
414+
| undefined = isMemberExpression(node.parent) ? node.parent : undefined;
415+
416+
if (!parentMemberExpression) {
417+
return false;
418+
}
419+
420+
// make sure that given node it's not userEvent object itself
421+
if (
422+
[userEventName, USER_EVENT_NAME].includes(node.name) ||
423+
(ASTUtils.isIdentifier(parentMemberExpression.object) &&
424+
parentMemberExpression.object.name === node.name)
425+
) {
426+
return false;
427+
}
428+
429+
// check userEvent.click() usage
430+
return (
431+
ASTUtils.isIdentifier(parentMemberExpression.object) &&
432+
parentMemberExpression.object.name === userEventName
433+
);
407434
};
408435

409436
/**
@@ -553,6 +580,27 @@ export function detectTestingLibraryUtils<
553580
}
554581
};
555582

583+
const findImportedUserEventSpecifier: () => TSESTree.Identifier | null = () => {
584+
if (!importedUserEventLibraryNode) {
585+
return null;
586+
}
587+
588+
if (isImportDeclaration(importedUserEventLibraryNode)) {
589+
const userEventIdentifier = importedUserEventLibraryNode.specifiers.find(
590+
(specifier) => isImportDefaultSpecifier(specifier)
591+
);
592+
593+
if (userEventIdentifier) {
594+
return userEventIdentifier.local;
595+
}
596+
} else {
597+
const requireNode = importedUserEventLibraryNode.parent as TSESTree.VariableDeclarator;
598+
return requireNode.id as TSESTree.Identifier;
599+
}
600+
601+
return null;
602+
};
603+
556604
const getImportedUtilSpecifier = (
557605
node: TSESTree.MemberExpression | TSESTree.Identifier
558606
): TSESTree.ImportClause | TSESTree.Identifier | undefined => {
@@ -607,6 +655,7 @@ export function detectTestingLibraryUtils<
607655
isFireEventUtil,
608656
isUserEventUtil,
609657
isFireEventMethod,
658+
isUserEventMethod,
610659
isRenderUtil,
611660
isRenderVariableDeclarator,
612661
isDebugUtil,
@@ -644,6 +693,15 @@ export function detectTestingLibraryUtils<
644693
) {
645694
importedCustomModuleNode = node;
646695
}
696+
697+
// check only if user-event import not found yet so we avoid
698+
// to override importedUserEventLibraryNode after it's found
699+
if (
700+
!importedUserEventLibraryNode &&
701+
String(node.source.value) === USER_EVENT_PACKAGE
702+
) {
703+
importedUserEventLibraryNode = node;
704+
}
647705
},
648706

649707
// Check if Testing Library related modules are loaded with required.
@@ -676,6 +734,18 @@ export function detectTestingLibraryUtils<
676734
) {
677735
importedCustomModuleNode = callExpression;
678736
}
737+
738+
if (
739+
!importedCustomModuleNode &&
740+
args.some(
741+
(arg) =>
742+
isLiteral(arg) &&
743+
typeof arg.value === 'string' &&
744+
arg.value === USER_EVENT_PACKAGE
745+
)
746+
) {
747+
importedUserEventLibraryNode = callExpression;
748+
}
679749
},
680750
};
681751

0 commit comments

Comments
 (0)