From a2c1bc8bad6de4b47fd365459126f3e2be40a2b6 Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Sat, 11 Oct 2025 10:50:00 +0900 Subject: [PATCH 1/3] fix: support userEvent setup instances --- lib/rules/no-unnecessary-act.ts | 69 ++++++++++++++++++++++++++------- 1 file changed, 54 insertions(+), 15 deletions(-) diff --git a/lib/rules/no-unnecessary-act.ts b/lib/rules/no-unnecessary-act.ts index a96a2f41..c26aa659 100644 --- a/lib/rules/no-unnecessary-act.ts +++ b/lib/rules/no-unnecessary-act.ts @@ -7,8 +7,10 @@ import { getStatementCallExpression, isEmptyFunction, isExpressionStatement, + isMemberExpression, isReturnStatement, } from '../node-utils'; +import { resolveToTestingLibraryFn } from '../utils'; import type { TSESTree } from '@typescript-eslint/utils'; @@ -58,6 +60,8 @@ export default createTestingLibraryRule({ ], create(context, [{ isStrict = true }], helpers) { + const userEventInstanceNames = new Set(); + function getStatementIdentifier(statement: TSESTree.Statement) { const callExpression = getStatementCallExpression(statement); @@ -87,11 +91,18 @@ export default createTestingLibraryRule({ return null; } - /** - * Determines whether some call is non Testing Library related for a given list of statements. - */ - function hasSomeNonTestingLibraryCall( - statements: TSESTree.Statement[] + function hasUserEventInstanceName(identifier: TSESTree.Identifier) { + if (!isMemberExpression(identifier.parent)) { + return false; + } + + const propertyIdentifier = getPropertyIdentifierNode(identifier.parent); + return userEventInstanceNames.has(propertyIdentifier?.name ?? ''); + } + + function hasStatementReference( + statements: TSESTree.Statement[], + predicate: (identifier: TSESTree.Identifier) => boolean ): boolean { return statements.some((statement) => { const identifier = getStatementIdentifier(statement); @@ -100,20 +111,31 @@ export default createTestingLibraryRule({ return false; } - return !helpers.isTestingLibraryUtil(identifier); + return predicate(identifier); }); } - function hasTestingLibraryCall(statements: TSESTree.Statement[]) { - return statements.some((statement) => { - const identifier = getStatementIdentifier(statement); - - if (!identifier) { - return false; - } + /** + * Determines whether some call is non Testing Library related for a given list of statements. + */ + function hasSomeNonTestingLibraryCall( + statements: TSESTree.Statement[] + ): boolean { + return hasStatementReference( + statements, + (identifier) => + !helpers.isTestingLibraryUtil(identifier) && + !hasUserEventInstanceName(identifier) + ); + } - return helpers.isTestingLibraryUtil(identifier); - }); + function hasTestingLibraryCall(statements: TSESTree.Statement[]) { + return hasStatementReference( + statements, + (identifier) => + helpers.isTestingLibraryUtil(identifier) || + hasUserEventInstanceName(identifier) + ); } function checkNoUnnecessaryActFromBlockStatement( @@ -196,7 +218,24 @@ export default createTestingLibraryRule({ }); } + function registerUserEventInstance( + node: TSESTree.CallExpression & { parent: TSESTree.VariableDeclarator } + ) { + const propertyIdentifier = getPropertyIdentifierNode(node); + const deepestIdentifier = getDeepestIdentifierNode(node); + const testingLibraryFn = resolveToTestingLibraryFn(node, context); + + if ( + propertyIdentifier?.name === testingLibraryFn?.local && + deepestIdentifier?.name === 'setup' && + ASTUtils.isIdentifier(node.parent?.id) + ) { + userEventInstanceNames.add(node.parent.id.name); + } + } + return { + 'VariableDeclarator > CallExpression': registerUserEventInstance, 'CallExpression > ArrowFunctionExpression > BlockStatement': checkNoUnnecessaryActFromBlockStatement, 'CallExpression > FunctionExpression > BlockStatement': From 5a2516f0258a74d1a82deddebd68ca5c3df8d04f Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Sat, 11 Oct 2025 10:50:39 +0900 Subject: [PATCH 2/3] test: add tests --- tests/lib/rules/no-unnecessary-act.test.ts | 71 ++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/tests/lib/rules/no-unnecessary-act.test.ts b/tests/lib/rules/no-unnecessary-act.test.ts index 727f0718..ba813564 100644 --- a/tests/lib/rules/no-unnecessary-act.test.ts +++ b/tests/lib/rules/no-unnecessary-act.test.ts @@ -63,6 +63,21 @@ const validNonStrictTestCases: RuleValidTestCase[] = [ }); `, }, + { + code: ` + import { act } from '@testing-library/react' + import userEvent from '@testing-library/user-event' + + test('valid case', async () => { + const user = userEvent.setup(); + + await act(async () => { + await user.click(element); + stuffThatDoesNotUseRTL() + }); + }) + `, + }, ]; const validTestCases: RuleValidTestCase[] = [ @@ -245,6 +260,28 @@ const invalidStrictTestCases: RuleInvalidTestCase[] = }, ], }, + { + code: ` + import { act } from '${testingFramework}' + import userEvent from '@testing-library/user-event' + + test('invalid case', async () => { + const user = userEvent.setup(); + + await act(async () => { + await user.click(element); + stuffThatDoesNotUseRTL() + }); + }) + `, + errors: [ + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 8, + column: 15, + }, + ], + }, ]); const invalidTestCases: RuleInvalidTestCase[] = [ @@ -566,6 +603,40 @@ const invalidTestCases: RuleInvalidTestCase[] = [ { messageId: 'noUnnecessaryActEmptyFunction', line: 8, column: 9 }, ], }, + { + code: ` + import { act } from '@testing-library/react' + import userEvent from '@testing-library/user-event' + + test('invalid case', async () => { + const user = userEvent.setup(); + + await act(async () => { + await user.click(element); + }); + }) + `, + errors: [ + { messageId: 'noUnnecessaryActTestingLibraryUtil', line: 8, column: 15 }, + ], + }, + { + code: ` + import { act } from '@testing-library/react' + import userEvent from '@testing-library/user-event' + + test('invalid case', async () => { + const user = userEvent.setup(); + + act(async () => { + await user.click(element); + }); + }) + `, + errors: [ + { messageId: 'noUnnecessaryActTestingLibraryUtil', line: 8, column: 9 }, + ], + }, { settings: { 'testing-library/utils-module': 'test-utils', From 3395f27405d3d0c65655fba19984989a948b36be Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Mon, 13 Oct 2025 18:13:52 +0900 Subject: [PATCH 3/3] chore: add comment --- tests/lib/rules/no-unnecessary-act.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lib/rules/no-unnecessary-act.test.ts b/tests/lib/rules/no-unnecessary-act.test.ts index ba813564..352bddd6 100644 --- a/tests/lib/rules/no-unnecessary-act.test.ts +++ b/tests/lib/rules/no-unnecessary-act.test.ts @@ -64,7 +64,7 @@ const validNonStrictTestCases: RuleValidTestCase[] = [ `, }, { - code: ` + code: `// case: RTL act wrapping both userEvent and non-RTL calls import { act } from '@testing-library/react' import userEvent from '@testing-library/user-event'