Skip to content

Commit 8656f4d

Browse files
authored
Merge branch 'main' into fix/no-node-access
2 parents a757b7e + b29277f commit 8656f4d

11 files changed

+358
-29
lines changed

lib/create-testing-library-rule/detect-testing-library-utils.ts

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,13 @@ import {
2323
ASYNC_UTILS,
2424
DEBUG_UTILS,
2525
PRESENCE_MATCHERS,
26+
USER_EVENT_MODULE,
2627
} from '../utils';
28+
import {
29+
isCustomTestingLibraryModule,
30+
isOfficialTestingLibraryModule,
31+
isTestingLibraryModule,
32+
} from '../utils/is-testing-library-module';
2733

2834
const SETTING_OPTION_OFF = 'off';
2935

@@ -133,7 +139,6 @@ export interface DetectionHelpers {
133139
isNodeComingFromTestingLibrary: IsNodeComingFromTestingLibraryFn;
134140
}
135141

136-
const USER_EVENT_PACKAGE = '@testing-library/user-event';
137142
const REACT_DOM_TEST_UTILS_PACKAGE = 'react-dom/test-utils';
138143
const FIRE_EVENT_NAME = 'fireEvent';
139144
const CREATE_EVENT_NAME = 'createEvent';
@@ -960,12 +965,11 @@ export function detectTestingLibraryUtils<
960965
}
961966

962967
const hasImportElementMatch = hasImportMatch(importNode, identifierName);
963-
const hasImportModuleMatch =
964-
/testing-library/g.test(importDeclarationName) ||
965-
(typeof customModuleSetting === 'string' &&
966-
importDeclarationName.endsWith(customModuleSetting));
967968

968-
return hasImportElementMatch && hasImportModuleMatch;
969+
return (
970+
hasImportElementMatch &&
971+
isTestingLibraryModule(importDeclarationName, customModuleSetting)
972+
);
969973
};
970974

