diff --git a/README.md b/README.md index 0fa46197..3b55524f 100644 --- a/README.md +++ b/README.md @@ -296,7 +296,7 @@ module.exports = [ | :------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------- | :------------------------------------------------------------------ | :-- | | [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-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][] | | | +| [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][] | | | | [no-await-sync-queries](docs/rules/no-await-sync-queries.md) | Disallow unnecessary `await` for sync queries | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-vue][] | | | diff --git a/docs/rules/await-async-utils.md b/docs/rules/await-async-utils.md index b5433d83..47a6facf 100644 --- a/docs/rules/await-async-utils.md +++ b/docs/rules/await-async-utils.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 utils are handled properly. diff --git a/lib/rules/await-async-utils.ts b/lib/rules/await-async-utils.ts index a52e96d0..84bbe222 100644 --- a/lib/rules/await-async-utils.ts +++ b/lib/rules/await-async-utils.ts @@ -7,6 +7,7 @@ import { getFunctionName, getInnermostReturningFunction, getVariableReferences, + isMemberExpression, isObjectPattern, isPromiseHandled, isProperty, @@ -36,6 +37,7 @@ export default createTestingLibraryRule({ 'Promise returned from {{ name }} wrapper over async util must be handled', }, schema: [], + fixable: 'code', }, defaultOptions: [], @@ -149,6 +151,12 @@ export default createTestingLibraryRule({ data: { name: node.name, }, + fix: (fixer) => { + if (isMemberExpression(node.parent)) { + return fixer.insertTextBefore(node.parent, 'await '); + } + return fixer.insertTextBefore(node, 'await '); + }, }); } } else { @@ -161,6 +169,9 @@ export default createTestingLibraryRule({ data: { name: node.name, }, + fix: (fixer) => { + return fixer.insertTextBefore(referenceNode, 'await '); + }, }); return; } diff --git a/tests/lib/rules/await-async-utils.test.ts b/tests/lib/rules/await-async-utils.test.ts index fc538079..7f4f8768 100644 --- a/tests/lib/rules/await-async-utils.test.ts +++ b/tests/lib/rules/await-async-utils.test.ts @@ -347,6 +347,13 @@ ruleTester.run(RULE_NAME, rule, { data: { name: asyncUtil }, }, ], + output: ` + import { ${asyncUtil} } from '${testingFramework}'; + test('${asyncUtil} util not waited is invalid', () => { + doSomethingElse(); + await ${asyncUtil}(() => getByLabelText('email')); + }); + `, }) as const ), ...ASYNC_UTILS.map( @@ -367,6 +374,13 @@ ruleTester.run(RULE_NAME, rule, { data: { name: asyncUtil }, }, ], + output: ` + import { ${asyncUtil} } from '${testingFramework}'; + test('${asyncUtil} util not waited is invalid', () => { + doSomethingElse(); + const el = await ${asyncUtil}(() => getByLabelText('email')); + }); + `, }) as const ), ...ASYNC_UTILS.map( @@ -387,6 +401,13 @@ ruleTester.run(RULE_NAME, rule, { data: { name: asyncUtil }, }, ], + output: ` + import * as asyncUtil from '${testingFramework}'; + test('asyncUtil.${asyncUtil} util not handled is invalid', () => { + doSomethingElse(); + await asyncUtil.${asyncUtil}(() => getByLabelText('email')); + }); + `, }) as const ), ...ASYNC_UTILS.map( @@ -407,6 +428,13 @@ ruleTester.run(RULE_NAME, rule, { data: { name: asyncUtil }, }, ], + output: ` + import { ${asyncUtil} } from '${testingFramework}'; + test('${asyncUtil} util promise saved not handled is invalid', () => { + doSomethingElse(); + const aPromise = await ${asyncUtil}(() => getByLabelText('email')); + }); + `, }) as const ), ...ASYNC_UTILS.map( @@ -434,6 +462,14 @@ ruleTester.run(RULE_NAME, rule, { data: { name: asyncUtil }, }, ], + output: ` + import { ${asyncUtil} } from '${testingFramework}'; + test('several ${asyncUtil} utils not handled are invalid', () => { + const aPromise = ${asyncUtil}(() => getByLabelText('username')); + doSomethingElse(await aPromise); + await ${asyncUtil}(() => getByLabelText('email')); + }); + `, }) as const ), ...ASYNC_UTILS.map( @@ -461,6 +497,14 @@ ruleTester.run(RULE_NAME, rule, { data: { name: asyncUtil }, }, ], + output: ` + import { ${asyncUtil} } from '${testingFramework}'; + test('unhandled expression that evaluates to promise is invalid', () => { + const aPromise = ${asyncUtil}(() => getByLabelText('username')); + doSomethingElse(await aPromise); + await ${asyncUtil}(() => getByLabelText('email')); + }); + `, }) as const ), ...ASYNC_UTILS.map( @@ -486,6 +530,18 @@ ruleTester.run(RULE_NAME, rule, { data: { name: 'waitForSomethingAsync' }, }, ], + output: ` + import { ${asyncUtil}, render } from '${testingFramework}'; + + function waitForSomethingAsync() { + return ${asyncUtil}(() => somethingAsync()) + } + + test('unhandled promise from function wrapping ${asyncUtil} util is invalid', async () => { + render() + await waitForSomethingAsync() + }); + `, }) as const ), ...ASYNC_UTILS.map( @@ -508,6 +564,15 @@ ruleTester.run(RULE_NAME, rule, { data: { name: asyncUtil }, }, ], + output: ` + import { ${asyncUtil} } from 'some-other-library'; + test( + 'aggressive reporting - util "${asyncUtil}" which is not related to testing library is invalid', + async () => { + doSomethingElse(); + await ${asyncUtil}(); + }); + `, }) as const ), ...ASYNC_UTILS.map( @@ -533,6 +598,18 @@ ruleTester.run(RULE_NAME, rule, { data: { name: 'waitForSomethingAsync' }, }, ], + output: ` + import { ${asyncUtil}, render } from '${testingFramework}'; + + function waitForSomethingAsync() { + return ${asyncUtil}(() => somethingAsync()) + } + + test('unhandled promise from function wrapping ${asyncUtil} util is invalid', async () => { + render() + const el = await waitForSomethingAsync() + }); + `, }) as const ), @@ -556,6 +633,15 @@ ruleTester.run(RULE_NAME, rule, { data: { name: asyncUtil }, }, ], + output: ` + import * as asyncUtils from 'some-other-library'; + test( + 'aggressive reporting - util "asyncUtils.${asyncUtil}" which is not related to testing library is invalid', + async () => { + doSomethingElse(); + await asyncUtils.${asyncUtil}(); + }); + `, }) as const ), ...ASYNC_UTILS.map( @@ -586,6 +672,22 @@ ruleTester.run(RULE_NAME, rule, { data: { name: 'waitForAsyncUtil' }, }, ], + output: ` + function setup() { + const utils = render(); + + const waitForAsyncUtil = () => { + return ${asyncUtil}(screen.queryByTestId('my-test-id')); + }; + + return { waitForAsyncUtil, ...utils }; + } + + test('unhandled promise from destructed property of async function wrapper is invalid', () => { + const { user, waitForAsyncUtil } = setup(); + await waitForAsyncUtil(); + }); + `, }) as const ), ...ASYNC_UTILS.map( @@ -617,6 +719,23 @@ ruleTester.run(RULE_NAME, rule, { data: { name: 'myAlias' }, }, ], + output: ` + function setup() { + const utils = render(); + + const waitForAsyncUtil = () => { + return ${asyncUtil}(screen.queryByTestId('my-test-id')); + }; + + return { waitForAsyncUtil, ...utils }; + } + + test('unhandled promise from destructed property of async function wrapper is invalid', () => { + const { user, waitForAsyncUtil } = setup(); + const myAlias = waitForAsyncUtil; + await myAlias(); + }); + `, }) as const ), ...ASYNC_UTILS.map( @@ -647,6 +766,22 @@ ruleTester.run(RULE_NAME, rule, { data: { name: 'waitForAsyncUtil' }, }, ], + output: ` + function setup() { + const utils = render(); + + const waitForAsyncUtil = () => { + return ${asyncUtil}(screen.queryByTestId('my-test-id')); + }; + + return { waitForAsyncUtil, ...utils }; + } + + test('unhandled promise from destructed property of async function wrapper is invalid', () => { + const { ...clone } = setup(); + await clone.waitForAsyncUtil(); + }); + `, }) as const ), ...ASYNC_UTILS.map( @@ -677,6 +812,22 @@ ruleTester.run(RULE_NAME, rule, { data: { name: 'myAlias' }, }, ], + output: ` + function setup() { + const utils = render(); + + const waitForAsyncUtil = () => { + return ${asyncUtil}(screen.queryByTestId('my-test-id')); + }; + + return { waitForAsyncUtil, ...utils }; + } + + test('unhandled promise from destructed property of async function wrapper is invalid', () => { + const { waitForAsyncUtil: myAlias } = setup(); + await myAlias(); + }); + `, }) as const ), ...ASYNC_UTILS.map( @@ -706,6 +857,21 @@ ruleTester.run(RULE_NAME, rule, { data: { name: 'waitForAsyncUtil' }, }, ], + output: ` + function setup() { + const utils = render(); + + const waitForAsyncUtil = () => { + return ${asyncUtil}(screen.queryByTestId('my-test-id')); + }; + + return { waitForAsyncUtil, ...utils }; + } + + test('unhandled promise from destructed property of async function wrapper is invalid', () => { + await setup().waitForAsyncUtil(); + }); + `, }) as const ), ...ASYNC_UTILS.map( @@ -736,6 +902,22 @@ ruleTester.run(RULE_NAME, rule, { data: { name: 'myAlias' }, }, ], + output: ` + function setup() { + const utils = render(); + + const waitForAsyncUtil = () => { + return ${asyncUtil}(screen.queryByTestId('my-test-id')); + }; + + return { waitForAsyncUtil, ...utils }; + } + + test('unhandled promise from destructed property of async function wrapper is invalid', () => { + const myAlias = setup().waitForAsyncUtil; + await myAlias(); + }); + `, }) as const ), ]),