Skip to content

Commit 8ba464f

Browse files
committed
feat(await-async-events): instance of userEvent is recognized as async
feat(await-async-events): added comments feat(await-async-events): better test case feat(await-async-events): edge case fixed, test added feat(await-async-events): use actual userEvent import for check, tests
1 parent b531af8 commit 8ba464f

File tree

4 files changed

+240
-39
lines changed

4 files changed

+240
-39
lines changed

lib/create-testing-library-rule/detect-testing-library-utils.ts

+49-34
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,10 @@ type IsAsyncUtilFn = (
7272
validNames?: readonly (typeof ASYNC_UTILS)[number][]
7373
) => boolean;
7474
type IsFireEventMethodFn = (node: TSESTree.Identifier) => boolean;
75-
type IsUserEventMethodFn = (node: TSESTree.Identifier) => boolean;
75+
type IsUserEventMethodFn = (
76+
node: TSESTree.Identifier,
77+
userEventSession?: string
78+
) => boolean;
7679
type IsRenderUtilFn = (node: TSESTree.Identifier) => boolean;
7780
type IsCreateEventUtil = (
7881
node: TSESTree.CallExpression | TSESTree.Identifier
@@ -97,6 +100,9 @@ type FindImportedTestingLibraryUtilSpecifierFn = (
97100
type IsNodeComingFromTestingLibraryFn = (
98101
node: TSESTree.Identifier | TSESTree.MemberExpression
99102
) => boolean;
103+
type getUserEventImportIdentifierFn = (
104+
node: ImportModuleNode | null
105+
) => TSESTree.Identifier | null;
100106

101107
export interface DetectionHelpers {
102108
getTestingLibraryImportNode: GetTestingLibraryImportNodeFn;
@@ -130,6 +136,7 @@ export interface DetectionHelpers {
130136
canReportErrors: CanReportErrorsFn;
131137
findImportedTestingLibraryUtilSpecifier: FindImportedTestingLibraryUtilSpecifierFn;
132138
isNodeComingFromTestingLibrary: IsNodeComingFromTestingLibraryFn;
139+
getUserEventImportIdentifier: getUserEventImportIdentifierFn;
133140
}
134141

135142
const USER_EVENT_PACKAGE = '@testing-library/user-event';
@@ -326,6 +333,35 @@ export function detectTestingLibraryUtils<
326333
return getImportModuleName(importedCustomModuleNode);
327334
};
328335

336+
const getUserEventImportIdentifier = (node: ImportModuleNode | null) => {
337+
if (!node) {
338+
return null;
339+
}
340+
341+
if (isImportDeclaration(node)) {
342+
const userEventIdentifier = node.specifiers.find((specifier) =>
343+
isImportDefaultSpecifier(specifier)
344+
);
345+
346+
if (userEventIdentifier) {
347+
return userEventIdentifier.local;
348+
}
349+
} else {
350+
if (!ASTUtils.isVariableDeclarator(node.parent)) {
351+
return null;
352+
}
353+
354+
const requireNode = node.parent;
355+
if (!ASTUtils.isIdentifier(requireNode.id)) {
356+
return null;
357+
}
358+
359+
return requireNode.id;
360+
}
361+
362+
return null;
363+
};
364+
329365
/**
330366
* Determines whether Testing Library utils are imported or not for
331367
* current file being analyzed.
@@ -557,7 +593,10 @@ export function detectTestingLibraryUtils<
557593
return regularCall || wildcardCall || wildcardCallWithCallExpression;
558594
};
559595

560-
const isUserEventMethod: IsUserEventMethodFn = (node) => {
596+
const isUserEventMethod: IsUserEventMethodFn = (
597+
node,
598+
userEventInstance
599+
) => {
561600
const userEvent = findImportedUserEventSpecifier();
562601
let userEventName: string | undefined;
563602

@@ -567,7 +606,7 @@ export function detectTestingLibraryUtils<
567606
userEventName = USER_EVENT_NAME;
568607
}
569608

570-
if (!userEventName) {
609+
if (!userEventName && !userEventInstance) {
571610
return false;
572611
}
573612

@@ -591,8 +630,11 @@ export function detectTestingLibraryUtils<
591630

592631
// check userEvent.click() usage
593632
return (
594-
ASTUtils.isIdentifier(parentMemberExpression.object) &&
595-
parentMemberExpression.object.name === userEventName
633+
(ASTUtils.isIdentifier(parentMemberExpression.object) &&
634+
parentMemberExpression.object.name === userEventName) ||
635+
// check userEventInstance.click() usage
636+
(ASTUtils.isIdentifier(parentMemberExpression.object) &&
637+
parentMemberExpression.object.name === userEventInstance)
596638
);
597639
};
598640

@@ -853,35 +895,7 @@ export function detectTestingLibraryUtils<
853895

854896
const findImportedUserEventSpecifier: () => TSESTree.Identifier | null =
855897
() => {
856-
if (!importedUserEventLibraryNode) {
857-
return null;
858-
}
859-
860-
if (isImportDeclaration(importedUserEventLibraryNode)) {
861-
const userEventIdentifier =
862-
importedUserEventLibraryNode.specifiers.find((specifier) =>
863-
isImportDefaultSpecifier(specifier)
864-
);
865-
866-
if (userEventIdentifier) {
867-
return userEventIdentifier.local;
868-
}
869-
} else {
870-
if (
871-
!ASTUtils.isVariableDeclarator(importedUserEventLibraryNode.parent)
872-
) {
873-
return null;
874-
}
875-
876-
const requireNode = importedUserEventLibraryNode.parent;
877-
if (!ASTUtils.isIdentifier(requireNode.id)) {
878-
return null;
879-
}
880-
881-
return requireNode.id;
882-
}
883-
884-
return null;
898+
return getUserEventImportIdentifier(importedUserEventLibraryNode);
885899
};
886900

887901
const getTestingLibraryImportedUtilSpecifier = (
@@ -997,6 +1011,7 @@ export function detectTestingLibraryUtils<
9971011
canReportErrors,
9981012
findImportedTestingLibraryUtilSpecifier,
9991013
isNodeComingFromTestingLibrary,
1014+
getUserEventImportIdentifier,
10001015
};
10011016

10021017
// Instructions for Testing Library detection.

lib/node-utils/index.ts

+34
Original file line numberDiff line numberDiff line change
@@ -679,3 +679,37 @@ export function findImportSpecifier(
679679
return (property as TSESTree.Property).key as TSESTree.Identifier;
680680
}
681681
}
682+
683+
/**
684+
* Finds if the userEvent is used as an instance
685+
*/
686+
687+
export function getUserEventInstance(
688+
context: TSESLint.RuleContext<string, unknown[]>,
689+
userEventImport: TSESTree.Identifier | null
690+
): string | undefined {
691+
const { tokensAndComments } = context.getSourceCode();
692+
if (!userEventImport) {
693+
return undefined;
694+
}
695+
/**
696+
* Check for the following pattern:
697+
* userEvent.setup(
698+
* For a line like this:
699+
* const user = userEvent.setup();
700+
* function will return 'user'
701+
*/
702+
for (const [index, token] of tokensAndComments.entries()) {
703+
if (
704+
token.type === 'Identifier' &&
705+
token.value === userEventImport.name &&
706+
tokensAndComments[index + 1].value === '.' &&
707+
tokensAndComments[index + 2].value === 'setup' &&
708+
tokensAndComments[index + 3].value === '(' &&
709+
tokensAndComments[index - 1].value === '='
710+
) {
711+
return tokensAndComments[index - 2].value;
712+
}
713+
}
714+
return undefined;
715+
}

lib/rules/await-async-events.ts

+17-4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
findClosestFunctionExpressionNode,
77
getFunctionName,
88
getInnermostReturningFunction,
9+
getUserEventInstance,
910
getVariableReferences,
1011
isMemberExpression,
1112
isPromiseHandled,
@@ -91,9 +92,6 @@ export default createTestingLibraryRule<Options, MessageIds>({
9192
messageId?: MessageIds;
9293
fix?: TSESLint.ReportFixFunction;
9394
}): void {
94-
if (node.name === USER_EVENT_SETUP_FUNCTION_NAME) {
95-
return;
96-
}
9795
if (!isPromiseHandled(node)) {
9896
context.report({
9997
node: closestCallExpression.callee,
@@ -121,9 +119,20 @@ export default createTestingLibraryRule<Options, MessageIds>({
121119

122120
return {
123121
'CallExpression Identifier'(node: TSESTree.Identifier) {
122+
const importedUserEventLibraryNode =
123+
helpers.getTestingLibraryImportNode();
124+
const userEventImport = helpers.getUserEventImportIdentifier(
125+
importedUserEventLibraryNode
126+
);
127+
// Check if userEvent is used as an instance, like const user = userEvent.setup()
128+
const userEventInstance = getUserEventInstance(
129+
context,
130+
userEventImport
131+
);
124132
if (
125133
(isFireEventEnabled && helpers.isFireEventMethod(node)) ||
126-
(isUserEventEnabled && helpers.isUserEventMethod(node))
134+
(isUserEventEnabled &&
135+
helpers.isUserEventMethod(node, userEventInstance))
127136
) {
128137
detectEventMethodWrapper(node);
129138

@@ -136,6 +145,10 @@ export default createTestingLibraryRule<Options, MessageIds>({
136145
return;
137146
}
138147

148+
if (node.name === USER_EVENT_SETUP_FUNCTION_NAME) {
149+
return;
150+
}
151+
139152
const references = getVariableReferences(
140153
context,
141154
closestCallExpression.parent

0 commit comments

Comments
 (0)