Skip to content

Commit 636dee8

Browse files
KvanttinenBelco90
authored andcommitted
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 27dfa51 commit 636dee8

File tree

4 files changed

+236
-36
lines changed

4 files changed

+236
-36
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

+13-1
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,
@@ -118,9 +119,20 @@ export default createTestingLibraryRule<Options, MessageIds>({
118119

119120
return {
120121
'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+
);
121132
if (
122133
(isFireEventEnabled && helpers.isFireEventMethod(node)) ||
123-
(isUserEventEnabled && helpers.isUserEventMethod(node))
134+
(isUserEventEnabled &&
135+
helpers.isUserEventMethod(node, userEventInstance))
124136
) {
125137
detectEventMethodWrapper(node);
126138

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;
@@ -374,6 +374,27 @@ ruleTester.run(RULE_NAME, rule, {
374374
`,
375375
options: [{ eventModule: ['userEvent', 'fireEvent'] }] as Options,
376376
},
377+
{
378+
code: `
379+
import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}'
380+
test('userEvent as instance', async () => {
381+
const user = userEvent.setup()
382+
await user.click(getByLabelText('username'))
383+
})
384+
`,
385+
options: [{ eventModule: ['userEvent'] }] as Options,
386+
},
387+
{
388+
code: `
389+
import u from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}'
390+
test('userEvent as named import', async () => {
391+
const user = u.setup()
392+
await user.click(getByLabelText('username'))
393+
await u.click(getByLabelText('username'))
394+
})
395+
`,
396+
options: [{ eventModule: ['userEvent'] }] as Options,
397+
},
377398
]),
378399
],
379400

@@ -960,6 +981,70 @@ ruleTester.run(RULE_NAME, rule, {
960981
}
961982
962983
triggerEvent()
984+
`,
985+
} as const)
986+
),
987+
...USER_EVENT_ASYNC_FUNCTIONS.map(
988+
(eventMethod) =>
989+
({
990+
code: `
991+
import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}'
992+
test('instance of userEvent is recognized as async event', async function() {
993+
const user = userEvent.setup()
994+
user.${eventMethod}(getByLabelText('username'))
995+
})
996+
`,
997+
errors: [
998+
{
999+
line: 5,
1000+
column: 5,
1001+
messageId: 'awaitAsyncEvent',
1002+
data: { name: eventMethod },
1003+
},
1004+
],
1005+
options: [{ eventModule: 'userEvent' }],
1006+
output: `
1007+
import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}'
1008+
test('instance of userEvent is recognized as async event', async function() {
1009+
const user = userEvent.setup()
1010+
await user.${eventMethod}(getByLabelText('username'))
1011+
})
1012+
`,
1013+
} as const)
1014+
),
1015+
...USER_EVENT_ASYNC_FUNCTIONS.map(
1016+
(eventMethod) =>
1017+
({
1018+
code: `
1019+
import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}'
1020+
test('instance of userEvent is recognized as async event along with static userEvent', async function() {
1021+
const user = userEvent.setup()
1022+
user.${eventMethod}(getByLabelText('username'))
1023+
userEvent.${eventMethod}(getByLabelText('username'))
1024+
})
1025+
`,
1026+
errors: [
1027+
{
1028+
line: 5,
1029+
column: 5,
1030+
messageId: 'awaitAsyncEvent',
1031+
data: { name: eventMethod },
1032+
},
1033+
{
1034+
line: 6,
1035+
column: 5,
1036+
messageId: 'awaitAsyncEvent',
1037+
data: { name: eventMethod },
1038+
},
1039+
],
1040+
options: [{ eventModule: 'userEvent' }],
1041+
output: `
1042+
import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}'
1043+
test('instance of userEvent is recognized as async event along with static userEvent', async function() {
1044+
const user = userEvent.setup()
1045+
await user.${eventMethod}(getByLabelText('username'))
1046+
await userEvent.${eventMethod}(getByLabelText('username'))
1047+
})
9631048
`,
9641049
} as const)
9651050
),
@@ -1021,6 +1106,60 @@ ruleTester.run(RULE_NAME, rule, {
10211106
fireEvent.click(getByLabelText('username'))
10221107
await userEvent.click(getByLabelText('username'))
10231108
})
1109+
`,
1110+
},
1111+
{
1112+
code: `
1113+
import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}'
1114+
let user;
1115+
beforeEach(() => {
1116+
user = userEvent.setup()
1117+
})
1118+
test('instance of userEvent is recognized as async event when instance is initialized in beforeEach', async function() {
1119+
user.click(getByLabelText('username'))
1120+
})
1121+
`,
1122+
errors: [
1123+
{
1124+
line: 8,
1125+
column: 5,
1126+
messageId: 'awaitAsyncEvent',
1127+
data: { name: 'click' },
1128+
},
1129+
],
1130+
output: `
1131+
import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}'
1132+
let user;
1133+
beforeEach(() => {
1134+
user = userEvent.setup()
1135+
})
1136+
test('instance of userEvent is recognized as async event when instance is initialized in beforeEach', async function() {
1137+
await user.click(getByLabelText('username'))
1138+
})
1139+
`,
1140+
},
1141+
{
1142+
code: `
1143+
import u from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}'
1144+
test('userEvent as named import', async function() {
1145+
const user = u.setup()
1146+
user.click(getByLabelText('username'))
1147+
})
1148+
`,
1149+
errors: [
1150+
{
1151+
line: 5,
1152+
column: 5,
1153+
messageId: 'awaitAsyncEvent',
1154+
data: { name: 'click' },
1155+
},
1156+
],
1157+
output: `
1158+
import u from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}'
1159+
test('userEvent as named import', async function() {
1160+
const user = u.setup()
1161+
await user.click(getByLabelText('username'))
1162+
})
10241163
`,
10251164
},
10261165
],

0 commit comments

Comments
 (0)