Skip to content

Commit 6fe3bea

Browse files
committed
feat: adding prefer-user-event rule
1 parent b2ef721 commit 6fe3bea

File tree

8 files changed

+407
-27
lines changed

8 files changed

+407
-27
lines changed

README.md

+20-19
Original file line numberDiff line numberDiff line change
@@ -125,25 +125,26 @@ To enable this configuration use the `extends` property in your
125125

126126
## Supported Rules
127127

128-
| Rule | Description | Configurations | Fixable |
129-
| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | ----------------------------------------------------------------- | ------------------ |
130-
| [await-async-query](docs/rules/await-async-query.md) | Enforce async queries to have proper `await` | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
131-
| [await-async-utils](docs/rules/await-async-utils.md) | Enforce async utils to be awaited properly | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
132-
| [await-fire-event](docs/rules/await-fire-event.md) | Enforce async fire event methods to be awaited | ![vue-badge][] | |
133-
| [consistent-data-testid](docs/rules/consistent-data-testid.md) | Ensure `data-testid` values match a provided regex. | | |
134-
| [no-await-sync-query](docs/rules/no-await-sync-query.md) | Disallow unnecessary `await` for sync queries | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
135-
| [no-container](docs/rules/no-container.md) | Disallow the use of `container` methods | ![angular-badge][] ![react-badge][] ![vue-badge][] | |
136-
| [no-debug](docs/rules/no-debug.md) | Disallow the use of `debug` | ![angular-badge][] ![react-badge][] ![vue-badge][] | |
137-
| [no-dom-import](docs/rules/no-dom-import.md) | Disallow importing from DOM Testing Library | ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] |
138-
| [no-manual-cleanup](docs/rules/no-manual-cleanup.md) | Disallow the use of `cleanup` | | |
139-
| [no-multiple-assertions-wait-for](docs/rules/no-multiple-assertions-wait-for.md) | Disallow the use of multiple expect inside `waitFor` | | |
140-
| [no-promise-in-fire-event](docs/rules/no-promise-in-fire-event.md) | Disallow the use of promises passed to a `fireEvent` method | | |
141-
| [no-wait-for-empty-callback](docs/rules/no-wait-for-empty-callback.md) | Disallow empty callbacks for `waitFor` and `waitForElementToBeRemoved` | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
142-
| [prefer-explicit-assert](docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than just `getBy*` queries | | |
143-
| [prefer-find-by](docs/rules/prefer-find-by.md) | Suggest using `findBy*` methods instead of the `waitFor` + `getBy` queries | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] |
144-
| [prefer-presence-queries](docs/rules/prefer-presence-queries.md) | Enforce specific queries when checking element is present or not | | |
145-
| [prefer-screen-queries](docs/rules/prefer-screen-queries.md) | Suggest using screen while using queries | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
146-
| [prefer-wait-for](docs/rules/prefer-wait-for.md) | Use `waitFor` instead of deprecated wait methods | | ![fixable-badge][] |
128+
| Rule | Description | Configurations | Fixable |
129+
| -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | ------------------ |
130+
| [await-async-query](docs/rules/await-async-query.md) | Enforce async queries to have proper `await` | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
131+
| [await-async-utils](docs/rules/await-async-utils.md) | Enforce async utils to be awaited properly | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
132+
| [await-fire-event](docs/rules/await-fire-event.md) | Enforce async fire event methods to be awaited | ![vue-badge][] | |
133+
| [consistent-data-testid](docs/rules/consistent-data-testid.md) | Ensure `data-testid` values match a provided regex. | | |
134+
| [no-await-sync-query](docs/rules/no-await-sync-query.md) | Disallow unnecessary `await` for sync queries | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
135+
| [no-container](docs/rules/no-container.md) | Disallow the use of `container` methods | ![angular-badge][] ![react-badge][] ![vue-badge][] | |
136+
| [no-debug](docs/rules/no-debug.md) | Disallow the use of `debug` | ![angular-badge][] ![react-badge][] ![vue-badge][] | |
137+
| [no-dom-import](docs/rules/no-dom-import.md) | Disallow importing from DOM Testing Library | ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] |
138+
| [no-manual-cleanup](docs/rules/no-manual-cleanup.md) | Disallow the use of `cleanup` | | |
139+
| [no-multiple-assertions-wait-for](docs/rules/no-multiple-assertions-wait-for.md) | Disallow the use of multiple expect inside `waitFor` | | |
140+
| [no-promise-in-fire-event](docs/rules/no-promise-in-fire-event.md) | Disallow the use of promises passed to a `fireEvent` method | | |
141+
| [no-wait-for-empty-callback](docs/rules/no-wait-for-empty-callback.md) | Disallow empty callbacks for `waitFor` and `waitForElementToBeRemoved` | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
142+
| [prefer-explicit-assert](docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than just `getBy*` queries | | |
143+
| [prefer-find-by](docs/rules/prefer-find-by.md) | Suggest using `findBy*` methods instead of the `waitFor` + `getBy` queries | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] |
144+
| [prefer-presence-queries](docs/rules/prefer-presence-queries.md) | Enforce specific queries when checking element is present or not | | |
145+
| [prefer-screen-queries](docs/rules/prefer-screen-queries.md) | Suggest using screen while using queries | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
146+
| [prefer-user-event](docs/rules/prefer-user-event.md) | Suggest using `userEvent` library instead of `fireEvent` for simulating user interaction | ![angular-badge][] ![react-badge][] ![vue-badge][] | |
147+
| [prefer-wait-for](docs/rules/prefer-wait-for.md) | Use `waitFor` instead of deprecated wait methods | | ![fixable-badge][] |
147148

