Skip to content

Commit cf02697

Browse files
authored
feat(prefer-find-by): handle waitFor wrapping findBy queries (#1013)
Closes #910
1 parent e4c0daa commit cf02697

File tree

3 files changed

+129
-7
lines changed

3 files changed

+129
-7
lines changed

docs/rules/prefer-find-by.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ const submitButton = await waitFor(() =>
4141
const submitButton = await waitFor(() =>
4242
expect(queryByLabel('button', { name: /submit/i })).not.toBeFalsy()
4343
);
44+
45+
// unnecessary usage of waitFor with findBy*, which already includes waiting logic
46+
await waitFor(async () => {
47+
const button = await findByRole('button', { name: 'Submit' });
48+
expect(button).toBeInTheDocument();
49+
});
4450
```
4551

4652
Examples of **correct** code for this rule:

lib/rules/prefer-find-by.ts

Lines changed: 72 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@ import { TSESTree, ASTUtils, TSESLint } from '@typescript-eslint/utils';
22

33
import { createTestingLibraryRule } from '../create-testing-library-rule';
44
import {
5+
getDeepestIdentifierNode,
56
isArrowFunctionExpression,
7+
isBlockStatement,
68
isCallExpression,
79
isMemberExpression,
810
isObjectExpression,
911
isObjectPattern,
1012
isProperty,
13+
isVariableDeclaration,
1114
} from '../node-utils';
1215
import { getScope, getSourceCode } from '../utils';
1316

@@ -329,20 +332,82 @@ export default createTestingLibraryRule<Options, MessageIds>({
329332
}
330333

331334
return {
332-
'AwaitExpression > CallExpression'(node: TSESTree.CallExpression) {
335+
'AwaitExpression > CallExpression'(
336+
node: TSESTree.CallExpression & { parent: TSESTree.AwaitExpression }
337+
) {
333338
if (
334339
!ASTUtils.isIdentifier(node.callee) ||
335340
!helpers.isAsyncUtil(node.callee, ['waitFor'])
336341
) {
337342
return;
338343
}
339-
// ensure the only argument is an arrow function expression - if the arrow function is a block
340-
// we skip it
344+
// ensure the only argument is an arrow function expression
341345
const argument = node.arguments[0];
342-
if (
343-
!isArrowFunctionExpression(argument) ||
344-
!isCallExpression(argument.body)
345-
) {
346+
347+
if (!isArrowFunctionExpression(argument)) {
348+
return;
349+
}
350+
351+
if (isBlockStatement(argument.body) && argument.async) {
352+
const { body } = argument.body;
353+
const declarations = body
354+
.filter(isVariableDeclaration)
355+
?.flatMap((declaration) => declaration.declarations);
356+
357+
const findByDeclarator = declarations.find((declaration) => {
358+
if (
359+
!ASTUtils.isAwaitExpression(declaration.init) ||
360+
!isCallExpression(declaration.init.argument)
361+
) {
362+
return false;
363+
}
364+
365+
const { callee } = declaration.init.argument;
366+
const node = getDeepestIdentifierNode(callee);
367+
return node ? helpers.isFindQueryVariant(node) : false;
368+
});
369+
370+
const init = ASTUtils.isAwaitExpression(findByDeclarator?.init)
371+
? findByDeclarator.init.argument
372+
: null;
373+
374+
if (!isCallExpression(init)) {
375+
return;
376+
}
377+
const queryIdentifier = getDeepestIdentifierNode(init.callee);
378+
379+
// ensure the query is a supported async query like findBy*
380+
if (!queryIdentifier || !helpers.isAsyncQuery(queryIdentifier)) {
381+
return;
382+
}
383+
384+
const fullQueryMethod = queryIdentifier.name;
385+
const queryMethod = fullQueryMethod.split('By')[1];
386+
const queryVariant = getFindByQueryVariant(fullQueryMethod);
387+
388+
reportInvalidUsage(node, {
389+
queryMethod,
390+
queryVariant,
391+
prevQuery: fullQueryMethod,
392+
fix(fixer) {
393+
const { parent: expressionStatement } = node.parent;
394+
const bodyText = sourceCode
395+
.getText(argument.body)
396+
.slice(1, -1)
397+
.trim();
398+
const { line, column } = expressionStatement.loc.start;
399+
const indent = sourceCode.getLines()[line - 1].slice(0, column);
400+
const newText = bodyText
401+
.split('\n')
402+
.map((line) => line.trim())
403+
.join(`\n${indent}`);
404+
return fixer.replaceText(expressionStatement, newText);
405+
},
406+
});
407+
return;
408+
}
409+
410+
if (!isCallExpression(argument.body)) {
346411
return;
347412
}
348413

tests/lib/rules/prefer-find-by.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,17 @@ ruleTester.run(RULE_NAME, rule, {
5151
it('tests', async () => {
5252
const submitButton = await screen.${queryMethod}('foo')
5353
})
54+
`,
55+
})),
56+
...ASYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({
57+
code: `
58+
import {waitFor} from '${testingFramework}';
59+
it('tests', async () => {
60+
await waitFor(async () => {
61+
const button = screen.${queryMethod}("button", { name: "Submit" })
62+
expect(button).toBeInTheDocument()
63+
})
64+
})
5465
`,
5566
})),
5667
...SYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({
@@ -164,6 +175,17 @@ ruleTester.run(RULE_NAME, rule, {
164175
const { container } = render()
165176
await waitFor(() => expect(container.querySelector('baz')).toBeInTheDocument());
166177
})
178+
`,
179+
},
180+
{
181+
code: `
182+
import {waitFor} from '${testingFramework}';
183+
it('tests', async () => {
184+
await waitFor(async () => {
185+
const button = await foo("button", { name: "Submit" })
186+
expect(button).toBeInTheDocument()
187+
})
188+
})
167189
`,
168190
},
169191
]),
@@ -689,6 +711,35 @@ ruleTester.run(RULE_NAME, rule, {
689711
const button = await screen.${buildFindByMethod(
690712
queryMethod
691713
)}('Count is: 0', { timeout: 100, interval: 200 })
714+
`,
715+
})),
716+
...ASYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({
717+
code: `
718+
import {waitFor} from '${testingFramework}';
719+
it('tests', async () => {
720+
await waitFor(async () => {
721+
const button = await screen.${queryMethod}("button", { name: "Submit" })
722+
expect(button).toBeInTheDocument()
723+
})
724+
})
725+
`,
726+
errors: [
727+
{
728+
messageId: 'preferFindBy',
729+
data: {
730+
queryVariant: getFindByQueryVariant(queryMethod),
731+
queryMethod: queryMethod.split('By')[1],
732+
prevQuery: queryMethod,
733+
waitForMethodName: 'waitFor',
734+
},
735+
},
736+
],
737+
output: `
738+
import {waitFor} from '${testingFramework}';
739+
it('tests', async () => {
740+
const button = await screen.${queryMethod}("button", { name: "Submit" })
741+
expect(button).toBeInTheDocument()
742+
})
692743
`,
693744
})),
694745
]),

0 commit comments

Comments
 (0)