From e6d4168dd6c19df1be3e0cf582d07c2f6eba0744 Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Wed, 28 May 2025 23:51:38 +0900 Subject: [PATCH 1/6] feat: enhance detection of async queries in block statements --- lib/rules/prefer-find-by.ts | 83 +++++++++++++++++++++++++++++++++---- 1 file changed, 76 insertions(+), 7 deletions(-) diff --git a/lib/rules/prefer-find-by.ts b/lib/rules/prefer-find-by.ts index 1fc26ebd..aa798d14 100644 --- a/lib/rules/prefer-find-by.ts +++ b/lib/rules/prefer-find-by.ts @@ -2,12 +2,15 @@ import { TSESTree, ASTUtils, TSESLint } from '@typescript-eslint/utils'; import { createTestingLibraryRule } from '../create-testing-library-rule'; import { + getDeepestIdentifierNode, isArrowFunctionExpression, + isBlockStatement, isCallExpression, isMemberExpression, isObjectExpression, isObjectPattern, isProperty, + isVariableDeclaration, } from '../node-utils'; import { getScope, getSourceCode } from '../utils'; @@ -21,6 +24,10 @@ export function getFindByQueryVariant( return queryMethod.includes('All') ? 'findAllBy' : 'findBy'; } +function isFindByQuery(name: string): boolean { + return /^find(All)?By/.test(name); +} + function findRenderDefinitionDeclaration( scope: TSESLint.Scope.Scope | null, query: string @@ -329,20 +336,82 @@ export default createTestingLibraryRule({ } return { - 'AwaitExpression > CallExpression'(node: TSESTree.CallExpression) { + 'AwaitExpression > CallExpression'( + node: TSESTree.CallExpression & { parent: TSESTree.AwaitExpression } + ) { if ( !ASTUtils.isIdentifier(node.callee) || !helpers.isAsyncUtil(node.callee, ['waitFor']) ) { return; } - // ensure the only argument is an arrow function expression - if the arrow function is a block - // we skip it + // ensure the only argument is an arrow function expression const argument = node.arguments[0]; - if ( - !isArrowFunctionExpression(argument) || - !isCallExpression(argument.body) - ) { + + if (!isArrowFunctionExpression(argument)) { + return; + } + + if (isBlockStatement(argument.body) && argument.async) { + const { body } = argument.body; + const declarations = body + .filter(isVariableDeclaration) + ?.flatMap((declaration) => declaration.declarations); + + const findByDeclarator = declarations.find((declaration) => { + if ( + !ASTUtils.isAwaitExpression(declaration.init) || + !isCallExpression(declaration.init.argument) + ) { + return false; + } + + const { callee } = declaration.init.argument; + + const name = getDeepestIdentifierNode(callee)?.name; + return name ? isFindByQuery(name) : false; + }); + + const init = ASTUtils.isAwaitExpression(findByDeclarator?.init) + ? findByDeclarator.init?.argument + : null; + + if (!isCallExpression(init)) { + return; + } + const queryIdentifier = getDeepestIdentifierNode(init.callee); + + if (!queryIdentifier || !helpers.isAsyncQuery(queryIdentifier)) { + return; + } + + const fullQueryMethod = queryIdentifier.name; + const queryMethod = fullQueryMethod.split('By')[1]; + const queryVariant = getFindByQueryVariant(fullQueryMethod); + + reportInvalidUsage(node, { + queryMethod, + queryVariant, + prevQuery: fullQueryMethod, + fix(fixer) { + const { parent: expressionStatement } = node.parent; + const bodyText = sourceCode + .getText(argument.body) + .slice(1, -1) + .trim(); + const { line, column } = expressionStatement.loc.start; + const indent = sourceCode.getLines()[line - 1].slice(0, column); + const newText = bodyText + .split('\n') + .map((line) => line.trim()) + .join(`\n${indent}`); + return fixer.replaceText(expressionStatement, newText); + }, + }); + return; + } + + if (!isCallExpression(argument.body)) { return; } From 58daeedeb9b6d30cf0d3d01c724e3fd3a56f964f Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Wed, 28 May 2025 23:51:49 +0900 Subject: [PATCH 2/6] test: add test cases --- tests/lib/rules/prefer-find-by.test.ts | 29 ++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/lib/rules/prefer-find-by.test.ts b/tests/lib/rules/prefer-find-by.test.ts index 2f27043e..aa7dd7bf 100644 --- a/tests/lib/rules/prefer-find-by.test.ts +++ b/tests/lib/rules/prefer-find-by.test.ts @@ -689,6 +689,35 @@ ruleTester.run(RULE_NAME, rule, { const button = await screen.${buildFindByMethod( queryMethod )}('Count is: 0', { timeout: 100, interval: 200 }) + `, + })), + ...ASYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ + code: ` + import {waitFor} from '${testingFramework}'; + it('tests', async () => { + await waitFor(async () => { + const button = await screen.${queryMethod}("button", { name: "Submit" }) + expect(button).toBeInTheDocument() + }) + }) + `, + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: getFindByQueryVariant(queryMethod), + queryMethod: queryMethod.split('By')[1], + prevQuery: queryMethod, + waitForMethodName: 'waitFor', + }, + }, + ], + output: ` + import {waitFor} from '${testingFramework}'; + it('tests', async () => { + const button = await screen.${queryMethod}("button", { name: "Submit" }) + expect(button).toBeInTheDocument() + }) `, })), ]), From 22e1dfa7b66a8fb6d97d65ebae72d3ab29b04abc Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Thu, 29 May 2025 20:39:43 +0900 Subject: [PATCH 3/6] refactor: use isFindQueryVariant --- lib/rules/prefer-find-by.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/lib/rules/prefer-find-by.ts b/lib/rules/prefer-find-by.ts index aa798d14..02d523f2 100644 --- a/lib/rules/prefer-find-by.ts +++ b/lib/rules/prefer-find-by.ts @@ -24,10 +24,6 @@ export function getFindByQueryVariant( return queryMethod.includes('All') ? 'findAllBy' : 'findBy'; } -function isFindByQuery(name: string): boolean { - return /^find(All)?By/.test(name); -} - function findRenderDefinitionDeclaration( scope: TSESLint.Scope.Scope | null, query: string @@ -367,13 +363,12 @@ export default createTestingLibraryRule({ } const { callee } = declaration.init.argument; - - const name = getDeepestIdentifierNode(callee)?.name; - return name ? isFindByQuery(name) : false; + const node = getDeepestIdentifierNode(callee); + return node ? helpers.isFindQueryVariant(node) : false; }); const init = ASTUtils.isAwaitExpression(findByDeclarator?.init) - ? findByDeclarator.init?.argument + ? findByDeclarator.init.argument : null; if (!isCallExpression(init)) { From 46480ff69bc10981ae696a0eac32815df469862d Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Thu, 29 May 2025 20:44:52 +0900 Subject: [PATCH 4/6] chore: add comment --- lib/rules/prefer-find-by.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/rules/prefer-find-by.ts b/lib/rules/prefer-find-by.ts index 02d523f2..5e3f35a7 100644 --- a/lib/rules/prefer-find-by.ts +++ b/lib/rules/prefer-find-by.ts @@ -376,6 +376,7 @@ export default createTestingLibraryRule({ } const queryIdentifier = getDeepestIdentifierNode(init.callee); + // ensure the query is a supported async query like findBy* if (!queryIdentifier || !helpers.isAsyncQuery(queryIdentifier)) { return; } From 64c976f1025ab794653a8e1185c6e2b9e4321603 Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Thu, 29 May 2025 21:01:22 +0900 Subject: [PATCH 5/6] test: add test cases --- tests/lib/rules/prefer-find-by.test.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/lib/rules/prefer-find-by.test.ts b/tests/lib/rules/prefer-find-by.test.ts index aa7dd7bf..3521db71 100644 --- a/tests/lib/rules/prefer-find-by.test.ts +++ b/tests/lib/rules/prefer-find-by.test.ts @@ -51,6 +51,17 @@ ruleTester.run(RULE_NAME, rule, { it('tests', async () => { const submitButton = await screen.${queryMethod}('foo') }) + `, + })), + ...ASYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ + code: ` + import {waitFor} from '${testingFramework}'; + it('tests', async () => { + await waitFor(async () => { + const button = screen.${queryMethod}("button", { name: "Submit" }) + expect(button).toBeInTheDocument() + }) + }) `, })), ...SYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ @@ -164,6 +175,17 @@ ruleTester.run(RULE_NAME, rule, { const { container } = render() await waitFor(() => expect(container.querySelector('baz')).toBeInTheDocument()); }) + `, + }, + { + code: ` + import {waitFor} from '${testingFramework}'; + it('tests', async () => { + await waitFor(async () => { + const button = await foo("button", { name: "Submit" }) + expect(button).toBeInTheDocument() + }) + }) `, }, ]), From e68646bd9dc45c557d9ca97ccd052c43f9ba63cb Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Fri, 30 May 2025 22:21:57 +0900 Subject: [PATCH 6/6] docs: add incorrect case --- docs/rules/prefer-find-by.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/rules/prefer-find-by.md b/docs/rules/prefer-find-by.md index 2a39b131..0431e27b 100644 --- a/docs/rules/prefer-find-by.md +++ b/docs/rules/prefer-find-by.md @@ -41,6 +41,12 @@ const submitButton = await waitFor(() => const submitButton = await waitFor(() => expect(queryByLabel('button', { name: /submit/i })).not.toBeFalsy() ); + +// unnecessary usage of waitFor with findBy*, which already includes waiting logic +await waitFor(async () => { + const button = await findByRole('button', { name: 'Submit' }); + expect(button).toBeInTheDocument(); +}); ``` Examples of **correct** code for this rule: