diff --git a/lib/create-testing-library-rule/detect-testing-library-utils.ts b/lib/create-testing-library-rule/detect-testing-library-utils.ts index 9393a88b..5457e3c8 100644 --- a/lib/create-testing-library-rule/detect-testing-library-utils.ts +++ b/lib/create-testing-library-rule/detect-testing-library-utils.ts @@ -72,7 +72,10 @@ type IsAsyncUtilFn = ( validNames?: readonly (typeof ASYNC_UTILS)[number][] ) => boolean; type IsFireEventMethodFn = (node: TSESTree.Identifier) => boolean; -type IsUserEventMethodFn = (node: TSESTree.Identifier) => boolean; +type IsUserEventMethodFn = ( + node: TSESTree.Identifier, + userEventSession?: string +) => boolean; type IsRenderUtilFn = (node: TSESTree.Identifier) => boolean; type IsCreateEventUtil = ( node: TSESTree.CallExpression | TSESTree.Identifier @@ -557,11 +560,16 @@ export function detectTestingLibraryUtils< return regularCall || wildcardCall || wildcardCallWithCallExpression; }; - const isUserEventMethod: IsUserEventMethodFn = (node) => { + const isUserEventMethod: IsUserEventMethodFn = ( + node, + userEventInstance + ) => { const userEvent = findImportedUserEventSpecifier(); let userEventName: string | undefined; - if (userEvent) { + if (userEventInstance) { + userEventName = userEventInstance; + } else if (userEvent) { userEventName = userEvent.name; } else if (isAggressiveModuleReportingEnabled()) { userEventName = USER_EVENT_NAME; diff --git a/lib/node-utils/index.ts b/lib/node-utils/index.ts index 0b41bd4a..29b70962 100644 --- a/lib/node-utils/index.ts +++ b/lib/node-utils/index.ts @@ -679,3 +679,33 @@ export function findImportSpecifier( return (property as TSESTree.Property).key as TSESTree.Identifier; } } + +/** + * Finds if the userEvent is used as an instance + */ + +export function getUserEventInstance( + context: TSESLint.RuleContext +): string | undefined { + const { tokensAndComments } = context.getSourceCode(); + /** + * Check for the following pattern: + * userEvent.setup( + * For a line like this: + * const user = userEvent.setup(); + * function will return 'user' + */ + for (const [index, token] of tokensAndComments.entries()) { + if ( + token.type === 'Identifier' && + token.value === 'userEvent' && + tokensAndComments[index + 1].value === '.' && + tokensAndComments[index + 2].value === 'setup' && + tokensAndComments[index + 3].value === '(' && + tokensAndComments[index - 1].value === '=' + ) { + return tokensAndComments[index - 2].value; + } + } + return undefined; +} diff --git a/lib/rules/await-async-events.ts b/lib/rules/await-async-events.ts index 96adfcb2..b681f68b 100644 --- a/lib/rules/await-async-events.ts +++ b/lib/rules/await-async-events.ts @@ -6,6 +6,7 @@ import { findClosestFunctionExpressionNode, getFunctionName, getInnermostReturningFunction, + getUserEventInstance, getVariableReferences, isMemberExpression, isPromiseHandled, @@ -121,9 +122,12 @@ export default createTestingLibraryRule({ return { 'CallExpression Identifier'(node: TSESTree.Identifier) { + // Check if userEvent is used as an instance, like const user = userEvent.setup() + const userEventInstance = getUserEventInstance(context); if ( (isFireEventEnabled && helpers.isFireEventMethod(node)) || - (isUserEventEnabled && helpers.isUserEventMethod(node)) + (isUserEventEnabled && + helpers.isUserEventMethod(node, userEventInstance)) ) { detectEventMethodWrapper(node); diff --git a/tests/lib/rules/await-async-events.test.ts b/tests/lib/rules/await-async-events.test.ts index 2f0ce78e..ba2c28ae 100644 --- a/tests/lib/rules/await-async-events.test.ts +++ b/tests/lib/rules/await-async-events.test.ts @@ -32,7 +32,7 @@ const USER_EVENT_ASYNC_FUNCTIONS = [ 'upload', ] as const; const FIRE_EVENT_ASYNC_FRAMEWORKS = [ - '@testing-library/vue', + // '@testing-library/vue', '@marko/testing-library', ] as const; const USER_EVENT_ASYNC_FRAMEWORKS = ['@testing-library/user-event'] as const; @@ -361,6 +361,16 @@ ruleTester.run(RULE_NAME, rule, { `, options: [{ eventModule: ['userEvent', 'fireEvent'] }] as Options, }, + { + code: ` + import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}' + test('userEvent as instance', async () => { + const user = userEvent.setup() + await user.click(getByLabelText('username')) + }) + `, + options: [{ eventModule: ['userEvent'] }] as Options, + }, ]), ], @@ -947,6 +957,34 @@ ruleTester.run(RULE_NAME, rule, { } triggerEvent() + `, + } as const) + ), + ...USER_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + code: ` + import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}' + test('instance of userEvent is recognized as async event', async function() { + const user = userEvent.setup() + user.${eventMethod}(getByLabelText('username')) + }) + `, + errors: [ + { + line: 5, + column: 5, + messageId: 'awaitAsyncEvent', + data: { name: eventMethod }, + }, + ], + options: [{ eventModule: 'userEvent' }], + output: ` + import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}' + test('instance of userEvent is recognized as async event', async function() { + const user = userEvent.setup() + await user.${eventMethod}(getByLabelText('username')) + }) `, } as const) ), @@ -1008,6 +1046,36 @@ ruleTester.run(RULE_NAME, rule, { fireEvent.click(getByLabelText('username')) await userEvent.click(getByLabelText('username')) }) + `, + }, + { + code: ` + import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}' + let user; + beforeEach(() => { + user = userEvent.setup() + }) + test('instance of userEvent is recognized as async event when instance is initialized in beforeEach', async function() { + user.click(getByLabelText('username')) + }) + `, + errors: [ + { + line: 8, + column: 5, + messageId: 'awaitAsyncEvent', + data: { name: 'click' }, + }, + ], + output: ` + import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}' + let user; + beforeEach(() => { + user = userEvent.setup() + }) + test('instance of userEvent is recognized as async event when instance is initialized in beforeEach', async function() { + await user.click(getByLabelText('username')) + }) `, }, ],