Skip to content

Commit

Permalink
feat: adding prefer-user-event rule
Browse files Browse the repository at this point in the history
  • Loading branch information
gndelia committed Jul 3, 2020
1 parent b2ef721 commit 6fe3bea
Show file tree
Hide file tree
Showing 8 changed files with 407 additions and 27 deletions.
39 changes: 20 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,25 +125,26 @@ To enable this configuration use the `extends` property in your

## Supported Rules

| Rule | Description | Configurations | Fixable |
| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | ----------------------------------------------------------------- | ------------------ |
| [await-async-query](docs/rules/await-async-query.md) | Enforce async queries to have proper `await` | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| [await-async-utils](docs/rules/await-async-utils.md) | Enforce async utils to be awaited properly | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| [await-fire-event](docs/rules/await-fire-event.md) | Enforce async fire event methods to be awaited | ![vue-badge][] | |
| [consistent-data-testid](docs/rules/consistent-data-testid.md) | Ensure `data-testid` values match a provided regex. | | |
| [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][] | |
| [no-container](docs/rules/no-container.md) | Disallow the use of `container` methods | ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| [no-debug](docs/rules/no-debug.md) | Disallow the use of `debug` | ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| [no-dom-import](docs/rules/no-dom-import.md) | Disallow importing from DOM Testing Library | ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] |
| [no-manual-cleanup](docs/rules/no-manual-cleanup.md) | Disallow the use of `cleanup` | | |
| [no-multiple-assertions-wait-for](docs/rules/no-multiple-assertions-wait-for.md) | Disallow the use of multiple expect inside `waitFor` | | |
| [no-promise-in-fire-event](docs/rules/no-promise-in-fire-event.md) | Disallow the use of promises passed to a `fireEvent` method | | |
| [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][] | |
| [prefer-explicit-assert](docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than just `getBy*` queries | | |
| [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][] |
| [prefer-presence-queries](docs/rules/prefer-presence-queries.md) | Enforce specific queries when checking element is present or not | | |
| [prefer-screen-queries](docs/rules/prefer-screen-queries.md) | Suggest using screen while using queries | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| [prefer-wait-for](docs/rules/prefer-wait-for.md) | Use `waitFor` instead of deprecated wait methods | | ![fixable-badge][] |
| Rule | Description | Configurations | Fixable |
| -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | ------------------ |
| [await-async-query](docs/rules/await-async-query.md) | Enforce async queries to have proper `await` | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| [await-async-utils](docs/rules/await-async-utils.md) | Enforce async utils to be awaited properly | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| [await-fire-event](docs/rules/await-fire-event.md) | Enforce async fire event methods to be awaited | ![vue-badge][] | |
| [consistent-data-testid](docs/rules/consistent-data-testid.md) | Ensure `data-testid` values match a provided regex. | | |
| [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][] | |
| [no-container](docs/rules/no-container.md) | Disallow the use of `container` methods | ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| [no-debug](docs/rules/no-debug.md) | Disallow the use of `debug` | ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| [no-dom-import](docs/rules/no-dom-import.md) | Disallow importing from DOM Testing Library | ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] |
| [no-manual-cleanup](docs/rules/no-manual-cleanup.md) | Disallow the use of `cleanup` | | |
| [no-multiple-assertions-wait-for](docs/rules/no-multiple-assertions-wait-for.md) | Disallow the use of multiple expect inside `waitFor` | | |
| [no-promise-in-fire-event](docs/rules/no-promise-in-fire-event.md) | Disallow the use of promises passed to a `fireEvent` method | | |
| [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][] | |
| [prefer-explicit-assert](docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than just `getBy*` queries | | |
| [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][] |
| [prefer-presence-queries](docs/rules/prefer-presence-queries.md) | Enforce specific queries when checking element is present or not | | |
| [prefer-screen-queries](docs/rules/prefer-screen-queries.md) | Suggest using screen while using queries | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| [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][] | |
| [prefer-wait-for](docs/rules/prefer-wait-for.md) | Use `waitFor` instead of deprecated wait methods | | ![fixable-badge][] |

[build-badge]: https://img.shields.io/travis/testing-library/eslint-plugin-testing-library?style=flat-square
[build-url]: https://travis-ci.org/testing-library/eslint-plugin-testing-library
Expand Down
126 changes: 126 additions & 0 deletions docs/rules/prefer-user-event.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# Use [userEvent](https://github.com/testing-library/user-event) over using `fireEvent` for user interactions (prefer-user-event)

From
[testing-library/dom-testing-library#107](https://github.com/testing-library/dom-testing-library/issues/107):

> [...] it is becoming apparent the need to express user actions on a web page
> using a higher-level abstraction than `fireEvent`
`userEvent` adds related event calls from browsers to make tests more realistic than its counterpart `fireEvent`, which is a low-level api.
See the appendix at the end to check how are the events from `fireEvent` mapped to `userEvent`

## Rule Details

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`

See below for examples of valid usages of `fireEvent` methods with the configuration.

Examples of **incorrect** code for this rule:

```ts
// a method in fireEvent that has a userEvent equivalent
import { fireEvent } from '@testing-library/dom';
fireEvent.click(node);

// using fireEvent with an alias
import { fireEvent as fireEventAliased } from '@testing-library/dom';
fireEventAliased.click(node);

// using fireEvent after importing the entire library
import * as dom from '@testing-library/dom';
dom.fireEvent.click(node);
```

Examples of **correct** code for this rule:

```ts
// any userEvent method
userEvent.click();
// fireEvent method that does not have an alternative in userEvent
fireEvent.cut(node);
import * as dom from '@testing-library/dom';
dom.fireEvent.cut(node);

// a function called fireEvent that's not imported from testing-library
function fireEvent() {
// do stuff
}
fireEvent();
```

#### Options

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.

The configuration consists of an array of strings with the names of fireEvents methods to be excluded.
An example looks like this

```json
{
"rules": {
"prefer-user-event": [
"error",
{
"allowedMethods": ["click", "change"]
}
]
}
}
```

With this configuration example, the following use cases are considered valid

```ts
// using a named import
import { fireEvent } from '@testing-library/dom';
fireEvent.click(node);
fireEvent.change(node, { target: { value: 'foo' } });

// using fireEvent with an alias
import { fireEvent as fireEventAliased } from '@testing-library/dom';
fireEventAliased.click(node);
fireEventAliased.change(node, { target: { value: 'foo' } });

// using fireEvent after importing the entire library
import * as dom from '@testing-library/dom';
dom.fireEvent.click(node);
dom.fireEvent.change(node, { target: { value: 'foo' } });
```

## When Not To Use It

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)

## Further Reading

- [userEvent repository](https://github.com/testing-library/user-event)
- [userEvent in the react-testing-library docs](https://testing-library.com/docs/ecosystem-user-event)

## Appendix

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)

| fireEvent method | Possible options in userEvent |
| ---------------- | ----------------------------------------------------------------------------------------------------------- |
| `click` | <ul><li>`click`</li><li>`type`</li><li>`selectOptions`</li><li>`deselectOptions`</li></ul> |
| `change` | <ul><li>`upload`</li><li>`type`</li><li>`clear`</li><li>`selectOptions`</li><li>`deselectOptions`</li></ul> |
| `dblClick` | <ul><li>`dblClick`</li></ul> |
| `input` | <ul><li>`type`</li><li>`upload`</li><li>`selectOptions`</li><li>`deselectOptions`</li><li>`paste`</li></ul> |
| `keyDown` | <ul><li>`type`</li><li>`tab`</li></ul> |
| `keyPress` | <ul><li>`type`</li></ul> |
| `keyUp` | <ul><li>`type`</li><li>`tab`</li></ul> |
| `mouseDown` | <ul><li>`click`</li><li>`dblClick`</li><li>`selectOptions`</li><li>`deselectOptions`</li></ul> |
| `mouseEnter` | <ul><li>`hover`</li><li>`selectOptions`</li><li>`deselectOptions`</li></ul> |
| `mouseLeave` | <ul><li>`unhover`</li></ul> |
| `mouseMove` | <ul><li>`hover`</li><li>`unhover`</li><li>`selectOptions`</li><li>`deselectOptions`</li></ul> |
| `mouseOut` | <ul><li>`unhover`</li></ul> |
| `mouseOver` | <ul><li>`hover`</li><li>`selectOptions`</li><li>`deselectOptions`</li></ul> |
| `mouseUp` | <ul><li>`click`</li><li>`dblClick`</li><li>`selectOptions`</li><li>`deselectOptions`</li></ul> |
| `paste` | <ul><li>`paste`</li></ul> |
| `pointerDown` | <ul><li>`click`</li><li>`dblClick`</li><li>`selectOptions`</li><li>`deselectOptions`</li></ul> |
| `pointerEnter` | <ul><li>`hover`</li><li>`selectOptions`</li><li>`deselectOptions`</li></ul> |
| `pointerLeave` | <ul><li>`unhover`</li></ul> |
| `pointerMove` | <ul><li>`hover`</li><li>`unhover`</li><li>`selectOptions`</li><li>`deselectOptions`</li></ul> |
| `pointerOut` | <ul><li>`unhover`</li></ul> |
| `pointerOver` | <ul><li>`hover`</li><li>`selectOptions`</li><li>`deselectOptions`</li></ul> |
| `pointerUp` | <ul><li>`click`</li><li>`dblClick`</li><li>`selectOptions`</li><li>`deselectOptions`</li></ul> |
5 changes: 5 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import noPromiseInFireEvent from './rules/no-promise-in-fire-event';
import preferExplicitAssert from './rules/prefer-explicit-assert';
import preferPresenceQueries from './rules/prefer-presence-queries';
import preferScreenQueries from './rules/prefer-screen-queries';
import preferUserEvent from './rules/prefer-user-event';
import preferWaitFor from './rules/prefer-wait-for';
import noMultipleAssertionsWaitFor from './rules/no-multiple-assertions-wait-for'
import preferFindBy from './rules/prefer-find-by';
Expand All @@ -33,6 +34,7 @@ const rules = {
'prefer-find-by': preferFindBy,
'prefer-presence-queries': preferPresenceQueries,
'prefer-screen-queries': preferScreenQueries,
'prefer-user-event': preferUserEvent,
'prefer-wait-for': preferWaitFor,
};

Expand All @@ -50,13 +52,15 @@ const angularRules = {
...domRules,
'testing-library/no-container': 'error',
'testing-library/no-debug': 'warn',
'testing-library/prefer-user-event': 'warn',
'testing-library/no-dom-import': ['error', 'angular'],
};

const reactRules = {
...domRules,
'testing-library/no-container': 'error',
'testing-library/no-debug': 'warn',
'testing-library/prefer-user-event': 'warn',
'testing-library/no-dom-import': ['error', 'react'],
};

Expand All @@ -65,6 +69,7 @@ const vueRules = {
'testing-library/await-fire-event': 'error',
'testing-library/no-container': 'error',
'testing-library/no-debug': 'warn',
'testing-library/prefer-user-event': 'warn',
'testing-library/no-dom-import': ['error', 'vue'],
};

Expand Down
9 changes: 1 addition & 8 deletions lib/rules/no-debug.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils';
import { getDocsUrl, LIBRARY_MODULES } from '../utils';
import { getDocsUrl, LIBRARY_MODULES, hasTestingLibraryImportModule } from '../utils';
import {
isObjectPattern,
isProperty,
Expand All @@ -13,13 +13,6 @@ import {

export const RULE_NAME = 'no-debug';

function hasTestingLibraryImportModule(
importDeclarationNode: TSESTree.ImportDeclaration
) {
const literal = importDeclarationNode.source;
return LIBRARY_MODULES.some(module => module === literal.value);
}

export default ESLintUtils.RuleCreator(getDocsUrl)({
name: RULE_NAME,
meta: {
Expand Down
Loading

0 comments on commit 6fe3bea

Please sign in to comment.