Skip to content

Commit 8ef0782

Browse files
authored
fix: detect userEvent imported from custom module (#1264)
Fixes #1253 Affected rules: - await-async-events - no-await-sync-events - no-unnecessary-act - no-wait-for-side-effects
2 parents 9ce8966 + 66b22c3 commit 8ef0782

6 files changed

Lines changed: 385 additions & 35 deletions

File tree

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

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -480,7 +480,9 @@ export function detectTestingLibraryUtils<
480480
let userEventName: string | undefined;
481481

482482
if (userEvent) {
483-
userEventName = userEvent.name;
483+
userEventName = ASTUtils.isIdentifier(userEvent)
484+
? userEvent.name
485+
: userEvent.local.name;
484486
} else if (isAggressiveModuleReportingEnabled()) {
485487
userEventName = USER_EVENT_NAME;
486488
}
@@ -574,7 +576,9 @@ export function detectTestingLibraryUtils<
574576
let userEventName: string | undefined;
575577

576578
if (userEvent) {
577-
userEventName = userEvent.name;
579+
userEventName = ASTUtils.isIdentifier(userEvent)
580+
? userEvent.name
581+
: userEvent.local.name;
578582
} else if (isAggressiveModuleReportingEnabled()) {
579583
userEventName = USER_EVENT_NAME;
580584
}
@@ -876,38 +880,44 @@ export function detectTestingLibraryUtils<
876880
return findImportSpecifier(specifierName, node);
877881
};
878882

