Skip to content

Commit

Permalink
feat(no-await-sync-events): add eventModules option (#569)
Browse files Browse the repository at this point in the history
* feat(no-await-sync-events): add eventModules to rule options

* feat(no-await-sync-events): report only when event module enabled

* docs(no-await-sync-events): add options section

* test: remove "name" property

Closes #567
  • Loading branch information
Belco90 committed Apr 11, 2022
1 parent 7f751e1 commit 88f6735
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 14 deletions.
45 changes: 40 additions & 5 deletions docs/rules/no-await-sync-events.md
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
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
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

0 comments on commit 88f6735

Please sign in to comment.