@@ -12,6 +12,7 @@ import {
1212 hasImportMatch ,
1313 ImportModuleNode ,
1414 isImportDeclaration ,
15+ isImportDefaultSpecifier ,
1516 isImportNamespaceSpecifier ,
1617 isImportSpecifier ,
1718 isLiteral ,
@@ -20,9 +21,9 @@ import {
2021} from './node-utils' ;
2122import {
2223 ABSENCE_MATCHERS ,
24+ ALL_QUERIES_COMBINATIONS ,
2325 ASYNC_UTILS ,
2426 PRESENCE_MATCHERS ,
25- ALL_QUERIES_COMBINATIONS ,
2627} from './utils' ;
2728
2829export type TestingLibrarySettings = {
@@ -67,6 +68,7 @@ type IsAsyncUtilFn = (
6768 validNames ?: readonly typeof ASYNC_UTILS [ number ] [ ]
6869) => boolean ;
6970type IsFireEventMethodFn = ( node : TSESTree . Identifier ) => boolean ;
71+ type IsUserEventMethodFn = ( node : TSESTree . Identifier ) => boolean ;
7072type IsRenderUtilFn = ( node : TSESTree . Identifier ) => boolean ;
7173type IsRenderVariableDeclaratorFn = (
7274 node : TSESTree . VariableDeclarator
@@ -99,6 +101,7 @@ export interface DetectionHelpers {
99101 isFireEventUtil : ( node : TSESTree . Identifier ) => boolean ;
100102 isUserEventUtil : ( node : TSESTree . Identifier ) => boolean ;
101103 isFireEventMethod : IsFireEventMethodFn ;
104+ isUserEventMethod : IsUserEventMethodFn ;
102105 isRenderUtil : IsRenderUtilFn ;
103106 isRenderVariableDeclarator : IsRenderVariableDeclaratorFn ;
104107 isDebugUtil : IsDebugUtilFn ;
@@ -109,6 +112,9 @@ export interface DetectionHelpers {
109112 isNodeComingFromTestingLibrary : IsNodeComingFromTestingLibraryFn ;
110113}
111114
115+ const USER_EVENT_PACKAGE = '@testing-library/user-event' ;
116+ const FIRE_EVENT_NAME = 'fireEvent' ;
117+ const USER_EVENT_NAME = 'userEvent' ;
112118const RENDER_NAME = 'render' ;
113119
114120/**
@@ -125,6 +131,7 @@ export function detectTestingLibraryUtils<
125131 ) : TSESLint . RuleListener => {
126132 let importedTestingLibraryNode : ImportModuleNode | null = null ;
127133 let importedCustomModuleNode : ImportModuleNode | null = null ;
134+ let importedUserEventLibraryNode : ImportModuleNode | null = null ;
128135
129136 // Init options based on shared ESLint settings
130137 const customModule = context . settings [ 'testing-library/utils-module' ] ;
@@ -174,69 +181,6 @@ export function detectTestingLibraryUtils<
174181 return isNodeComingFromTestingLibrary ( referenceNodeIdentifier ) ;
175182 }
176183
177- /**
178- * Determines whether a given node is a simulate event util related to
179- * Testing Library or not.
180- *
181- * In order to determine this, the node must match:
182- * - indicated simulate event name: fireEvent or userEvent
183- * - imported from valid Testing Library module (depends on Aggressive
184- * Reporting)
185- *
186- */
187- function isTestingLibrarySimulateEventUtil (
188- node : TSESTree . Identifier ,
189- utilName : 'fireEvent' | 'userEvent'
190- ) : boolean {
191- const simulateEventUtil = findImportedUtilSpecifier ( utilName ) ;
192- let simulateEventUtilName : string | undefined ;
193-
194- if ( simulateEventUtil ) {
195- simulateEventUtilName = ASTUtils . isIdentifier ( simulateEventUtil )
196- ? simulateEventUtil . name
197- : simulateEventUtil . local . name ;
198- } else if ( isAggressiveModuleReportingEnabled ( ) ) {
199- simulateEventUtilName = utilName ;
200- }
201-
202- if ( ! simulateEventUtilName ) {
203- return false ;
204- }
205-
206- const parentMemberExpression :
207- | TSESTree . MemberExpression
208- | undefined = isMemberExpression ( node . parent ) ? node . parent : undefined ;
209-
210- if ( ! parentMemberExpression ) {
211- return false ;
212- }
213-
214- // make sure that given node it's not fireEvent/userEvent object itself
215- if (
216- [ simulateEventUtilName , utilName ] . includes ( node . name ) ||
217- ( ASTUtils . isIdentifier ( parentMemberExpression . object ) &&
218- parentMemberExpression . object . name === node . name )
219- ) {
220- return false ;
221- }
222-
223- // check fireEvent.click()/userEvent.click() usage
224- const regularCall =
225- ASTUtils . isIdentifier ( parentMemberExpression . object ) &&
226- parentMemberExpression . object . name === simulateEventUtilName ;
227-
228- // check testingLibraryUtils.fireEvent.click() or
229- // testingLibraryUtils.userEvent.click() usage
230- const wildcardCall =
231- isMemberExpression ( parentMemberExpression . object ) &&
232- ASTUtils . isIdentifier ( parentMemberExpression . object . object ) &&
233- parentMemberExpression . object . object . name === simulateEventUtilName &&
234- ASTUtils . isIdentifier ( parentMemberExpression . object . property ) &&
235- parentMemberExpression . object . property . name === utilName ;
236-
237- return regularCall || wildcardCall ;
238- }
239-
240184 /**
241185 * Determines whether aggressive module reporting is enabled or not.
242186 *
@@ -403,7 +347,90 @@ export function detectTestingLibraryUtils<
403347 * Determines whether a given node is fireEvent method or not
404348 */
405349 const isFireEventMethod : IsFireEventMethodFn = ( node ) => {
406- return isTestingLibrarySimulateEventUtil ( node , 'fireEvent' ) ;
350+ const fireEventUtil = findImportedUtilSpecifier ( FIRE_EVENT_NAME ) ;
351+ let fireEventUtilName : string | undefined ;
352+
353+ if ( fireEventUtil ) {
354+ fireEventUtilName = ASTUtils . isIdentifier ( fireEventUtil )
355+ ? fireEventUtil . name
356+ : fireEventUtil . local . name ;
357+ } else if ( isAggressiveModuleReportingEnabled ( ) ) {
358+ fireEventUtilName = FIRE_EVENT_NAME ;
359+ }
360+
361+ if ( ! fireEventUtilName ) {
362+ return false ;
363+ }
364+
365+ const parentMemberExpression :
366+ | TSESTree . MemberExpression
367+ | undefined = isMemberExpression ( node . parent ) ? node . parent : undefined ;
368+
369+ if ( ! parentMemberExpression ) {
370+ return false ;
371+ }
372+
373+ // make sure that given node it's not fireEvent object itself
374+ if (
375+ [ fireEventUtilName , FIRE_EVENT_NAME ] . includes ( node . name ) ||
376+ ( ASTUtils . isIdentifier ( parentMemberExpression . object ) &&
377+ parentMemberExpression . object . name === node . name )
378+ ) {
379+ return false ;
380+ }
381+
382+ // check fireEvent.click() usage
383+ const regularCall =
384+ ASTUtils . isIdentifier ( parentMemberExpression . object ) &&
385+ parentMemberExpression . object . name === fireEventUtilName ;
386+
387+ // check testingLibraryUtils.fireEvent.click() usage
388+ const wildcardCall =
389+ isMemberExpression ( parentMemberExpression . object ) &&
390+ ASTUtils . isIdentifier ( parentMemberExpression . object . object ) &&
391+ parentMemberExpression . object . object . name === fireEventUtilName &&
392+ ASTUtils . isIdentifier ( parentMemberExpression . object . property ) &&
393+ parentMemberExpression . object . property . name === FIRE_EVENT_NAME ;
394+
395+ return regularCall || wildcardCall ;
396+ } ;
397+
398+ const isUserEventMethod : IsUserEventMethodFn = ( node ) => {
399+ const userEvent = findImportedUserEventSpecifier ( ) ;
400+ let userEventName : string | undefined ;
401+
402+ if ( userEvent ) {
403+ userEventName = userEvent . name ;
404+ } else if ( isAggressiveModuleReportingEnabled ( ) ) {
405+ userEventName = USER_EVENT_NAME ;
406+ }
407+
408+ if ( ! userEventName ) {
409+ return false ;
410+ }
411+
412+ const parentMemberExpression :
413+ | TSESTree . MemberExpression
414+ | undefined = isMemberExpression ( node . parent ) ? node . parent : undefined ;
415+
416+ if ( ! parentMemberExpression ) {
417+ return false ;
418+ }
419+
420+ // make sure that given node it's not userEvent object itself
421+ if (
422+ [ userEventName , USER_EVENT_NAME ] . includes ( node . name ) ||
423+ ( ASTUtils . isIdentifier ( parentMemberExpression . object ) &&
424+ parentMemberExpression . object . name === node . name )
425+ ) {
426+ return false ;
427+ }
428+
429+ // check userEvent.click() usage
430+ return (
431+ ASTUtils . isIdentifier ( parentMemberExpression . object ) &&
432+ parentMemberExpression . object . name === userEventName
433+ ) ;
407434 } ;
408435
409436 /**
@@ -553,6 +580,27 @@ export function detectTestingLibraryUtils<
553580 }
554581 } ;
555582
583+ const findImportedUserEventSpecifier : ( ) => TSESTree . Identifier | null = ( ) => {
584+ if ( ! importedUserEventLibraryNode ) {
585+ return null ;
586+ }
587+
588+ if ( isImportDeclaration ( importedUserEventLibraryNode ) ) {
589+ const userEventIdentifier = importedUserEventLibraryNode . specifiers . find (
590+ ( specifier ) => isImportDefaultSpecifier ( specifier )
591+ ) ;
592+
593+ if ( userEventIdentifier ) {
594+ return userEventIdentifier . local ;
595+ }
596+ } else {
597+ const requireNode = importedUserEventLibraryNode . parent as TSESTree . VariableDeclarator ;
598+ return requireNode . id as TSESTree . Identifier ;
599+ }
600+
601+ return null ;
602+ } ;
603+
556604 const getImportedUtilSpecifier = (
557605 node : TSESTree . MemberExpression | TSESTree . Identifier
558606 ) : TSESTree . ImportClause | TSESTree . Identifier | undefined => {
@@ -607,6 +655,7 @@ export function detectTestingLibraryUtils<
607655 isFireEventUtil,
608656 isUserEventUtil,
609657 isFireEventMethod,
658+ isUserEventMethod,
610659 isRenderUtil,
611660 isRenderVariableDeclarator,
612661 isDebugUtil,
@@ -644,6 +693,15 @@ export function detectTestingLibraryUtils<
644693 ) {
645694 importedCustomModuleNode = node ;
646695 }
696+
697+ // check only if user-event import not found yet so we avoid
698+ // to override importedUserEventLibraryNode after it's found
699+ if (
700+ ! importedUserEventLibraryNode &&
701+ String ( node . source . value ) === USER_EVENT_PACKAGE
702+ ) {
703+ importedUserEventLibraryNode = node ;
704+ }
647705 } ,
648706
649707 // Check if Testing Library related modules are loaded with required.
@@ -676,6 +734,18 @@ export function detectTestingLibraryUtils<
676734 ) {
677735 importedCustomModuleNode = callExpression ;
678736 }
737+
738+ if (
739+ ! importedCustomModuleNode &&
740+ args . some (
741+ ( arg ) =>
742+ isLiteral ( arg ) &&
743+ typeof arg . value === 'string' &&
744+ arg . value === USER_EVENT_PACKAGE
745+ )
746+ ) {
747+ importedUserEventLibraryNode = callExpression ;
748+ }
679749 } ,
680750 } ;
681751
0 commit comments