971975
const helpers: DetectionHelpers = {
@@ -1017,17 +1021,16 @@ export function detectTestingLibraryUtils<
10171021
}
10181022
// check only if testing library import not found yet so we avoid
10191023
// to override importedTestingLibraryNodes after it's found
1020-
if (/testing-library/g.test(node.source.value)) {
1024+
if (isOfficialTestingLibraryModule(node.source.value)) {
10211025
importedTestingLibraryNodes.push(node);
10221026
}
10231027

10241028
// check only if custom module import not found yet so we avoid
10251029
// to override importedCustomModuleNode after it's found
10261030
const customModule = getCustomModule();
10271031
if (
1028-
customModule &&
10291032
!importedCustomModuleNode &&
1030-
node.source.value.endsWith(customModule)
1033+
isCustomTestingLibraryModule(node.source.value, customModule)
10311034
) {
10321035
importedCustomModuleNode = node;
10331036
}
@@ -1036,7 +1039,7 @@ export function detectTestingLibraryUtils<
10361039
// to override importedUserEventLibraryNode after it's found
10371040
if (
10381041
!importedUserEventLibraryNode &&
1039-
node.source.value === USER_EVENT_PACKAGE
1042+
node.source.value === USER_EVENT_MODULE
10401043
) {
10411044
importedUserEventLibraryNode = node;
10421045
}
@@ -1063,7 +1066,7 @@ export function detectTestingLibraryUtils<
10631066
(arg) =>
10641067
isLiteral(arg) &&
10651068
typeof arg.value === 'string' &&
1066-
/testing-library/g.test(arg.value)
1069+
isOfficialTestingLibraryModule(arg.value)
10671070
)
10681071
) {
10691072
importedTestingLibraryNodes.push(callExpression);
@@ -1074,10 +1077,9 @@ export function detectTestingLibraryUtils<
10741077
!importedCustomModuleNode &&
10751078
args.some(
10761079
(arg) =>
1077-
customModule &&
10781080
isLiteral(arg) &&
10791081
typeof arg.value === 'string' &&
1080-
arg.value.endsWith(customModule)
1082+
isCustomTestingLibraryModule(arg.value, customModule)
10811083
)
10821084
) {
10831085
importedCustomModuleNode = callExpression;
@@ -1089,7 +1091,7 @@ export function detectTestingLibraryUtils<
10891091
(arg) =>
10901092
isLiteral(arg) &&
10911093
typeof arg.value === 'string' &&
1092-
arg.value === USER_EVENT_PACKAGE
1094+
arg.value === USER_EVENT_MODULE
10931095
)
10941096
) {
10951097
importedUserEventLibraryNode = callExpression;

lib/rules/await-async-utils.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
getFunctionName,
88
getInnermostReturningFunction,
99
getVariableReferences,
10+
isCallExpression,
1011
isObjectPattern,
1112
isPromiseHandled,
1213
isProperty,
@@ -110,6 +111,8 @@ export default createTestingLibraryRule<Options, MessageIds>({
110111
const isAssigningKnownAsyncFunctionWrapper =
111112
ASTUtils.isIdentifier(node.id) &&
112113
node.init !== null &&
114+
!isCallExpression(node.init) &&
115+
!ASTUtils.isAwaitExpression(node.init) &&
113116
functionWrappersNames.includes(
114117
getDeepestIdentifierNode(node.init)?.name ?? ''
115118
);

lib/utils/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ const LIBRARY_MODULES = [
3333

3434
const USER_EVENT_MODULE = '@testing-library/user-event';
3535

36+
const OLD_LIBRARY_MODULES = [
37+
'dom-testing-library',
38+
'vue-testing-library',
39+
'react-testing-library',
40+
] as const;
41+
3642
const SYNC_QUERIES_VARIANTS = [
3743
'getBy',
3844
'getAllBy',
@@ -154,4 +160,5 @@ export {
154160
ABSENCE_MATCHERS,
155161
EVENT_HANDLER_METHODS,
156162
USER_EVENT_MODULE,
163+
OLD_LIBRARY_MODULES,
157164
};
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { TestingLibrarySettings } from '../create-testing-library-rule/detect-testing-library-utils';
2+
3+
import { LIBRARY_MODULES, OLD_LIBRARY_MODULES, USER_EVENT_MODULE } from '.';
4+
5+
export const isOfficialTestingLibraryModule = (importSourceName: string) =>
6+
[...OLD_LIBRARY_MODULES, ...LIBRARY_MODULES, USER_EVENT_MODULE].includes(
7+
importSourceName
8+
);
9+
10+
export const isCustomTestingLibraryModule = (
11+
importSourceName: string,
12+
customModuleSetting: TestingLibrarySettings['testing-library/utils-module']
13+
) =>
14+
typeof customModuleSetting === 'string' &&
15+
importSourceName.endsWith(customModuleSetting);
16+
17+
export const isTestingLibraryModule = (
18+
importSourceName: string,
19+
customModuleSetting?: TestingLibrarySettings['testing-library/utils-module']
20+
) =>
21+
isOfficialTestingLibraryModule(importSourceName) ||
22+
isCustomTestingLibraryModule(importSourceName, customModuleSetting);

lib/utils/resolve-to-testing-library-fn.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
isSupportedAccessor,
2525
} from '../node-utils/accessors';
2626

27-
import { LIBRARY_MODULES, USER_EVENT_MODULE } from '.';
27+
import { isTestingLibraryModule } from './is-testing-library-module';
2828

2929
interface ImportDetails {
3030
source: string;
@@ -171,11 +171,8 @@ export const resolveToTestingLibraryFn = <
171171
}
172172

173173
const customModuleSetting = context.settings['testing-library/utils-module'];
174-
if (
175-
[...LIBRARY_MODULES, USER_EVENT_MODULE, customModuleSetting].some(
176-
(module) => module === maybeImport.source
177-
)
178-
) {
174+
175+
if (isTestingLibraryModule(maybeImport.source, customModuleSetting)) {
179176
return {
180177
original: maybeImport.imported,
181178
local: maybeImport.local,

tests/lib/rules/await-async-events.test.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,21 @@ ruleTester.run(RULE_NAME, rule, {
134134
135135
await triggerEvent()
136136
})
137+
`,
138+
options: [{ eventModule: 'fireEvent' }] as const,
139+
})),
140+
...FIRE_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({
141+
code: `
142+
import { fireEvent } from '${testingFramework}'
143+
test('await promise assigned to a variable from function wrapping event method is valid', () => {
144+
function triggerEvent() {
145+
doSomething()
146+
return fireEvent.${eventMethod}(getByLabelText('username'))
147+
}
148+
149+
const result = await triggerEvent()
150+
expect(result).toBe(undefined)
151+
})
137152
`,
138153
options: [{ eventModule: 'fireEvent' }] as const,
139154
})),
@@ -364,6 +379,21 @@ ruleTester.run(RULE_NAME, rule, {
364379
...USER_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({
365380
code: `
366381
import userEvent from '${testingFramework}'
382+
test('await promise assigned to a variable from function wrapping event method is valid', () => {
383+
function triggerEvent() {
384+
doSomething()
385+
return userEvent.${eventMethod}(getByLabelText('username'))
386+
}
387+
388+
const result = await triggerEvent()
389+
expect(result).toBe(undefined)
390+
})
391+
`,
392+
options: [{ eventModule: 'userEvent' }] as const,
393+
})),
394+
...USER_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({
395+
code: `
396+
import userEvent from '${testingFramework}'
367397
test('await expression that evaluates to promise is valid', async () => {
368398
await (null, userEvent.${eventMethod}(getByLabelText('username')));
369399
await (condition ? null : userEvent.${eventMethod}(getByLabelText('username')));
@@ -775,6 +805,44 @@ ruleTester.run(RULE_NAME, rule, {
775805
({
776806
code: `
777807
import { fireEvent } from '${testingFramework}'
808+
test('unhandled promise assigned to a variable returned from function wrapping event method is invalid', () => {
809+
function triggerEvent() {
810+
doSomething()
811+
return fireEvent.${eventMethod}(getByLabelText('username'))
812+
}
813+
814+
const result = triggerEvent()
815+
expect(result).toBe(undefined)
816+
})
817+
`,
818+
errors: [
819+
{
820+
line: 9,
821+
column: 24,
822+
messageId: 'awaitAsyncEventWrapper',
823+
data: { name: 'triggerEvent' },
824+
},
825+
],
826+
options: [{ eventModule: 'fireEvent' }],
827+
output: `
828+
import { fireEvent } from '${testingFramework}'
829+
test('unhandled promise assigned to a variable returned from function wrapping event method is invalid', async () => {
830+
function triggerEvent() {
831+
doSomething()
832+
return fireEvent.${eventMethod}(getByLabelText('username'))
833+
}
834+
835+
const result = await triggerEvent()
836+
expect(result).toBe(undefined)
837+
})
838+
`,
839+
}) as const
840+
),
841+
...FIRE_EVENT_ASYNC_FUNCTIONS.map(
842+
(eventMethod) =>
843+
({
844+
code: `
845+
import { fireEvent } from '${testingFramework}'
778846
779847
function triggerEvent() {
780848
doSomething()
@@ -977,6 +1045,44 @@ ruleTester.run(RULE_NAME, rule, {
9771045
({
9781046
code: `
9791047
import userEvent from '${testingFramework}'
1048+
test('unhandled promise assigned to a variable returned from function wrapping event method is invalid', function() {
1049+
function triggerEvent() {
1050+
doSomething()
1051+
return userEvent.${eventMethod}(getByLabelText('username'))
1052+
}
1053+
1054+
const result = triggerEvent()
1055+
expect(result).toBe(undefined)
1056+
})
1057+
`,
1058+
errors: [
1059+
{
1060+
line: 9,
1061+
column: 24,
1062+
messageId: 'awaitAsyncEventWrapper',
1063+
data: { name: 'triggerEvent' },
1064+
},
1065+
],
1066+
options: [{ eventModule: 'userEvent' }],
1067+
output: `
1068+
import userEvent from '${testingFramework}'
1069+
test('unhandled promise assigned to a variable returned from function wrapping event method is invalid', async function() {
1070+
function triggerEvent() {
1071+
doSomething()
1072+
return userEvent.${eventMethod}(getByLabelText('username'))
1073+
}
1074+
1075+
const result = await triggerEvent()
1076+
expect(result).toBe(undefined)
1077+
})
1078+
`,
1079+
}) as const
1080+
),
1081+
...USER_EVENT_ASYNC_FUNCTIONS.map(
1082+
(eventMethod) =>
1083+
({
1084+
code: `
1085+
import userEvent from '${testingFramework}'
9801086
9811087
function triggerEvent() {
9821088
doSomething()

tests/lib/rules/await-async-queries.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,23 @@ ruleTester.run(RULE_NAME, rule, {
291291
`,
292292
})),
293293

294+
// handled promise assigned to variable returned from async query wrapper is valid
295+
...ALL_ASYNC_COMBINATIONS_TO_TEST.map(
296+
(query) =>
297+
({
298+
code: `
299+
const queryWrapper = () => {
300+
return screen.${query}('foo')
301+
}
302+
303+
test("A valid example test", async () => {
304+
const element = await queryWrapper()
305+
expect(element).toBeVisible()
306+
})
307+
`,
308+
}) as const
309+
),
310+
294311
// non-matching query is valid
295312
`
296313
test('A valid example test', async () => {

tests/lib/rules/await-async-utils.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,20 @@ ruleTester.run(RULE_NAME, rule, {
250250
test('handled promise from function wrapping ${asyncUtil} util is valid', async () => {
251251
await waitForSomethingAsync()
252252
});
253+
`,
254+
})),
255+
...ASYNC_UTILS.map((asyncUtil) => ({
256+
code: `
257+
import { ${asyncUtil} } from '${testingFramework}';
258+
259+
function waitForSomethingAsync() {
260+
return ${asyncUtil}(() => somethingAsync())
261+
}
262+
263+
test('handled promise in variable declaration from function wrapping ${asyncUtil} util is valid', async () => {
264+
const result = await waitForSomethingAsync()
265+
expect(result).toBe('foo')
266+
});
253267
`,
254268
})),
255269
{
@@ -506,6 +520,32 @@ ruleTester.run(RULE_NAME, rule, {
506520
(asyncUtil) =>
507521
({
508522
code: `
523+
import { ${asyncUtil}, render } from '${testingFramework}';
524+
525+
function waitForSomethingAsync() {
526+
return ${asyncUtil}(() => somethingAsync())
527+
}
528+
529+
test('unhandled promise in variable declaration from function wrapping ${asyncUtil} util is invalid', async () => {
530+
render()
531+
const result = waitForSomethingAsync()
532+
expect(result).toBe('foo')
533+
});
534+
`,
535+
errors: [
536+
{
537+
messageId: 'asyncUtilWrapper',
538+
line: 10,
539+
column: 24,
540+
data: { name: 'waitForSomethingAsync' },
541+
},
542+
],
543+
}) as const
544+
),
545+
...ASYNC_UTILS.map(
546+
(asyncUtil) =>
547+
({
548+
code: `
509549
import { ${asyncUtil} } from 'some-other-library'; // rather than ${testingFramework}
510550
test(
511551
'aggressive reporting - util "${asyncUtil}" which is not related to testing library is invalid',

0 commit comments

Comments
 (0)