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 09688bef..fd5a4973 100644 --- a/lib/create-testing-library-rule/detect-testing-library-utils.ts +++ b/lib/create-testing-library-rule/detect-testing-library-utils.ts @@ -23,7 +23,13 @@ import { ASYNC_UTILS, DEBUG_UTILS, PRESENCE_MATCHERS, + USER_EVENT_MODULE, } from '../utils'; +import { + isCustomTestingLibraryModule, + isOfficialTestingLibraryModule, + isTestingLibraryModule, +} from '../utils/is-testing-library-module'; const SETTING_OPTION_OFF = 'off'; @@ -133,7 +139,6 @@ export interface DetectionHelpers { isNodeComingFromTestingLibrary: IsNodeComingFromTestingLibraryFn; } -const USER_EVENT_PACKAGE = '@testing-library/user-event'; const REACT_DOM_TEST_UTILS_PACKAGE = 'react-dom/test-utils'; const FIRE_EVENT_NAME = 'fireEvent'; const CREATE_EVENT_NAME = 'createEvent'; @@ -960,12 +965,11 @@ export function detectTestingLibraryUtils< } const hasImportElementMatch = hasImportMatch(importNode, identifierName); - const hasImportModuleMatch = - /testing-library/g.test(importDeclarationName) || - (typeof customModuleSetting === 'string' && - importDeclarationName.endsWith(customModuleSetting)); - return hasImportElementMatch && hasImportModuleMatch; + return ( + hasImportElementMatch && + isTestingLibraryModule(importDeclarationName, customModuleSetting) + ); }; const helpers: DetectionHelpers = { @@ -1017,7 +1021,7 @@ export function detectTestingLibraryUtils< } // check only if testing library import not found yet so we avoid // to override importedTestingLibraryNodes after it's found - if (/testing-library/g.test(node.source.value)) { + if (isOfficialTestingLibraryModule(node.source.value)) { importedTestingLibraryNodes.push(node); } @@ -1025,9 +1029,8 @@ export function detectTestingLibraryUtils< // to override importedCustomModuleNode after it's found const customModule = getCustomModule(); if ( - customModule && !importedCustomModuleNode && - node.source.value.endsWith(customModule) + isCustomTestingLibraryModule(node.source.value, customModule) ) { importedCustomModuleNode = node; } @@ -1036,7 +1039,7 @@ export function detectTestingLibraryUtils< // to override importedUserEventLibraryNode after it's found if ( !importedUserEventLibraryNode && - node.source.value === USER_EVENT_PACKAGE + node.source.value === USER_EVENT_MODULE ) { importedUserEventLibraryNode = node; } @@ -1063,7 +1066,7 @@ export function detectTestingLibraryUtils< (arg) => isLiteral(arg) && typeof arg.value === 'string' && - /testing-library/g.test(arg.value) + isOfficialTestingLibraryModule(arg.value) ) ) { importedTestingLibraryNodes.push(callExpression); @@ -1074,10 +1077,9 @@ export function detectTestingLibraryUtils< !importedCustomModuleNode && args.some( (arg) => - customModule && isLiteral(arg) && typeof arg.value === 'string' && - arg.value.endsWith(customModule) + isCustomTestingLibraryModule(arg.value, customModule) ) ) { importedCustomModuleNode = callExpression; @@ -1089,7 +1091,7 @@ export function detectTestingLibraryUtils< (arg) => isLiteral(arg) && typeof arg.value === 'string' && - arg.value === USER_EVENT_PACKAGE + arg.value === USER_EVENT_MODULE ) ) { importedUserEventLibraryNode = callExpression; diff --git a/lib/utils/index.ts b/lib/utils/index.ts index 2c445c8f..faa97c51 100644 --- a/lib/utils/index.ts +++ b/lib/utils/index.ts @@ -33,6 +33,12 @@ const LIBRARY_MODULES = [ const USER_EVENT_MODULE = '@testing-library/user-event'; +const OLD_LIBRARY_MODULES = [ + 'dom-testing-library', + 'vue-testing-library', + 'react-testing-library', +] as const; + const SYNC_QUERIES_VARIANTS = [ 'getBy', 'getAllBy', @@ -154,4 +160,5 @@ export { ABSENCE_MATCHERS, EVENT_HANDLER_METHODS, USER_EVENT_MODULE, + OLD_LIBRARY_MODULES, }; diff --git a/lib/utils/is-testing-library-module.ts b/lib/utils/is-testing-library-module.ts new file mode 100644 index 00000000..548773a5 --- /dev/null +++ b/lib/utils/is-testing-library-module.ts @@ -0,0 +1,22 @@ +import { TestingLibrarySettings } from '../create-testing-library-rule/detect-testing-library-utils'; + +import { LIBRARY_MODULES, OLD_LIBRARY_MODULES, USER_EVENT_MODULE } from '.'; + +export const isOfficialTestingLibraryModule = (importSourceName: string) => + [...OLD_LIBRARY_MODULES, ...LIBRARY_MODULES, USER_EVENT_MODULE].includes( + importSourceName + ); + +export const isCustomTestingLibraryModule = ( + importSourceName: string, + customModuleSetting: TestingLibrarySettings['testing-library/utils-module'] +) => + typeof customModuleSetting === 'string' && + importSourceName.endsWith(customModuleSetting); + +export const isTestingLibraryModule = ( + importSourceName: string, + customModuleSetting?: TestingLibrarySettings['testing-library/utils-module'] +) => + isOfficialTestingLibraryModule(importSourceName) || + isCustomTestingLibraryModule(importSourceName, customModuleSetting); diff --git a/lib/utils/resolve-to-testing-library-fn.ts b/lib/utils/resolve-to-testing-library-fn.ts index 75bc74a8..6d575cc3 100644 --- a/lib/utils/resolve-to-testing-library-fn.ts +++ b/lib/utils/resolve-to-testing-library-fn.ts @@ -24,7 +24,7 @@ import { isSupportedAccessor, } from '../node-utils/accessors'; -import { LIBRARY_MODULES, USER_EVENT_MODULE } from '.'; +import { isTestingLibraryModule } from './is-testing-library-module'; interface ImportDetails { source: string; @@ -171,11 +171,8 @@ export const resolveToTestingLibraryFn = < } const customModuleSetting = context.settings['testing-library/utils-module']; - if ( - [...LIBRARY_MODULES, USER_EVENT_MODULE, customModuleSetting].some( - (module) => module === maybeImport.source - ) - ) { + + if (isTestingLibraryModule(maybeImport.source, customModuleSetting)) { return { original: maybeImport.imported, local: maybeImport.local, diff --git a/tests/lib/rules/no-node-access.test.ts b/tests/lib/rules/no-node-access.test.ts index 8e9ec456..05d5754b 100644 --- a/tests/lib/rules/no-node-access.test.ts +++ b/tests/lib/rules/no-node-access.test.ts @@ -227,6 +227,16 @@ ruleTester.run(RULE_NAME, rule, { const buttonText = screen.getByText('submit'); fe.click(buttonText); + `, + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + // case: custom module set but not imported using ${testingFramework} (aggressive reporting limited) + import { screen, fireEvent } from '../test-utils'; + + const buttonText = screen.getByText('submit'); + fireEvent.click(buttonText); `, }, { diff --git a/tests/lib/utils/is-testing-library-module.test.ts b/tests/lib/utils/is-testing-library-module.test.ts new file mode 100644 index 00000000..eb010388 --- /dev/null +++ b/tests/lib/utils/is-testing-library-module.test.ts @@ -0,0 +1,83 @@ +import { + LIBRARY_MODULES, + OLD_LIBRARY_MODULES, + USER_EVENT_MODULE, +} from '../../../lib/utils'; +import { + isCustomTestingLibraryModule, + isOfficialTestingLibraryModule, + isTestingLibraryModule, +} from '../../../lib/utils/is-testing-library-module'; + +describe('isOfficialTestingLibraryModule', () => { + it.each([...OLD_LIBRARY_MODULES, ...LIBRARY_MODULES, USER_EVENT_MODULE])( + 'returns true when arg is "%s"', + (importSourceName) => { + const result = isOfficialTestingLibraryModule(importSourceName); + + expect(result).toBe(true); + } + ); + + it.each(['custom-modules', 'hoge-testing-library', '@testing-library/hoge'])( + 'returns false when arg is "%s"', + (importSourceName) => { + const result = isOfficialTestingLibraryModule(importSourceName); + + expect(result).toBe(false); + } + ); +}); + +describe('isCustomTestingLibraryModule', () => { + it.each(['test-utils', '../test-utils', '@/test-utils'])( + 'returns true when arg is "%s"', + (importSourceName) => { + const result = isCustomTestingLibraryModule( + importSourceName, + 'test-utils' + ); + + expect(result).toBe(true); + } + ); + + it.each([ + 'custom-modules', + 'react-testing-library', + '@testing-library/react', + 'test-util', + 'test-utils-module', + ])('returns false when arg is "%s"', (importSourceName) => { + const result = isCustomTestingLibraryModule(importSourceName, 'test-utils'); + + expect(result).toBe(false); + }); +}); + +describe('isTestingLibraryModule', () => { + it.each([ + ...OLD_LIBRARY_MODULES, + ...LIBRARY_MODULES, + USER_EVENT_MODULE, + 'test-utils', + '../test-utils', + '@/test-utils', + ])('returns true when arg is "%s"', (importSourceName) => { + const result = isTestingLibraryModule(importSourceName, 'test-utils'); + + expect(result).toBe(true); + }); + + it.each([ + 'custom-modules', + 'hoge-testing-library', + '@testing-library/hoge', + 'test-util', + 'test-utils-module', + ])('returns false when arg is "%s"', (importSourceName) => { + const result = isTestingLibraryModule(importSourceName, 'test-utils'); + + expect(result).toBe(false); + }); +}); diff --git a/tests/lib/utils/resolve-to-testing-library-fn.test.ts b/tests/lib/utils/resolve-to-testing-library-fn.test.ts index 586d5451..c8c026a9 100644 --- a/tests/lib/utils/resolve-to-testing-library-fn.test.ts +++ b/tests/lib/utils/resolve-to-testing-library-fn.test.ts @@ -80,15 +80,6 @@ ruleTester.run('esm', rule, { userEvent.default.setup() `, }, - { - // Verifies that a local './test-utils' import doesn't match the configured 'test-utils' utils module - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` - import { userEvent } from './test-utils'; - - userEvent.setup() - `, - }, ...LIBRARY_MODULES.map((module) => ({ code: ` import * as testingLibrary from '${module}'; @@ -172,6 +163,44 @@ ruleTester.run('esm', rule, { }, ], }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import userEvent from '../test-utils'; + + userEvent.setup() + `, + errors: [ + { + messageId: 'details', + data: { + data: { + original: null, + local: 'userEvent', + }, + }, + }, + ], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import userEvent from '@/test-utils'; + + userEvent.setup() + `, + errors: [ + { + messageId: 'details', + data: { + data: { + original: null, + local: 'userEvent', + }, + }, + }, + ], + }, { settings: { 'testing-library/custom-renders': ['customRender', 'renderWithRedux'],