148149
[build-badge]: https://img.shields.io/travis/testing-library/eslint-plugin-testing-library?style=flat-square
149150
[build-url]: https://travis-ci.org/testing-library/eslint-plugin-testing-library

docs/rules/prefer-user-event.md

+126
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# Use [userEvent](https://github.com/testing-library/user-event) over using `fireEvent` for user interactions (prefer-user-event)
2+
3+
From
4+
[testing-library/dom-testing-library#107](https://github.com/testing-library/dom-testing-library/issues/107):
5+
6+
> [...] it is becoming apparent the need to express user actions on a web page
7+
> using a higher-level abstraction than `fireEvent`
8+
9+
`userEvent` adds related event calls from browsers to make tests more realistic than its counterpart `fireEvent`, which is a low-level api.
10+
See the appendix at the end to check how are the events from `fireEvent` mapped to `userEvent`
11+
12+
## Rule Details
13+
14+
This rules enforces the usage of [userEvent](https://github.com/testing-library/user-event) methods over `fireEvent`. By default, the methods from `userEvent` takes precedence, but you add exceptions by configuring the rule in `.eslintrc`
15+
16+
See below for examples of valid usages of `fireEvent` methods with the configuration.
17+
18+
Examples of **incorrect** code for this rule:
19+
20+
```ts
21+
// a method in fireEvent that has a userEvent equivalent
22+
import { fireEvent } from '@testing-library/dom';
23+
fireEvent.click(node);
24+
25+
// using fireEvent with an alias
26+
import { fireEvent as fireEventAliased } from '@testing-library/dom';
27+
fireEventAliased.click(node);
28+
29+
// using fireEvent after importing the entire library
30+
import * as dom from '@testing-library/dom';
31+
dom.fireEvent.click(node);
32+
```
33+
34+
Examples of **correct** code for this rule:
35+
36+
```ts
37+
// any userEvent method
38+
userEvent.click();
39+
// fireEvent method that does not have an alternative in userEvent
40+
fireEvent.cut(node);
41+
import * as dom from '@testing-library/dom';
42+
dom.fireEvent.cut(node);
43+
44+
// a function called fireEvent that's not imported from testing-library
45+
function fireEvent() {
46+
// do stuff
47+
}
48+
fireEvent();
49+
```
50+
51+
#### Options
52+
53+
This rule allows to exclude specific functions with an equivalent in `userEvent` through configuration. This is useful if you need to allow an event from `fireEvent` to be used in the solution. For specific scenarios, you might want to consider disabling the rule inline.
54+
55+
The configuration consists of an array of strings with the names of fireEvents methods to be excluded.
56+
An example looks like this
57+
58+
```json
59+
{
60+
"rules": {
61+
"prefer-user-event": [
62+
"error",
63+
{
64+
"allowedMethods": ["click", "change"]
65+
}
66+
]
67+
}
68+
}
69+
```
70+
71+
With this configuration example, the following use cases are considered valid
72+
73+
```ts
74+
// using a named import
75+
import { fireEvent } from '@testing-library/dom';
76+
fireEvent.click(node);
77+
fireEvent.change(node, { target: { value: 'foo' } });
78+
79+
// using fireEvent with an alias
80+
import { fireEvent as fireEventAliased } from '@testing-library/dom';
81+
fireEventAliased.click(node);
82+
fireEventAliased.change(node, { target: { value: 'foo' } });
83+
84+
// using fireEvent after importing the entire library
85+
import * as dom from '@testing-library/dom';
86+
dom.fireEvent.click(node);
87+
dom.fireEvent.change(node, { target: { value: 'foo' } });
88+
```
89+
90+
## When Not To Use It
91+
92+
When you don't want to use `userEvent`, such as if a legacy codebase is still using `fireEvent` or you need to have more low-level control over firing events (rather than the recommended approach of testing from a user's perspective)
93+
94+
## Further Reading
95+
96+
- [userEvent repository](https://github.com/testing-library/user-event)
97+
- [userEvent in the react-testing-library docs](https://testing-library.com/docs/ecosystem-user-event)
98+
99+
## Appendix
100+
101+
The following table lists all the possible equivalents from the low-level API `fireEvent` to the higher abstraction API `userEvent`. All the events not listed here do not have an equivalent (yet)
102+
103+
| fireEvent method | Possible options in userEvent |
104+
| ---------------- | ----------------------------------------------------------------------------------------------------------- |
105+
| `click` | <ul><li>`click`</li><li>`type`</li><li>`selectOptions`</li><li>`deselectOptions`</li></ul> |
106+
| `change` | <ul><li>`upload`</li><li>`type`</li><li>`clear`</li><li>`selectOptions`</li><li>`deselectOptions`</li></ul> |
107+
| `dblClick` | <ul><li>`dblClick`</li></ul> |
108+
| `input` | <ul><li>`type`</li><li>`upload`</li><li>`selectOptions`</li><li>`deselectOptions`</li><li>`paste`</li></ul> |
109+
| `keyDown` | <ul><li>`type`</li><li>`tab`</li></ul> |
110+
| `keyPress` | <ul><li>`type`</li></ul> |
111+
| `keyUp` | <ul><li>`type`</li><li>`tab`</li></ul> |
112+
| `mouseDown` | <ul><li>`click`</li><li>`dblClick`</li><li>`selectOptions`</li><li>`deselectOptions`</li></ul> |
113+
| `mouseEnter` | <ul><li>`hover`</li><li>`selectOptions`</li><li>`deselectOptions`</li></ul> |
114+
| `mouseLeave` | <ul><li>`unhover`</li></ul> |
115+
| `mouseMove` | <ul><li>`hover`</li><li>`unhover`</li><li>`selectOptions`</li><li>`deselectOptions`</li></ul> |
116+
| `mouseOut` | <ul><li>`unhover`</li></ul> |
117+
| `mouseOver` | <ul><li>`hover`</li><li>`selectOptions`</li><li>`deselectOptions`</li></ul> |
118+
| `mouseUp` | <ul><li>`click`</li><li>`dblClick`</li><li>`selectOptions`</li><li>`deselectOptions`</li></ul> |
119+
| `paste` | <ul><li>`paste`</li></ul> |
120+
| `pointerDown` | <ul><li>`click`</li><li>`dblClick`</li><li>`selectOptions`</li><li>`deselectOptions`</li></ul> |
121+
| `pointerEnter` | <ul><li>`hover`</li><li>`selectOptions`</li><li>`deselectOptions`</li></ul> |
122+
| `pointerLeave` | <ul><li>`unhover`</li></ul> |
123+
| `pointerMove` | <ul><li>`hover`</li><li>`unhover`</li><li>`selectOptions`</li><li>`deselectOptions`</li></ul> |
124+
| `pointerOut` | <ul><li>`unhover`</li></ul> |
125+
| `pointerOver` | <ul><li>`hover`</li><li>`selectOptions`</li><li>`deselectOptions`</li></ul> |
126+
| `pointerUp` | <ul><li>`click`</li><li>`dblClick`</li><li>`selectOptions`</li><li>`deselectOptions`</li></ul> |

lib/index.ts

+5
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import noPromiseInFireEvent from './rules/no-promise-in-fire-event';
1212
import preferExplicitAssert from './rules/prefer-explicit-assert';
1313
import preferPresenceQueries from './rules/prefer-presence-queries';
1414
import preferScreenQueries from './rules/prefer-screen-queries';
15+
import preferUserEvent from './rules/prefer-user-event';
1516
import preferWaitFor from './rules/prefer-wait-for';
1617
import noMultipleAssertionsWaitFor from './rules/no-multiple-assertions-wait-for'
1718
import preferFindBy from './rules/prefer-find-by';
@@ -33,6 +34,7 @@ const rules = {
3334
'prefer-find-by': preferFindBy,
3435
'prefer-presence-queries': preferPresenceQueries,
3536
'prefer-screen-queries': preferScreenQueries,
37+
'prefer-user-event': preferUserEvent,
3638
'prefer-wait-for': preferWaitFor,
3739
};
3840

@@ -50,13 +52,15 @@ const angularRules = {
5052
...domRules,
5153
'testing-library/no-container': 'error',
5254
'testing-library/no-debug': 'warn',
55+
'testing-library/prefer-user-event': 'warn',
5356
'testing-library/no-dom-import': ['error', 'angular'],
5457
};
5558

5659
const reactRules = {
5760
...domRules,
5861
'testing-library/no-container': 'error',
5962
'testing-library/no-debug': 'warn',
63+
'testing-library/prefer-user-event': 'warn',
6064
'testing-library/no-dom-import': ['error', 'react'],
6165
};
6266

@@ -65,6 +69,7 @@ const vueRules = {
6569
'testing-library/await-fire-event': 'error',
6670
'testing-library/no-container': 'error',
6771
'testing-library/no-debug': 'warn',
72+
'testing-library/prefer-user-event': 'warn',
6873
'testing-library/no-dom-import': ['error', 'vue'],
6974
};
7075

lib/rules/no-debug.ts

+1-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils';
2-
import { getDocsUrl, LIBRARY_MODULES } from '../utils';
2+
import { getDocsUrl, LIBRARY_MODULES, hasTestingLibraryImportModule } from '../utils';
33
import {
44
isObjectPattern,
55
isProperty,
@@ -13,13 +13,6 @@ import {
1313

1414
export const RULE_NAME = 'no-debug';
1515

16-
function hasTestingLibraryImportModule(
17-
importDeclarationNode: TSESTree.ImportDeclaration
18-
) {
19-
const literal = importDeclarationNode.source;
20-
return LIBRARY_MODULES.some(module => module === literal.value);
21-
}
22-
2316
export default ESLintUtils.RuleCreator(getDocsUrl)({
2417
name: RULE_NAME,
2518
meta: {

0 commit comments

Comments
 (0)