879-
const findImportedUserEventSpecifier: () => TSESTree.Identifier | null =
880-
() => {
881-
if (!importedUserEventLibraryNode) {
882-
return null;
883+
const findImportedUserEventSpecifier: () =>
884+
| TSESTree.Identifier
885+
| TSESTree.ImportClause
886+
| null = () => {
887+
if (!importedUserEventLibraryNode) {
888+
const customModuleNode = getCustomModuleImportNode();
889+
if (customModuleNode) {
890+
return findImportSpecifier(USER_EVENT_NAME, customModuleNode) ?? null;
883891
}
892+
return null;
893+
}
884894

885-
if (isImportDeclaration(importedUserEventLibraryNode)) {
886-
const userEventIdentifier =
887-
importedUserEventLibraryNode.specifiers.find((specifier) =>
888-
isImportDefaultSpecifier(specifier)
889-
);
890-
891-
if (userEventIdentifier) {
892-
return userEventIdentifier.local;
893-
}
894-
} else {
895-
if (
896-
!ASTUtils.isVariableDeclarator(importedUserEventLibraryNode.parent)
897-
) {
898-
return null;
899-
}
895+
if (isImportDeclaration(importedUserEventLibraryNode)) {
896+
const userEventIdentifier =
897+
importedUserEventLibraryNode.specifiers.find((specifier) =>
898+
isImportDefaultSpecifier(specifier)
899+
);
900900

901-
const requireNode = importedUserEventLibraryNode.parent;
902-
if (!ASTUtils.isIdentifier(requireNode.id)) {
903-
return null;
904-
}
901+
if (userEventIdentifier) {
902+
return userEventIdentifier.local;
903+
}
904+
} else {
905+
if (
906+
!ASTUtils.isVariableDeclarator(importedUserEventLibraryNode.parent)
907+
) {
908+
return null;
909+
}
905910

906-
return requireNode.id;
911+
const requireNode = importedUserEventLibraryNode.parent;
912+
if (!ASTUtils.isIdentifier(requireNode.id)) {
913+
return null;
907914
}
908915

909-
return null;
910-
};
916+
return requireNode.id;
917+
}
918+
919+
return null;
920+
};
911921

912922
const getTestingLibraryImportedUtilSpecifier = (
913923
node: TSESTree.Identifier | TSESTree.MemberExpression

tests/create-testing-library-rule.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,13 @@ ruleTester.run(rule.name, rule, {
108108
code: `
109109
import * as incorrect from '@testing-library/user-event'
110110
userEvent.click()
111+
`,
112+
},
113+
{
114+
settings: { 'testing-library/utils-module': 'test-utils' },
115+
code: `
116+
import { userEvent } from 'somewhere-else'
117+
userEvent.click(element)
111118
`,
112119
},
113120

@@ -615,6 +622,22 @@ ruleTester.run(rule.name, rule, {
615622
code: `
616623
const renamed = require('@testing-library/user-event')
617624
renamed.click(element)
625+
`,
626+
errors: [{ line: 3, column: 15, messageId: 'userEventError' }],
627+
},
628+
{
629+
settings: { 'testing-library/utils-module': 'test-utils' },
630+
code: `
631+
import { userEvent } from 'test-utils'
632+
userEvent.click(element)
633+
`,
634+
errors: [{ line: 3, column: 17, messageId: 'userEventError' }],
635+
},
636+
{
637+
settings: { 'testing-library/utils-module': 'test-utils' },
638+
code: `
639+
import { userEvent as renamed } from 'test-utils'
640+
renamed.click(element)
618641
`,
619642
errors: [{ line: 3, column: 15, messageId: 'userEventError' }],
620643
},

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

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -887,6 +887,96 @@ ruleTester.run(rule.name, rule, {
887887
}) as const
888888
),
889889
]),
890+
...USER_EVENT_ASYNC_FUNCTIONS.map(
891+
(eventMethod) =>
892+
({
893+
settings: {
894+
'testing-library/utils-module': 'test-utils',
895+
},
896+
code: `
897+
import { userEvent } from 'test-utils'
898+
test('unhandled promise from userEvent imported from custom module is invalid', () => {
899+
userEvent.${eventMethod}(getByLabelText('username'))
900+
})
901+
`,
902+
errors: [
903+
{
904+
line: 4,
905+
column: 9,
906+
endColumn: 19 + eventMethod.length,
907+
messageId: 'awaitAsyncEvent',
908+
data: { name: eventMethod },
909+
},
910+
],
911+
options: [{ eventModule: 'userEvent' }],
912+
output: `
913+
import { userEvent } from 'test-utils'
914+
test('unhandled promise from userEvent imported from custom module is invalid', async () => {
915+
await userEvent.${eventMethod}(getByLabelText('username'))
916+
})
917+
`,
918+
}) as const
919+
),
920+
...USER_EVENT_ASYNC_FUNCTIONS.map(
921+
(eventMethod) =>
922+
({
923+
settings: {
924+
'testing-library/utils-module': 'test-utils',
925+
},
926+
code: `
927+
const { userEvent } = require('test-utils')
928+
test('unhandled promise from userEvent required from custom module is invalid', () => {
929+
userEvent.${eventMethod}(getByLabelText('username'))
930+
})
931+
`,
932+
errors: [
933+
{
934+
line: 4,
935+
column: 9,
936+
endColumn: 19 + eventMethod.length,
937+
messageId: 'awaitAsyncEvent',
938+
data: { name: eventMethod },
939+
},
940+
],
941+
options: [{ eventModule: 'userEvent' }],
942+
output: `
943+
const { userEvent } = require('test-utils')
944+
test('unhandled promise from userEvent required from custom module is invalid', async () => {
945+
await userEvent.${eventMethod}(getByLabelText('username'))
946+
})
947+
`,
948+
}) as const
949+
),
950+
...USER_EVENT_ASYNC_FUNCTIONS.map(
951+
(eventMethod) =>
952+
({
953+
settings: {
954+
'testing-library/utils-module': 'test-utils',
955+
},
956+
code: `
957+
import { userEvent as ue } from 'test-utils'
958+
test('unhandled promise from aliased userEvent imported from custom module is invalid', () => {
959+
ue.${eventMethod}(getByLabelText('username'))
960+
})
961+
`,
962+
errors: [
963+
{
964+
line: 4,
965+
column: 9,
966+
endColumn: 12 + eventMethod.length,
967+
messageId: 'awaitAsyncEvent',
968+
data: { name: eventMethod },
969+
},
970+
],
971+
options: [{ eventModule: 'userEvent' }],
972+
output: `
973+
import { userEvent as ue } from 'test-utils'
974+
test('unhandled promise from aliased userEvent imported from custom module is invalid', async () => {
975+
await ue.${eventMethod}(getByLabelText('username'))
976+
})
977+
`,
978+
}) as const
979+
),
890980
...USER_EVENT_ASYNC_FRAMEWORKS.flatMap((testingFramework) => [
891981
...USER_EVENT_ASYNC_FUNCTIONS.map(
892982
(eventMethod) =>

tests/rules/no-await-sync-events.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,18 @@ ruleTester.run(rule.name, rule, {
242242
});
243243
`,
244244
})),
245+
// userEvent from non-custom module should not be reported when custom module is set
246+
{
247+
settings: { 'testing-library/utils-module': 'test-utils' },
248+
code: `
249+
import { userEvent } from 'somewhere-else';
250+
251+
test('should not report userEvent from non-custom module', async() => {
252+
await userEvent.type('foo', 'bar', { delay: 0 });
253+
});
254+
`,
255+
options: [{ eventModules: ['user-event'] }],
256+
},
245257
],
246258

247259
invalid: [
@@ -387,6 +399,63 @@ ruleTester.run(rule.name, rule, {
387399
},
388400
],
389401
},
402+
{
403+
settings: { 'testing-library/utils-module': 'test-utils' },
404+
code: `
405+
import { userEvent } from 'test-utils';
406+
407+
test('should report userEvent from custom module', async() => {
408+
await userEvent.type('foo', 'bar', { delay: 0 });
409+
});
410+
`,
411+
options: [{ eventModules: ['user-event'] }],
412+
errors: [
413+
{
414+
line: 5,
415+
column: 17,
416+
messageId: 'noAwaitSyncEvents',
417+
data: { name: 'userEvent.type' },
418+
},
419+
],
420+
},
421+
{
422+
settings: { 'testing-library/utils-module': 'test-utils' },
423+
code: `
424+
const { userEvent } = require('test-utils');
425+
426+
test('should report userEvent required from custom module', async() => {
427+
await userEvent.type('foo', 'bar', { delay: 0 });
428+
});
429+
`,
430+
options: [{ eventModules: ['user-event'] }],
431+
errors: [
432+
{
433+
line: 5,
434+
column: 17,
435+
messageId: 'noAwaitSyncEvents',
436+
data: { name: 'userEvent.type' },
437+
},
438+
],
439+
},
440+
{
441+
settings: { 'testing-library/utils-module': 'test-utils' },
442+
code: `
443+
import { userEvent as ue } from 'test-utils';
444+
445+
test('should report aliased userEvent from custom module', async() => {
446+
await ue.type('foo', 'bar', { delay: 0 });
447+
});
448+
`,
449+
options: [{ eventModules: ['user-event'] }],
450+
errors: [
451+
{
452+
line: 5,
453+
column: 17,
454+
messageId: 'noAwaitSyncEvents',
455+
data: { name: 'ue.type' },
456+
},
457+
],
458+
},
390459
{
391460
code: `async() => {
392461
const delay = 0

0 commit comments

Comments
 (0)