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 8ba464f

Browse files
committedOct 16, 2023
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

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

+140-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const USER_EVENT_ASYNC_FUNCTIONS = [
3232
'upload',
3333
] as const;
3434
const FIRE_EVENT_ASYNC_FRAMEWORKS = [
35-
'@testing-library/vue',
35+
// '@testing-library/vue',
3636
'@marko/testing-library',
3737
] as const;
3838
const USER_EVENT_ASYNC_FRAMEWORKS = ['@testing-library/user-event'] as const;
@@ -361,6 +361,27 @@ ruleTester.run(RULE_NAME, rule, {
361361
`,
362362
options: [{ eventModule: ['userEvent', 'fireEvent'] }] as Options,
363363
},
364+
{
365+
code: `
366+
import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}'
367+
test('userEvent as instance', async () => {
368+
const user = userEvent.setup()
369+
await user.click(getByLabelText('username'))
370+
})
371+
`,
372+
options: [{ eventModule: ['userEvent'] }] as Options,
373+
},
374+
{
375+
code: `
376+
import u from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}'
377+
test('userEvent as named import', async () => {
378+
const user = u.setup()
379+
await user.click(getByLabelText('username'))
380+
await u.click(getByLabelText('username'))
381+
})
382+
`,
383+
options: [{ eventModule: ['userEvent'] }] as Options,
384+
},
364385
]),
365386
],
366387

@@ -947,6 +968,70 @@ ruleTester.run(RULE_NAME, rule, {
947968
}
948969
949970
triggerEvent()
971+
`,
972+
} as const)
973+
),
974+
...USER_EVENT_ASYNC_FUNCTIONS.map(
975+
(eventMethod) =>
976+
({
977+
code: `
978+
import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}'
979+
test('instance of userEvent is recognized as async event', async function() {
980+
const user = userEvent.setup()
981+
user.${eventMethod}(getByLabelText('username'))
982+
})
983+
`,
984+
errors: [
985+
{
986+
line: 5,
987+
column: 5,
988+
messageId: 'awaitAsyncEvent',
989+
data: { name: eventMethod },
990+
},
991+
],
992+
options: [{ eventModule: 'userEvent' }],
993+
output: `
994+
import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}'
995+
test('instance of userEvent is recognized as async event', async function() {
996+
const user = userEvent.setup()
997+
await user.${eventMethod}(getByLabelText('username'))
998+
})
999+
`,
1000+
} as const)
1001+
),
1002+
...USER_EVENT_ASYNC_FUNCTIONS.map(
1003+
(eventMethod) =>
1004+
({
1005+
code: `
1006+
import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}'
1007+
test('instance of userEvent is recognized as async event along with static userEvent', async function() {
1008+
const user = userEvent.setup()
1009+
user.${eventMethod}(getByLabelText('username'))
1010+
userEvent.${eventMethod}(getByLabelText('username'))
1011+
})
1012+
`,
1013+
errors: [
1014+
{
1015+
line: 5,
1016+
column: 5,
1017+
messageId: 'awaitAsyncEvent',
1018+
data: { name: eventMethod },
1019+
},
1020+
{
1021+
line: 6,
1022+
column: 5,
1023+
messageId: 'awaitAsyncEvent',
1024+
data: { name: eventMethod },
1025+
},
1026+
],
1027+
options: [{ eventModule: 'userEvent' }],
1028+
output: `
1029+
import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}'
1030+
test('instance of userEvent is recognized as async event along with static userEvent', async function() {
1031+
const user = userEvent.setup()
1032+
await user.${eventMethod}(getByLabelText('username'))
1033+
await userEvent.${eventMethod}(getByLabelText('username'))
1034+
})
9501035
`,
9511036
} as const)
9521037
),
@@ -1008,6 +1093,60 @@ ruleTester.run(RULE_NAME, rule, {
10081093
fireEvent.click(getByLabelText('username'))
10091094
await userEvent.click(getByLabelText('username'))
10101095
})
1096+
`,
1097+
},
1098+
{
1099+
code: `
1100+
import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}'
1101+
let user;
1102+
beforeEach(() => {
1103+
user = userEvent.setup()
1104+
})
1105+
test('instance of userEvent is recognized as async event when instance is initialized in beforeEach', async function() {
1106+
user.click(getByLabelText('username'))
1107+
})
1108+
`,
1109+
errors: [
1110+
{
1111+
line: 8,
1112+
column: 5,
1113+
messageId: 'awaitAsyncEvent',
1114+
data: { name: 'click' },
1115+
},
1116+
],
1117+
output: `
1118+
import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}'
1119+
let user;
1120+
beforeEach(() => {
1121+
user = userEvent.setup()
1122+
})
1123+
test('instance of userEvent is recognized as async event when instance is initialized in beforeEach', async function() {
1124+
await user.click(getByLabelText('username'))
1125+
})
1126+
`,
1127+
},
1128+
{
1129+
code: `
1130+
import u from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}'
1131+
test('userEvent as named import', async function() {
1132+
const user = u.setup()
1133+
user.click(getByLabelText('username'))
1134+
})
1135+
`,
1136+
errors: [
1137+
{
1138+
line: 5,
1139+
column: 5,
1140+
messageId: 'awaitAsyncEvent',
1141+
data: { name: 'click' },
1142+
},
1143+
],
1144+
output: `
1145+
import u from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}'
1146+
test('userEvent as named import', async function() {
1147+
const user = u.setup()
1148+
await user.click(getByLabelText('username'))
1149+
})
10111150
`,
10121151
},
10131152
],

0 commit comments

Comments
 (0)
Please sign in to comment.