diff --git a/README.md b/README.md index cf49ebc4..b1e30400 100644 --- a/README.md +++ b/README.md @@ -206,7 +206,7 @@ To enable this configuration use the `extends` property in your | Name | Description | 🔧 | Included in configurations | | ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------- | --- | ---------------------------------------------------------------------------------- | -| [`await-async-event`](./docs/rules/await-async-event.md) | Enforce promises from async event methods are handled | | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] ![marko-badge][] | +| [`await-async-event`](./docs/rules/await-async-event.md) | Enforce promises from async event methods are handled | 🔧 | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] ![marko-badge][] | | [`await-async-query`](./docs/rules/await-async-query.md) | Enforce promises from async queries to be handled | | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] ![marko-badge][] | | [`await-async-utils`](./docs/rules/await-async-utils.md) | Enforce promises from async utils to be awaited properly | | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] ![marko-badge][] | | [`consistent-data-testid`](./docs/rules/consistent-data-testid.md) | Ensures consistent usage of `data-testid` | | | diff --git a/lib/rules/await-async-event.ts b/lib/rules/await-async-event.ts index 0ad2e433..c5aebff6 100644 --- a/lib/rules/await-async-event.ts +++ b/lib/rules/await-async-event.ts @@ -1,4 +1,4 @@ -import { ASTUtils, TSESTree } from '@typescript-eslint/utils'; +import { ASTUtils, TSESLint, TSESTree } from '@typescript-eslint/utils'; import { createTestingLibraryRule } from '../create-testing-library-rule'; import { @@ -6,6 +6,7 @@ import { getFunctionName, getInnermostReturningFunction, getVariableReferences, + isMemberExpression, isPromiseHandled, } from '../node-utils'; import { EVENTS_SIMULATORS } from '../utils'; @@ -41,6 +42,7 @@ export default createTestingLibraryRule({ awaitAsyncEventWrapper: 'Promise returned from `{{ name }}` wrapper over async event method must be handled', }, + fixable: 'code', schema: [ { type: 'object', @@ -76,16 +78,23 @@ export default createTestingLibraryRule({ create(context, [options], helpers) { const functionWrappersNames: string[] = []; - function reportUnhandledNode( - node: TSESTree.Identifier, - closestCallExpressionNode: TSESTree.CallExpression, - messageId: MessageIds = 'awaitAsyncEvent' - ): void { + function reportUnhandledNode({ + node, + closestCallExpression, + messageId = 'awaitAsyncEvent', + fix, + }: { + node: TSESTree.Identifier; + closestCallExpression: TSESTree.CallExpression; + messageId?: MessageIds; + fix?: TSESLint.ReportFixFunction; + }): void { if (!isPromiseHandled(node)) { context.report({ - node: closestCallExpressionNode.callee, + node: closestCallExpression.callee, messageId, data: { name: node.name }, + fix, }); } } @@ -128,14 +137,24 @@ export default createTestingLibraryRule({ ); if (references.length === 0) { - reportUnhandledNode(node, closestCallExpression); + reportUnhandledNode({ + node, + closestCallExpression, + fix: (fixer) => { + if (isMemberExpression(node.parent)) { + return fixer.insertTextBefore(node.parent, 'await '); + } + + return null; + }, + }); } else { for (const reference of references) { if (ASTUtils.isIdentifier(reference.identifier)) { - reportUnhandledNode( - reference.identifier, - closestCallExpression - ); + reportUnhandledNode({ + node: reference.identifier, + closestCallExpression, + }); } } } @@ -151,11 +170,14 @@ export default createTestingLibraryRule({ return; } - reportUnhandledNode( + reportUnhandledNode({ node, closestCallExpression, - 'awaitAsyncEventWrapper' - ); + messageId: 'awaitAsyncEventWrapper', + fix: (fixer) => { + return fixer.insertTextBefore(node, 'await '); + }, + }); } }, }; diff --git a/tests/lib/rules/await-async-event.test.ts b/tests/lib/rules/await-async-event.test.ts index 54718d24..5b245908 100644 --- a/tests/lib/rules/await-async-event.test.ts +++ b/tests/lib/rules/await-async-event.test.ts @@ -359,6 +359,12 @@ ruleTester.run(RULE_NAME, rule, { }, ], options: [{ eventModule: 'fireEvent' }], + output: ` + import { fireEvent } from '${testingFramework}' + test('unhandled promise from event method is invalid', async () => { + await fireEvent.${eventMethod}(getByLabelText('username')) + }) + `, } as const) ), ...FIRE_EVENT_ASYNC_FUNCTIONS.map( @@ -380,6 +386,12 @@ ruleTester.run(RULE_NAME, rule, { }, ], options: [{ eventModule: 'fireEvent' }], + output: ` + import { fireEvent as testingLibraryFireEvent } from '${testingFramework}' + test('unhandled promise from aliased event method is invalid', async () => { + await testingLibraryFireEvent.${eventMethod}(getByLabelText('username')) + }) + `, } as const) ), ...FIRE_EVENT_ASYNC_FUNCTIONS.map( @@ -401,6 +413,12 @@ ruleTester.run(RULE_NAME, rule, { }, ], options: [{ eventModule: 'fireEvent' }], + output: ` + import * as testingLibrary from '${testingFramework}' + test('unhandled promise from wildcard imported event method is invalid', async () => { + await testingLibrary.fireEvent.${eventMethod}(getByLabelText('username')) + }) + `, } as const) ), ...FIRE_EVENT_ASYNC_FUNCTIONS.map( @@ -428,6 +446,13 @@ ruleTester.run(RULE_NAME, rule, { }, ], options: [{ eventModule: 'fireEvent' }], + output: ` + import { fireEvent } from '${testingFramework}' + test('several unhandled promises from event methods is invalid', async () => { + await fireEvent.${eventMethod}(getByLabelText('username')) + await fireEvent.${eventMethod}(getByLabelText('username')) + }) + `, } as const) ), ...FIRE_EVENT_ASYNC_FUNCTIONS.map( @@ -451,6 +476,12 @@ ruleTester.run(RULE_NAME, rule, { }, ], options: [{ eventModule: 'fireEvent' }], + output: ` + import { fireEvent } from '${testingFramework}' + test('unhandled promise from event method with aggressive reporting opted-out is invalid', async () => { + await fireEvent.${eventMethod}(getByLabelText('username')) + }) + `, } as const) ), ...FIRE_EVENT_ASYNC_FUNCTIONS.map( @@ -476,6 +507,14 @@ ruleTester.run(RULE_NAME, rule, { }, ], options: [{ eventModule: 'fireEvent' }], + output: ` + import { fireEvent } from 'test-utils' + test( + 'unhandled promise from event method imported from custom module with aggressive reporting opted-out is invalid', + () => { + await fireEvent.${eventMethod}(getByLabelText('username')) + }) + `, } as const) ), ...FIRE_EVENT_ASYNC_FUNCTIONS.map( @@ -501,6 +540,14 @@ ruleTester.run(RULE_NAME, rule, { }, ], options: [{ eventModule: 'fireEvent' }], + output: ` + import { fireEvent } from '${testingFramework}' + test( + 'unhandled promise from event method imported from default module with aggressive reporting opted-out is invalid', + () => { + await fireEvent.${eventMethod}(getByLabelText('username')) + }) + `, } as const) ), @@ -524,6 +571,14 @@ ruleTester.run(RULE_NAME, rule, { }, ], options: [{ eventModule: 'fireEvent' }], + output: ` + import { fireEvent } from '${testingFramework}' + test( + 'unhandled promise from event method kept in a var is invalid', + () => { + const promise = await fireEvent.${eventMethod}(getByLabelText('username')) + }) + `, } as const) ), ...FIRE_EVENT_ASYNC_FUNCTIONS.map( @@ -549,6 +604,17 @@ ruleTester.run(RULE_NAME, rule, { }, ], options: [{ eventModule: 'fireEvent' }], + output: ` + import { fireEvent } from '${testingFramework}' + test('unhandled promise returned from function wrapping event method is invalid', () => { + function triggerEvent() { + doSomething() + return fireEvent.${eventMethod}(getByLabelText('username')) + } + + await triggerEvent() + }) + `, } as const) ), ]), @@ -572,6 +638,12 @@ ruleTester.run(RULE_NAME, rule, { }, ], options: [{ eventModule: 'userEvent' }], + output: ` + import userEvent from '${testingFramework}' + test('unhandled promise from event method is invalid', async () => { + await userEvent.${eventMethod}(getByLabelText('username')) + }) + `, } as const) ), ...USER_EVENT_ASYNC_FUNCTIONS.map( @@ -593,6 +665,12 @@ ruleTester.run(RULE_NAME, rule, { }, ], options: [{ eventModule: 'userEvent' }], + output: ` + import testingLibraryUserEvent from '${testingFramework}' + test('unhandled promise imported from alternate name event method is invalid', async () => { + await testingLibraryUserEvent.${eventMethod}(getByLabelText('username')) + }) + `, } as const) ), ...USER_EVENT_ASYNC_FUNCTIONS.map( @@ -620,6 +698,13 @@ ruleTester.run(RULE_NAME, rule, { }, ], options: [{ eventModule: 'userEvent' }], + output: ` + import userEvent from '${testingFramework}' + test('several unhandled promises from event methods is invalid', async () => { + await userEvent.${eventMethod}(getByLabelText('username')) + await userEvent.${eventMethod}(getByLabelText('username')) + }) + `, } as const) ), ...USER_EVENT_ASYNC_FUNCTIONS.map( @@ -642,6 +727,14 @@ ruleTester.run(RULE_NAME, rule, { }, ], options: [{ eventModule: 'userEvent' }], + output: ` + import userEvent from '${testingFramework}' + test( + 'unhandled promise from event method kept in a var is invalid', + () => { + const promise = await userEvent.${eventMethod}(getByLabelText('username')) + }) + `, } as const) ), ...USER_EVENT_ASYNC_FUNCTIONS.map( @@ -667,6 +760,17 @@ ruleTester.run(RULE_NAME, rule, { }, ], options: [{ eventModule: 'userEvent' }], + output: ` + import userEvent from '${testingFramework}' + test('unhandled promise returned from function wrapping event method is invalid', () => { + function triggerEvent() { + doSomething() + return userEvent.${eventMethod}(getByLabelText('username')) + } + + await triggerEvent() + }) + `, } as const) ), ]), @@ -674,7 +778,7 @@ ruleTester.run(RULE_NAME, rule, { code: ` import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}' import { fireEvent } from '${FIRE_EVENT_ASYNC_FRAMEWORKS[0]}' - test('unhandled promises from multiple event modules', async () => { + test('unhandled promises from multiple event modules', async () => { fireEvent.click(getByLabelText('username')) userEvent.click(getByLabelText('username')) }) @@ -694,6 +798,14 @@ ruleTester.run(RULE_NAME, rule, { }, ], options: [{ eventModule: ['userEvent', 'fireEvent'] }] as Options, + output: ` + import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}' + import { fireEvent } from '${FIRE_EVENT_ASYNC_FRAMEWORKS[0]}' + test('unhandled promises from multiple event modules', async () => { + await fireEvent.click(getByLabelText('username')) + await userEvent.click(getByLabelText('username')) + }) + `, }, { code: ` @@ -712,6 +824,14 @@ ruleTester.run(RULE_NAME, rule, { data: { name: 'click' }, }, ], + output: ` + import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}' + import { fireEvent } from '${FIRE_EVENT_ASYNC_FRAMEWORKS[0]}' + test('unhandled promise from userEvent relying on default options', async () => { + fireEvent.click(getByLabelText('username')) + await userEvent.click(getByLabelText('username')) + }) + `, }, ], });