Skip to content

feat(no-await-sync-events): add eventModules option #569

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 40 additions & 5 deletions docs/rules/no-await-sync-events.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.

Expand All @@ -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');
// ...
Expand Down Expand Up @@ -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.
44 changes: 35 additions & 9 deletions lib/rules/no-await-sync-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Options, MessageIds>({
name: RULE_NAME,
Expand All @@ -32,11 +35,23 @@ export default createTestingLibraryRule<Options, MessageIds>({
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:
Expand All @@ -50,14 +65,25 @@ export default createTestingLibraryRule<Options, MessageIds>({
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 =
Expand Down
67 changes: 67 additions & 0 deletions tests/lib/rules/no-await-sync-events.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down Expand Up @@ -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';
Expand Down