diff --git a/src/create-testing-library-rule/detect-testing-library-utils.ts b/src/create-testing-library-rule/detect-testing-library-utils.ts index d0603637..f3cec665 100644 --- a/src/create-testing-library-rule/detect-testing-library-utils.ts +++ b/src/create-testing-library-rule/detect-testing-library-utils.ts @@ -480,7 +480,9 @@ export function detectTestingLibraryUtils< let userEventName: string | undefined; if (userEvent) { - userEventName = userEvent.name; + userEventName = ASTUtils.isIdentifier(userEvent) + ? userEvent.name + : userEvent.local.name; } else if (isAggressiveModuleReportingEnabled()) { userEventName = USER_EVENT_NAME; } @@ -574,7 +576,9 @@ export function detectTestingLibraryUtils< let userEventName: string | undefined; if (userEvent) { - userEventName = userEvent.name; + userEventName = ASTUtils.isIdentifier(userEvent) + ? userEvent.name + : userEvent.local.name; } else if (isAggressiveModuleReportingEnabled()) { userEventName = USER_EVENT_NAME; } @@ -876,38 +880,44 @@ export function detectTestingLibraryUtils< return findImportSpecifier(specifierName, node); }; - const findImportedUserEventSpecifier: () => TSESTree.Identifier | null = - () => { - if (!importedUserEventLibraryNode) { - return null; + const findImportedUserEventSpecifier: () => + | TSESTree.Identifier + | TSESTree.ImportClause + | null = () => { + if (!importedUserEventLibraryNode) { + const customModuleNode = getCustomModuleImportNode(); + if (customModuleNode) { + return findImportSpecifier(USER_EVENT_NAME, customModuleNode) ?? null; } + return null; + } - if (isImportDeclaration(importedUserEventLibraryNode)) { - const userEventIdentifier = - importedUserEventLibraryNode.specifiers.find((specifier) => - isImportDefaultSpecifier(specifier) - ); - - if (userEventIdentifier) { - return userEventIdentifier.local; - } - } else { - if ( - !ASTUtils.isVariableDeclarator(importedUserEventLibraryNode.parent) - ) { - return null; - } + if (isImportDeclaration(importedUserEventLibraryNode)) { + const userEventIdentifier = + importedUserEventLibraryNode.specifiers.find((specifier) => + isImportDefaultSpecifier(specifier) + ); - const requireNode = importedUserEventLibraryNode.parent; - if (!ASTUtils.isIdentifier(requireNode.id)) { - return null; - } + if (userEventIdentifier) { + return userEventIdentifier.local; + } + } else { + if ( + !ASTUtils.isVariableDeclarator(importedUserEventLibraryNode.parent) + ) { + return null; + } - return requireNode.id; + const requireNode = importedUserEventLibraryNode.parent; + if (!ASTUtils.isIdentifier(requireNode.id)) { + return null; } - return null; - }; + return requireNode.id; + } + + return null; + }; const getTestingLibraryImportedUtilSpecifier = ( node: TSESTree.Identifier | TSESTree.MemberExpression diff --git a/tests/create-testing-library-rule.test.ts b/tests/create-testing-library-rule.test.ts index 1b237e20..daea7c20 100644 --- a/tests/create-testing-library-rule.test.ts +++ b/tests/create-testing-library-rule.test.ts @@ -108,6 +108,13 @@ ruleTester.run(rule.name, rule, { code: ` import * as incorrect from '@testing-library/user-event' userEvent.click() + `, + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { userEvent } from 'somewhere-else' + userEvent.click(element) `, }, @@ -615,6 +622,22 @@ ruleTester.run(rule.name, rule, { code: ` const renamed = require('@testing-library/user-event') renamed.click(element) + `, + errors: [{ line: 3, column: 15, messageId: 'userEventError' }], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { userEvent } from 'test-utils' + userEvent.click(element) + `, + errors: [{ line: 3, column: 17, messageId: 'userEventError' }], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { userEvent as renamed } from 'test-utils' + renamed.click(element) `, errors: [{ line: 3, column: 15, messageId: 'userEventError' }], }, diff --git a/tests/rules/await-async-events.test.ts b/tests/rules/await-async-events.test.ts index a4f0db95..9d52266a 100644 --- a/tests/rules/await-async-events.test.ts +++ b/tests/rules/await-async-events.test.ts @@ -887,6 +887,96 @@ ruleTester.run(rule.name, rule, { }) as const ), ]), + ...USER_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` + import { userEvent } from 'test-utils' + test('unhandled promise from userEvent imported from custom module is invalid', () => { + userEvent.${eventMethod}(getByLabelText('username')) + }) + `, + errors: [ + { + line: 4, + column: 9, + endColumn: 19 + eventMethod.length, + messageId: 'awaitAsyncEvent', + data: { name: eventMethod }, + }, + ], + options: [{ eventModule: 'userEvent' }], + output: ` + import { userEvent } from 'test-utils' + test('unhandled promise from userEvent imported from custom module is invalid', async () => { + await userEvent.${eventMethod}(getByLabelText('username')) + }) + `, + }) as const + ), + ...USER_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` + const { userEvent } = require('test-utils') + test('unhandled promise from userEvent required from custom module is invalid', () => { + userEvent.${eventMethod}(getByLabelText('username')) + }) + `, + errors: [ + { + line: 4, + column: 9, + endColumn: 19 + eventMethod.length, + messageId: 'awaitAsyncEvent', + data: { name: eventMethod }, + }, + ], + options: [{ eventModule: 'userEvent' }], + output: ` + const { userEvent } = require('test-utils') + test('unhandled promise from userEvent required from custom module is invalid', async () => { + await userEvent.${eventMethod}(getByLabelText('username')) + }) + `, + }) as const + ), + ...USER_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` + import { userEvent as ue } from 'test-utils' + test('unhandled promise from aliased userEvent imported from custom module is invalid', () => { + ue.${eventMethod}(getByLabelText('username')) + }) + `, + errors: [ + { + line: 4, + column: 9, + endColumn: 12 + eventMethod.length, + messageId: 'awaitAsyncEvent', + data: { name: eventMethod }, + }, + ], + options: [{ eventModule: 'userEvent' }], + output: ` + import { userEvent as ue } from 'test-utils' + test('unhandled promise from aliased userEvent imported from custom module is invalid', async () => { + await ue.${eventMethod}(getByLabelText('username')) + }) + `, + }) as const + ), ...USER_EVENT_ASYNC_FRAMEWORKS.flatMap((testingFramework) => [ ...USER_EVENT_ASYNC_FUNCTIONS.map( (eventMethod) => diff --git a/tests/rules/no-await-sync-events.test.ts b/tests/rules/no-await-sync-events.test.ts index 2af5fc4b..ba9ec67a 100644 --- a/tests/rules/no-await-sync-events.test.ts +++ b/tests/rules/no-await-sync-events.test.ts @@ -242,6 +242,18 @@ ruleTester.run(rule.name, rule, { }); `, })), + // userEvent from non-custom module should not be reported when custom module is set + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { userEvent } from 'somewhere-else'; + + test('should not report userEvent from non-custom module', async() => { + await userEvent.type('foo', 'bar', { delay: 0 }); + }); + `, + options: [{ eventModules: ['user-event'] }], + }, ], invalid: [ @@ -387,6 +399,63 @@ ruleTester.run(rule.name, rule, { }, ], }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { userEvent } from 'test-utils'; + + test('should report userEvent from custom module', async() => { + await userEvent.type('foo', 'bar', { delay: 0 }); + }); + `, + options: [{ eventModules: ['user-event'] }], + errors: [ + { + line: 5, + column: 17, + messageId: 'noAwaitSyncEvents', + data: { name: 'userEvent.type' }, + }, + ], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + const { userEvent } = require('test-utils'); + + test('should report userEvent required from custom module', async() => { + await userEvent.type('foo', 'bar', { delay: 0 }); + }); + `, + options: [{ eventModules: ['user-event'] }], + errors: [ + { + line: 5, + column: 17, + messageId: 'noAwaitSyncEvents', + data: { name: 'userEvent.type' }, + }, + ], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { userEvent as ue } from 'test-utils'; + + test('should report aliased userEvent from custom module', async() => { + await ue.type('foo', 'bar', { delay: 0 }); + }); + `, + options: [{ eventModules: ['user-event'] }], + errors: [ + { + line: 5, + column: 17, + messageId: 'noAwaitSyncEvents', + data: { name: 'ue.type' }, + }, + ], + }, { code: `async() => { const delay = 0 diff --git a/tests/rules/no-unnecessary-act.test.ts b/tests/rules/no-unnecessary-act.test.ts index 4859581f..20a0c7f8 100644 --- a/tests/rules/no-unnecessary-act.test.ts +++ b/tests/rules/no-unnecessary-act.test.ts @@ -73,6 +73,21 @@ const validNonStrictTestCases: RuleValidTestCase[] = [ stuffThatDoesNotUseRTL() }); }) + `, + }, + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: `// case: act wrapping both userEvent from custom module and non-RTL calls + import { act, userEvent } from 'test-utils' + + test('valid case', async () => { + await act(async () => { + await userEvent.click(element); + stuffThatDoesNotUseRTL() + }); + }) `, }, { @@ -87,6 +102,21 @@ const validNonStrictTestCases: RuleValidTestCase[] = [ }); `, }, + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: `// case: act wrapping userEvent from non-custom module should not be reported + import { act } from 'test-utils' + import { userEvent } from 'somewhere-else' + + test('valid case', async () => { + act(() => { + userEvent.click(el) + }); + }); + `, + }, ]; const validTestCases: RuleValidTestCase[] = [ @@ -477,6 +507,87 @@ const invalidTestCases: RuleInvalidTestCase[] = [ }, ], }, + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: `// case: act wrapping userEvent imported from custom module - block statement + import { act, userEvent } from 'test-utils' + + test('invalid case', async () => { + act(() => { + userEvent.click(el) + }); + + await act(async () => { + userEvent.type(el, 'hi') + }); + }); + `, + errors: [ + { messageId: 'noUnnecessaryActTestingLibraryUtil', line: 5, column: 9 }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 9, + column: 15, + }, + ], + }, + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: `// case: act wrapping userEvent imported from custom module - implicit return + import { act, userEvent } from 'test-utils' + + test('invalid case', async () => { + act(() => userEvent.click(el)) + await act(async () => userEvent.type(el, 'hi')) + }); + `, + errors: [ + { messageId: 'noUnnecessaryActTestingLibraryUtil', line: 5, column: 9 }, + { + messageId: 'noUnnecessaryActTestingLibraryUtil', + line: 6, + column: 15, + }, + ], + }, + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: `// case: act wrapping userEvent required from custom module + const { act, userEvent } = require('test-utils') + + test('invalid case', async () => { + act(() => { + userEvent.click(el) + }); + }); + `, + errors: [ + { messageId: 'noUnnecessaryActTestingLibraryUtil', line: 5, column: 9 }, + ], + }, + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: `// case: act wrapping aliased userEvent from custom module + import { act, userEvent as ue } from 'test-utils' + + test('invalid case', async () => { + act(() => { + ue.click(el) + }); + }); + `, + errors: [ + { messageId: 'noUnnecessaryActTestingLibraryUtil', line: 5, column: 9 }, + ], + }, { code: `// case: RTL act wrapping RTL calls - callbacks with return import { act, fireEvent, screen, render, waitFor } from '@testing-library/react' diff --git a/tests/rules/no-wait-for-side-effects.test.ts b/tests/rules/no-wait-for-side-effects.test.ts index ee40c6e4..01a1b15b 100644 --- a/tests/rules/no-wait-for-side-effects.test.ts +++ b/tests/rules/no-wait-for-side-effects.test.ts @@ -222,13 +222,6 @@ ruleTester.run(rule.name, rule, { `, }) ), - { - settings: { 'testing-library/utils-module': '~/test-utils' }, - code: ` - import { waitFor, userEvent } from '~/test-utils'; - await waitFor(() => userEvent.click(button)) - `, - }, { settings: { 'testing-library/utils-module': 'test-utils' }, code: ` @@ -924,6 +917,60 @@ ruleTester.run(rule.name, rule, { import { waitFor } from '~/test-utils'; import userEvent from '@testing-library/user-event' userEvent.click(); + `, + }, + { + settings: { 'testing-library/utils-module': '~/test-utils' }, + code: ` + import { waitFor, userEvent } from '~/test-utils'; + await waitFor(() => userEvent.click(button)) + `, + errors: [{ line: 3, column: 29, messageId: 'noSideEffectsWaitFor' }], + output: ` + import { waitFor, userEvent } from '~/test-utils'; + userEvent.click(button) + `, + }, + { + settings: { 'testing-library/utils-module': '~/test-utils' }, + code: ` + import { userEvent, waitFor } from '~/test-utils'; + async function test() { + await waitFor(() => { + userEvent.click(element); + }); + } + `, + errors: [{ line: 5, column: 13, messageId: 'noSideEffectsWaitFor' }], + output: ` + import { userEvent, waitFor } from '~/test-utils'; + async function test() { + userEvent.click(element); + } + `, + }, + { + settings: { 'testing-library/utils-module': '~/test-utils' }, + code: ` + const { waitFor, userEvent } = require('~/test-utils'); + await waitFor(() => userEvent.click(button)) + `, + errors: [{ line: 3, column: 29, messageId: 'noSideEffectsWaitFor' }], + output: ` + const { waitFor, userEvent } = require('~/test-utils'); + userEvent.click(button) + `, + }, + { + settings: { 'testing-library/utils-module': '~/test-utils' }, + code: ` + import { waitFor, userEvent as ue } from '~/test-utils'; + await waitFor(() => ue.click(button)) + `, + errors: [{ line: 3, column: 29, messageId: 'noSideEffectsWaitFor' }], + output: ` + import { waitFor, userEvent as ue } from '~/test-utils'; + ue.click(button) `, }, ...SUPPORTED_TESTING_FRAMEWORKS.flatMap(