From 46496af563df6c6442325fd89b6192a2081279ba Mon Sep 17 00:00:00 2001 From: Amine Benzaazaa <61782040+AmineBenzaazaa@users.noreply.github.com> Date: Sat, 17 Oct 2026 01:59:46 +0000 Subject: [PATCH] fix(no-wait-for-side-effects): detect userEvent.setup() instances and void-wrapped side effects Co-Authored-By: Claude Opus 5.5 --- src/rules/no-wait-for-side-effects.ts | 57 +++++++++---- tests/rules/no-wait-for-side-effects.test.ts | 86 ++++++++++++++++++++ 2 files changed, 126 insertions(+), 17 deletions(-) diff --git a/src/rules/no-wait-for-side-effects.ts b/src/rules/no-wait-for-side-effects.ts index c00a0164..72ba23a6 100644 --- a/src/rules/no-wait-for-side-effects.ts +++ b/src/rules/no-wait-for-side-effects.ts @@ -1,3 +1,4 @@ +import { AST_NODE_TYPES, ASTUtils } from '@typescript-eslint/utils'; import { isAwaitExpression } from '@typescript-eslint/utils/ast-utils'; import { createTestingLibraryRule } from '../create-testing-library-rule'; @@ -7,6 +8,7 @@ import { isVariableDeclaration, isAssignmentExpression, isCallExpression, + isMemberExpression, isSequenceExpression, hasThenProperty, } from '../node-utils'; @@ -44,6 +46,32 @@ export default createTestingLibraryRule({ create(context, _, helpers) { const sourceCode = getSourceCode(context); + // Variables holding a `userEvent.setup()` instance, e.g. `const user = userEvent.setup()` + const userEventSetupVars = new Set(); + + function isSideEffectUtil(identifier: TSESTree.Identifier): boolean { + return ( + helpers.isFireEventUtil(identifier) || + helpers.isUserEventUtil(identifier) || + userEventSetupVars.has(identifier.name) || + helpers.isRenderUtil(identifier) + ); + } + + function trackUserEventSetupVar(node: TSESTree.VariableDeclarator) { + if ( + ASTUtils.isIdentifier(node.id) && + isCallExpression(node.init) && + isMemberExpression(node.init.callee) && + ASTUtils.isIdentifier(node.init.callee.object) && + ASTUtils.isIdentifier(node.init.callee.property) && + node.init.callee.property.name === 'setup' && + helpers.isUserEventUtil(node.init.callee.object) + ) { + userEventSetupVars.add(node.id.name); + } + } + function isCallerWaitFor( node: | TSESTree.AssignmentExpression @@ -149,11 +177,7 @@ export default createTestingLibraryRule({ return false; } - return ( - helpers.isFireEventUtil(test) || - helpers.isUserEventUtil(test) || - helpers.isRenderUtil(test) - ); + return isSideEffectUtil(test); } return false; }); @@ -181,17 +205,20 @@ export default createTestingLibraryRule({ return true; } - const expressionIdentifier = getPropertyIdentifierNode(node); + // unwrap `void userEvent.click(el)` + const expressionIdentifier = getPropertyIdentifierNode( + isExpressionStatement(node) && + node.expression.type === AST_NODE_TYPES.UnaryExpression && + node.expression.operator === 'void' + ? node.expression.argument + : node + ); if (!expressionIdentifier) { return false; } - return ( - helpers.isFireEventUtil(expressionIdentifier) || - helpers.isUserEventUtil(expressionIdentifier) || - helpers.isRenderUtil(expressionIdentifier) - ); + return isSideEffectUtil(expressionIdentifier); }) as TSESTree.ExpressionStatement[]; } @@ -301,12 +328,7 @@ export default createTestingLibraryRule({ return; } - if ( - expressionIdentifier && - !helpers.isFireEventUtil(expressionIdentifier) && - !helpers.isUserEventUtil(expressionIdentifier) && - !helpers.isRenderUtil(expressionIdentifier) - ) { + if (expressionIdentifier && !isSideEffectUtil(expressionIdentifier)) { return; } @@ -325,6 +347,7 @@ export default createTestingLibraryRule({ } return { + VariableDeclarator: trackUserEventSetupVar, 'CallExpression > ArrowFunctionExpression > BlockStatement': reportSideEffects, 'CallExpression > ArrowFunctionExpression > CallExpression': diff --git a/tests/rules/no-wait-for-side-effects.test.ts b/tests/rules/no-wait-for-side-effects.test.ts index 01a1b15b..86d37eb5 100644 --- a/tests/rules/no-wait-for-side-effects.test.ts +++ b/tests/rules/no-wait-for-side-effects.test.ts @@ -341,6 +341,23 @@ ruleTester.run(rule.name, rule, { await waitFor(() => result = render()) `, }, + ...SUPPORTED_TESTING_FRAMEWORKS.flatMap( + (testingFramework) => [ + { + code: ` + import { waitFor } from '${testingFramework}'; + import userEvent from '@testing-library/user-event' + + it("some test", async () => { + const user = createUser(); + await waitFor(() => { + user.click(button); + }); + }); + `, + }, + ] + ), ], invalid: [ // render @@ -1296,6 +1313,75 @@ ruleTester.run(rule.name, rule, { it("some test", async () => { await fireEvent.click(screen.getByTestId("something")); }); + `, + }, + ] + ), + ...SUPPORTED_TESTING_FRAMEWORKS.flatMap>( + (testingFramework) => [ + { + code: ` + import { waitFor } from '${testingFramework}'; + import userEvent from '@testing-library/user-event' + + it("some test", async () => { + const user = userEvent.setup(); + await waitFor(async () => { + await user.click(screen.getByTestId("something")); + }); + }); + `, + errors: [{ line: 8, column: 13, messageId: 'noSideEffectsWaitFor' }], + output: ` + import { waitFor } from '${testingFramework}'; + import userEvent from '@testing-library/user-event' + + it("some test", async () => { + const user = userEvent.setup(); + await user.click(screen.getByTestId("something")); + }); + `, + }, + { + code: ` + import { waitFor } from '${testingFramework}'; + import userEvent from '@testing-library/user-event' + + it("some test", async () => { + const user = userEvent.setup(); + await waitFor(() => user.click(button)); + }); + `, + errors: [{ line: 7, column: 31, messageId: 'noSideEffectsWaitFor' }], + output: ` + import { waitFor } from '${testingFramework}'; + import userEvent from '@testing-library/user-event' + + it("some test", async () => { + const user = userEvent.setup(); + user.click(button); + }); + `, + }, + { + code: ` + import { waitFor } from '${testingFramework}'; + import userEvent from '@testing-library/user-event' + + it("some test", async () => { + await waitFor(() => { + void userEvent.click(button); + }); + }); + `, + errors: [{ line: 7, column: 13, messageId: 'noSideEffectsWaitFor' }], + output: ` + import { waitFor } from '${testingFramework}'; + import userEvent from '@testing-library/user-event' + + it("some test", async () => { + void userEvent.click(button); + }); `, }, ]