From 8544edaecce16012bdfb8cdee8c345b760342823 Mon Sep 17 00:00:00 2001 From: neriyarden Date: Sat, 20 Jul 2024 20:12:48 +0300 Subject: [PATCH 1/7] feat: create a fixer --- lib/rules/await-async-queries.ts | 15 +++++++++ tests/lib/rules/await-async-queries.test.ts | 35 +++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/lib/rules/await-async-queries.ts b/lib/rules/await-async-queries.ts index 6da6b0d2..7d35fa0f 100644 --- a/lib/rules/await-async-queries.ts +++ b/lib/rules/await-async-queries.ts @@ -7,6 +7,7 @@ import { getFunctionName, getInnermostReturningFunction, getVariableReferences, + isMemberExpression, isPromiseHandled, } from '../node-utils'; @@ -34,6 +35,7 @@ export default createTestingLibraryRule({ asyncQueryWrapper: 'promise returned from `{{ name }}` wrapper over async query must be handled', }, + fixable: 'code', schema: [], }, defaultOptions: [], @@ -82,6 +84,19 @@ export default createTestingLibraryRule({ node: identifierNode, messageId: 'awaitAsyncQuery', data: { name: identifierNode.name }, + fix: (fixer) => { + if ( + isMemberExpression(identifierNode.parent) && + ASTUtils.isIdentifier(identifierNode.parent.object) && + identifierNode.parent.object.name === 'screen' + ) { + return fixer.insertTextBefore( + identifierNode.parent, + 'await ' + ); + } + return fixer.insertTextBefore(identifierNode, 'await '); + }, }); return; } diff --git a/tests/lib/rules/await-async-queries.test.ts b/tests/lib/rules/await-async-queries.test.ts index efab2cbd..5bc2c661 100644 --- a/tests/lib/rules/await-async-queries.test.ts +++ b/tests/lib/rules/await-async-queries.test.ts @@ -361,6 +361,14 @@ ruleTester.run(RULE_NAME, rule, { }); `, errors: [{ messageId: 'awaitAsyncQuery', line: 6, column: 21 }], + output: `// async queries without await operator or then method are not valid + import { render } from '${testingFramework}' + + test("An example test", async () => { + doSomething() + const foo = await ${query}('foo') + }); + `, }) as const ) ), @@ -382,6 +390,13 @@ ruleTester.run(RULE_NAME, rule, { data: { name: query }, }, ], + output: `// async screen queries without await operator or then method are not valid + import { render } from '@testing-library/react' + + test("An example test", async () => { + await screen.${query}('foo') + }); + `, }) as const ), ...ALL_ASYNC_COMBINATIONS_TO_TEST.map( @@ -403,6 +418,14 @@ ruleTester.run(RULE_NAME, rule, { data: { name: query }, }, ], + output: ` + import { render } from '@testing-library/react' + + test("An example test", async () => { + doSomething() + const foo = await ${query}('foo') + }); + `, }) as const ), ...ALL_ASYNC_COMBINATIONS_TO_TEST.map( @@ -440,6 +463,13 @@ ruleTester.run(RULE_NAME, rule, { }) `, errors: [{ messageId: 'awaitAsyncQuery', line: 5, column: 27 }], + output: ` + import { render } from "another-library" + + test('An example test', async () => { + const example = await ${query}("my example") + }) + `, }) as const ), @@ -517,6 +547,11 @@ ruleTester.run(RULE_NAME, rule, { }) `, errors: [{ messageId: 'awaitAsyncQuery', line: 3, column: 25 }], + output: ` + test('An invalid example test', () => { + const element = await findByIcon('search') + }) + `, }, { From 5059bc2bbccbba6a32cdcf70d25a7737c6fe7523 Mon Sep 17 00:00:00 2001 From: neriyarden Date: Fri, 26 Jul 2024 17:51:03 +0300 Subject: [PATCH 2/7] feat: fix promise references --- lib/rules/await-async-queries.ts | 9 +++++++++ tests/lib/rules/await-async-queries.test.ts | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/lib/rules/await-async-queries.ts b/lib/rules/await-async-queries.ts index 7d35fa0f..b0379485 100644 --- a/lib/rules/await-async-queries.ts +++ b/lib/rules/await-async-queries.ts @@ -114,6 +114,15 @@ export default createTestingLibraryRule({ node: identifierNode, messageId: 'awaitAsyncQuery', data: { name: identifierNode.name }, + fix: (fixer) => { + const fixes = []; + for (const ref of references) { + fixes.push( + fixer.insertTextBefore(ref.identifier, 'await ') + ); + } + return fixes; + }, }); return; } diff --git a/tests/lib/rules/await-async-queries.test.ts b/tests/lib/rules/await-async-queries.test.ts index 5bc2c661..f97b281c 100644 --- a/tests/lib/rules/await-async-queries.test.ts +++ b/tests/lib/rules/await-async-queries.test.ts @@ -448,6 +448,15 @@ ruleTester.run(RULE_NAME, rule, { data: { name: query }, }, ], + output: ` + import { render } from '@testing-library/react' + + test("An example test", async () => { + const foo = ${query}('foo') + expect(await foo).toBeInTheDocument() + expect(await foo).toHaveAttribute('src', 'bar'); + }); + `, }) as const ), From fe691cddf3f814e48c17a4c1e6f5c6b371419cf4 Mon Sep 17 00:00:00 2001 From: neriyarden Date: Fri, 26 Jul 2024 17:51:28 +0300 Subject: [PATCH 3/7] feat: fix promise wrappers --- lib/rules/await-async-queries.ts | 40 ++++++++++++ tests/lib/rules/await-async-queries.test.ts | 71 ++++++++++++++++++++- 2 files changed, 108 insertions(+), 3 deletions(-) diff --git a/lib/rules/await-async-queries.ts b/lib/rules/await-async-queries.ts index b0379485..6329448d 100644 --- a/lib/rules/await-async-queries.ts +++ b/lib/rules/await-async-queries.ts @@ -3,6 +3,7 @@ import { ASTUtils, TSESTree } from '@typescript-eslint/utils'; import { createTestingLibraryRule } from '../create-testing-library-rule'; import { findClosestCallExpressionNode, + findClosestFunctionExpressionNode, getDeepestIdentifierNode, getFunctionName, getInnermostReturningFunction, @@ -136,6 +137,45 @@ export default createTestingLibraryRule({ node: identifierNode, messageId: 'asyncQueryWrapper', data: { name: identifierNode.name }, + fix: (fixer) => { + const functionExpression = + findClosestFunctionExpressionNode(node); + + if (!functionExpression) return null; + + let IdentifierNodeFixer; + // If the wrapper is a property of an object, + // add 'await' before the object, e.g.: + // const obj = { wrapper: () => screen.findByText(/foo/i) }; + // await obj.wrapper(); + if (isMemberExpression(identifierNode.parent)) { + IdentifierNodeFixer = fixer.insertTextBefore( + identifierNode.parent, + 'await ' + ); + // Otherwise, add 'await' before the wrapper function, e.g.: + // const wrapper = () => screen.findByText(/foo/i); + // await wrapper(); + } else { + IdentifierNodeFixer = fixer.insertTextBefore( + identifierNode, + 'await ' + ); + } + + if (functionExpression.async) { + return IdentifierNodeFixer; + } else { + // Mutate the actual node so if other nodes exist in this + // function expression body they don't also try to fix it. + functionExpression.async = true; + + return [ + IdentifierNodeFixer, + fixer.insertTextBefore(functionExpression, 'async '), + ]; + } + }, }); } }, diff --git a/tests/lib/rules/await-async-queries.test.ts b/tests/lib/rules/await-async-queries.test.ts index f97b281c..2dc2ccf1 100644 --- a/tests/lib/rules/await-async-queries.test.ts +++ b/tests/lib/rules/await-async-queries.test.ts @@ -497,11 +497,26 @@ ruleTester.run(RULE_NAME, rule, { const element = queryWrapper() }) - test("An invalid example test", async () => { + test("A valid example test", async () => { const element = await queryWrapper() }) `, errors: [{ messageId: 'asyncQueryWrapper', line: 9, column: 27 }], + output: ` + function queryWrapper() { + doSomethingElse(); + + return screen.${query}('foo') + } + + test("An invalid example test", async () => { + const element = await queryWrapper() + }) + + test("A valid example test", async () => { + const element = await queryWrapper() + }) + `, }) as const ), // unhandled promise from async query arrow function wrapper is invalid @@ -519,11 +534,26 @@ ruleTester.run(RULE_NAME, rule, { const element = queryWrapper() }) - test("An invalid example test", async () => { + test("A valid example test", async () => { const element = await queryWrapper() }) `, errors: [{ messageId: 'asyncQueryWrapper', line: 9, column: 27 }], + output: ` + const queryWrapper = () => { + doSomethingElse(); + + return ${query}('foo') + } + + test("An invalid example test", async () => { + const element = await queryWrapper() + }) + + test("A valid example test", async () => { + const element = await queryWrapper() + }) + `, }) as const ), // unhandled promise implicitly returned from async query arrow function wrapper is invalid @@ -537,11 +567,22 @@ ruleTester.run(RULE_NAME, rule, { const element = queryWrapper() }) - test("An invalid example test", async () => { + test("A valid example test", async () => { const element = await queryWrapper() }) `, errors: [{ messageId: 'asyncQueryWrapper', line: 5, column: 27 }], + output: ` + const queryWrapper = () => screen.${query}('foo') + + test("An invalid example test", async () => { + const element = await queryWrapper() + }) + + test("A valid example test", async () => { + const element = await queryWrapper() + }) + `, }) as const ), @@ -589,6 +630,30 @@ ruleTester.run(RULE_NAME, rule, { }) `, errors: [{ messageId: 'asyncQueryWrapper', line: 19, column: 34 }], + output: `// similar to issue #359 but forcing an error in no-awaited wrapper + import { render, screen } from 'mocks/test-utils' + import userEvent from '@testing-library/user-event' + + const testData = { + name: 'John Doe', + email: 'john@doe.com', + password: 'extremeSecret', + } + + const selectors = { + username: () => screen.findByRole('textbox', { name: /username/i }), + email: () => screen.findByRole('textbox', { name: /e-mail/i }), + password: () => screen.findByLabelText(/password/i), + } + + test('this is a valid case', async () => { + render() + userEvent.type(await selectors.username(), testData.name) // <-- unhandled here + userEvent.type(await selectors.email(), testData.email) + userEvent.type(await selectors.password(), testData.password) + // ... + }) + `, }, ], }); From 5c724063705051ecb7a82f42bcea2087871aecb5 Mon Sep 17 00:00:00 2001 From: neriyarden Date: Fri, 26 Jul 2024 18:10:03 +0300 Subject: [PATCH 4/7] docs: update docs --- README.md | 2 +- docs/rules/await-async-queries.md | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0fa46197..17393614 100644 --- a/README.md +++ b/README.md @@ -295,7 +295,7 @@ module.exports = [ | Name                            | Description | 💼 | ⚠️ | 🔧 | | :------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------- | :------------------------------------------------------------------ | :-- | | [await-async-events](docs/rules/await-async-events.md) | Enforce promises from async event methods are handled | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-vue][] | | 🔧 | -| [await-async-queries](docs/rules/await-async-queries.md) | Enforce promises from async queries to be handled | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-vue][] | | | +| [await-async-queries](docs/rules/await-async-queries.md) | Enforce promises from async queries to be handled | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-vue][] | | 🔧 | | [await-async-utils](docs/rules/await-async-utils.md) | Enforce promises from async utils to be awaited properly | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-vue][] | | | | [consistent-data-testid](docs/rules/consistent-data-testid.md) | Ensures consistent usage of `data-testid` | | | | | [no-await-sync-events](docs/rules/no-await-sync-events.md) | Disallow unnecessary `await` for sync events | ![badge-angular][] ![badge-dom][] ![badge-react][] | | | diff --git a/docs/rules/await-async-queries.md b/docs/rules/await-async-queries.md index 520b6968..baf26842 100644 --- a/docs/rules/await-async-queries.md +++ b/docs/rules/await-async-queries.md @@ -2,6 +2,8 @@ 💼 This rule is enabled in the following configs: `angular`, `dom`, `marko`, `react`, `vue`. +🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). + Ensure that promises returned by async queries are handled properly. From 5fbbc5050e6fa8e323afba3b3dcd837f7243bc20 Mon Sep 17 00:00:00 2001 From: neriyarden Date: Sat, 27 Jul 2024 16:54:36 +0300 Subject: [PATCH 5/7] feat: refactor loop to map in fixer --- lib/rules/await-async-queries.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/rules/await-async-queries.ts b/lib/rules/await-async-queries.ts index 6329448d..be47f390 100644 --- a/lib/rules/await-async-queries.ts +++ b/lib/rules/await-async-queries.ts @@ -116,12 +116,9 @@ export default createTestingLibraryRule({ messageId: 'awaitAsyncQuery', data: { name: identifierNode.name }, fix: (fixer) => { - const fixes = []; - for (const ref of references) { - fixes.push( - fixer.insertTextBefore(ref.identifier, 'await ') - ); - } + const fixes = references.map((ref) => + fixer.insertTextBefore(ref.identifier, 'await ') + ); return fixes; }, }); From a5826063dd9b68c988eb9ec1d1b08b82f056f6b7 Mon Sep 17 00:00:00 2001 From: neriyarden Date: Sat, 27 Jul 2024 17:01:41 +0300 Subject: [PATCH 6/7] refactor: improve comments readability --- lib/rules/await-async-queries.ts | 40 ++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/lib/rules/await-async-queries.ts b/lib/rules/await-async-queries.ts index be47f390..5e53dca0 100644 --- a/lib/rules/await-async-queries.ts +++ b/lib/rules/await-async-queries.ts @@ -77,8 +77,10 @@ export default createTestingLibraryRule({ closestCallExpressionNode.parent ); - // check direct usage of async query: - // const element = await findByRole('button') + /** + * Check direct usage of async query: + * const element = await findByRole('button'); + */ if (references.length === 0) { if (!isPromiseHandled(identifierNode)) { context.report({ @@ -103,9 +105,11 @@ export default createTestingLibraryRule({ } } - // check references usages of async query: - // const promise = findByRole('button') - // const element = await promise + /** + * Check references usages of async query: + * const promise = findByRole('button'); + * const element = await promise; + */ for (const reference of references) { if ( ASTUtils.isIdentifier(reference.identifier) && @@ -129,7 +133,7 @@ export default createTestingLibraryRule({ functionWrappersNames.includes(identifierNode.name) && !isPromiseHandled(identifierNode) ) { - // check async queries used within a wrapper previously detected + // Check async queries used within a wrapper previously detected context.report({ node: identifierNode, messageId: 'asyncQueryWrapper', @@ -141,19 +145,23 @@ export default createTestingLibraryRule({ if (!functionExpression) return null; let IdentifierNodeFixer; - // If the wrapper is a property of an object, - // add 'await' before the object, e.g.: - // const obj = { wrapper: () => screen.findByText(/foo/i) }; - // await obj.wrapper(); if (isMemberExpression(identifierNode.parent)) { + /** + * If the wrapper is a property of an object, + * add 'await' before the object, e.g.: + * const obj = { wrapper: () => screen.findByText(/foo/i) }; + * await obj.wrapper(); + */ IdentifierNodeFixer = fixer.insertTextBefore( identifierNode.parent, 'await ' ); - // Otherwise, add 'await' before the wrapper function, e.g.: - // const wrapper = () => screen.findByText(/foo/i); - // await wrapper(); } else { + /** + * Add 'await' before the wrapper function, e.g.: + * const wrapper = () => screen.findByText(/foo/i); + * await wrapper(); + */ IdentifierNodeFixer = fixer.insertTextBefore( identifierNode, 'await ' @@ -163,8 +171,10 @@ export default createTestingLibraryRule({ if (functionExpression.async) { return IdentifierNodeFixer; } else { - // Mutate the actual node so if other nodes exist in this - // function expression body they don't also try to fix it. + /** + * Mutate the actual node so if other nodes exist in this + * function expression body they don't also try to fix it. + */ functionExpression.async = true; return [ From bfbd4579ac443c31b0a3a98a3d4d2fa017bde153 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20De=20Boey?= Date: Sat, 19 Oct 2024 03:57:38 +0200 Subject: [PATCH 7/7] Update lib/rules/await-async-queries.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michaël De Boey --- lib/rules/await-async-queries.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/rules/await-async-queries.ts b/lib/rules/await-async-queries.ts index 5e53dca0..3d77f428 100644 --- a/lib/rules/await-async-queries.ts +++ b/lib/rules/await-async-queries.ts @@ -119,12 +119,10 @@ export default createTestingLibraryRule({ node: identifierNode, messageId: 'awaitAsyncQuery', data: { name: identifierNode.name }, - fix: (fixer) => { - const fixes = references.map((ref) => + fix: (fixer) => + references.map((ref) => fixer.insertTextBefore(ref.identifier, 'await ') - ); - return fixes; - }, + ), }); return; }