diff --git a/docs/rules/no-await-sync-events.md b/docs/rules/no-await-sync-events.md index 58206373..141c3632 100644 --- a/docs/rules/no-await-sync-events.md +++ b/docs/rules/no-await-sync-events.md @@ -4,7 +4,7 @@ Ensure that sync simulated events are not awaited unnecessarily. ## Rule Details -Methods for simulating events in Testing Library ecosystem -`fireEvent` and `userEvent`- +Methods for simulating events in Testing Library ecosystem -`fireEvent` and `userEvent` prior to v14 - do NOT return any Promise, with an exception of `userEvent.type` and `userEvent.keyboard`, which delays the promise resolve only if [`delay` option](https://github.com/testing-library/user-event#typeelement-text-options) is specified. @@ -13,8 +13,8 @@ Some examples of simulating events not returning any Promise are: - `fireEvent.click` - `fireEvent.select` -- `userEvent.tab` -- `userEvent.hover` +- `userEvent.tab` (prior to `user-event` v14) +- `userEvent.hover` (prior to `user-event` v14) This rule aims to prevent users from waiting for those function calls. @@ -29,12 +29,14 @@ const foo = async () => { const bar = async () => { // ... + // userEvent prior to v14 await userEvent.tab(); // ... }; const baz = async () => { // ... + // userEvent prior to v14 await userEvent.type(textInput, 'abc'); await userEvent.keyboard('abc'); // ... @@ -66,9 +68,42 @@ const baz = async () => { userEvent.keyboard('123'); // ... }; + +const qux = async () => { + // userEvent v14 + await userEvent.tab(); + await userEvent.click(button); + await userEvent.type(textInput, 'abc'); + await userEvent.keyboard('abc'); + // ... +}; +``` + +## Options + +This rule provides the following options: + +- `eventModules`: array of strings. The possibilities are: `"fire-event"` and `"user-event"`. Defaults to `["fire-event", "user-event"]` + +### `eventModules` + +This option gives you more granular control of which event modules you want to report, so you can choose to only report methods from either `fire-event`, `user-event` or both. + +Example: + +```json +{ + "testing-library/no-await-sync-events": [ + "error", + { + "eventModules": ["fire-event", "user-event"] + } + ] +} ``` ## Notes -There is another rule `await-fire-event`, which is only in Vue Testing -Library. Please do not confuse with this rule. +- Since `user-event` v14 all its methods are async, so you should disable reporting them by setting the `eventModules` to just `"fire-event"` so `user-event` methods are not reported. +- There is another rule `await-fire-event`, which is only in Vue Testing + Library. Please do not confuse with this rule. diff --git a/lib/rules/no-await-sync-events.ts b/lib/rules/no-await-sync-events.ts index ea7ce157..1adad982 100644 --- a/lib/rules/no-await-sync-events.ts +++ b/lib/rules/no-await-sync-events.ts @@ -9,11 +9,14 @@ import { isProperty, } from '../node-utils'; +const USER_EVENT_ASYNC_EXCEPTIONS: string[] = ['type', 'keyboard']; +const VALID_EVENT_MODULES = ['fire-event', 'user-event'] as const; + export const RULE_NAME = 'no-await-sync-events'; export type MessageIds = 'noAwaitSyncEvents'; -type Options = []; - -const USER_EVENT_ASYNC_EXCEPTIONS: string[] = ['type', 'keyboard']; +type Options = [ + { eventModules?: readonly typeof VALID_EVENT_MODULES[number][] } +]; export default createTestingLibraryRule({ name: RULE_NAME, @@ -32,11 +35,23 @@ export default createTestingLibraryRule({ noAwaitSyncEvents: '`{{ name }}` is sync and does not need `await` operator', }, - schema: [], + schema: [ + { + type: 'object', + properties: { + eventModules: { + enum: VALID_EVENT_MODULES, + }, + }, + additionalProperties: false, + }, + ], }, - defaultOptions: [], + defaultOptions: [{ eventModules: VALID_EVENT_MODULES }], + + create(context, [options], helpers) { + const { eventModules = VALID_EVENT_MODULES } = options; - create(context, _, helpers) { // userEvent.type() and userEvent.keyboard() are exceptions, which returns a // Promise. But it is only necessary to wait when delay option other than 0 // is specified. So this rule has a special exception for the case await: @@ -50,14 +65,25 @@ export default createTestingLibraryRule({ return; } - const isSimulateEventMethod = - helpers.isUserEventMethod(simulateEventFunctionIdentifier) || - helpers.isFireEventMethod(simulateEventFunctionIdentifier); + const isUserEventMethod = helpers.isUserEventMethod( + simulateEventFunctionIdentifier + ); + const isFireEventMethod = helpers.isFireEventMethod( + simulateEventFunctionIdentifier + ); + const isSimulateEventMethod = isUserEventMethod || isFireEventMethod; if (!isSimulateEventMethod) { return; } + if (isFireEventMethod && !eventModules.includes('fire-event')) { + return; + } + if (isUserEventMethod && !eventModules.includes('user-event')) { + return; + } + const lastArg = node.arguments[node.arguments.length - 1]; const hasDelay = diff --git a/tests/lib/rules/no-await-sync-events.test.ts b/tests/lib/rules/no-await-sync-events.test.ts index e63c130b..bf9ebe43 100644 --- a/tests/lib/rules/no-await-sync-events.test.ts +++ b/tests/lib/rules/no-await-sync-events.test.ts @@ -166,6 +166,28 @@ ruleTester.run(RULE_NAME, rule, { }); `, }, + + // valid tests for fire-event when only user-event set in eventModules + ...FIRE_EVENT_FUNCTIONS.map((func) => ({ + code: ` + import { fireEvent } from '@testing-library/framework'; + test('should not report fireEvent.${func} sync event awaited', async() => { + await fireEvent.${func}('foo'); + }); + `, + options: [{ eventModules: 'user-event' }], + })), + + // valid tests for user-event when only fire-event set in eventModules + ...USER_EVENT_SYNC_FUNCTIONS.map((func) => ({ + code: ` + import userEvent from '@testing-library/user-event'; + test('should not report userEvent.${func} sync event awaited', async() => { + await userEvent.${func}('foo'); + }); + `, + options: [{ eventModules: 'fire-event' }], + })), ], invalid: [ @@ -210,6 +232,51 @@ ruleTester.run(RULE_NAME, rule, { } as const) ), + // sync fireEvent methods with await operator are not valid + // when only fire-event set in eventModules + ...FIRE_EVENT_FUNCTIONS.map( + (func) => + ({ + code: ` + import { fireEvent } from '@testing-library/framework'; + test('should report fireEvent.${func} sync event awaited', async() => { + await fireEvent.${func}('foo'); + }); + `, + options: [{ eventModules: 'fire-event' }], + errors: [ + { + line: 4, + column: 17, + messageId: 'noAwaitSyncEvents', + data: { name: `fireEvent.${func}` }, + }, + ], + } as const) + ), + // sync userEvent sync methods with await operator are not valid + // when only fire-event set in eventModules + ...USER_EVENT_SYNC_FUNCTIONS.map( + (func) => + ({ + code: ` + import userEvent from '@testing-library/user-event'; + test('should report userEvent.${func} sync event awaited', async() => { + await userEvent.${func}('foo'); + }); + `, + options: [{ eventModules: 'user-event' }], + errors: [ + { + line: 4, + column: 17, + messageId: 'noAwaitSyncEvents', + data: { name: `userEvent.${func}` }, + }, + ], + } as const) + ), + { code: ` import userEvent from '@testing-library/user-event';