Skip to content

Commit f28f7e1

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
1 parent b531af8 commit f28f7e1

File tree

4 files changed

+158
-10
lines changed

4 files changed

+158
-10
lines changed

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

+14-5
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
@@ -557,7 +560,10 @@ export function detectTestingLibraryUtils<
557560
return regularCall || wildcardCall || wildcardCallWithCallExpression;
558561
};
559562

560-
const isUserEventMethod: IsUserEventMethodFn = (node) => {
563+
const isUserEventMethod: IsUserEventMethodFn = (
564+
node,
565+
userEventInstance
566+
) => {
561567
const userEvent = findImportedUserEventSpecifier();
562568
let userEventName: string | undefined;
563569

@@ -567,7 +573,7 @@ export function detectTestingLibraryUtils<
567573
userEventName = USER_EVENT_NAME;
568574
}
569575

570-
if (!userEventName) {
576+
if (!userEventName && !userEventInstance) {
571577
return false;
572578
}
573579

@@ -591,8 +597,11 @@ export function detectTestingLibraryUtils<
591597

592598
// check userEvent.click() usage
593599
return (
594-
ASTUtils.isIdentifier(parentMemberExpression.object) &&
595-
parentMemberExpression.object.name === userEventName
600+
(ASTUtils.isIdentifier(parentMemberExpression.object) &&
601+
parentMemberExpression.object.name === userEventName) ||
602+
// check userEventInstance.click() usage
603+
(ASTUtils.isIdentifier(parentMemberExpression.object) &&
604+
parentMemberExpression.object.name === userEventInstance)
596605
);
597606
};
598607

lib/node-utils/index.ts

+30
Original file line numberDiff line numberDiff line change
@@ -679,3 +679,33 @@ 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+
): string | undefined {
690+
const { tokensAndComments } = context.getSourceCode();
691+
/**
692+
* Check for the following pattern:
693+
* userEvent.setup(
694+
* For a line like this:
695+
* const user = userEvent.setup();
696+
* function will return 'user'
697+
*/
698+
for (const [index, token] of tokensAndComments.entries()) {
699+
if (
700+
token.type === 'Identifier' &&
701+
token.value === 'userEvent' &&
702+
tokensAndComments[index + 1].value === '.' &&
703+
tokensAndComments[index + 2].value === 'setup' &&
704+
tokensAndComments[index + 3].value === '(' &&
705+
tokensAndComments[index - 1].value === '='
706+
) {
707+
return tokensAndComments[index - 2].value;
708+
}
709+
}
710+
return undefined;
711+
}

lib/rules/await-async-events.ts

+9-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,12 @@ export default createTestingLibraryRule<Options, MessageIds>({
121119

122120
return {
123121
'CallExpression Identifier'(node: TSESTree.Identifier) {
122+
// Check if userEvent is used as an instance, like const user = userEvent.setup()
123+
const userEventInstance = getUserEventInstance(context);
124124
if (
125125
(isFireEventEnabled && helpers.isFireEventMethod(node)) ||
126-
(isUserEventEnabled && helpers.isUserEventMethod(node))
126+
(isUserEventEnabled &&
127+
helpers.isUserEventMethod(node, userEventInstance))
127128
) {
128129
detectEventMethodWrapper(node);
129130

@@ -136,6 +137,10 @@ export default createTestingLibraryRule<Options, MessageIds>({
136137
return;
137138
}
138139

140+
if (node.name === USER_EVENT_SETUP_FUNCTION_NAME) {
141+
return;
142+
}
143+
139144
const references = getVariableReferences(
140145
context,
141146
closestCallExpression.parent

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

+105-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,16 @@ 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+
},
364374
]),
365375
],
366376

@@ -947,6 +957,70 @@ ruleTester.run(RULE_NAME, rule, {
947957
}
948958
949959
triggerEvent()
960+
`,
961+
} as const)
962+
),
963+
...USER_EVENT_ASYNC_FUNCTIONS.map(
964+
(eventMethod) =>
965+
({
966+
code: `
967+
import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}'
968+
test('instance of userEvent is recognized as async event', async function() {
969+
const user = userEvent.setup()
970+
user.${eventMethod}(getByLabelText('username'))
971+
})
972+
`,
973+
errors: [
974+
{
975+
line: 5,
976+
column: 5,
977+
messageId: 'awaitAsyncEvent',
978+
data: { name: eventMethod },
979+
},
980+
],
981+
options: [{ eventModule: 'userEvent' }],
982+
output: `
983+
import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}'
984+
test('instance of userEvent is recognized as async event', async function() {
985+
const user = userEvent.setup()
986+
await user.${eventMethod}(getByLabelText('username'))
987+
})
988+
`,
989+
} as const)
990+
),
991+
...USER_EVENT_ASYNC_FUNCTIONS.map(
992+
(eventMethod) =>
993+
({
994+
code: `
995+
import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}'
996+
test('instance of userEvent is recognized as async event along with static userEvent', async function() {
997+
const user = userEvent.setup()
998+
user.${eventMethod}(getByLabelText('username'))
999+
userEvent.${eventMethod}(getByLabelText('username'))
1000+
})
1001+
`,
1002+
errors: [
1003+
{
1004+
line: 5,
1005+
column: 5,
1006+
messageId: 'awaitAsyncEvent',
1007+
data: { name: eventMethod },
1008+
},
1009+
{
1010+
line: 6,
1011+
column: 5,
1012+
messageId: 'awaitAsyncEvent',
1013+
data: { name: eventMethod },
1014+
},
1015+
],
1016+
options: [{ eventModule: 'userEvent' }],
1017+
output: `
1018+
import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}'
1019+
test('instance of userEvent is recognized as async event along with static userEvent', async function() {
1020+
const user = userEvent.setup()
1021+
await user.${eventMethod}(getByLabelText('username'))
1022+
await userEvent.${eventMethod}(getByLabelText('username'))
1023+
})
9501024
`,
9511025
} as const)
9521026
),
@@ -1008,6 +1082,36 @@ ruleTester.run(RULE_NAME, rule, {
10081082
fireEvent.click(getByLabelText('username'))
10091083
await userEvent.click(getByLabelText('username'))
10101084
})
1085+
`,
1086+
},
1087+
{
1088+
code: `
1089+
import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}'
1090+
let user;
1091+
beforeEach(() => {
1092+
user = userEvent.setup()
1093+
})
1094+
test('instance of userEvent is recognized as async event when instance is initialized in beforeEach', async function() {
1095+
user.click(getByLabelText('username'))
1096+
})
1097+
`,
1098+
errors: [
1099+
{
1100+
line: 8,
1101+
column: 5,
1102+
messageId: 'awaitAsyncEvent',
1103+
data: { name: 'click' },
1104+
},
1105+
],
1106+
output: `
1107+
import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}'
1108+
let user;
1109+
beforeEach(() => {
1110+
user = userEvent.setup()
1111+
})
1112+
test('instance of userEvent is recognized as async event when instance is initialized in beforeEach', async function() {
1113+
await user.click(getByLabelText('username'))
1114+
})
10111115
`,
10121116
},
10131117
],

0 commit comments

Comments
 (0)