This commit is contained in:
Iliyan Angelov
2025-09-14 23:24:25 +03:00
commit c67067a2a4
71311 changed files with 6800714 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Mario Beltrán Alarcón
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,457 @@
<div align="center">
<a href="https://eslint.org/">
<img width="150" height="150" src="https://raw.githubusercontent.com/eslint/eslint/main/docs/src/static/favicon.png">
</a>
<a href="https://testing-library.com/">
<img width="150" height="150" src="https://raw.githubusercontent.com/testing-library/dom-testing-library/master/other/octopus.png">
</a>
<h1>eslint-plugin-testing-library</h1>
<p>ESLint plugin to follow best practices and anticipate common mistakes when writing tests with Testing Library</p>
</div>
---
[![Build status][build-badge]][build-url]
[![Package version][version-badge]][version-url]
[![eslint-remote-tester][eslint-remote-tester-badge]][eslint-remote-tester-workflow]
[![eslint-plugin-testing-library][package-health-badge]][package-health-url]
[![MIT License][license-badge]][license-url]
<br />
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg?style=flat-square)](https://github.com/semantic-release/semantic-release)
[![PRs Welcome][pr-badge]][pr-url]
[![All Contributors][all-contributors-badge]](#contributors-)
<br />
[![Watch on Github][gh-watchers-badge]][gh-watchers-url]
[![Star on Github][gh-stars-badge]][gh-stars-url]
[![Tweet][tweet-badge]][tweet-url]
## Installation
You'll first need to install [ESLint](https://eslint.org):
```shell
$ npm install --save-dev eslint
# or
$ yarn add --dev eslint
```
Next, install `eslint-plugin-testing-library`:
```shell
$ npm install --save-dev eslint-plugin-testing-library
# or
$ yarn add --dev eslint-plugin-testing-library
```
**Note:** If you installed ESLint globally (using the `-g` flag) then you must also install `eslint-plugin-testing-library` globally.
## Migrating
You can find detailed guides for migrating `eslint-plugin-testing-library` in the [migration guide docs](docs/migration-guides):
- [Migrate guide for v4](docs/migration-guides/v4.md)
- [Migrate guide for v5](docs/migration-guides/v5.md)
## Usage
Add `testing-library` to the plugins section of your `.eslintrc.js` configuration file. You can omit the `eslint-plugin-` prefix:
```js
module.exports = {
plugins: ['testing-library'],
};
```
Then configure the rules you want to use within `rules` property of your `.eslintrc`:
```js
module.exports = {
rules: {
'testing-library/await-async-query': 'error',
'testing-library/no-await-sync-query': 'error',
'testing-library/no-debugging-utils': 'warn',
'testing-library/no-dom-import': 'off',
},
};
```
### Run the plugin only against test files
With the default setup mentioned before, `eslint-plugin-testing-library` will be run against your whole codebase. If you want to run this plugin only against your tests files, you have the following options:
#### ESLint `overrides`
One way of restricting ESLint config by file patterns is by using [ESLint `overrides`](https://eslint.org/docs/user-guide/configuring/configuration-files#configuration-based-on-glob-patterns).
Assuming you are using the same pattern for your test files as [Jest by default](https://jestjs.io/docs/configuration#testmatch-arraystring), the following config would run `eslint-plugin-testing-library` only against your test files:
```js
// .eslintrc.js
module.exports = {
// 1) Here we have our usual config which applies to the whole project, so we don't put testing-library preset here.
extends: ['airbnb', 'plugin:prettier/recommended'],
// 2) We load other plugins than eslint-plugin-testing-library globally if we want to.
plugins: ['react-hooks'],
overrides: [
{
// 3) Now we enable eslint-plugin-testing-library rules or preset only for matching testing files!
files: ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[jt]s?(x)'],
extends: ['plugin:testing-library/react'],
},
],
};
```
#### ESLint Cascading and Hierarchy
Another approach for customizing ESLint config by paths is through [ESLint Cascading and Hierarchy](https://eslint.org/docs/user-guide/configuring/configuration-files#cascading-and-hierarchy). This is useful if all your tests are placed under the same folder, so you can place there another `.eslintrc` where you enable `eslint-plugin-testing-library` for applying it only to the files under such folder, rather than enabling it on your global `.eslintrc` which would apply to your whole project.
## Shareable configurations
This plugin exports several recommended configurations that enforce good practices for specific Testing Library packages.
You can find more info about enabled rules in the [Supported Rules section](#supported-rules), under the `Configurations` column.
Since each one of these configurations is aimed at a particular Testing Library package, they are not extendable between them, so you should use only one of them at once per `.eslintrc` file. For example, if you want to enable recommended configuration for React, you don't need to combine it somehow with DOM one:
```js
// ❌ Don't do this
module.exports = {
extends: ['plugin:testing-library/dom', 'plugin:testing-library/react'],
};
```
```js
// ✅ Just do this instead
module.exports = {
extends: ['plugin:testing-library/react'],
};
```
### DOM Testing Library
Enforces recommended rules for DOM Testing Library.
To enable this configuration use the `extends` property in your
`.eslintrc.js` config file:
```js
module.exports = {
extends: ['plugin:testing-library/dom'],
};
```
### Angular
Enforces recommended rules for Angular Testing Library.
To enable this configuration use the `extends` property in your
`.eslintrc.js` config file:
```js
module.exports = {
extends: ['plugin:testing-library/angular'],
};
```
### React
Enforces recommended rules for React Testing Library.
To enable this configuration use the `extends` property in your
`.eslintrc.js` config file:
```js
module.exports = {
extends: ['plugin:testing-library/react'],
};
```
### Vue
Enforces recommended rules for Vue Testing Library.
To enable this configuration use the `extends` property in your
`.eslintrc.js` config file:
```js
module.exports = {
extends: ['plugin:testing-library/vue'],
};
```
### Marko
Enforces recommended rules for Marko Testing Library.
To enable this configuration use the `extends` property in your
`.eslintrc.js` config file:
```js
module.exports = {
extends: ['plugin:testing-library/marko'],
};
```
## Supported Rules
> Remember that all rules from this plugin are prefixed by `"testing-library/"`
<!-- begin auto-generated rules list -->
💼 Configurations enabled in.\
🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).
| Name                            | Description | 💼 | 🔧 |
| :------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------- | :-- |
| [await-async-query](docs/rules/await-async-query.md) | Enforce promises from async queries to be handled | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-vue][] | |
| [await-async-utils](docs/rules/await-async-utils.md) | Enforce promises from async utils to be awaited properly | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-vue][] | |
| [await-fire-event](docs/rules/await-fire-event.md) | Enforce promises from `fireEvent` methods to be handled | ![badge-marko][] ![badge-vue][] | |
| [consistent-data-testid](docs/rules/consistent-data-testid.md) | Ensures consistent usage of `data-testid` | | |
| [no-await-sync-events](docs/rules/no-await-sync-events.md) | Disallow unnecessary `await` for sync events | | |
| [no-await-sync-query](docs/rules/no-await-sync-query.md) | Disallow unnecessary `await` for sync queries | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-vue][] | |
| [no-container](docs/rules/no-container.md) | Disallow the use of `container` methods | ![badge-angular][] ![badge-marko][] ![badge-react][] ![badge-vue][] | |
| [no-debugging-utils](docs/rules/no-debugging-utils.md) | Disallow the use of debugging utilities like `debug` | ![badge-angular][] ![badge-marko][] ![badge-react][] ![badge-vue][] | |
| [no-dom-import](docs/rules/no-dom-import.md) | Disallow importing from DOM Testing Library | ![badge-angular][] ![badge-marko][] ![badge-react][] ![badge-vue][] | 🔧 |
| [no-global-regexp-flag-in-query](docs/rules/no-global-regexp-flag-in-query.md) | Disallow the use of the global RegExp flag (/g) in queries | | 🔧 |
| [no-manual-cleanup](docs/rules/no-manual-cleanup.md) | Disallow the use of `cleanup` | | |
| [no-node-access](docs/rules/no-node-access.md) | Disallow direct Node access | ![badge-angular][] ![badge-marko][] ![badge-react][] ![badge-vue][] | |
| [no-promise-in-fire-event](docs/rules/no-promise-in-fire-event.md) | Disallow the use of promises passed to a `fireEvent` method | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-vue][] | |
| [no-render-in-setup](docs/rules/no-render-in-setup.md) | Disallow the use of `render` in testing frameworks setup functions | ![badge-angular][] ![badge-marko][] ![badge-react][] ![badge-vue][] | |
| [no-unnecessary-act](docs/rules/no-unnecessary-act.md) | Disallow wrapping Testing Library utils or empty callbacks in `act` | ![badge-marko][] ![badge-react][] | |
| [no-wait-for-empty-callback](docs/rules/no-wait-for-empty-callback.md) | Disallow empty callbacks for `waitFor` and `waitForElementToBeRemoved` | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-vue][] | |
| [no-wait-for-multiple-assertions](docs/rules/no-wait-for-multiple-assertions.md) | Disallow the use of multiple `expect` calls inside `waitFor` | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-vue][] | |
| [no-wait-for-side-effects](docs/rules/no-wait-for-side-effects.md) | Disallow the use of side effects in `waitFor` | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-vue][] | |
| [no-wait-for-snapshot](docs/rules/no-wait-for-snapshot.md) | Ensures no snapshot is generated inside of a `waitFor` call | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-vue][] | |
| [prefer-explicit-assert](docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than standalone queries | | |
| [prefer-find-by](docs/rules/prefer-find-by.md) | Suggest using `find(All)By*` query instead of `waitFor` + `get(All)By*` to wait for elements | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-vue][] | 🔧 |
| [prefer-presence-queries](docs/rules/prefer-presence-queries.md) | Ensure appropriate `get*`/`query*` queries are used with their respective matchers | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-vue][] | |
| [prefer-query-by-disappearance](docs/rules/prefer-query-by-disappearance.md) | Suggest using `queryBy*` queries when waiting for disappearance | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-vue][] | |
| [prefer-query-matchers](docs/rules/prefer-query-matchers.md) | Ensure the configured `get*`/`query*` query is used with the corresponding matchers | | |
| [prefer-screen-queries](docs/rules/prefer-screen-queries.md) | Suggest using `screen` while querying | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-vue][] | |
| [prefer-user-event](docs/rules/prefer-user-event.md) | Suggest using `userEvent` over `fireEvent` for simulating user interactions | | |
| [prefer-wait-for](docs/rules/prefer-wait-for.md) | Use `waitFor` instead of deprecated wait methods | | 🔧 |
| [render-result-naming-convention](docs/rules/render-result-naming-convention.md) | Enforce a valid naming for return value from `render` | ![badge-angular][] ![badge-marko][] ![badge-react][] ![badge-vue][] | |
<!-- end auto-generated rules list -->
## Aggressive Reporting
In v4 this plugin introduced a new feature called "Aggressive Reporting", which intends to detect Testing Library utils usages even if they don't come directly from a Testing Library package (i.e. [using a custom utility file to re-export everything from Testing Library](https://testing-library.com/docs/react-testing-library/setup/#custom-render)). You can [read more about this feature here](docs/migration-guides/v4.md#aggressive-reporting).
If you are looking to restricting or switching off this feature, please refer to the [Shared Settings section](#shared-settings) to do so.
## Shared Settings
There are some configuration options available that will be shared across all the plugin rules. This is achieved using [ESLint Shared Settings](https://eslint.org/docs/user-guide/configuring/configuration-files#adding-shared-settings). These Shared Settings are meant to be used if you need to restrict or switch off the Aggressive Reporting, which is an out of the box advanced feature to lint Testing Library usages in a simpler way for most of the users. **So please before configuring any of these settings**, read more about [the advantages of `eslint-plugin-testing-library` Aggressive Reporting feature](docs/migration-guides/v4.md#aggressive-reporting), and [how it's affected by these settings](docs/migration-guides/v4.md#shared-settings).
If you are sure about configuring the settings, these are the options available:
### `testing-library/utils-module`
The name of your custom utility file from where you re-export everything from the Testing Library package, or `"off"` to switch related Aggressive Reporting mechanism off. Relates to [Aggressive Imports Reporting](docs/migration-guides/v4.md#imports).
```js
// .eslintrc.js
module.exports = {
settings: {
'testing-library/utils-module': 'my-custom-test-utility-file',
},
};
```
[You can find more details about the `utils-module` setting here](docs/migration-guides/v4.md#testing-libraryutils-module).
### `testing-library/custom-renders`
A list of function names that are valid as Testing Library custom renders, or `"off"` to switch related Aggressive Reporting mechanism off. Relates to [Aggressive Renders Reporting](docs/migration-guides/v4.md#renders).
```js
// .eslintrc.js
module.exports = {
settings: {
'testing-library/custom-renders': ['display', 'renderWithProviders'],
},
};
```
[You can find more details about the `custom-renders` setting here](docs/migration-guides/v4.md#testing-librarycustom-renders).
### `testing-library/custom-queries`
A list of query names/patterns that are valid as Testing Library custom queries, or `"off"` to switch related Aggressive Reporting mechanism off. Relates to [Aggressive Reporting - Queries](docs/migration-guides/v4.md#queries)
```js
// .eslintrc.js
module.exports = {
settings: {
'testing-library/custom-queries': ['ByIcon', 'getByComplexText'],
},
};
```
[You can find more details about the `custom-queries` setting here](docs/migration-guides/v4.md#testing-librarycustom-queries).
### Switching all Aggressive Reporting mechanisms off
Since each Shared Setting is related to one Aggressive Reporting mechanism, and they accept `"off"` to opt out of that mechanism, you can switch the entire feature off by doing:
```js
// .eslintrc.js
module.exports = {
settings: {
'testing-library/utils-module': 'off',
'testing-library/custom-renders': 'off',
'testing-library/custom-queries': 'off',
},
};
```
## Troubleshooting
### Errors reported in non-testing files
If you find ESLint errors related to `eslint-plugin-testing-library` in files other than testing, this could be caused by [Aggressive Reporting](#aggressive-reporting).
You can avoid this by:
1. [running `eslint-plugin-testing-library` only against testing files](#run-the-plugin-only-against-test-files)
2. [limiting the scope of Aggressive Reporting through Shared Settings](#shared-settings)
3. [switching Aggressive Reporting feature off](#switching-all-aggressive-reporting-mechanisms-off)
If you think the error you are getting is not related to this at all, please [fill a new issue](https://github.com/testing-library/eslint-plugin-testing-library/issues/new/choose) with as many details as possible.
### False positives in testing files
If you are getting false positive ESLint errors in your testing files, this could be caused by [Aggressive Reporting](#aggressive-reporting).
You can avoid this by:
1. [limiting the scope of Aggressive Reporting through Shared Settings](#shared-settings)
2. [switching Aggressive Reporting feature off](#switching-all-aggressive-reporting-mechanisms-off)
If you think the error you are getting is not related to this at all, please [fill a new issue](https://github.com/testing-library/eslint-plugin-testing-library/issues/new/choose) with as many details as possible.
## Other documentation
- [Semantic Versioning Policy](/docs/semantic-versioning-policy.md)
## Contributors ✨
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tbody>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://mario.dev"><img src="https://avatars1.githubusercontent.com/u/2677072?v=4?s=100" width="100px;" alt="Mario Beltrán Alarcón"/><br /><sub><b>Mario Beltrán Alarcón</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=Belco90" title="Code">💻</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=Belco90" title="Documentation">📖</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/pulls?q=is%3Apr+reviewed-by%3ABelco90" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=Belco90" title="Tests">⚠️</a> <a href="#infra-Belco90" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/issues?q=author%3ABelco90" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://thomlom.dev"><img src="https://avatars3.githubusercontent.com/u/16003285?v=4?s=100" width="100px;" alt="Thomas Lombart"/><br /><sub><b>Thomas Lombart</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=thomlom" title="Code">💻</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=thomlom" title="Documentation">📖</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/pulls?q=is%3Apr+reviewed-by%3Athomlom" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=thomlom" title="Tests">⚠️</a> <a href="#infra-thomlom" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/benmonro"><img src="https://avatars3.githubusercontent.com/u/399236?v=4?s=100" width="100px;" alt="Ben Monro"/><br /><sub><b>Ben Monro</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=benmonro" title="Code">💻</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=benmonro" title="Documentation">📖</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=benmonro" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://emmenko.org/"><img src="https://avatars2.githubusercontent.com/u/1110551?v=4?s=100" width="100px;" alt="Nicola Molinari"/><br /><sub><b>Nicola Molinari</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=emmenko" title="Code">💻</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=emmenko" title="Tests">⚠️</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=emmenko" title="Documentation">📖</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/pulls?q=is%3Apr+reviewed-by%3Aemmenko" title="Reviewed Pull Requests">👀</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://aarongarciah.com"><img src="https://avatars0.githubusercontent.com/u/7225802?v=4?s=100" width="100px;" alt="Aarón García Hervás"/><br /><sub><b>Aarón García Hervás</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=aarongarciah" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.matej.snuderl.si/"><img src="https://avatars3.githubusercontent.com/u/8524109?v=4?s=100" width="100px;" alt="Matej Šnuderl"/><br /><sub><b>Matej Šnuderl</b></sub></a><br /><a href="#ideas-Meemaw" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=Meemaw" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://afontcu.dev"><img src="https://avatars0.githubusercontent.com/u/9197791?v=4?s=100" width="100px;" alt="Adrià Fontcuberta"/><br /><sub><b>Adrià Fontcuberta</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=afontcu" title="Code">💻</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=afontcu" title="Tests">⚠️</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jonaldinger"><img src="https://avatars1.githubusercontent.com/u/663362?v=4?s=100" width="100px;" alt="Jon Aldinger"/><br /><sub><b>Jon Aldinger</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=jonaldinger" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.thomasknickman.com"><img src="https://avatars1.githubusercontent.com/u/2933988?v=4?s=100" width="100px;" alt="Thomas Knickman"/><br /><sub><b>Thomas Knickman</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=tknickman" title="Code">💻</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=tknickman" title="Documentation">📖</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=tknickman" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://exercism.io/profiles/wolverineks/619ce225090a43cb891d2edcbbf50401"><img src="https://avatars2.githubusercontent.com/u/8462274?v=4?s=100" width="100px;" alt="Kevin Sullivan"/><br /><sub><b>Kevin Sullivan</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=wolverineks" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://kubajastrz.com"><img src="https://avatars0.githubusercontent.com/u/6443113?v=4?s=100" width="100px;" alt="Jakub Jastrzębski"/><br /><sub><b>Jakub Jastrzębski</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=KubaJastrz" title="Code">💻</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=KubaJastrz" title="Documentation">📖</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=KubaJastrz" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://arvigeus.github.com"><img src="https://avatars2.githubusercontent.com/u/4872470?v=4?s=100" width="100px;" alt="Nikolay Stoynov"/><br /><sub><b>Nikolay Stoynov</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=arvigeus" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://marudor.de"><img src="https://avatars0.githubusercontent.com/u/1881725?v=4?s=100" width="100px;" alt="marudor"/><br /><sub><b>marudor</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=marudor" title="Code">💻</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=marudor" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://timdeschryver.dev"><img src="https://avatars1.githubusercontent.com/u/28659384?v=4?s=100" width="100px;" alt="Tim Deschryver"/><br /><sub><b>Tim Deschryver</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=timdeschryver" title="Code">💻</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=timdeschryver" title="Documentation">📖</a> <a href="#ideas-timdeschryver" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/pulls?q=is%3Apr+reviewed-by%3Atimdeschryver" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=timdeschryver" title="Tests">⚠️</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/issues?q=author%3Atimdeschryver" title="Bug reports">🐛</a> <a href="#infra-timdeschryver" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="#platform-timdeschryver" title="Packaging/porting to new platform">📦</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://tdeekens.name"><img src="https://avatars3.githubusercontent.com/u/1877073?v=4?s=100" width="100px;" alt="Tobias Deekens"/><br /><sub><b>Tobias Deekens</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/issues?q=author%3Atdeekens" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/victorandcode"><img src="https://avatars0.githubusercontent.com/u/18427801?v=4?s=100" width="100px;" alt="Victor Cordova"/><br /><sub><b>Victor Cordova</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=victorandcode" title="Code">💻</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=victorandcode" title="Tests">⚠️</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/issues?q=author%3Avictorandcode" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dmitry-lobanov"><img src="https://avatars0.githubusercontent.com/u/7376755?v=4?s=100" width="100px;" alt="Dmitry Lobanov"/><br /><sub><b>Dmitry Lobanov</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=dmitry-lobanov" title="Code">💻</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=dmitry-lobanov" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://kentcdodds.com"><img src="https://avatars0.githubusercontent.com/u/1500684?v=4?s=100" width="100px;" alt="Kent C. Dodds"/><br /><sub><b>Kent C. Dodds</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/issues?q=author%3Akentcdodds" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gndelia"><img src="https://avatars1.githubusercontent.com/u/352474?v=4?s=100" width="100px;" alt="Gonzalo D'Elia"/><br /><sub><b>Gonzalo D'Elia</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=gndelia" title="Code">💻</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=gndelia" title="Tests">⚠️</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=gndelia" title="Documentation">📖</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/pulls?q=is%3Apr+reviewed-by%3Agndelia" title="Reviewed Pull Requests">👀</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jmcriffey"><img src="https://avatars0.githubusercontent.com/u/2831294?v=4?s=100" width="100px;" alt="Jeff Rifwald"/><br /><sub><b>Jeff Rifwald</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=jmcriffey" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://blog.lourenci.com/"><img src="https://avatars3.githubusercontent.com/u/2339362?v=4?s=100" width="100px;" alt="Leandro Lourenci"/><br /><sub><b>Leandro Lourenci</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/issues?q=author%3Alourenci" title="Bug reports">🐛</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=lourenci" title="Code">💻</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=lourenci" title="Tests">⚠️</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://xxxl.digital/"><img src="https://avatars2.githubusercontent.com/u/42043025?v=4?s=100" width="100px;" alt="Miguel Erja González"/><br /><sub><b>Miguel Erja González</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/issues?q=author%3Amiguelerja" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://pustovalov.dev"><img src="https://avatars2.githubusercontent.com/u/1568885?v=4?s=100" width="100px;" alt="Pavel Pustovalov"/><br /><sub><b>Pavel Pustovalov</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/issues?q=author%3Apustovalov" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jrparish"><img src="https://avatars3.githubusercontent.com/u/5173987?v=4?s=100" width="100px;" alt="Jacob Parish"/><br /><sub><b>Jacob Parish</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/issues?q=author%3Ajrparish" title="Bug reports">🐛</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=jrparish" title="Code">💻</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=jrparish" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://nickmccurdy.com/"><img src="https://avatars0.githubusercontent.com/u/927220?v=4?s=100" width="100px;" alt="Nick McCurdy"/><br /><sub><b>Nick McCurdy</b></sub></a><br /><a href="#ideas-nickmccurdy" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=nickmccurdy" title="Code">💻</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/pulls?q=is%3Apr+reviewed-by%3Anickmccurdy" title="Reviewed Pull Requests">👀</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://stefancameron.com/"><img src="https://avatars3.githubusercontent.com/u/2855350?v=4?s=100" width="100px;" alt="Stefan Cameron"/><br /><sub><b>Stefan Cameron</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/issues?q=author%3Astefcameron" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.linkedin.com/in/mateusfelix/"><img src="https://avatars2.githubusercontent.com/u/4968788?v=4?s=100" width="100px;" alt="Mateus Felix"/><br /><sub><b>Mateus Felix</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=thebinaryfelix" title="Code">💻</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=thebinaryfelix" title="Tests">⚠️</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=thebinaryfelix" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/renatoagds"><img src="https://avatars2.githubusercontent.com/u/1663717?v=4?s=100" width="100px;" alt="Renato Augusto Gama dos Santos"/><br /><sub><b>Renato Augusto Gama dos Santos</b></sub></a><br /><a href="#ideas-renatoagds" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=renatoagds" title="Code">💻</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=renatoagds" title="Documentation">📖</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=renatoagds" title="Tests">⚠️</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/codecog"><img src="https://avatars0.githubusercontent.com/u/5106076?v=4?s=100" width="100px;" alt="Josh Kelly"/><br /><sub><b>Josh Kelly</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=codecog" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://aless.co"><img src="https://avatars0.githubusercontent.com/u/5139846?v=4?s=100" width="100px;" alt="Alessia Bellisario"/><br /><sub><b>Alessia Bellisario</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=alessbell" title="Code">💻</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=alessbell" title="Tests">⚠️</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=alessbell" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://skovy.dev"><img src="https://avatars1.githubusercontent.com/u/5247455?v=4?s=100" width="100px;" alt="Spencer Miskoviak"/><br /><sub><b>Spencer Miskoviak</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=skovy" title="Code">💻</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=skovy" title="Tests">⚠️</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=skovy" title="Documentation">📖</a> <a href="#ideas-skovy" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://twitter.com/Gpx"><img src="https://avatars0.githubusercontent.com/u/767959?v=4?s=100" width="100px;" alt="Giorgio Polvara"/><br /><sub><b>Giorgio Polvara</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=Gpx" title="Code">💻</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=Gpx" title="Tests">⚠️</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=Gpx" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jdanil"><img src="https://avatars0.githubusercontent.com/u/8342105?v=4?s=100" width="100px;" alt="Josh David"/><br /><sub><b>Josh David</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=jdanil" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://michaeldeboey.be"><img src="https://avatars3.githubusercontent.com/u/6643991?v=4?s=100" width="100px;" alt="Michaël De Boey"/><br /><sub><b>Michaël De Boey</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=MichaelDeBoey" title="Code">💻</a> <a href="#platform-MichaelDeBoey" title="Packaging/porting to new platform">📦</a> <a href="#maintenance-MichaelDeBoey" title="Maintenance">🚧</a> <a href="#infra-MichaelDeBoey" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/pulls?q=is%3Apr+reviewed-by%3AMichaelDeBoey" title="Reviewed Pull Requests">👀</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/J-Huang"><img src="https://avatars0.githubusercontent.com/u/4263459?v=4?s=100" width="100px;" alt="Jian Huang"/><br /><sub><b>Jian Huang</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=J-Huang" title="Code">💻</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=J-Huang" title="Tests">⚠️</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=J-Huang" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ph-fritsche"><img src="https://avatars.githubusercontent.com/u/39068198?v=4?s=100" width="100px;" alt="Philipp Fritsche"/><br /><sub><b>Philipp Fritsche</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=ph-fritsche" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://zaicevas.me"><img src="https://avatars.githubusercontent.com/u/34719980?v=4?s=100" width="100px;" alt="Tomas Zaicevas"/><br /><sub><b>Tomas Zaicevas</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/issues?q=author%3Azaicevas" title="Bug reports">🐛</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=zaicevas" title="Code">💻</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=zaicevas" title="Tests">⚠️</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=zaicevas" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/G-Rath"><img src="https://avatars.githubusercontent.com/u/3151613?v=4?s=100" width="100px;" alt="Gareth Jones"/><br /><sub><b>Gareth Jones</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=G-Rath" title="Code">💻</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=G-Rath" title="Documentation">📖</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=G-Rath" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/HonkingGoose"><img src="https://avatars.githubusercontent.com/u/34918129?v=4?s=100" width="100px;" alt="HonkingGoose"/><br /><sub><b>HonkingGoose</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=HonkingGoose" title="Documentation">📖</a> <a href="#maintenance-HonkingGoose" title="Maintenance">🚧</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://everlong.org/"><img src="https://avatars.githubusercontent.com/u/454175?v=4?s=100" width="100px;" alt="Julien Wajsberg"/><br /><sub><b>Julien Wajsberg</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/issues?q=author%3Ajulienw" title="Bug reports">🐛</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=julienw" title="Code">💻</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=julienw" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.linkedin.com/in/maratdyatko/"><img src="https://avatars.githubusercontent.com/u/31615495?v=4?s=100" width="100px;" alt="Marat Dyatko"/><br /><sub><b>Marat Dyatko</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/issues?q=author%3Adyatko" title="Bug reports">🐛</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=dyatko" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/DaJoTo"><img src="https://avatars.githubusercontent.com/u/28302401?v=4?s=100" width="100px;" alt="David Tolman"/><br /><sub><b>David Tolman</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/issues?q=author%3ADaJoTo" title="Bug reports">🐛</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://codepen.io/ariperkkio/"><img src="https://avatars.githubusercontent.com/u/14806298?v=4?s=100" width="100px;" alt="Ari Perkkiö"/><br /><sub><b>Ari Perkkiö</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=AriPerkkio" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://diegocasmo.github.io/"><img src="https://avatars.githubusercontent.com/u/4553097?v=4?s=100" width="100px;" alt="Diego Castillo"/><br /><sub><b>Diego Castillo</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=diegocasmo" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://bpinto.github.com"><img src="https://avatars.githubusercontent.com/u/526122?v=4?s=100" width="100px;" alt="Bruno Pinto"/><br /><sub><b>Bruno Pinto</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=bpinto" title="Code">💻</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=bpinto" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/themagickoala"><img src="https://avatars.githubusercontent.com/u/48416253?v=4?s=100" width="100px;" alt="themagickoala"/><br /><sub><b>themagickoala</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=themagickoala" title="Code">💻</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=themagickoala" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/PrashantAshok"><img src="https://avatars.githubusercontent.com/u/5200733?v=4?s=100" width="100px;" alt="Prashant Ashok"/><br /><sub><b>Prashant Ashok</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=PrashantAshok" title="Code">💻</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=PrashantAshok" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/IvanAprea"><img src="https://avatars.githubusercontent.com/u/54630721?v=4?s=100" width="100px;" alt="Ivan Aprea"/><br /><sub><b>Ivan Aprea</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=IvanAprea" title="Code">💻</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=IvanAprea" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://semigradsky.dev/"><img src="https://avatars.githubusercontent.com/u/1198848?v=4?s=100" width="100px;" alt="Dmitry Semigradsky"/><br /><sub><b>Dmitry Semigradsky</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=Semigradsky" title="Code">💻</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=Semigradsky" title="Tests">⚠️</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=Semigradsky" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sjarva"><img src="https://avatars.githubusercontent.com/u/1133238?v=4?s=100" width="100px;" alt="Senja"/><br /><sub><b>Senja</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=sjarva" title="Code">💻</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=sjarva" title="Tests">⚠️</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=sjarva" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://dbrno.vercel.app"><img src="https://avatars.githubusercontent.com/u/106157862?v=4?s=100" width="100px;" alt="Breno Cota"/><br /><sub><b>Breno Cota</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=brenocota-hotmart" title="Code">💻</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=brenocota-hotmart" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://nickbolles.com"><img src="https://avatars.githubusercontent.com/u/7891759?v=4?s=100" width="100px;" alt="Nick Bolles"/><br /><sub><b>Nick Bolles</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=NickBolles" title="Code">💻</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=NickBolles" title="Tests">⚠️</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=NickBolles" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.linkedin.com/in/bmish"><img src="https://avatars.githubusercontent.com/u/698306?v=4?s=100" width="100px;" alt="Bryan Mishkin"/><br /><sub><b>Bryan Mishkin</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=bmish" title="Documentation">📖</a> <a href="#tool-bmish" title="Tools">🔧</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/theredspoon"><img src="https://avatars.githubusercontent.com/u/20975696?v=4?s=100" width="100px;" alt="Nim G"/><br /><sub><b>Nim G</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=theredspoon" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/patriscus"><img src="https://avatars.githubusercontent.com/u/23729362?v=4?s=100" width="100px;" alt="Patrick Ahmetovic"/><br /><sub><b>Patrick Ahmetovic</b></sub></a><br /><a href="#ideas-patriscus" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=patriscus" title="Code">💻</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=patriscus" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://codingitwrong.com"><img src="https://avatars.githubusercontent.com/u/15832198?v=4?s=100" width="100px;" alt="Josh Justice"/><br /><sub><b>Josh Justice</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=CodingItWrong" title="Code">💻</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=CodingItWrong" title="Tests">⚠️</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=CodingItWrong" title="Documentation">📖</a> <a href="#ideas-CodingItWrong" title="Ideas, Planning, & Feedback">🤔</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://dale.io"><img src="https://avatars.githubusercontent.com/u/389851?v=4?s=100" width="100px;" alt="Dale Karp"/><br /><sub><b>Dale Karp</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=obsoke" title="Code">💻</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=obsoke" title="Tests">⚠️</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=obsoke" title="Documentation">📖</a></td>
</tr>
</tbody>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
[build-badge]: https://github.com/testing-library/eslint-plugin-testing-library/actions/workflows/pipeline.yml/badge.svg
[build-url]: https://github.com/testing-library/eslint-plugin-testing-library/actions/workflows/pipeline.yml
[version-badge]: https://img.shields.io/npm/v/eslint-plugin-testing-library
[version-url]: https://www.npmjs.com/package/eslint-plugin-testing-library
[license-badge]: https://img.shields.io/npm/l/eslint-plugin-testing-library
[eslint-remote-tester-badge]: https://img.shields.io/github/workflow/status/AriPerkkio/eslint-remote-tester/eslint-plugin-testing-library?label=eslint-remote-tester
[eslint-remote-tester-workflow]: https://github.com/AriPerkkio/eslint-remote-tester/actions?query=workflow%3Aeslint-plugin-testing-library
[package-health-badge]: https://snyk.io/advisor/npm-package/eslint-plugin-testing-library/badge.svg
[package-health-url]: https://snyk.io/advisor/npm-package/eslint-plugin-testing-library
[license-url]: https://github.com/testing-library/eslint-plugin-testing-library/blob/main/license
[pr-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square
[all-contributors-badge]: https://img.shields.io/github/all-contributors/testing-library/eslint-plugin-testing-library?color=orange&style=flat-square
[pr-url]: http://makeapullrequest.com
[gh-watchers-badge]: https://img.shields.io/github/watchers/testing-library/eslint-plugin-testing-library?style=social
[gh-watchers-url]: https://github.com/testing-library/eslint-plugin-testing-library/watchers
[gh-stars-badge]: https://img.shields.io/github/stars/testing-library/eslint-plugin-testing-library?style=social
[gh-stars-url]: https://github.com/testing-library/eslint-plugin-testing-library/stargazers
[tweet-badge]: https://img.shields.io/twitter/url?style=social&url=https%3A%2F%2Fgithub.com%2Ftesting-library%2Feslint-plugin-testing-library
[tweet-url]: https://twitter.com/intent/tweet?url=https%3a%2f%2fgithub.com%2ftesting-library%2feslint-plugin-testing-library&text=check%20out%20eslint-plugin-testing-library%20by%20@belcodev
[badge-dom]: https://img.shields.io/badge/%F0%9F%90%99-DOM-black?style=flat-square
[badge-angular]: https://img.shields.io/badge/-Angular-black?style=flat-square&logo=angular&logoColor=white&labelColor=DD0031&color=black
[badge-react]: https://img.shields.io/badge/-React-black?style=flat-square&logo=react&logoColor=white&labelColor=61DAFB&color=black
[badge-vue]: https://img.shields.io/badge/-Vue-black?style=flat-square&logo=vue.js&logoColor=white&labelColor=4FC08D&color=black
[badge-marko]: https://img.shields.io/badge/-Marko-black?style=flat-square&logo=marko&logoColor=white&labelColor=2596BE&color=black

View File

@@ -0,0 +1,24 @@
"use strict";
module.exports = {
plugins: ['testing-library'],
rules: {
'testing-library/await-async-query': 'error',
'testing-library/await-async-utils': 'error',
'testing-library/no-await-sync-query': 'error',
'testing-library/no-container': 'error',
'testing-library/no-debugging-utils': 'error',
'testing-library/no-dom-import': ['error', 'angular'],
'testing-library/no-node-access': 'error',
'testing-library/no-promise-in-fire-event': 'error',
'testing-library/no-render-in-setup': 'error',
'testing-library/no-wait-for-empty-callback': 'error',
'testing-library/no-wait-for-multiple-assertions': 'error',
'testing-library/no-wait-for-side-effects': 'error',
'testing-library/no-wait-for-snapshot': 'error',
'testing-library/prefer-find-by': 'error',
'testing-library/prefer-presence-queries': 'error',
'testing-library/prefer-query-by-disappearance': 'error',
'testing-library/prefer-screen-queries': 'error',
'testing-library/render-result-naming-convention': 'error',
},
};

View File

@@ -0,0 +1,18 @@
"use strict";
module.exports = {
plugins: ['testing-library'],
rules: {
'testing-library/await-async-query': 'error',
'testing-library/await-async-utils': 'error',
'testing-library/no-await-sync-query': 'error',
'testing-library/no-promise-in-fire-event': 'error',
'testing-library/no-wait-for-empty-callback': 'error',
'testing-library/no-wait-for-multiple-assertions': 'error',
'testing-library/no-wait-for-side-effects': 'error',
'testing-library/no-wait-for-snapshot': 'error',
'testing-library/prefer-find-by': 'error',
'testing-library/prefer-presence-queries': 'error',
'testing-library/prefer-query-by-disappearance': 'error',
'testing-library/prefer-screen-queries': 'error',
},
};

View File

@@ -0,0 +1,7 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const path_1 = require("path");
const utils_1 = require("../utils");
const configsDir = __dirname;
const getConfigForFramework = (framework) => (0, utils_1.importDefault)((0, path_1.join)(configsDir, framework));
exports.default = utils_1.SUPPORTED_TESTING_FRAMEWORKS.reduce((allConfigs, framework) => (Object.assign(Object.assign({}, allConfigs), { [framework]: getConfigForFramework(framework) })), {});

View File

@@ -0,0 +1,26 @@
"use strict";
module.exports = {
plugins: ['testing-library'],
rules: {
'testing-library/await-async-query': 'error',
'testing-library/await-async-utils': 'error',
'testing-library/await-fire-event': 'error',
'testing-library/no-await-sync-query': 'error',
'testing-library/no-container': 'error',
'testing-library/no-debugging-utils': 'error',
'testing-library/no-dom-import': ['error', 'marko'],
'testing-library/no-node-access': 'error',
'testing-library/no-promise-in-fire-event': 'error',
'testing-library/no-render-in-setup': 'error',
'testing-library/no-unnecessary-act': 'error',
'testing-library/no-wait-for-empty-callback': 'error',
'testing-library/no-wait-for-multiple-assertions': 'error',
'testing-library/no-wait-for-side-effects': 'error',
'testing-library/no-wait-for-snapshot': 'error',
'testing-library/prefer-find-by': 'error',
'testing-library/prefer-presence-queries': 'error',
'testing-library/prefer-query-by-disappearance': 'error',
'testing-library/prefer-screen-queries': 'error',
'testing-library/render-result-naming-convention': 'error',
},
};

View File

@@ -0,0 +1,25 @@
"use strict";
module.exports = {
plugins: ['testing-library'],
rules: {
'testing-library/await-async-query': 'error',
'testing-library/await-async-utils': 'error',
'testing-library/no-await-sync-query': 'error',
'testing-library/no-container': 'error',
'testing-library/no-debugging-utils': 'error',
'testing-library/no-dom-import': ['error', 'react'],
'testing-library/no-node-access': 'error',
'testing-library/no-promise-in-fire-event': 'error',
'testing-library/no-render-in-setup': 'error',
'testing-library/no-unnecessary-act': 'error',
'testing-library/no-wait-for-empty-callback': 'error',
'testing-library/no-wait-for-multiple-assertions': 'error',
'testing-library/no-wait-for-side-effects': 'error',
'testing-library/no-wait-for-snapshot': 'error',
'testing-library/prefer-find-by': 'error',
'testing-library/prefer-presence-queries': 'error',
'testing-library/prefer-query-by-disappearance': 'error',
'testing-library/prefer-screen-queries': 'error',
'testing-library/render-result-naming-convention': 'error',
},
};

View File

@@ -0,0 +1,25 @@
"use strict";
module.exports = {
plugins: ['testing-library'],
rules: {
'testing-library/await-async-query': 'error',
'testing-library/await-async-utils': 'error',
'testing-library/await-fire-event': 'error',
'testing-library/no-await-sync-query': 'error',
'testing-library/no-container': 'error',
'testing-library/no-debugging-utils': 'error',
'testing-library/no-dom-import': ['error', 'vue'],
'testing-library/no-node-access': 'error',
'testing-library/no-promise-in-fire-event': 'error',
'testing-library/no-render-in-setup': 'error',
'testing-library/no-wait-for-empty-callback': 'error',
'testing-library/no-wait-for-multiple-assertions': 'error',
'testing-library/no-wait-for-side-effects': 'error',
'testing-library/no-wait-for-snapshot': 'error',
'testing-library/prefer-find-by': 'error',
'testing-library/prefer-presence-queries': 'error',
'testing-library/prefer-query-by-disappearance': 'error',
'testing-library/prefer-screen-queries': 'error',
'testing-library/render-result-naming-convention': 'error',
},
};

View File

@@ -0,0 +1,536 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.detectTestingLibraryUtils = void 0;
const utils_1 = require("@typescript-eslint/utils");
const node_utils_1 = require("../node-utils");
const utils_2 = require("../utils");
const SETTING_OPTION_OFF = 'off';
const USER_EVENT_PACKAGE = '@testing-library/user-event';
const REACT_DOM_TEST_UTILS_PACKAGE = 'react-dom/test-utils';
const FIRE_EVENT_NAME = 'fireEvent';
const CREATE_EVENT_NAME = 'createEvent';
const USER_EVENT_NAME = 'userEvent';
const RENDER_NAME = 'render';
function detectTestingLibraryUtils(ruleCreate, { skipRuleReportingCheck = false } = {}) {
return (context, optionsWithDefault) => {
const importedTestingLibraryNodes = [];
let importedCustomModuleNode = null;
let importedUserEventLibraryNode = null;
let importedReactDomTestUtilsNode = null;
const customModuleSetting = context.settings['testing-library/utils-module'];
const customRendersSetting = context.settings['testing-library/custom-renders'];
const customQueriesSetting = context.settings['testing-library/custom-queries'];
function isPotentialTestingLibraryFunction(node, isPotentialFunctionCallback) {
if (!node) {
return false;
}
const referenceNode = (0, node_utils_1.getReferenceNode)(node);
const referenceNodeIdentifier = (0, node_utils_1.getPropertyIdentifierNode)(referenceNode);
if (!referenceNodeIdentifier) {
return false;
}
const importedUtilSpecifier = getTestingLibraryImportedUtilSpecifier(referenceNodeIdentifier);
const originalNodeName = (0, node_utils_1.isImportSpecifier)(importedUtilSpecifier) &&
importedUtilSpecifier.local.name !== importedUtilSpecifier.imported.name
? importedUtilSpecifier.imported.name
: undefined;
if (!isPotentialFunctionCallback(node.name, originalNodeName)) {
return false;
}
if (isAggressiveModuleReportingEnabled()) {
return true;
}
return isNodeComingFromTestingLibrary(referenceNodeIdentifier);
}
const isAggressiveModuleReportingEnabled = () => !customModuleSetting;
const isAggressiveRenderReportingEnabled = () => {
const isSwitchedOff = customRendersSetting === SETTING_OPTION_OFF;
const hasCustomOptions = Array.isArray(customRendersSetting) && customRendersSetting.length > 0;
return !isSwitchedOff && !hasCustomOptions;
};
const isAggressiveQueryReportingEnabled = () => {
const isSwitchedOff = customQueriesSetting === SETTING_OPTION_OFF;
const hasCustomOptions = Array.isArray(customQueriesSetting) && customQueriesSetting.length > 0;
return !isSwitchedOff && !hasCustomOptions;
};
const getCustomModule = () => {
if (!isAggressiveModuleReportingEnabled() &&
customModuleSetting !== SETTING_OPTION_OFF) {
return customModuleSetting;
}
return undefined;
};
const getCustomRenders = () => {
if (!isAggressiveRenderReportingEnabled() &&
customRendersSetting !== SETTING_OPTION_OFF) {
return customRendersSetting;
}
return [];
};
const getCustomQueries = () => {
if (!isAggressiveQueryReportingEnabled() &&
customQueriesSetting !== SETTING_OPTION_OFF) {
return customQueriesSetting;
}
return [];
};
const getTestingLibraryImportNode = () => {
return importedTestingLibraryNodes[0];
};
const getAllTestingLibraryImportNodes = () => {
return importedTestingLibraryNodes;
};
const getCustomModuleImportNode = () => {
return importedCustomModuleNode;
};
const getTestingLibraryImportName = () => {
return (0, node_utils_1.getImportModuleName)(importedTestingLibraryNodes[0]);
};
const getCustomModuleImportName = () => {
return (0, node_utils_1.getImportModuleName)(importedCustomModuleNode);
};
const isTestingLibraryImported = (isStrict = false) => {
const isSomeModuleImported = importedTestingLibraryNodes.length !== 0 || !!importedCustomModuleNode;
return ((!isStrict && isAggressiveModuleReportingEnabled()) ||
isSomeModuleImported);
};
const isQuery = (node) => {
const hasQueryPattern = /^(get|query|find)(All)?By.+$/.test(node.name);
if (!hasQueryPattern) {
return false;
}
if (isAggressiveQueryReportingEnabled()) {
return true;
}
const customQueries = getCustomQueries();
const isBuiltInQuery = utils_2.ALL_QUERIES_COMBINATIONS.includes(node.name);
const isReportableCustomQuery = customQueries.some((pattern) => new RegExp(pattern).test(node.name));
return isBuiltInQuery || isReportableCustomQuery;
};
const isGetQueryVariant = (node) => {
return isQuery(node) && node.name.startsWith('get');
};
const isQueryQueryVariant = (node) => {
return isQuery(node) && node.name.startsWith('query');
};
const isFindQueryVariant = (node) => {
return isQuery(node) && node.name.startsWith('find');
};
const isSyncQuery = (node) => {
return isGetQueryVariant(node) || isQueryQueryVariant(node);
};
const isAsyncQuery = (node) => {
return isFindQueryVariant(node);
};
const isCustomQuery = (node) => {
return isQuery(node) && !utils_2.ALL_QUERIES_COMBINATIONS.includes(node.name);
};
const isBuiltInQuery = (node) => {
return isQuery(node) && utils_2.ALL_QUERIES_COMBINATIONS.includes(node.name);
};
const isAsyncUtil = (node, validNames = utils_2.ASYNC_UTILS) => {
return isPotentialTestingLibraryFunction(node, (identifierNodeName, originalNodeName) => {
return (validNames.includes(identifierNodeName) ||
(!!originalNodeName &&
validNames.includes(originalNodeName)));
});
};
const isFireEventUtil = (node) => {
return isPotentialTestingLibraryFunction(node, (identifierNodeName, originalNodeName) => {
return [identifierNodeName, originalNodeName].includes('fireEvent');
});
};
const isUserEventUtil = (node) => {
const userEvent = findImportedUserEventSpecifier();
let userEventName;
if (userEvent) {
userEventName = userEvent.name;
}
else if (isAggressiveModuleReportingEnabled()) {
userEventName = USER_EVENT_NAME;
}
if (!userEventName) {
return false;
}
return node.name === userEventName;
};
const isFireEventMethod = (node) => {
const fireEventUtil = findImportedTestingLibraryUtilSpecifier(FIRE_EVENT_NAME);
let fireEventUtilName;
if (fireEventUtil) {
fireEventUtilName = utils_1.ASTUtils.isIdentifier(fireEventUtil)
? fireEventUtil.name
: fireEventUtil.local.name;
}
else if (isAggressiveModuleReportingEnabled()) {
fireEventUtilName = FIRE_EVENT_NAME;
}
if (!fireEventUtilName) {
return false;
}
const parentMemberExpression = node.parent && (0, node_utils_1.isMemberExpression)(node.parent)
? node.parent
: undefined;
const parentCallExpression = node.parent && (0, node_utils_1.isCallExpression)(node.parent) ? node.parent : undefined;
if (!parentMemberExpression && !parentCallExpression) {
return false;
}
if (parentCallExpression) {
return [fireEventUtilName, FIRE_EVENT_NAME].includes(node.name);
}
const definedParentMemberExpression = parentMemberExpression;
const regularCall = utils_1.ASTUtils.isIdentifier(definedParentMemberExpression.object) &&
(0, node_utils_1.isCallExpression)(definedParentMemberExpression.parent) &&
definedParentMemberExpression.object.name === fireEventUtilName &&
node.name !== FIRE_EVENT_NAME &&
node.name !== fireEventUtilName;
const wildcardCall = (0, node_utils_1.isMemberExpression)(definedParentMemberExpression.object) &&
utils_1.ASTUtils.isIdentifier(definedParentMemberExpression.object.object) &&
definedParentMemberExpression.object.object.name ===
fireEventUtilName &&
utils_1.ASTUtils.isIdentifier(definedParentMemberExpression.object.property) &&
definedParentMemberExpression.object.property.name ===
FIRE_EVENT_NAME &&
node.name !== FIRE_EVENT_NAME &&
node.name !== fireEventUtilName;
const wildcardCallWithCallExpression = utils_1.ASTUtils.isIdentifier(definedParentMemberExpression.object) &&
definedParentMemberExpression.object.name === fireEventUtilName &&
utils_1.ASTUtils.isIdentifier(definedParentMemberExpression.property) &&
definedParentMemberExpression.property.name === FIRE_EVENT_NAME &&
!(0, node_utils_1.isMemberExpression)(definedParentMemberExpression.parent) &&
node.name === FIRE_EVENT_NAME &&
node.name !== fireEventUtilName;
return regularCall || wildcardCall || wildcardCallWithCallExpression;
};
const isUserEventMethod = (node) => {
const userEvent = findImportedUserEventSpecifier();
let userEventName;
if (userEvent) {
userEventName = userEvent.name;
}
else if (isAggressiveModuleReportingEnabled()) {
userEventName = USER_EVENT_NAME;
}
if (!userEventName) {
return false;
}
const parentMemberExpression = node.parent && (0, node_utils_1.isMemberExpression)(node.parent)
? node.parent
: undefined;
if (!parentMemberExpression) {
return false;
}
if ([userEventName, USER_EVENT_NAME].includes(node.name) ||
(utils_1.ASTUtils.isIdentifier(parentMemberExpression.object) &&
parentMemberExpression.object.name === node.name)) {
return false;
}
return (utils_1.ASTUtils.isIdentifier(parentMemberExpression.object) &&
parentMemberExpression.object.name === userEventName);
};
const isRenderUtil = (node) => isPotentialTestingLibraryFunction(node, (identifierNodeName, originalNodeName) => {
if (isAggressiveRenderReportingEnabled()) {
return identifierNodeName.toLowerCase().includes(RENDER_NAME);
}
return [RENDER_NAME, ...getCustomRenders()].some((validRenderName) => validRenderName === identifierNodeName ||
(Boolean(originalNodeName) &&
validRenderName === originalNodeName));
});
const isCreateEventUtil = (node) => {
const isCreateEventCallback = (identifierNodeName, originalNodeName) => [identifierNodeName, originalNodeName].includes(CREATE_EVENT_NAME);
if ((0, node_utils_1.isCallExpression)(node) &&
(0, node_utils_1.isMemberExpression)(node.callee) &&
utils_1.ASTUtils.isIdentifier(node.callee.object)) {
return isPotentialTestingLibraryFunction(node.callee.object, isCreateEventCallback);
}
if ((0, node_utils_1.isCallExpression)(node) &&
(0, node_utils_1.isMemberExpression)(node.callee) &&
(0, node_utils_1.isMemberExpression)(node.callee.object) &&
utils_1.ASTUtils.isIdentifier(node.callee.object.property)) {
return isPotentialTestingLibraryFunction(node.callee.object.property, isCreateEventCallback);
}
const identifier = (0, node_utils_1.getDeepestIdentifierNode)(node);
return isPotentialTestingLibraryFunction(identifier, isCreateEventCallback);
};
const isRenderVariableDeclarator = (node) => {
if (!node.init) {
return false;
}
const initIdentifierNode = (0, node_utils_1.getDeepestIdentifierNode)(node.init);
if (!initIdentifierNode) {
return false;
}
return isRenderUtil(initIdentifierNode);
};
const isDebugUtil = (identifierNode, validNames = utils_2.DEBUG_UTILS) => {
const isBuiltInConsole = (0, node_utils_1.isMemberExpression)(identifierNode.parent) &&
utils_1.ASTUtils.isIdentifier(identifierNode.parent.object) &&
identifierNode.parent.object.name === 'console';
return (!isBuiltInConsole &&
isPotentialTestingLibraryFunction(identifierNode, (identifierNodeName, originalNodeName) => {
return (validNames.includes(identifierNodeName) ||
(!!originalNodeName &&
validNames.includes(originalNodeName)));
}));
};
const isActUtil = (node) => {
const isTestingLibraryAct = isPotentialTestingLibraryFunction(node, (identifierNodeName, originalNodeName) => {
return [identifierNodeName, originalNodeName]
.filter(Boolean)
.includes('act');
});
const isReactDomTestUtilsAct = (() => {
if (!importedReactDomTestUtilsNode) {
return false;
}
const referenceNode = (0, node_utils_1.getReferenceNode)(node);
const referenceNodeIdentifier = (0, node_utils_1.getPropertyIdentifierNode)(referenceNode);
if (!referenceNodeIdentifier) {
return false;
}
const importedUtilSpecifier = (0, node_utils_1.findImportSpecifier)(node.name, importedReactDomTestUtilsNode);
if (!importedUtilSpecifier) {
return false;
}
const importDeclaration = (() => {
if ((0, node_utils_1.isImportDeclaration)(importedUtilSpecifier.parent)) {
return importedUtilSpecifier.parent;
}
const variableDeclarator = (0, node_utils_1.findClosestVariableDeclaratorNode)(importedUtilSpecifier);
if ((0, node_utils_1.isCallExpression)(variableDeclarator === null || variableDeclarator === void 0 ? void 0 : variableDeclarator.init)) {
return variableDeclarator === null || variableDeclarator === void 0 ? void 0 : variableDeclarator.init;
}
return undefined;
})();
if (!importDeclaration) {
return false;
}
const importDeclarationName = (0, node_utils_1.getImportModuleName)(importDeclaration);
if (!importDeclarationName) {
return false;
}
if (importDeclarationName !== REACT_DOM_TEST_UTILS_PACKAGE) {
return false;
}
return (0, node_utils_1.hasImportMatch)(importedUtilSpecifier, referenceNodeIdentifier.name);
})();
return isTestingLibraryAct || isReactDomTestUtilsAct;
};
const isTestingLibraryUtil = (node) => {
return (isAsyncUtil(node) ||
isQuery(node) ||
isRenderUtil(node) ||
isFireEventMethod(node) ||
isUserEventMethod(node) ||
isActUtil(node) ||
isCreateEventUtil(node));
};
const isPresenceAssert = (node) => {
const { matcher, isNegated } = (0, node_utils_1.getAssertNodeInfo)(node);
if (!matcher) {
return false;
}
return isNegated
? utils_2.ABSENCE_MATCHERS.includes(matcher)
: utils_2.PRESENCE_MATCHERS.includes(matcher);
};
const isAbsenceAssert = (node) => {
const { matcher, isNegated } = (0, node_utils_1.getAssertNodeInfo)(node);
if (!matcher) {
return false;
}
return isNegated
? utils_2.PRESENCE_MATCHERS.includes(matcher)
: utils_2.ABSENCE_MATCHERS.includes(matcher);
};
const isMatchingAssert = (node, matcherName) => {
const { matcher } = (0, node_utils_1.getAssertNodeInfo)(node);
if (!matcher) {
return false;
}
return matcher === matcherName;
};
const findImportedTestingLibraryUtilSpecifier = (specifierName) => {
var _a;
const node = (_a = getCustomModuleImportNode()) !== null && _a !== void 0 ? _a : getTestingLibraryImportNode();
if (!node) {
return undefined;
}
return (0, node_utils_1.findImportSpecifier)(specifierName, node);
};
const findImportedUserEventSpecifier = () => {
if (!importedUserEventLibraryNode) {
return null;
}
if ((0, node_utils_1.isImportDeclaration)(importedUserEventLibraryNode)) {
const userEventIdentifier = importedUserEventLibraryNode.specifiers.find((specifier) => (0, node_utils_1.isImportDefaultSpecifier)(specifier));
if (userEventIdentifier) {
return userEventIdentifier.local;
}
}
else {
if (!utils_1.ASTUtils.isVariableDeclarator(importedUserEventLibraryNode.parent)) {
return null;
}
const requireNode = importedUserEventLibraryNode.parent;
if (!utils_1.ASTUtils.isIdentifier(requireNode.id)) {
return null;
}
return requireNode.id;
}
return null;
};
const getTestingLibraryImportedUtilSpecifier = (node) => {
var _a;
const identifierName = (_a = (0, node_utils_1.getPropertyIdentifierNode)(node)) === null || _a === void 0 ? void 0 : _a.name;
if (!identifierName) {
return undefined;
}
return findImportedTestingLibraryUtilSpecifier(identifierName);
};
const canReportErrors = () => {
return skipRuleReportingCheck || isTestingLibraryImported();
};
const isNodeComingFromTestingLibrary = (node) => {
var _a;
const importNode = getTestingLibraryImportedUtilSpecifier(node);
if (!importNode) {
return false;
}
const referenceNode = (0, node_utils_1.getReferenceNode)(node);
const referenceNodeIdentifier = (0, node_utils_1.getPropertyIdentifierNode)(referenceNode);
if (!referenceNodeIdentifier) {
return false;
}
const importDeclaration = (() => {
if ((0, node_utils_1.isImportDeclaration)(importNode.parent)) {
return importNode.parent;
}
const variableDeclarator = (0, node_utils_1.findClosestVariableDeclaratorNode)(importNode);
if ((0, node_utils_1.isCallExpression)(variableDeclarator === null || variableDeclarator === void 0 ? void 0 : variableDeclarator.init)) {
return variableDeclarator === null || variableDeclarator === void 0 ? void 0 : variableDeclarator.init;
}
return undefined;
})();
if (!importDeclaration) {
return false;
}
const importDeclarationName = (0, node_utils_1.getImportModuleName)(importDeclaration);
if (!importDeclarationName) {
return false;
}
const identifierName = (_a = (0, node_utils_1.getPropertyIdentifierNode)(node)) === null || _a === void 0 ? void 0 : _a.name;
if (!identifierName) {
return false;
}
const hasImportElementMatch = (0, node_utils_1.hasImportMatch)(importNode, identifierName);
const hasImportModuleMatch = /testing-library/g.test(importDeclarationName) ||
(typeof customModuleSetting === 'string' &&
importDeclarationName.endsWith(customModuleSetting));
return hasImportElementMatch && hasImportModuleMatch;
};
const helpers = {
getTestingLibraryImportNode,
getAllTestingLibraryImportNodes,
getCustomModuleImportNode,
getTestingLibraryImportName,
getCustomModuleImportName,
isTestingLibraryImported,
isTestingLibraryUtil,
isGetQueryVariant,
isQueryQueryVariant,
isFindQueryVariant,
isSyncQuery,
isAsyncQuery,
isQuery,
isCustomQuery,
isBuiltInQuery,
isAsyncUtil,
isFireEventUtil,
isUserEventUtil,
isFireEventMethod,
isUserEventMethod,
isRenderUtil,
isCreateEventUtil,
isRenderVariableDeclarator,
isDebugUtil,
isActUtil,
isPresenceAssert,
isMatchingAssert,
isAbsenceAssert,
canReportErrors,
findImportedTestingLibraryUtilSpecifier,
isNodeComingFromTestingLibrary,
};
const detectionInstructions = {
ImportDeclaration(node) {
if (typeof node.source.value !== 'string') {
return;
}
if (/testing-library/g.test(node.source.value)) {
importedTestingLibraryNodes.push(node);
}
const customModule = getCustomModule();
if (customModule &&
!importedCustomModuleNode &&
node.source.value.endsWith(customModule)) {
importedCustomModuleNode = node;
}
if (!importedUserEventLibraryNode &&
node.source.value === USER_EVENT_PACKAGE) {
importedUserEventLibraryNode = node;
}
if (!importedUserEventLibraryNode &&
node.source.value === REACT_DOM_TEST_UTILS_PACKAGE) {
importedReactDomTestUtilsNode = node;
}
},
[`CallExpression > Identifier[name="require"]`](node) {
const callExpression = node.parent;
const { arguments: args } = callExpression;
if (args.some((arg) => (0, node_utils_1.isLiteral)(arg) &&
typeof arg.value === 'string' &&
/testing-library/g.test(arg.value))) {
importedTestingLibraryNodes.push(callExpression);
}
const customModule = getCustomModule();
if (!importedCustomModuleNode &&
args.some((arg) => customModule &&
(0, node_utils_1.isLiteral)(arg) &&
typeof arg.value === 'string' &&
arg.value.endsWith(customModule))) {
importedCustomModuleNode = callExpression;
}
if (!importedCustomModuleNode &&
args.some((arg) => (0, node_utils_1.isLiteral)(arg) &&
typeof arg.value === 'string' &&
arg.value === USER_EVENT_PACKAGE)) {
importedUserEventLibraryNode = callExpression;
}
if (!importedReactDomTestUtilsNode &&
args.some((arg) => (0, node_utils_1.isLiteral)(arg) &&
typeof arg.value === 'string' &&
arg.value === REACT_DOM_TEST_UTILS_PACKAGE)) {
importedReactDomTestUtilsNode = callExpression;
}
},
};
const ruleInstructions = ruleCreate(context, optionsWithDefault, helpers);
const enhancedRuleInstructions = {};
const allKeys = new Set(Object.keys(detectionInstructions).concat(Object.keys(ruleInstructions)));
allKeys.forEach((instruction) => {
enhancedRuleInstructions[instruction] = (node) => {
var _a, _b;
if (instruction in detectionInstructions) {
(_a = detectionInstructions[instruction]) === null || _a === void 0 ? void 0 : _a.call(detectionInstructions, node);
}
if (canReportErrors() && ruleInstructions[instruction]) {
return (_b = ruleInstructions[instruction]) === null || _b === void 0 ? void 0 : _b.call(ruleInstructions, node);
}
return undefined;
};
});
return enhancedRuleInstructions;
};
}
exports.detectTestingLibraryUtils = detectTestingLibraryUtils;

View File

@@ -0,0 +1,22 @@
"use strict";
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createTestingLibraryRule = void 0;
const utils_1 = require("@typescript-eslint/utils");
const utils_2 = require("../utils");
const detect_testing_library_utils_1 = require("./detect-testing-library-utils");
function createTestingLibraryRule(_a) {
var { create, detectionOptions = {}, meta } = _a, remainingConfig = __rest(_a, ["create", "detectionOptions", "meta"]);
return utils_1.ESLintUtils.RuleCreator(utils_2.getDocsUrl)(Object.assign(Object.assign({}, remainingConfig), { create: (0, detect_testing_library_utils_1.detectTestingLibraryUtils)(create, detectionOptions), meta: Object.assign(Object.assign({}, meta), { docs: Object.assign(Object.assign({}, meta.docs), { recommended: false }) }) }));
}
exports.createTestingLibraryRule = createTestingLibraryRule;

View File

@@ -0,0 +1,10 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
const configs_1 = __importDefault(require("./configs"));
const rules_1 = __importDefault(require("./rules"));
module.exports = {
configs: configs_1.default,
rules: rules_1.default,
};

View File

@@ -0,0 +1,385 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.findImportSpecifier = exports.isEmptyFunction = exports.getStatementCallExpression = exports.hasImportMatch = exports.getInnermostReturningFunction = exports.hasClosestExpectResolvesRejects = exports.getAssertNodeInfo = exports.getImportModuleName = exports.getFunctionName = exports.getReferenceNode = exports.getDeepestIdentifierNode = exports.getPropertyIdentifierNode = exports.getFunctionReturnStatementNode = exports.getInnermostFunctionScope = exports.getVariableReferences = exports.isPromiseHandled = exports.isPromisesArrayResolved = exports.isPromiseAllSettled = exports.isPromiseAll = exports.isPromiseIdentifier = exports.hasChainedThen = exports.hasThenProperty = exports.findClosestCallNode = exports.findClosestVariableDeclaratorNode = exports.findClosestCallExpressionNode = void 0;
const utils_1 = require("@typescript-eslint/utils");
const is_node_of_type_1 = require("./is-node-of-type");
__exportStar(require("./is-node-of-type"), exports);
const ValidLeftHandSideExpressions = [
utils_1.AST_NODE_TYPES.CallExpression,
utils_1.AST_NODE_TYPES.ClassExpression,
utils_1.AST_NODE_TYPES.ClassDeclaration,
utils_1.AST_NODE_TYPES.FunctionExpression,
utils_1.AST_NODE_TYPES.Literal,
utils_1.AST_NODE_TYPES.TemplateLiteral,
utils_1.AST_NODE_TYPES.MemberExpression,
utils_1.AST_NODE_TYPES.ArrayExpression,
utils_1.AST_NODE_TYPES.ArrayPattern,
utils_1.AST_NODE_TYPES.ClassExpression,
utils_1.AST_NODE_TYPES.FunctionExpression,
utils_1.AST_NODE_TYPES.Identifier,
utils_1.AST_NODE_TYPES.JSXElement,
utils_1.AST_NODE_TYPES.JSXFragment,
utils_1.AST_NODE_TYPES.JSXOpeningElement,
utils_1.AST_NODE_TYPES.MetaProperty,
utils_1.AST_NODE_TYPES.ObjectExpression,
utils_1.AST_NODE_TYPES.ObjectPattern,
utils_1.AST_NODE_TYPES.Super,
utils_1.AST_NODE_TYPES.ThisExpression,
utils_1.AST_NODE_TYPES.TSNullKeyword,
utils_1.AST_NODE_TYPES.TaggedTemplateExpression,
utils_1.AST_NODE_TYPES.TSNonNullExpression,
utils_1.AST_NODE_TYPES.TSAsExpression,
utils_1.AST_NODE_TYPES.ArrowFunctionExpression,
];
function findClosestCallExpressionNode(node, shouldRestrictInnerScope = false) {
if ((0, is_node_of_type_1.isCallExpression)(node)) {
return node;
}
if (!(node === null || node === void 0 ? void 0 : node.parent)) {
return null;
}
if (shouldRestrictInnerScope &&
!ValidLeftHandSideExpressions.includes(node.parent.type)) {
return null;
}
return findClosestCallExpressionNode(node.parent, shouldRestrictInnerScope);
}
exports.findClosestCallExpressionNode = findClosestCallExpressionNode;
function findClosestVariableDeclaratorNode(node) {
if (!node) {
return null;
}
if (utils_1.ASTUtils.isVariableDeclarator(node)) {
return node;
}
return findClosestVariableDeclaratorNode(node.parent);
}
exports.findClosestVariableDeclaratorNode = findClosestVariableDeclaratorNode;
function findClosestCallNode(node, name) {
if (!node.parent) {
return null;
}
if ((0, is_node_of_type_1.isCallExpression)(node) &&
utils_1.ASTUtils.isIdentifier(node.callee) &&
node.callee.name === name) {
return node;
}
else {
return findClosestCallNode(node.parent, name);
}
}
exports.findClosestCallNode = findClosestCallNode;
function hasThenProperty(node) {
return ((0, is_node_of_type_1.isMemberExpression)(node) &&
utils_1.ASTUtils.isIdentifier(node.property) &&
node.property.name === 'then');
}
exports.hasThenProperty = hasThenProperty;
function hasChainedThen(node) {
const parent = node.parent;
if ((0, is_node_of_type_1.isCallExpression)(parent) && parent.parent) {
return hasThenProperty(parent.parent);
}
return !!parent && hasThenProperty(parent);
}
exports.hasChainedThen = hasChainedThen;
function isPromiseIdentifier(node) {
return utils_1.ASTUtils.isIdentifier(node) && node.name === 'Promise';
}
exports.isPromiseIdentifier = isPromiseIdentifier;
function isPromiseAll(node) {
return ((0, is_node_of_type_1.isMemberExpression)(node.callee) &&
isPromiseIdentifier(node.callee.object) &&
utils_1.ASTUtils.isIdentifier(node.callee.property) &&
node.callee.property.name === 'all');
}
exports.isPromiseAll = isPromiseAll;
function isPromiseAllSettled(node) {
return ((0, is_node_of_type_1.isMemberExpression)(node.callee) &&
isPromiseIdentifier(node.callee.object) &&
utils_1.ASTUtils.isIdentifier(node.callee.property) &&
node.callee.property.name === 'allSettled');
}
exports.isPromiseAllSettled = isPromiseAllSettled;
function isPromisesArrayResolved(node) {
const closestCallExpression = findClosestCallExpressionNode(node, true);
if (!closestCallExpression) {
return false;
}
return (!!closestCallExpression.parent &&
(0, is_node_of_type_1.isArrayExpression)(closestCallExpression.parent) &&
(0, is_node_of_type_1.isCallExpression)(closestCallExpression.parent.parent) &&
(isPromiseAll(closestCallExpression.parent.parent) ||
isPromiseAllSettled(closestCallExpression.parent.parent)));
}
exports.isPromisesArrayResolved = isPromisesArrayResolved;
function isPromiseHandled(nodeIdentifier) {
const closestCallExpressionNode = findClosestCallExpressionNode(nodeIdentifier, true);
const suspiciousNodes = [nodeIdentifier, closestCallExpressionNode].filter(Boolean);
for (const node of suspiciousNodes) {
if (!(node === null || node === void 0 ? void 0 : node.parent)) {
continue;
}
if (utils_1.ASTUtils.isAwaitExpression(node.parent)) {
return true;
}
if ((0, is_node_of_type_1.isArrowFunctionExpression)(node.parent) ||
(0, is_node_of_type_1.isReturnStatement)(node.parent)) {
return true;
}
if (hasClosestExpectResolvesRejects(node.parent)) {
return true;
}
if (hasChainedThen(node)) {
return true;
}
if (isPromisesArrayResolved(node)) {
return true;
}
}
return false;
}
exports.isPromiseHandled = isPromiseHandled;
function getVariableReferences(context, node) {
var _a, _b, _c;
if (utils_1.ASTUtils.isVariableDeclarator(node)) {
return (_c = (_b = (_a = context.getDeclaredVariables(node)[0]) === null || _a === void 0 ? void 0 : _a.references) === null || _b === void 0 ? void 0 : _b.slice(1)) !== null && _c !== void 0 ? _c : [];
}
return [];
}
exports.getVariableReferences = getVariableReferences;
function getInnermostFunctionScope(context, asyncQueryNode) {
const innermostScope = utils_1.ASTUtils.getInnermostScope(context.getScope(), asyncQueryNode);
if (innermostScope.type === 'function' &&
utils_1.ASTUtils.isFunction(innermostScope.block)) {
return innermostScope;
}
return null;
}
exports.getInnermostFunctionScope = getInnermostFunctionScope;
function getFunctionReturnStatementNode(functionNode) {
if ((0, is_node_of_type_1.isBlockStatement)(functionNode.body)) {
const returnStatementNode = functionNode.body.body.find((statement) => (0, is_node_of_type_1.isReturnStatement)(statement));
if (!returnStatementNode) {
return null;
}
return returnStatementNode.argument;
}
else if (functionNode.expression) {
return functionNode.body;
}
return null;
}
exports.getFunctionReturnStatementNode = getFunctionReturnStatementNode;
function getPropertyIdentifierNode(node) {
if (utils_1.ASTUtils.isIdentifier(node)) {
return node;
}
if ((0, is_node_of_type_1.isMemberExpression)(node)) {
return getPropertyIdentifierNode(node.object);
}
if ((0, is_node_of_type_1.isCallExpression)(node)) {
return getPropertyIdentifierNode(node.callee);
}
if ((0, is_node_of_type_1.isExpressionStatement)(node)) {
return getPropertyIdentifierNode(node.expression);
}
return null;
}
exports.getPropertyIdentifierNode = getPropertyIdentifierNode;
function getDeepestIdentifierNode(node) {
if (utils_1.ASTUtils.isIdentifier(node)) {
return node;
}
if ((0, is_node_of_type_1.isMemberExpression)(node) && utils_1.ASTUtils.isIdentifier(node.property)) {
return node.property;
}
if ((0, is_node_of_type_1.isCallExpression)(node)) {
return getDeepestIdentifierNode(node.callee);
}
if (utils_1.ASTUtils.isAwaitExpression(node)) {
return getDeepestIdentifierNode(node.argument);
}
return null;
}
exports.getDeepestIdentifierNode = getDeepestIdentifierNode;
function getReferenceNode(node) {
if (node.parent &&
((0, is_node_of_type_1.isMemberExpression)(node.parent) || (0, is_node_of_type_1.isCallExpression)(node.parent))) {
return getReferenceNode(node.parent);
}
return node;
}
exports.getReferenceNode = getReferenceNode;
function getFunctionName(node) {
var _a, _b;
return ((_b = (_a = utils_1.ASTUtils.getFunctionNameWithKind(node)
.match(/('\w+')/g)) === null || _a === void 0 ? void 0 : _a[0].replace(/'/g, '')) !== null && _b !== void 0 ? _b : '');
}
exports.getFunctionName = getFunctionName;
function getImportModuleName(node) {
if ((0, is_node_of_type_1.isImportDeclaration)(node) && typeof node.source.value === 'string') {
return node.source.value;
}
if ((0, is_node_of_type_1.isCallExpression)(node) &&
(0, is_node_of_type_1.isLiteral)(node.arguments[0]) &&
typeof node.arguments[0].value === 'string') {
return node.arguments[0].value;
}
return undefined;
}
exports.getImportModuleName = getImportModuleName;
function getAssertNodeInfo(node) {
const emptyInfo = { matcher: null, isNegated: false };
if (!(0, is_node_of_type_1.isCallExpression)(node.object) ||
!utils_1.ASTUtils.isIdentifier(node.object.callee)) {
return emptyInfo;
}
if (node.object.callee.name !== 'expect') {
return emptyInfo;
}
let matcher = utils_1.ASTUtils.getPropertyName(node);
const isNegated = matcher === 'not';
if (isNegated) {
matcher =
node.parent && (0, is_node_of_type_1.isMemberExpression)(node.parent)
? utils_1.ASTUtils.getPropertyName(node.parent)
: null;
}
if (!matcher) {
return emptyInfo;
}
return { matcher, isNegated };
}
exports.getAssertNodeInfo = getAssertNodeInfo;
const matcherNamesHandlePromise = [
'resolves',
'rejects',
'toResolve',
'toReject',
];
function hasClosestExpectResolvesRejects(node) {
if ((0, is_node_of_type_1.isCallExpression)(node) &&
utils_1.ASTUtils.isIdentifier(node.callee) &&
node.parent &&
(0, is_node_of_type_1.isMemberExpression)(node.parent) &&
node.callee.name === 'expect') {
const expectMatcher = node.parent.property;
return (utils_1.ASTUtils.isIdentifier(expectMatcher) &&
matcherNamesHandlePromise.includes(expectMatcher.name));
}
if (!node.parent) {
return false;
}
return hasClosestExpectResolvesRejects(node.parent);
}
exports.hasClosestExpectResolvesRejects = hasClosestExpectResolvesRejects;
function getInnermostReturningFunction(context, node) {
const functionScope = getInnermostFunctionScope(context, node);
if (!functionScope) {
return undefined;
}
const returnStatementNode = getFunctionReturnStatementNode(functionScope.block);
if (!returnStatementNode) {
return undefined;
}
const returnStatementIdentifier = getDeepestIdentifierNode(returnStatementNode);
if ((returnStatementIdentifier === null || returnStatementIdentifier === void 0 ? void 0 : returnStatementIdentifier.name) !== node.name) {
return undefined;
}
return functionScope.block;
}
exports.getInnermostReturningFunction = getInnermostReturningFunction;
function hasImportMatch(importNode, identifierName) {
if (utils_1.ASTUtils.isIdentifier(importNode)) {
return importNode.name === identifierName;
}
return importNode.local.name === identifierName;
}
exports.hasImportMatch = hasImportMatch;
function getStatementCallExpression(statement) {
if ((0, is_node_of_type_1.isExpressionStatement)(statement)) {
const { expression } = statement;
if ((0, is_node_of_type_1.isCallExpression)(expression)) {
return expression;
}
if (utils_1.ASTUtils.isAwaitExpression(expression) &&
(0, is_node_of_type_1.isCallExpression)(expression.argument)) {
return expression.argument;
}
if ((0, is_node_of_type_1.isAssignmentExpression)(expression)) {
if ((0, is_node_of_type_1.isCallExpression)(expression.right)) {
return expression.right;
}
if (utils_1.ASTUtils.isAwaitExpression(expression.right) &&
(0, is_node_of_type_1.isCallExpression)(expression.right.argument)) {
return expression.right.argument;
}
}
}
if ((0, is_node_of_type_1.isReturnStatement)(statement) && (0, is_node_of_type_1.isCallExpression)(statement.argument)) {
return statement.argument;
}
if ((0, is_node_of_type_1.isVariableDeclaration)(statement)) {
for (const declaration of statement.declarations) {
if ((0, is_node_of_type_1.isCallExpression)(declaration.init)) {
return declaration.init;
}
}
}
return undefined;
}
exports.getStatementCallExpression = getStatementCallExpression;
function isEmptyFunction(node) {
if (utils_1.ASTUtils.isFunction(node) && (0, is_node_of_type_1.isBlockStatement)(node.body)) {
return node.body.body.length === 0;
}
return false;
}
exports.isEmptyFunction = isEmptyFunction;
function findImportSpecifier(specifierName, node) {
if ((0, is_node_of_type_1.isImportDeclaration)(node)) {
const namedExport = node.specifiers.find((n) => {
return ((0, is_node_of_type_1.isImportSpecifier)(n) &&
[n.imported.name, n.local.name].includes(specifierName));
});
if (namedExport) {
return namedExport;
}
return node.specifiers.find((n) => (0, is_node_of_type_1.isImportNamespaceSpecifier)(n));
}
else {
if (!utils_1.ASTUtils.isVariableDeclarator(node.parent)) {
return undefined;
}
const requireNode = node.parent;
if (utils_1.ASTUtils.isIdentifier(requireNode.id)) {
return requireNode.id;
}
if (!(0, is_node_of_type_1.isObjectPattern)(requireNode.id)) {
return undefined;
}
const property = requireNode.id.properties.find((n) => (0, is_node_of_type_1.isProperty)(n) &&
utils_1.ASTUtils.isIdentifier(n.key) &&
n.key.name === specifierName);
if (!property) {
return undefined;
}
return property.key;
}
}
exports.findImportSpecifier = findImportSpecifier;

View File

@@ -0,0 +1,25 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.isFunctionExpression = exports.isReturnStatement = exports.isProperty = exports.isObjectPattern = exports.isObjectExpression = exports.isNewExpression = exports.isMemberExpression = exports.isLiteral = exports.isJSXAttribute = exports.isImportSpecifier = exports.isImportNamespaceSpecifier = exports.isImportDefaultSpecifier = exports.isImportDeclaration = exports.isSequenceExpression = exports.isAssignmentExpression = exports.isVariableDeclaration = exports.isExpressionStatement = exports.isCallExpression = exports.isBlockStatement = exports.isArrowFunctionExpression = exports.isArrayExpression = void 0;
const utils_1 = require("@typescript-eslint/utils");
exports.isArrayExpression = utils_1.ASTUtils.isNodeOfType(utils_1.AST_NODE_TYPES.ArrayExpression);
exports.isArrowFunctionExpression = utils_1.ASTUtils.isNodeOfType(utils_1.AST_NODE_TYPES.ArrowFunctionExpression);
exports.isBlockStatement = utils_1.ASTUtils.isNodeOfType(utils_1.AST_NODE_TYPES.BlockStatement);
exports.isCallExpression = utils_1.ASTUtils.isNodeOfType(utils_1.AST_NODE_TYPES.CallExpression);
exports.isExpressionStatement = utils_1.ASTUtils.isNodeOfType(utils_1.AST_NODE_TYPES.ExpressionStatement);
exports.isVariableDeclaration = utils_1.ASTUtils.isNodeOfType(utils_1.AST_NODE_TYPES.VariableDeclaration);
exports.isAssignmentExpression = utils_1.ASTUtils.isNodeOfType(utils_1.AST_NODE_TYPES.AssignmentExpression);
exports.isSequenceExpression = utils_1.ASTUtils.isNodeOfType(utils_1.AST_NODE_TYPES.SequenceExpression);
exports.isImportDeclaration = utils_1.ASTUtils.isNodeOfType(utils_1.AST_NODE_TYPES.ImportDeclaration);
exports.isImportDefaultSpecifier = utils_1.ASTUtils.isNodeOfType(utils_1.AST_NODE_TYPES.ImportDefaultSpecifier);
exports.isImportNamespaceSpecifier = utils_1.ASTUtils.isNodeOfType(utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier);
exports.isImportSpecifier = utils_1.ASTUtils.isNodeOfType(utils_1.AST_NODE_TYPES.ImportSpecifier);
exports.isJSXAttribute = utils_1.ASTUtils.isNodeOfType(utils_1.AST_NODE_TYPES.JSXAttribute);
exports.isLiteral = utils_1.ASTUtils.isNodeOfType(utils_1.AST_NODE_TYPES.Literal);
exports.isMemberExpression = utils_1.ASTUtils.isNodeOfType(utils_1.AST_NODE_TYPES.MemberExpression);
exports.isNewExpression = utils_1.ASTUtils.isNodeOfType(utils_1.AST_NODE_TYPES.NewExpression);
exports.isObjectExpression = utils_1.ASTUtils.isNodeOfType(utils_1.AST_NODE_TYPES.ObjectExpression);
exports.isObjectPattern = utils_1.ASTUtils.isNodeOfType(utils_1.AST_NODE_TYPES.ObjectPattern);
exports.isProperty = utils_1.ASTUtils.isNodeOfType(utils_1.AST_NODE_TYPES.Property);
exports.isReturnStatement = utils_1.ASTUtils.isNodeOfType(utils_1.AST_NODE_TYPES.ReturnStatement);
exports.isFunctionExpression = utils_1.ASTUtils.isNodeOfType(utils_1.AST_NODE_TYPES.FunctionExpression);

View File

@@ -0,0 +1,93 @@
{
"name": "eslint-plugin-testing-library",
"version": "5.11.1",
"description": "ESLint plugin to follow best practices and anticipate common mistakes when writing tests with Testing Library",
"keywords": [
"eslint",
"eslintplugin",
"eslint-plugin",
"lint",
"testing-library",
"testing"
],
"homepage": "https://github.com/testing-library/eslint-plugin-testing-library",
"bugs": {
"url": "https://github.com/testing-library/eslint-plugin-testing-library/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/testing-library/eslint-plugin-testing-library"
},
"license": "MIT",
"author": {
"name": "Mario Beltrán Alarcón",
"email": "me@mario.dev",
"url": "https://mario.dev/"
},
"main": "index.js",
"scripts": {
"prebuild": "del-cli dist",
"build": "tsc",
"postbuild": "cpy README.md ./dist && cpy package.json ./dist && cpy LICENSE ./dist",
"generate-all": "run-p \"generate:*\"",
"generate:configs": "ts-node tools/generate-configs",
"generate:rules-doc": "npm run build && npm run rule-doc-generator",
"format": "npm run prettier-base -- --write",
"format:check": "npm run prettier-base -- --check",
"lint": "eslint . --max-warnings 0 --ext .js,.ts",
"lint:fix": "npm run lint -- --fix",
"prepare": "is-ci || husky install",
"prettier-base": "prettier . --ignore-unknown --cache --loglevel warn",
"rule-doc-generator": "eslint-doc-generator --path-rule-list \"../README.md\" --path-rule-doc \"../docs/rules/{name}.md\" --url-rule-doc \"docs/rules/{name}.md\" dist/",
"semantic-release": "semantic-release",
"test": "jest",
"test:ci": "jest --ci --coverage",
"test:watch": "npm run test -- --watch",
"type-check": "tsc --noEmit"
},
"dependencies": {
"@typescript-eslint/utils": "^5.58.0"
},
"devDependencies": {
"@babel/core": "^7.21.4",
"@babel/eslint-parser": "^7.21.3",
"@babel/eslint-plugin": "^7.19.1",
"@commitlint/cli": "^17.5.1",
"@commitlint/config-conventional": "^17.4.4",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.23",
"@typescript-eslint/eslint-plugin": "^5.58.0",
"@typescript-eslint/parser": "^5.58.0",
"cpy-cli": "^4.2.0",
"del-cli": "^5.0.0",
"eslint": "^8.38.0",
"eslint-config-kentcdodds": "^20.5.0",
"eslint-config-prettier": "^8.8.0",
"eslint-doc-generator": "^1.4.3",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-jest": "^27.2.1",
"eslint-plugin-jest-formatting": "^3.1.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-promise": "^6.1.1",
"eslint-remote-tester": "^3.0.0",
"eslint-remote-tester-repositories": "^1.0.1",
"husky": "^8.0.3",
"is-ci": "^3.0.1",
"jest": "^28.1.3",
"lint-staged": "^13.2.1",
"npm-run-all": "^4.1.5",
"prettier": "2.8.7",
"semantic-release": "^19.0.5",
"ts-jest": "^28.0.8",
"ts-node": "^10.9.1",
"typescript": "^4.9.5"
},
"peerDependencies": {
"eslint": "^7.5.0 || ^8.0.0"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0",
"npm": ">=6"
}
}

View File

@@ -0,0 +1,83 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const utils_1 = require("@typescript-eslint/utils");
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'await-async-query';
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'problem',
docs: {
description: 'Enforce promises from async queries to be handled',
recommendedConfig: {
dom: 'error',
angular: 'error',
react: 'error',
vue: 'error',
marko: 'error',
},
},
messages: {
awaitAsyncQuery: 'promise returned from `{{ name }}` query must be handled',
asyncQueryWrapper: 'promise returned from `{{ name }}` wrapper over async query must be handled',
},
schema: [],
},
defaultOptions: [],
create(context, _, helpers) {
const functionWrappersNames = [];
function detectAsyncQueryWrapper(node) {
const innerFunction = (0, node_utils_1.getInnermostReturningFunction)(context, node);
if (innerFunction) {
functionWrappersNames.push((0, node_utils_1.getFunctionName)(innerFunction));
}
}
return {
CallExpression(node) {
const identifierNode = (0, node_utils_1.getDeepestIdentifierNode)(node);
if (!identifierNode) {
return;
}
if (helpers.isAsyncQuery(identifierNode)) {
detectAsyncQueryWrapper(identifierNode);
const closestCallExpressionNode = (0, node_utils_1.findClosestCallExpressionNode)(node, true);
if (!(closestCallExpressionNode === null || closestCallExpressionNode === void 0 ? void 0 : closestCallExpressionNode.parent)) {
return;
}
const references = (0, node_utils_1.getVariableReferences)(context, closestCallExpressionNode.parent);
if (references.length === 0) {
if (!(0, node_utils_1.isPromiseHandled)(identifierNode)) {
context.report({
node: identifierNode,
messageId: 'awaitAsyncQuery',
data: { name: identifierNode.name },
});
return;
}
}
for (const reference of references) {
if (utils_1.ASTUtils.isIdentifier(reference.identifier) &&
!(0, node_utils_1.isPromiseHandled)(reference.identifier)) {
context.report({
node: identifierNode,
messageId: 'awaitAsyncQuery',
data: { name: identifierNode.name },
});
return;
}
}
}
else if (functionWrappersNames.includes(identifierNode.name) &&
!(0, node_utils_1.isPromiseHandled)(identifierNode)) {
context.report({
node: identifierNode,
messageId: 'asyncQueryWrapper',
data: { name: identifierNode.name },
});
}
},
};
},
});

View File

@@ -0,0 +1,122 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const utils_1 = require("@typescript-eslint/utils");
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'await-async-utils';
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'problem',
docs: {
description: 'Enforce promises from async utils to be awaited properly',
recommendedConfig: {
dom: 'error',
angular: 'error',
react: 'error',
vue: 'error',
marko: 'error',
},
},
messages: {
awaitAsyncUtil: 'Promise returned from `{{ name }}` must be handled',
asyncUtilWrapper: 'Promise returned from {{ name }} wrapper over async util must be handled',
},
schema: [],
},
defaultOptions: [],
create(context, _, helpers) {
const functionWrappersNames = [];
function detectAsyncUtilWrapper(node) {
const innerFunction = (0, node_utils_1.getInnermostReturningFunction)(context, node);
if (!innerFunction) {
return;
}
const functionName = (0, node_utils_1.getFunctionName)(innerFunction);
if (functionName.length === 0) {
return;
}
functionWrappersNames.push(functionName);
}
function detectDestructuredAsyncUtilWrapperAliases(node) {
for (const property of node.properties) {
if (!(0, node_utils_1.isProperty)(property)) {
continue;
}
if (!utils_1.ASTUtils.isIdentifier(property.key) ||
!utils_1.ASTUtils.isIdentifier(property.value)) {
continue;
}
if (functionWrappersNames.includes(property.key.name)) {
const isDestructuredAsyncWrapperPropertyRenamed = property.key.name !== property.value.name;
if (isDestructuredAsyncWrapperPropertyRenamed) {
functionWrappersNames.push(property.value.name);
}
}
}
}
const getMessageId = (node) => {
if (helpers.isAsyncUtil(node)) {
return 'awaitAsyncUtil';
}
return 'asyncUtilWrapper';
};
return {
VariableDeclarator(node) {
var _a, _b;
if ((0, node_utils_1.isObjectPattern)(node.id)) {
detectDestructuredAsyncUtilWrapperAliases(node.id);
return;
}
const isAssigningKnownAsyncFunctionWrapper = utils_1.ASTUtils.isIdentifier(node.id) &&
node.init !== null &&
functionWrappersNames.includes((_b = (_a = (0, node_utils_1.getDeepestIdentifierNode)(node.init)) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : '');
if (isAssigningKnownAsyncFunctionWrapper) {
functionWrappersNames.push(node.id.name);
}
},
'CallExpression Identifier'(node) {
const isAsyncUtilOrKnownAliasAroundIt = helpers.isAsyncUtil(node) ||
functionWrappersNames.includes(node.name);
if (!isAsyncUtilOrKnownAliasAroundIt) {
return;
}
if (helpers.isAsyncUtil(node)) {
detectAsyncUtilWrapper(node);
}
const closestCallExpression = (0, node_utils_1.findClosestCallExpressionNode)(node, true);
if (!(closestCallExpression === null || closestCallExpression === void 0 ? void 0 : closestCallExpression.parent)) {
return;
}
const references = (0, node_utils_1.getVariableReferences)(context, closestCallExpression.parent);
if (references.length === 0) {
if (!(0, node_utils_1.isPromiseHandled)(node)) {
context.report({
node,
messageId: getMessageId(node),
data: {
name: node.name,
},
});
}
}
else {
for (const reference of references) {
const referenceNode = reference.identifier;
if (!(0, node_utils_1.isPromiseHandled)(referenceNode)) {
context.report({
node,
messageId: getMessageId(node),
data: {
name: node.name,
},
});
return;
}
}
}
},
};
},
});

View File

@@ -0,0 +1,76 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const utils_1 = require("@typescript-eslint/utils");
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'await-fire-event';
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'problem',
docs: {
description: 'Enforce promises from `fireEvent` methods to be handled',
recommendedConfig: {
dom: false,
angular: false,
react: false,
vue: 'error',
marko: 'error',
},
},
messages: {
awaitFireEvent: 'Promise returned from `fireEvent.{{ name }}` must be handled',
fireEventWrapper: 'Promise returned from `{{ name }}` wrapper over fire event method must be handled',
},
schema: [],
},
defaultOptions: [],
create(context, _, helpers) {
const functionWrappersNames = [];
function reportUnhandledNode(node, closestCallExpressionNode, messageId = 'awaitFireEvent') {
if (!(0, node_utils_1.isPromiseHandled)(node)) {
context.report({
node: closestCallExpressionNode.callee,
messageId,
data: { name: node.name },
});
}
}
function detectFireEventMethodWrapper(node) {
const innerFunction = (0, node_utils_1.getInnermostReturningFunction)(context, node);
if (innerFunction) {
functionWrappersNames.push((0, node_utils_1.getFunctionName)(innerFunction));
}
}
return {
'CallExpression Identifier'(node) {
if (helpers.isFireEventMethod(node)) {
detectFireEventMethodWrapper(node);
const closestCallExpression = (0, node_utils_1.findClosestCallExpressionNode)(node, true);
if (!(closestCallExpression === null || closestCallExpression === void 0 ? void 0 : closestCallExpression.parent)) {
return;
}
const references = (0, node_utils_1.getVariableReferences)(context, closestCallExpression.parent);
if (references.length === 0) {
reportUnhandledNode(node, closestCallExpression);
}
else {
for (const reference of references) {
if (utils_1.ASTUtils.isIdentifier(reference.identifier)) {
reportUnhandledNode(reference.identifier, closestCallExpression);
}
}
}
}
else if (functionWrappersNames.includes(node.name)) {
const closestCallExpression = (0, node_utils_1.findClosestCallExpressionNode)(node, true);
if (!closestCallExpression) {
return;
}
reportUnhandledNode(node, closestCallExpression, 'fireEventWrapper');
}
},
};
},
});

View File

@@ -0,0 +1,129 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'consistent-data-testid';
const FILENAME_PLACEHOLDER = '{fileName}';
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'suggestion',
docs: {
description: 'Ensures consistent usage of `data-testid`',
recommendedConfig: {
dom: false,
angular: false,
react: false,
vue: false,
marko: false,
},
},
messages: {
consistentDataTestId: '`{{attr}}` "{{value}}" should match `{{regex}}`',
consistentDataTestIdCustomMessage: '`{{message}}`',
},
schema: [
{
type: 'object',
default: {},
additionalProperties: false,
required: ['testIdPattern'],
properties: {
testIdPattern: {
type: 'string',
},
testIdAttribute: {
default: 'data-testid',
oneOf: [
{
type: 'string',
},
{
type: 'array',
items: {
type: 'string',
},
},
],
},
customMessage: {
default: undefined,
type: 'string',
},
},
},
],
},
defaultOptions: [
{
testIdPattern: '',
testIdAttribute: 'data-testid',
customMessage: undefined,
},
],
detectionOptions: {
skipRuleReportingCheck: true,
},
create: (context, [options]) => {
const { getFilename } = context;
const { testIdPattern, testIdAttribute: attr, customMessage } = options;
function getFileNameData() {
var _a;
const splitPath = getFilename().split('/');
const fileNameWithExtension = (_a = splitPath.pop()) !== null && _a !== void 0 ? _a : '';
if (fileNameWithExtension.includes('[') ||
fileNameWithExtension.includes(']')) {
return { fileName: undefined };
}
const parent = splitPath.pop();
const fileName = fileNameWithExtension.split('.').shift();
return {
fileName: fileName === 'index' ? parent : fileName,
};
}
function getTestIdValidator(fileName) {
return new RegExp(testIdPattern.replace(FILENAME_PLACEHOLDER, fileName));
}
function isTestIdAttribute(name) {
var _a;
if (typeof attr === 'string') {
return attr === name;
}
else {
return (_a = attr === null || attr === void 0 ? void 0 : attr.includes(name)) !== null && _a !== void 0 ? _a : false;
}
}
function getErrorMessageId() {
if (customMessage === undefined) {
return 'consistentDataTestId';
}
return 'consistentDataTestIdCustomMessage';
}
return {
JSXIdentifier: (node) => {
if (!node.parent ||
!(0, node_utils_1.isJSXAttribute)(node.parent) ||
!(0, node_utils_1.isLiteral)(node.parent.value) ||
!isTestIdAttribute(node.name)) {
return;
}
const value = node.parent.value.value;
const { fileName } = getFileNameData();
const regex = getTestIdValidator(fileName !== null && fileName !== void 0 ? fileName : '');
if (value && typeof value === 'string' && !regex.test(value)) {
context.report({
node,
messageId: getErrorMessageId(),
data: {
attr: node.name,
value,
regex,
message: customMessage,
},
});
}
},
};
},
});

View File

@@ -0,0 +1,11 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const fs_1 = require("fs");
const path_1 = require("path");
const utils_1 = require("../utils");
const rulesDir = __dirname;
const excludedFiles = ['index'];
exports.default = (0, fs_1.readdirSync)(rulesDir)
.map((rule) => (0, path_1.parse)(rule).name)
.filter((ruleName) => !excludedFiles.includes(ruleName))
.reduce((allRules, ruleName) => (Object.assign(Object.assign({}, allRules), { [ruleName]: (0, utils_1.importDefault)((0, path_1.join)(rulesDir, ruleName)) })), {});

View File

@@ -0,0 +1,113 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const utils_1 = require("@typescript-eslint/utils");
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
const USER_EVENT_ASYNC_EXCEPTIONS = ['type', 'keyboard'];
const VALID_EVENT_MODULES = ['fire-event', 'user-event'];
exports.RULE_NAME = 'no-await-sync-events';
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'problem',
docs: {
description: 'Disallow unnecessary `await` for sync events',
recommendedConfig: {
dom: false,
angular: false,
react: false,
vue: false,
marko: false,
},
},
messages: {
noAwaitSyncEvents: '`{{ name }}` is sync and does not need `await` operator',
},
schema: [
{
type: 'object',
properties: {
eventModules: {
type: 'array',
minItems: 1,
items: {
enum: VALID_EVENT_MODULES,
},
},
},
additionalProperties: false,
},
],
},
defaultOptions: [{ eventModules: VALID_EVENT_MODULES }],
create(context, [options], helpers) {
const { eventModules = VALID_EVENT_MODULES } = options;
let hasDelayDeclarationOrAssignmentGTZero;
return {
VariableDeclaration(node) {
hasDelayDeclarationOrAssignmentGTZero = node.declarations.some((property) => utils_1.ASTUtils.isIdentifier(property.id) &&
property.id.name === 'delay' &&
(0, node_utils_1.isLiteral)(property.init) &&
property.init.value &&
property.init.value > 0);
},
AssignmentExpression(node) {
if (utils_1.ASTUtils.isIdentifier(node.left) &&
node.left.name === 'delay' &&
(0, node_utils_1.isLiteral)(node.right) &&
node.right.value !== null) {
hasDelayDeclarationOrAssignmentGTZero = node.right.value > 0;
}
},
'AwaitExpression > CallExpression'(node) {
var _a;
const simulateEventFunctionIdentifier = (0, node_utils_1.getDeepestIdentifierNode)(node);
if (!simulateEventFunctionIdentifier) {
return;
}
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 hasDelayProperty = (0, node_utils_1.isObjectExpression)(lastArg) &&
lastArg.properties.some((property) => (0, node_utils_1.isProperty)(property) &&
utils_1.ASTUtils.isIdentifier(property.key) &&
property.key.name === 'delay');
const hasDelayLiteralGTZero = (0, node_utils_1.isObjectExpression)(lastArg) &&
lastArg.properties.some((property) => (0, node_utils_1.isProperty)(property) &&
utils_1.ASTUtils.isIdentifier(property.key) &&
property.key.name === 'delay' &&
(0, node_utils_1.isLiteral)(property.value) &&
!!property.value.value &&
property.value.value > 0);
const simulateEventFunctionName = simulateEventFunctionIdentifier.name;
if (USER_EVENT_ASYNC_EXCEPTIONS.includes(simulateEventFunctionName) &&
hasDelayProperty &&
(hasDelayDeclarationOrAssignmentGTZero || hasDelayLiteralGTZero)) {
return;
}
const eventModuleName = (_a = (0, node_utils_1.getPropertyIdentifierNode)(node)) === null || _a === void 0 ? void 0 : _a.name;
const eventFullName = eventModuleName
? `${eventModuleName}.${simulateEventFunctionName}`
: simulateEventFunctionName;
context.report({
node,
messageId: 'noAwaitSyncEvents',
data: {
name: eventFullName,
},
});
},
};
},
});

View File

@@ -0,0 +1,46 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'no-await-sync-query';
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'problem',
docs: {
description: 'Disallow unnecessary `await` for sync queries',
recommendedConfig: {
dom: 'error',
angular: 'error',
react: 'error',
vue: 'error',
marko: 'error',
},
},
messages: {
noAwaitSyncQuery: '`{{ name }}` query is sync so it does not need to be awaited',
},
schema: [],
},
defaultOptions: [],
create(context, _, helpers) {
return {
'AwaitExpression > CallExpression'(node) {
const deepestIdentifierNode = (0, node_utils_1.getDeepestIdentifierNode)(node);
if (!deepestIdentifierNode) {
return;
}
if (helpers.isSyncQuery(deepestIdentifierNode)) {
context.report({
node: deepestIdentifierNode,
messageId: 'noAwaitSyncQuery',
data: {
name: deepestIdentifierNode.name,
},
});
}
},
};
},
});

View File

@@ -0,0 +1,123 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const utils_1 = require("@typescript-eslint/utils");
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'no-container';
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'problem',
docs: {
description: 'Disallow the use of `container` methods',
recommendedConfig: {
dom: false,
angular: 'error',
react: 'error',
vue: 'error',
marko: 'error',
},
},
messages: {
noContainer: 'Avoid using container methods. Prefer using the methods from Testing Library, such as "getByRole()"',
},
schema: [],
},
defaultOptions: [],
create(context, _, helpers) {
const destructuredContainerPropNames = [];
const renderWrapperNames = [];
let renderResultVarName = null;
let containerName = null;
let containerCallsMethod = false;
function detectRenderWrapper(node) {
const innerFunction = (0, node_utils_1.getInnermostReturningFunction)(context, node);
if (innerFunction) {
renderWrapperNames.push((0, node_utils_1.getFunctionName)(innerFunction));
}
}
function showErrorIfChainedContainerMethod(innerNode) {
if ((0, node_utils_1.isMemberExpression)(innerNode)) {
if (utils_1.ASTUtils.isIdentifier(innerNode.object)) {
const isContainerName = innerNode.object.name === containerName;
if (isContainerName) {
context.report({
node: innerNode,
messageId: 'noContainer',
});
return;
}
const isRenderWrapper = innerNode.object.name === renderResultVarName;
containerCallsMethod =
utils_1.ASTUtils.isIdentifier(innerNode.property) &&
innerNode.property.name === 'container' &&
isRenderWrapper;
if (containerCallsMethod) {
context.report({
node: innerNode.property,
messageId: 'noContainer',
});
return;
}
}
showErrorIfChainedContainerMethod(innerNode.object);
}
}
return {
CallExpression(node) {
const callExpressionIdentifier = (0, node_utils_1.getDeepestIdentifierNode)(node);
if (!callExpressionIdentifier) {
return;
}
if (helpers.isRenderUtil(callExpressionIdentifier)) {
detectRenderWrapper(callExpressionIdentifier);
}
if ((0, node_utils_1.isMemberExpression)(node.callee)) {
showErrorIfChainedContainerMethod(node.callee);
}
else if (utils_1.ASTUtils.isIdentifier(node.callee) &&
destructuredContainerPropNames.includes(node.callee.name)) {
context.report({
node,
messageId: 'noContainer',
});
}
},
VariableDeclarator(node) {
if (!node.init) {
return;
}
const initIdentifierNode = (0, node_utils_1.getDeepestIdentifierNode)(node.init);
if (!initIdentifierNode) {
return;
}
const isRenderWrapperVariableDeclarator = renderWrapperNames.includes(initIdentifierNode.name);
if (!helpers.isRenderVariableDeclarator(node) &&
!isRenderWrapperVariableDeclarator) {
return;
}
if ((0, node_utils_1.isObjectPattern)(node.id)) {
const containerIndex = node.id.properties.findIndex((property) => (0, node_utils_1.isProperty)(property) &&
utils_1.ASTUtils.isIdentifier(property.key) &&
property.key.name === 'container');
const nodeValue = containerIndex !== -1 && node.id.properties[containerIndex].value;
if (!nodeValue) {
return;
}
if (utils_1.ASTUtils.isIdentifier(nodeValue)) {
containerName = nodeValue.name;
}
else if ((0, node_utils_1.isObjectPattern)(nodeValue)) {
nodeValue.properties.forEach((property) => (0, node_utils_1.isProperty)(property) &&
utils_1.ASTUtils.isIdentifier(property.key) &&
destructuredContainerPropNames.push(property.key.name));
}
}
else if (utils_1.ASTUtils.isIdentifier(node.id)) {
renderResultVarName = node.id.name;
}
},
};
},
});

View File

@@ -0,0 +1,125 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const utils_1 = require("@typescript-eslint/utils");
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
const utils_2 = require("../utils");
exports.RULE_NAME = 'no-debugging-utils';
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'problem',
docs: {
description: 'Disallow the use of debugging utilities like `debug`',
recommendedConfig: {
dom: false,
angular: 'error',
react: 'error',
vue: 'error',
marko: 'error',
},
},
messages: {
noDebug: 'Unexpected debug statement',
},
schema: [
{
type: 'object',
properties: {
utilsToCheckFor: {
type: 'object',
properties: utils_2.DEBUG_UTILS.reduce((obj, name) => (Object.assign({ [name]: { type: 'boolean' } }, obj)), {}),
additionalProperties: false,
},
},
additionalProperties: false,
},
],
},
defaultOptions: [
{ utilsToCheckFor: { debug: true, logTestingPlaygroundURL: true } },
],
create(context, [{ utilsToCheckFor = {} }], helpers) {
const suspiciousDebugVariableNames = [];
const suspiciousReferenceNodes = [];
const renderWrapperNames = [];
const builtInConsoleNodes = [];
const utilsToReport = Object.entries(utilsToCheckFor)
.filter(([, shouldCheckFor]) => shouldCheckFor)
.map(([name]) => name);
function detectRenderWrapper(node) {
const innerFunction = (0, node_utils_1.getInnermostReturningFunction)(context, node);
if (innerFunction) {
renderWrapperNames.push((0, node_utils_1.getFunctionName)(innerFunction));
}
}
return {
VariableDeclarator(node) {
if (!node.init) {
return;
}
const initIdentifierNode = (0, node_utils_1.getDeepestIdentifierNode)(node.init);
if (!initIdentifierNode) {
return;
}
if (initIdentifierNode.name === 'console') {
builtInConsoleNodes.push(node);
return;
}
const isRenderWrapperVariableDeclarator = renderWrapperNames.includes(initIdentifierNode.name);
if (!helpers.isRenderVariableDeclarator(node) &&
!isRenderWrapperVariableDeclarator) {
return;
}
if ((0, node_utils_1.isObjectPattern)(node.id)) {
for (const property of node.id.properties) {
if ((0, node_utils_1.isProperty)(property) &&
utils_1.ASTUtils.isIdentifier(property.key) &&
utilsToReport.includes(property.key.name)) {
const identifierNode = (0, node_utils_1.getDeepestIdentifierNode)(property.value);
if (identifierNode) {
suspiciousDebugVariableNames.push(identifierNode.name);
}
}
}
}
if (utils_1.ASTUtils.isIdentifier(node.id)) {
suspiciousReferenceNodes.push(node.id);
}
},
CallExpression(node) {
const callExpressionIdentifier = (0, node_utils_1.getDeepestIdentifierNode)(node);
if (!callExpressionIdentifier) {
return;
}
if (helpers.isRenderUtil(callExpressionIdentifier)) {
detectRenderWrapper(callExpressionIdentifier);
}
const referenceNode = (0, node_utils_1.getReferenceNode)(node);
const referenceIdentifier = (0, node_utils_1.getPropertyIdentifierNode)(referenceNode);
if (!referenceIdentifier) {
return;
}
const isDebugUtil = helpers.isDebugUtil(callExpressionIdentifier, utilsToReport);
const isDeclaredDebugVariable = suspiciousDebugVariableNames.includes(callExpressionIdentifier.name);
const isChainedReferenceDebug = suspiciousReferenceNodes.some((suspiciousReferenceIdentifier) => {
return (utilsToReport.includes(callExpressionIdentifier.name) &&
suspiciousReferenceIdentifier.name === referenceIdentifier.name);
});
const isVariableFromBuiltInConsole = builtInConsoleNodes.some((variableDeclarator) => {
const variables = context.getDeclaredVariables(variableDeclarator);
return variables.some(({ name }) => name === callExpressionIdentifier.name &&
(0, node_utils_1.isCallExpression)(callExpressionIdentifier.parent));
});
if (!isVariableFromBuiltInConsole &&
(isDebugUtil || isDeclaredDebugVariable || isChainedReferenceDebug)) {
context.report({
node: callExpressionIdentifier,
messageId: 'noDebug',
});
}
},
};
},
});

View File

@@ -0,0 +1,81 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'no-dom-import';
const DOM_TESTING_LIBRARY_MODULES = [
'dom-testing-library',
'@testing-library/dom',
];
const CORRECT_MODULE_NAME_BY_FRAMEWORK = {
angular: '@testing-library/angular',
marko: '@marko/testing-library',
};
const getCorrectModuleName = (moduleName, framework) => {
var _a;
return ((_a = CORRECT_MODULE_NAME_BY_FRAMEWORK[framework]) !== null && _a !== void 0 ? _a : moduleName.replace('dom', framework));
};
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'problem',
docs: {
description: 'Disallow importing from DOM Testing Library',
recommendedConfig: {
dom: false,
angular: ['error', 'angular'],
react: ['error', 'react'],
vue: ['error', 'vue'],
marko: ['error', 'marko'],
},
},
messages: {
noDomImport: 'import from DOM Testing Library is restricted, import from corresponding Testing Library framework instead',
noDomImportFramework: 'import from DOM Testing Library is restricted, import from {{module}} instead',
},
fixable: 'code',
schema: [{ type: 'string' }],
},
defaultOptions: [''],
create(context, [framework], helpers) {
function report(node, moduleName) {
if (!framework) {
return context.report({
node,
messageId: 'noDomImport',
});
}
const correctModuleName = getCorrectModuleName(moduleName, framework);
context.report({
data: { module: correctModuleName },
fix(fixer) {
if ((0, node_utils_1.isCallExpression)(node)) {
const name = node.arguments[0];
return fixer.replaceText(name, name.raw.replace(moduleName, correctModuleName));
}
else {
const name = node.source;
return fixer.replaceText(name, name.raw.replace(moduleName, correctModuleName));
}
},
messageId: 'noDomImportFramework',
node,
});
}
return {
'Program:exit'() {
let importName;
const allImportNodes = helpers.getAllTestingLibraryImportNodes();
allImportNodes.forEach((importNode) => {
importName = (0, node_utils_1.getImportModuleName)(importNode);
const domModuleName = DOM_TESTING_LIBRARY_MODULES.find((module) => module === importName);
if (!domModuleName) {
return;
}
report(importNode, domModuleName);
});
},
};
},
});

View File

@@ -0,0 +1,122 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const utils_1 = require("@typescript-eslint/utils");
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'no-global-regexp-flag-in-query';
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'suggestion',
docs: {
description: 'Disallow the use of the global RegExp flag (/g) in queries',
recommendedConfig: {
dom: false,
angular: false,
react: false,
vue: false,
marko: false,
},
},
messages: {
noGlobalRegExpFlagInQuery: 'Avoid using the global RegExp flag (/g) in queries',
},
fixable: 'code',
schema: [],
},
defaultOptions: [],
create(context, _, helpers) {
function reportLiteralWithRegex(literalNode) {
if ((0, node_utils_1.isLiteral)(literalNode) &&
'regex' in literalNode &&
literalNode.regex.flags.includes('g')) {
context.report({
node: literalNode,
messageId: 'noGlobalRegExpFlagInQuery',
fix(fixer) {
const splitter = literalNode.raw.lastIndexOf('/');
const raw = literalNode.raw.substring(0, splitter);
const flags = literalNode.raw.substring(splitter + 1);
const flagsWithoutGlobal = flags.replace('g', '');
return fixer.replaceText(literalNode, `${raw}/${flagsWithoutGlobal}`);
},
});
return true;
}
return false;
}
function getArguments(identifierNode) {
if ((0, node_utils_1.isCallExpression)(identifierNode.parent)) {
return identifierNode.parent.arguments;
}
else if ((0, node_utils_1.isMemberExpression)(identifierNode.parent) &&
(0, node_utils_1.isCallExpression)(identifierNode.parent.parent)) {
return identifierNode.parent.parent.arguments;
}
return [];
}
const variableNodesWithRegexs = [];
function hasRegexInVariable(identifier) {
return variableNodesWithRegexs.find((varNode) => {
if (utils_1.ASTUtils.isVariableDeclarator(varNode) &&
utils_1.ASTUtils.isIdentifier(varNode.id)) {
return varNode.id.name === identifier.name;
}
return undefined;
});
}
return {
VariableDeclarator(node) {
if (utils_1.ASTUtils.isVariableDeclarator(node) &&
(0, node_utils_1.isLiteral)(node.init) &&
'regex' in node.init &&
node.init.regex.flags.includes('g')) {
variableNodesWithRegexs.push(node);
}
},
CallExpression(node) {
const identifierNode = (0, node_utils_1.getDeepestIdentifierNode)(node);
if (!identifierNode || !helpers.isQuery(identifierNode)) {
return;
}
const [firstArg, secondArg] = getArguments(identifierNode);
const firstArgumentHasError = reportLiteralWithRegex(firstArg);
if (firstArgumentHasError) {
return;
}
if (utils_1.ASTUtils.isIdentifier(firstArg)) {
const regexVariableNode = hasRegexInVariable(firstArg);
if (regexVariableNode !== undefined) {
context.report({
node: firstArg,
messageId: 'noGlobalRegExpFlagInQuery',
fix(fixer) {
if (utils_1.ASTUtils.isVariableDeclarator(regexVariableNode) &&
(0, node_utils_1.isLiteral)(regexVariableNode.init) &&
'regex' in regexVariableNode.init &&
regexVariableNode.init.regex.flags.includes('g')) {
const splitter = regexVariableNode.init.raw.lastIndexOf('/');
const raw = regexVariableNode.init.raw.substring(0, splitter);
const flags = regexVariableNode.init.raw.substring(splitter + 1);
const flagsWithoutGlobal = flags.replace('g', '');
return fixer.replaceText(regexVariableNode.init, `${raw}/${flagsWithoutGlobal}`);
}
return null;
},
});
}
}
if ((0, node_utils_1.isObjectExpression)(secondArg)) {
const namePropertyNode = secondArg.properties.find((p) => (0, node_utils_1.isProperty)(p) &&
utils_1.ASTUtils.isIdentifier(p.key) &&
p.key.name === 'name' &&
(0, node_utils_1.isLiteral)(p.value));
if (namePropertyNode) {
reportLiteralWithRegex(namePropertyNode.value);
}
}
},
};
},
});

View File

@@ -0,0 +1,94 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const utils_1 = require("@typescript-eslint/utils");
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'no-manual-cleanup';
const CLEANUP_LIBRARY_REGEXP = /(@testing-library\/(preact|react|svelte|vue))|@marko\/testing-library/;
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'problem',
docs: {
description: 'Disallow the use of `cleanup`',
recommendedConfig: {
dom: false,
angular: false,
react: false,
vue: false,
marko: false,
},
},
messages: {
noManualCleanup: "`cleanup` is performed automatically by your test runner, you don't need manual cleanups.",
},
schema: [],
},
defaultOptions: [],
create(context, _, helpers) {
function reportImportReferences(references) {
for (const reference of references) {
const utilsUsage = reference.identifier.parent;
if (utilsUsage &&
(0, node_utils_1.isMemberExpression)(utilsUsage) &&
utils_1.ASTUtils.isIdentifier(utilsUsage.property) &&
utilsUsage.property.name === 'cleanup') {
context.report({
node: utilsUsage.property,
messageId: 'noManualCleanup',
});
}
}
}
function reportCandidateModule(moduleNode) {
if ((0, node_utils_1.isImportDeclaration)(moduleNode)) {
if ((0, node_utils_1.isImportDefaultSpecifier)(moduleNode.specifiers[0])) {
const { references } = context.getDeclaredVariables(moduleNode)[0];
reportImportReferences(references);
}
const cleanupSpecifier = moduleNode.specifiers.find((specifier) => (0, node_utils_1.isImportSpecifier)(specifier) &&
specifier.imported.name === 'cleanup');
if (cleanupSpecifier) {
context.report({
node: cleanupSpecifier,
messageId: 'noManualCleanup',
});
}
}
else {
const declaratorNode = moduleNode.parent;
if ((0, node_utils_1.isObjectPattern)(declaratorNode.id)) {
const cleanupProperty = declaratorNode.id.properties.find((property) => (0, node_utils_1.isProperty)(property) &&
utils_1.ASTUtils.isIdentifier(property.key) &&
property.key.name === 'cleanup');
if (cleanupProperty) {
context.report({
node: cleanupProperty,
messageId: 'noManualCleanup',
});
}
}
else {
const references = (0, node_utils_1.getVariableReferences)(context, declaratorNode);
reportImportReferences(references);
}
}
}
return {
'Program:exit'() {
const testingLibraryImportName = helpers.getTestingLibraryImportName();
const testingLibraryImportNode = helpers.getTestingLibraryImportNode();
const customModuleImportNode = helpers.getCustomModuleImportNode();
if (testingLibraryImportName &&
testingLibraryImportNode &&
testingLibraryImportName.match(CLEANUP_LIBRARY_REGEXP)) {
reportCandidateModule(testingLibraryImportNode);
}
if (customModuleImportNode) {
reportCandidateModule(customModuleImportNode);
}
},
};
},
});

View File

@@ -0,0 +1,67 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const utils_1 = require("@typescript-eslint/utils");
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const utils_2 = require("../utils");
exports.RULE_NAME = 'no-node-access';
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'problem',
docs: {
description: 'Disallow direct Node access',
recommendedConfig: {
dom: false,
angular: 'error',
react: 'error',
vue: 'error',
marko: 'error',
},
},
messages: {
noNodeAccess: 'Avoid direct Node access. Prefer using the methods from Testing Library.',
},
schema: [
{
type: 'object',
properties: {
allowContainerFirstChild: {
type: 'boolean',
},
},
},
],
},
defaultOptions: [
{
allowContainerFirstChild: false,
},
],
create(context, [{ allowContainerFirstChild = false }], helpers) {
function showErrorForNodeAccess(node) {
if (!helpers.isTestingLibraryImported(true)) {
return;
}
if (utils_1.ASTUtils.isIdentifier(node.property) &&
utils_2.ALL_RETURNING_NODES.includes(node.property.name)) {
if (allowContainerFirstChild && node.property.name === 'firstChild') {
return;
}
if (utils_1.ASTUtils.isIdentifier(node.object) &&
node.object.name === 'props') {
return;
}
context.report({
node,
loc: node.property.loc.start,
messageId: 'noNodeAccess',
});
}
}
return {
'ExpressionStatement MemberExpression': showErrorForNodeAccess,
'VariableDeclarator MemberExpression': showErrorForNodeAccess,
};
},
});

View File

@@ -0,0 +1,83 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const utils_1 = require("@typescript-eslint/utils");
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'no-promise-in-fire-event';
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'problem',
docs: {
description: 'Disallow the use of promises passed to a `fireEvent` method',
recommendedConfig: {
dom: 'error',
angular: 'error',
react: 'error',
vue: 'error',
marko: 'error',
},
},
messages: {
noPromiseInFireEvent: "A promise shouldn't be passed to a `fireEvent` method, instead pass the DOM element",
},
schema: [],
},
defaultOptions: [],
create(context, _, helpers) {
function checkSuspiciousNode(node, originalNode) {
if (utils_1.ASTUtils.isAwaitExpression(node)) {
return;
}
if ((0, node_utils_1.isNewExpression)(node)) {
if ((0, node_utils_1.isPromiseIdentifier)(node.callee)) {
context.report({
node: originalNode !== null && originalNode !== void 0 ? originalNode : node,
messageId: 'noPromiseInFireEvent',
});
return;
}
}
if ((0, node_utils_1.isCallExpression)(node)) {
const domElementIdentifier = (0, node_utils_1.getDeepestIdentifierNode)(node);
if (!domElementIdentifier) {
return;
}
if (helpers.isAsyncQuery(domElementIdentifier) ||
(0, node_utils_1.isPromiseIdentifier)(domElementIdentifier)) {
context.report({
node: originalNode !== null && originalNode !== void 0 ? originalNode : node,
messageId: 'noPromiseInFireEvent',
});
return;
}
}
if (utils_1.ASTUtils.isIdentifier(node)) {
const nodeVariable = utils_1.ASTUtils.findVariable(context.getScope(), node.name);
if (!nodeVariable) {
return;
}
for (const definition of nodeVariable.defs) {
const variableDeclarator = definition.node;
if (variableDeclarator.init) {
checkSuspiciousNode(variableDeclarator.init, node);
}
}
}
}
return {
'CallExpression Identifier'(node) {
if (!helpers.isFireEventMethod(node)) {
return;
}
const closestCallExpression = (0, node_utils_1.findClosestCallExpressionNode)(node, true);
if (!closestCallExpression) {
return;
}
const domElementArgument = closestCallExpression.arguments[0];
checkSuspiciousNode(domElementArgument);
},
};
},
});

View File

@@ -0,0 +1,94 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.findClosestBeforeHook = exports.RULE_NAME = void 0;
const utils_1 = require("@typescript-eslint/utils");
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
const utils_2 = require("../utils");
exports.RULE_NAME = 'no-render-in-setup';
function findClosestBeforeHook(node, testingFrameworkSetupHooksToFilter) {
if (node === null) {
return null;
}
if ((0, node_utils_1.isCallExpression)(node) &&
utils_1.ASTUtils.isIdentifier(node.callee) &&
testingFrameworkSetupHooksToFilter.includes(node.callee.name)) {
return node.callee;
}
if (node.parent) {
return findClosestBeforeHook(node.parent, testingFrameworkSetupHooksToFilter);
}
return null;
}
exports.findClosestBeforeHook = findClosestBeforeHook;
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'problem',
docs: {
description: 'Disallow the use of `render` in testing frameworks setup functions',
recommendedConfig: {
dom: false,
angular: 'error',
react: 'error',
vue: 'error',
marko: 'error',
},
},
messages: {
noRenderInSetup: 'Forbidden usage of `render` within testing framework `{{ name }}` setup',
},
schema: [
{
type: 'object',
properties: {
allowTestingFrameworkSetupHook: {
enum: utils_2.TESTING_FRAMEWORK_SETUP_HOOKS,
},
},
},
],
},
defaultOptions: [
{
allowTestingFrameworkSetupHook: '',
},
],
create(context, [{ allowTestingFrameworkSetupHook }], helpers) {
const renderWrapperNames = [];
function detectRenderWrapper(node) {
const innerFunction = (0, node_utils_1.getInnermostReturningFunction)(context, node);
if (innerFunction) {
renderWrapperNames.push((0, node_utils_1.getFunctionName)(innerFunction));
}
}
return {
CallExpression(node) {
const testingFrameworkSetupHooksToFilter = utils_2.TESTING_FRAMEWORK_SETUP_HOOKS.filter((hook) => hook !== allowTestingFrameworkSetupHook);
const callExpressionIdentifier = (0, node_utils_1.getDeepestIdentifierNode)(node);
if (!callExpressionIdentifier) {
return;
}
const isRenderIdentifier = helpers.isRenderUtil(callExpressionIdentifier);
if (isRenderIdentifier) {
detectRenderWrapper(callExpressionIdentifier);
}
if (!isRenderIdentifier &&
!renderWrapperNames.includes(callExpressionIdentifier.name)) {
return;
}
const beforeHook = findClosestBeforeHook(node, testingFrameworkSetupHooksToFilter);
if (!beforeHook) {
return;
}
context.report({
node: callExpressionIdentifier,
messageId: 'noRenderInSetup',
data: {
name: beforeHook.name,
},
});
},
};
},
});

View File

@@ -0,0 +1,141 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const utils_1 = require("@typescript-eslint/utils");
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'no-unnecessary-act';
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'problem',
docs: {
description: 'Disallow wrapping Testing Library utils or empty callbacks in `act`',
recommendedConfig: {
dom: false,
angular: false,
react: 'error',
vue: false,
marko: 'error',
},
},
messages: {
noUnnecessaryActTestingLibraryUtil: 'Avoid wrapping Testing Library util calls in `act`',
noUnnecessaryActEmptyFunction: 'Avoid wrapping empty function in `act`',
},
schema: [
{
type: 'object',
properties: {
isStrict: {
type: 'boolean',
},
},
},
],
},
defaultOptions: [
{
isStrict: true,
},
],
create(context, [{ isStrict = true }], helpers) {
function getStatementIdentifier(statement) {
const callExpression = (0, node_utils_1.getStatementCallExpression)(statement);
if (!callExpression &&
!(0, node_utils_1.isExpressionStatement)(statement) &&
!(0, node_utils_1.isReturnStatement)(statement)) {
return null;
}
if (callExpression) {
return (0, node_utils_1.getDeepestIdentifierNode)(callExpression);
}
if ((0, node_utils_1.isExpressionStatement)(statement) &&
utils_1.ASTUtils.isAwaitExpression(statement.expression)) {
return (0, node_utils_1.getPropertyIdentifierNode)(statement.expression.argument);
}
if ((0, node_utils_1.isReturnStatement)(statement) && statement.argument) {
return (0, node_utils_1.getPropertyIdentifierNode)(statement.argument);
}
return null;
}
function hasSomeNonTestingLibraryCall(statements) {
return statements.some((statement) => {
const identifier = getStatementIdentifier(statement);
if (!identifier) {
return false;
}
return !helpers.isTestingLibraryUtil(identifier);
});
}
function hasTestingLibraryCall(statements) {
return statements.some((statement) => {
const identifier = getStatementIdentifier(statement);
if (!identifier) {
return false;
}
return helpers.isTestingLibraryUtil(identifier);
});
}
function checkNoUnnecessaryActFromBlockStatement(blockStatementNode) {
const functionNode = blockStatementNode.parent;
const callExpressionNode = functionNode === null || functionNode === void 0 ? void 0 : functionNode.parent;
if (!callExpressionNode || !functionNode) {
return;
}
const identifierNode = (0, node_utils_1.getDeepestIdentifierNode)(callExpressionNode);
if (!identifierNode) {
return;
}
if (!helpers.isActUtil(identifierNode)) {
return;
}
if ((0, node_utils_1.isEmptyFunction)(functionNode)) {
context.report({
node: identifierNode,
messageId: 'noUnnecessaryActEmptyFunction',
});
return;
}
const shouldBeReported = isStrict
? hasTestingLibraryCall(blockStatementNode.body)
: !hasSomeNonTestingLibraryCall(blockStatementNode.body);
if (shouldBeReported) {
context.report({
node: identifierNode,
messageId: 'noUnnecessaryActTestingLibraryUtil',
});
}
}
function checkNoUnnecessaryActFromImplicitReturn(node) {
var _a;
const nodeIdentifier = (0, node_utils_1.getDeepestIdentifierNode)(node);
if (!nodeIdentifier) {
return;
}
const parentCallExpression = (_a = node.parent) === null || _a === void 0 ? void 0 : _a.parent;
if (!parentCallExpression) {
return;
}
const identifierNode = (0, node_utils_1.getDeepestIdentifierNode)(parentCallExpression);
if (!identifierNode) {
return;
}
if (!helpers.isActUtil(identifierNode)) {
return;
}
if (!helpers.isTestingLibraryUtil(nodeIdentifier)) {
return;
}
context.report({
node: identifierNode,
messageId: 'noUnnecessaryActTestingLibraryUtil',
});
}
return {
'CallExpression > ArrowFunctionExpression > BlockStatement': checkNoUnnecessaryActFromBlockStatement,
'CallExpression > FunctionExpression > BlockStatement': checkNoUnnecessaryActFromBlockStatement,
'CallExpression > ArrowFunctionExpression > CallExpression': checkNoUnnecessaryActFromImplicitReturn,
};
},
});

View File

@@ -0,0 +1,78 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const utils_1 = require("@typescript-eslint/utils");
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'no-wait-for-empty-callback';
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'suggestion',
docs: {
description: 'Disallow empty callbacks for `waitFor` and `waitForElementToBeRemoved`',
recommendedConfig: {
dom: 'error',
angular: 'error',
react: 'error',
vue: 'error',
marko: 'error',
},
},
messages: {
noWaitForEmptyCallback: 'Avoid passing empty callback to `{{ methodName }}`. Insert an assertion instead.',
},
schema: [],
},
defaultOptions: [],
create(context, _, helpers) {
function isValidWaitFor(node) {
const parentCallExpression = node.parent;
const parentIdentifier = (0, node_utils_1.getPropertyIdentifierNode)(parentCallExpression);
if (!parentIdentifier) {
return false;
}
return helpers.isAsyncUtil(parentIdentifier, [
'waitFor',
'waitForElementToBeRemoved',
]);
}
function reportIfEmpty(node) {
if (!isValidWaitFor(node)) {
return;
}
if ((0, node_utils_1.isEmptyFunction)(node) &&
(0, node_utils_1.isCallExpression)(node.parent) &&
utils_1.ASTUtils.isIdentifier(node.parent.callee)) {
context.report({
node,
loc: node.body.loc.start,
messageId: 'noWaitForEmptyCallback',
data: {
methodName: node.parent.callee.name,
},
});
}
}
function reportNoop(node) {
if (!isValidWaitFor(node)) {
return;
}
context.report({
node,
loc: node.loc.start,
messageId: 'noWaitForEmptyCallback',
data: {
methodName: (0, node_utils_1.isCallExpression)(node.parent) &&
utils_1.ASTUtils.isIdentifier(node.parent.callee) &&
node.parent.callee.name,
},
});
}
return {
'CallExpression > ArrowFunctionExpression': reportIfEmpty,
'CallExpression > FunctionExpression': reportIfEmpty,
'CallExpression > Identifier[name="noop"]': reportNoop,
};
},
});

View File

@@ -0,0 +1,70 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'no-wait-for-multiple-assertions';
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'suggestion',
docs: {
description: 'Disallow the use of multiple `expect` calls inside `waitFor`',
recommendedConfig: {
dom: 'error',
angular: 'error',
react: 'error',
vue: 'error',
marko: 'error',
},
},
messages: {
noWaitForMultipleAssertion: 'Avoid using multiple assertions within `waitFor` callback',
},
schema: [],
},
defaultOptions: [],
create(context, _, helpers) {
function getExpectNodes(body) {
return body.filter((node) => {
if (!(0, node_utils_1.isExpressionStatement)(node)) {
return false;
}
const expressionIdentifier = (0, node_utils_1.getPropertyIdentifierNode)(node);
if (!expressionIdentifier) {
return false;
}
return expressionIdentifier.name === 'expect';
});
}
function reportMultipleAssertion(node) {
if (!node.parent) {
return;
}
const callExpressionNode = node.parent.parent;
const callExpressionIdentifier = (0, node_utils_1.getPropertyIdentifierNode)(callExpressionNode);
if (!callExpressionIdentifier) {
return;
}
if (!helpers.isAsyncUtil(callExpressionIdentifier, ['waitFor'])) {
return;
}
const expectNodes = getExpectNodes(node.body);
if (expectNodes.length <= 1) {
return;
}
for (let i = 0; i < expectNodes.length; i++) {
if (i !== 0) {
context.report({
node: expectNodes[i],
messageId: 'noWaitForMultipleAssertion',
});
}
}
}
return {
'CallExpression > ArrowFunctionExpression > BlockStatement': reportMultipleAssertion,
'CallExpression > FunctionExpression > BlockStatement': reportMultipleAssertion,
};
},
});

View File

@@ -0,0 +1,155 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'no-wait-for-side-effects';
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'suggestion',
docs: {
description: 'Disallow the use of side effects in `waitFor`',
recommendedConfig: {
dom: 'error',
angular: 'error',
react: 'error',
vue: 'error',
marko: 'error',
},
},
messages: {
noSideEffectsWaitFor: 'Avoid using side effects within `waitFor` callback',
},
schema: [],
},
defaultOptions: [],
create(context, _, helpers) {
function isCallerWaitFor(node) {
if (!node.parent) {
return false;
}
const callExpressionNode = node.parent.parent;
const callExpressionIdentifier = (0, node_utils_1.getPropertyIdentifierNode)(callExpressionNode);
return (!!callExpressionIdentifier &&
helpers.isAsyncUtil(callExpressionIdentifier, ['waitFor']));
}
function isCallerThen(node) {
if (!node.parent) {
return false;
}
const callExpressionNode = node.parent.parent;
return (0, node_utils_1.hasThenProperty)(callExpressionNode.callee);
}
function isRenderInVariableDeclaration(node) {
return ((0, node_utils_1.isVariableDeclaration)(node) &&
node.declarations.some(helpers.isRenderVariableDeclarator));
}
function isRenderInExpressionStatement(node) {
if (!(0, node_utils_1.isExpressionStatement)(node) ||
!(0, node_utils_1.isAssignmentExpression)(node.expression)) {
return false;
}
const expressionIdentifier = (0, node_utils_1.getPropertyIdentifierNode)(node.expression.right);
if (!expressionIdentifier) {
return false;
}
return helpers.isRenderUtil(expressionIdentifier);
}
function isRenderInAssignmentExpression(node) {
if (!(0, node_utils_1.isAssignmentExpression)(node)) {
return false;
}
const expressionIdentifier = (0, node_utils_1.getPropertyIdentifierNode)(node.right);
if (!expressionIdentifier) {
return false;
}
return helpers.isRenderUtil(expressionIdentifier);
}
function isRenderInSequenceAssignment(node) {
if (!(0, node_utils_1.isSequenceExpression)(node)) {
return false;
}
return node.expressions.some(isRenderInAssignmentExpression);
}
function isSideEffectInVariableDeclaration(node) {
return node.declarations.some((declaration) => {
if ((0, node_utils_1.isCallExpression)(declaration.init)) {
const test = (0, node_utils_1.getPropertyIdentifierNode)(declaration.init);
if (!test) {
return false;
}
return (helpers.isFireEventUtil(test) ||
helpers.isUserEventUtil(test) ||
helpers.isRenderUtil(test));
}
return false;
});
return false;
}
function getSideEffectNodes(body) {
return body.filter((node) => {
if (!(0, node_utils_1.isExpressionStatement)(node) && !(0, node_utils_1.isVariableDeclaration)(node)) {
return false;
}
if (isRenderInVariableDeclaration(node) ||
isRenderInExpressionStatement(node)) {
return true;
}
if ((0, node_utils_1.isVariableDeclaration)(node) &&
isSideEffectInVariableDeclaration(node)) {
return true;
}
const expressionIdentifier = (0, node_utils_1.getPropertyIdentifierNode)(node);
if (!expressionIdentifier) {
return false;
}
return (helpers.isFireEventUtil(expressionIdentifier) ||
helpers.isUserEventUtil(expressionIdentifier) ||
helpers.isRenderUtil(expressionIdentifier));
});
}
function reportSideEffects(node) {
if (!isCallerWaitFor(node)) {
return;
}
if (isCallerThen(node)) {
return;
}
getSideEffectNodes(node.body).forEach((sideEffectNode) => context.report({
node: sideEffectNode,
messageId: 'noSideEffectsWaitFor',
}));
}
function reportImplicitReturnSideEffect(node) {
if (!isCallerWaitFor(node)) {
return;
}
const expressionIdentifier = (0, node_utils_1.isCallExpression)(node)
? (0, node_utils_1.getPropertyIdentifierNode)(node.callee)
: null;
if (!expressionIdentifier &&
!isRenderInAssignmentExpression(node) &&
!isRenderInSequenceAssignment(node)) {
return;
}
if (expressionIdentifier &&
!helpers.isFireEventUtil(expressionIdentifier) &&
!helpers.isUserEventUtil(expressionIdentifier) &&
!helpers.isRenderUtil(expressionIdentifier)) {
return;
}
context.report({
node,
messageId: 'noSideEffectsWaitFor',
});
}
return {
'CallExpression > ArrowFunctionExpression > BlockStatement': reportSideEffects,
'CallExpression > ArrowFunctionExpression > CallExpression': reportImplicitReturnSideEffect,
'CallExpression > ArrowFunctionExpression > AssignmentExpression': reportImplicitReturnSideEffect,
'CallExpression > ArrowFunctionExpression > SequenceExpression': reportImplicitReturnSideEffect,
'CallExpression > FunctionExpression > BlockStatement': reportSideEffects,
};
},
});

View File

@@ -0,0 +1,66 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const utils_1 = require("@typescript-eslint/utils");
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'no-wait-for-snapshot';
const SNAPSHOT_REGEXP = /^(toMatchSnapshot|toMatchInlineSnapshot)$/;
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'problem',
docs: {
description: 'Ensures no snapshot is generated inside of a `waitFor` call',
recommendedConfig: {
dom: 'error',
angular: 'error',
react: 'error',
vue: 'error',
marko: 'error',
},
},
messages: {
noWaitForSnapshot: "A snapshot can't be generated inside of a `{{ name }}` call",
},
schema: [],
},
defaultOptions: [],
create(context, _, helpers) {
function getClosestAsyncUtil(node) {
let n = node;
do {
const callExpression = (0, node_utils_1.findClosestCallExpressionNode)(n);
if (!callExpression) {
return null;
}
if (utils_1.ASTUtils.isIdentifier(callExpression.callee) &&
helpers.isAsyncUtil(callExpression.callee)) {
return callExpression.callee;
}
if ((0, node_utils_1.isMemberExpression)(callExpression.callee) &&
utils_1.ASTUtils.isIdentifier(callExpression.callee.property) &&
helpers.isAsyncUtil(callExpression.callee.property)) {
return callExpression.callee.property;
}
if (callExpression.parent) {
n = (0, node_utils_1.findClosestCallExpressionNode)(callExpression.parent);
}
} while (n !== null);
return null;
}
return {
[`Identifier[name=${String(SNAPSHOT_REGEXP)}]`](node) {
const closestAsyncUtil = getClosestAsyncUtil(node);
if (closestAsyncUtil === null) {
return;
}
context.report({
node,
messageId: 'noWaitForSnapshot',
data: { name: closestAsyncUtil.name },
});
},
};
},
});

View File

@@ -0,0 +1,154 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const utils_1 = require("@typescript-eslint/utils");
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
const utils_2 = require("../utils");
exports.RULE_NAME = 'prefer-explicit-assert';
const isAtTopLevel = (node) => {
var _a, _b, _c;
return (!!((_a = node.parent) === null || _a === void 0 ? void 0 : _a.parent) &&
node.parent.parent.type === 'ExpressionStatement') ||
(((_c = (_b = node.parent) === null || _b === void 0 ? void 0 : _b.parent) === null || _c === void 0 ? void 0 : _c.type) === 'AwaitExpression' &&
!!node.parent.parent.parent &&
node.parent.parent.parent.type === 'ExpressionStatement');
};
const isVariableDeclaration = (node) => {
if ((0, node_utils_1.isCallExpression)(node.parent) &&
utils_1.ASTUtils.isAwaitExpression(node.parent.parent) &&
utils_1.ASTUtils.isVariableDeclarator(node.parent.parent.parent)) {
return true;
}
if ((0, node_utils_1.isCallExpression)(node.parent) &&
utils_1.ASTUtils.isVariableDeclarator(node.parent.parent)) {
return true;
}
if ((0, node_utils_1.isMemberExpression)(node.parent) &&
(0, node_utils_1.isCallExpression)(node.parent.parent) &&
utils_1.ASTUtils.isAwaitExpression(node.parent.parent.parent) &&
utils_1.ASTUtils.isVariableDeclarator(node.parent.parent.parent.parent)) {
return true;
}
if ((0, node_utils_1.isMemberExpression)(node.parent) &&
(0, node_utils_1.isCallExpression)(node.parent.parent) &&
utils_1.ASTUtils.isVariableDeclarator(node.parent.parent.parent)) {
return true;
}
return false;
};
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'suggestion',
docs: {
description: 'Suggest using explicit assertions rather than standalone queries',
recommendedConfig: {
dom: false,
angular: false,
react: false,
vue: false,
marko: false,
},
},
messages: {
preferExplicitAssert: 'Wrap stand-alone `{{queryType}}` query with `expect` function for better explicit assertion',
preferExplicitAssertAssertion: '`getBy*` queries must be asserted with `{{assertion}}`',
},
schema: [
{
type: 'object',
additionalProperties: false,
properties: {
assertion: {
type: 'string',
enum: utils_2.PRESENCE_MATCHERS,
},
includeFindQueries: { type: 'boolean' },
},
},
],
},
defaultOptions: [{ includeFindQueries: true }],
create(context, [options], helpers) {
const { assertion, includeFindQueries } = options;
const getQueryCalls = [];
const findQueryCalls = [];
return {
'CallExpression Identifier'(node) {
if (helpers.isGetQueryVariant(node)) {
getQueryCalls.push(node);
}
if (helpers.isFindQueryVariant(node)) {
findQueryCalls.push(node);
}
},
'Program:exit'() {
if (includeFindQueries) {
findQueryCalls.forEach((queryCall) => {
const memberExpression = (0, node_utils_1.isMemberExpression)(queryCall.parent)
? queryCall.parent
: queryCall;
if (isVariableDeclaration(queryCall) ||
!isAtTopLevel(memberExpression)) {
return;
}
context.report({
node: queryCall,
messageId: 'preferExplicitAssert',
data: {
queryType: 'findBy*',
},
});
});
}
getQueryCalls.forEach((queryCall) => {
const node = (0, node_utils_1.isMemberExpression)(queryCall.parent)
? queryCall.parent
: queryCall;
if (isAtTopLevel(node)) {
context.report({
node: queryCall,
messageId: 'preferExplicitAssert',
data: {
queryType: 'getBy*',
},
});
}
if (assertion) {
const expectCallNode = (0, node_utils_1.findClosestCallNode)(node, 'expect');
if (!expectCallNode)
return;
const expectStatement = expectCallNode.parent;
if (!(0, node_utils_1.isMemberExpression)(expectStatement)) {
return;
}
const property = expectStatement.property;
if (!utils_1.ASTUtils.isIdentifier(property)) {
return;
}
let matcher = property.name;
let isNegatedMatcher = false;
if (matcher === 'not' &&
(0, node_utils_1.isMemberExpression)(expectStatement.parent) &&
utils_1.ASTUtils.isIdentifier(expectStatement.parent.property)) {
isNegatedMatcher = true;
matcher = expectStatement.parent.property.name;
}
const shouldEnforceAssertion = (!isNegatedMatcher && utils_2.PRESENCE_MATCHERS.includes(matcher)) ||
(isNegatedMatcher && utils_2.ABSENCE_MATCHERS.includes(matcher));
if (shouldEnforceAssertion && matcher !== assertion) {
context.report({
node: property,
messageId: 'preferExplicitAssertAssertion',
data: {
assertion,
},
});
}
}
});
},
};
},
});

View File

@@ -0,0 +1,293 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getFindByQueryVariant = exports.WAIT_METHODS = exports.RULE_NAME = void 0;
const utils_1 = require("@typescript-eslint/utils");
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'prefer-find-by';
exports.WAIT_METHODS = ['waitFor', 'waitForElement', 'wait'];
function getFindByQueryVariant(queryMethod) {
return queryMethod.includes('All') ? 'findAllBy' : 'findBy';
}
exports.getFindByQueryVariant = getFindByQueryVariant;
function findRenderDefinitionDeclaration(scope, query) {
var _a;
if (!scope) {
return null;
}
const variable = scope.variables.find((v) => v.name === query);
if (variable) {
return ((_a = variable.defs
.map(({ name }) => name)
.filter(utils_1.ASTUtils.isIdentifier)
.find(({ name }) => name === query)) !== null && _a !== void 0 ? _a : null);
}
return findRenderDefinitionDeclaration(scope.upper, query);
}
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'suggestion',
docs: {
description: 'Suggest using `find(All)By*` query instead of `waitFor` + `get(All)By*` to wait for elements',
recommendedConfig: {
dom: 'error',
angular: 'error',
react: 'error',
vue: 'error',
marko: 'error',
},
},
messages: {
preferFindBy: 'Prefer `{{queryVariant}}{{queryMethod}}` query over using `{{waitForMethodName}}` + `{{prevQuery}}`',
},
fixable: 'code',
schema: [],
},
defaultOptions: [],
create(context, _, helpers) {
const sourceCode = context.getSourceCode();
function reportInvalidUsage(node, replacementParams) {
const { queryMethod, queryVariant, prevQuery, waitForMethodName, fix } = replacementParams;
context.report({
node,
messageId: 'preferFindBy',
data: {
queryVariant,
queryMethod,
prevQuery,
waitForMethodName,
},
fix,
});
}
function getWrongQueryNameInAssertion(node) {
if (!(0, node_utils_1.isCallExpression)(node.body) ||
!(0, node_utils_1.isMemberExpression)(node.body.callee)) {
return null;
}
if ((0, node_utils_1.isCallExpression)(node.body.callee.object) &&
(0, node_utils_1.isCallExpression)(node.body.callee.object.arguments[0]) &&
utils_1.ASTUtils.isIdentifier(node.body.callee.object.arguments[0].callee)) {
return node.body.callee.object.arguments[0].callee.name;
}
if (!utils_1.ASTUtils.isIdentifier(node.body.callee.property)) {
return null;
}
if ((0, node_utils_1.isCallExpression)(node.body.callee.object) &&
(0, node_utils_1.isCallExpression)(node.body.callee.object.arguments[0]) &&
(0, node_utils_1.isMemberExpression)(node.body.callee.object.arguments[0].callee) &&
utils_1.ASTUtils.isIdentifier(node.body.callee.object.arguments[0].callee.property)) {
return node.body.callee.object.arguments[0].callee.property.name;
}
if ((0, node_utils_1.isMemberExpression)(node.body.callee.object) &&
(0, node_utils_1.isCallExpression)(node.body.callee.object.object) &&
(0, node_utils_1.isCallExpression)(node.body.callee.object.object.arguments[0]) &&
(0, node_utils_1.isMemberExpression)(node.body.callee.object.object.arguments[0].callee) &&
utils_1.ASTUtils.isIdentifier(node.body.callee.object.object.arguments[0].callee.property)) {
return node.body.callee.object.object.arguments[0].callee.property.name;
}
if ((0, node_utils_1.isMemberExpression)(node.body.callee.object) &&
(0, node_utils_1.isCallExpression)(node.body.callee.object.object) &&
(0, node_utils_1.isCallExpression)(node.body.callee.object.object.arguments[0]) &&
utils_1.ASTUtils.isIdentifier(node.body.callee.object.object.arguments[0].callee)) {
return node.body.callee.object.object.arguments[0].callee.name;
}
return node.body.callee.property.name;
}
function getWrongQueryName(node) {
if (!(0, node_utils_1.isCallExpression)(node.body)) {
return null;
}
if (utils_1.ASTUtils.isIdentifier(node.body.callee) &&
helpers.isSyncQuery(node.body.callee)) {
return node.body.callee.name;
}
return getWrongQueryNameInAssertion(node);
}
function getCaller(node) {
if (!(0, node_utils_1.isCallExpression)(node.body) ||
!(0, node_utils_1.isMemberExpression)(node.body.callee)) {
return null;
}
if (utils_1.ASTUtils.isIdentifier(node.body.callee.object)) {
return node.body.callee.object.name;
}
if ((0, node_utils_1.isCallExpression)(node.body.callee.object) &&
utils_1.ASTUtils.isIdentifier(node.body.callee.object.callee) &&
(0, node_utils_1.isCallExpression)(node.body.callee.object.arguments[0]) &&
(0, node_utils_1.isMemberExpression)(node.body.callee.object.arguments[0].callee) &&
utils_1.ASTUtils.isIdentifier(node.body.callee.object.arguments[0].callee.object)) {
return node.body.callee.object.arguments[0].callee.object.name;
}
if ((0, node_utils_1.isMemberExpression)(node.body.callee.object) &&
(0, node_utils_1.isCallExpression)(node.body.callee.object.object) &&
(0, node_utils_1.isCallExpression)(node.body.callee.object.object.arguments[0]) &&
(0, node_utils_1.isMemberExpression)(node.body.callee.object.object.arguments[0].callee) &&
utils_1.ASTUtils.isIdentifier(node.body.callee.object.object.arguments[0].callee.object)) {
return node.body.callee.object.object.arguments[0].callee.object.name;
}
return null;
}
function isSyncQuery(node) {
if (!(0, node_utils_1.isCallExpression)(node.body)) {
return false;
}
const isQuery = utils_1.ASTUtils.isIdentifier(node.body.callee) &&
helpers.isSyncQuery(node.body.callee);
const isWrappedInPresenceAssert = (0, node_utils_1.isMemberExpression)(node.body.callee) &&
(0, node_utils_1.isCallExpression)(node.body.callee.object) &&
(0, node_utils_1.isCallExpression)(node.body.callee.object.arguments[0]) &&
utils_1.ASTUtils.isIdentifier(node.body.callee.object.arguments[0].callee) &&
helpers.isSyncQuery(node.body.callee.object.arguments[0].callee) &&
helpers.isPresenceAssert(node.body.callee);
const isWrappedInNegatedPresenceAssert = (0, node_utils_1.isMemberExpression)(node.body.callee) &&
(0, node_utils_1.isMemberExpression)(node.body.callee.object) &&
(0, node_utils_1.isCallExpression)(node.body.callee.object.object) &&
(0, node_utils_1.isCallExpression)(node.body.callee.object.object.arguments[0]) &&
utils_1.ASTUtils.isIdentifier(node.body.callee.object.object.arguments[0].callee) &&
helpers.isSyncQuery(node.body.callee.object.object.arguments[0].callee) &&
helpers.isPresenceAssert(node.body.callee.object);
return (isQuery || isWrappedInPresenceAssert || isWrappedInNegatedPresenceAssert);
}
function isScreenSyncQuery(node) {
if (!(0, node_utils_1.isArrowFunctionExpression)(node) || !(0, node_utils_1.isCallExpression)(node.body)) {
return false;
}
if (!(0, node_utils_1.isMemberExpression)(node.body.callee) ||
!utils_1.ASTUtils.isIdentifier(node.body.callee.property)) {
return false;
}
if (!utils_1.ASTUtils.isIdentifier(node.body.callee.object) &&
!(0, node_utils_1.isCallExpression)(node.body.callee.object) &&
!(0, node_utils_1.isMemberExpression)(node.body.callee.object)) {
return false;
}
const isWrappedInPresenceAssert = helpers.isPresenceAssert(node.body.callee) &&
(0, node_utils_1.isCallExpression)(node.body.callee.object) &&
(0, node_utils_1.isCallExpression)(node.body.callee.object.arguments[0]) &&
(0, node_utils_1.isMemberExpression)(node.body.callee.object.arguments[0].callee) &&
utils_1.ASTUtils.isIdentifier(node.body.callee.object.arguments[0].callee.object);
const isWrappedInNegatedPresenceAssert = (0, node_utils_1.isMemberExpression)(node.body.callee.object) &&
helpers.isPresenceAssert(node.body.callee.object) &&
(0, node_utils_1.isCallExpression)(node.body.callee.object.object) &&
(0, node_utils_1.isCallExpression)(node.body.callee.object.object.arguments[0]) &&
(0, node_utils_1.isMemberExpression)(node.body.callee.object.object.arguments[0].callee);
return (helpers.isSyncQuery(node.body.callee.property) ||
isWrappedInPresenceAssert ||
isWrappedInNegatedPresenceAssert);
}
function getQueryArguments(node) {
if ((0, node_utils_1.isMemberExpression)(node.callee) &&
(0, node_utils_1.isCallExpression)(node.callee.object) &&
(0, node_utils_1.isCallExpression)(node.callee.object.arguments[0])) {
return node.callee.object.arguments[0].arguments;
}
if ((0, node_utils_1.isMemberExpression)(node.callee) &&
(0, node_utils_1.isMemberExpression)(node.callee.object) &&
(0, node_utils_1.isCallExpression)(node.callee.object.object) &&
(0, node_utils_1.isCallExpression)(node.callee.object.object.arguments[0])) {
return node.callee.object.object.arguments[0].arguments;
}
return node.arguments;
}
return {
'AwaitExpression > CallExpression'(node) {
if (!utils_1.ASTUtils.isIdentifier(node.callee) ||
!helpers.isAsyncUtil(node.callee, exports.WAIT_METHODS)) {
return;
}
const argument = node.arguments[0];
if (!(0, node_utils_1.isArrowFunctionExpression)(argument) ||
!(0, node_utils_1.isCallExpression)(argument.body)) {
return;
}
const waitForMethodName = node.callee.name;
if (isScreenSyncQuery(argument)) {
const caller = getCaller(argument);
if (!caller) {
return;
}
const fullQueryMethod = getWrongQueryName(argument);
if (!fullQueryMethod) {
return;
}
const waitOptions = node.arguments[1];
let waitOptionsSourceCode = '';
if ((0, node_utils_1.isObjectExpression)(waitOptions)) {
waitOptionsSourceCode = `, ${sourceCode.getText(waitOptions)}`;
}
const queryVariant = getFindByQueryVariant(fullQueryMethod);
const callArguments = getQueryArguments(argument.body);
const queryMethod = fullQueryMethod.split('By')[1];
if (!queryMethod) {
return;
}
reportInvalidUsage(node, {
queryMethod,
queryVariant,
prevQuery: fullQueryMethod,
waitForMethodName,
fix(fixer) {
const property = argument.body
.callee.property;
if (helpers.isCustomQuery(property)) {
return null;
}
const newCode = `${caller}.${queryVariant}${queryMethod}(${callArguments
.map((callArgNode) => sourceCode.getText(callArgNode))
.join(', ')}${waitOptionsSourceCode})`;
return fixer.replaceText(node, newCode);
},
});
return;
}
if (!isSyncQuery(argument)) {
return;
}
const fullQueryMethod = getWrongQueryName(argument);
if (!fullQueryMethod) {
return;
}
const queryMethod = fullQueryMethod.split('By')[1];
const queryVariant = getFindByQueryVariant(fullQueryMethod);
const callArguments = getQueryArguments(argument.body);
reportInvalidUsage(node, {
queryMethod,
queryVariant,
prevQuery: fullQueryMethod,
waitForMethodName,
fix(fixer) {
if (helpers.isCustomQuery(argument.body
.callee)) {
return null;
}
const findByMethod = `${queryVariant}${queryMethod}`;
const allFixes = [];
const newCode = `${findByMethod}(${callArguments
.map((callArgNode) => sourceCode.getText(callArgNode))
.join(', ')})`;
allFixes.push(fixer.replaceText(node, newCode));
const definition = findRenderDefinitionDeclaration(context.getScope(), fullQueryMethod);
if (!definition) {
return allFixes;
}
if (definition.parent &&
(0, node_utils_1.isObjectPattern)(definition.parent.parent)) {
const allVariableDeclarations = definition.parent.parent;
if (allVariableDeclarations.properties.some((p) => (0, node_utils_1.isProperty)(p) &&
utils_1.ASTUtils.isIdentifier(p.key) &&
p.key.name === findByMethod)) {
return allFixes;
}
const textDestructuring = sourceCode.getText(allVariableDeclarations);
const text = textDestructuring.replace(/(\s*})$/, `, ${findByMethod}$1`);
allFixes.push(fixer.replaceText(allVariableDeclarations, text));
}
return allFixes;
},
});
},
};
},
});

View File

@@ -0,0 +1,78 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'prefer-presence-queries';
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
docs: {
description: 'Ensure appropriate `get*`/`query*` queries are used with their respective matchers',
recommendedConfig: {
dom: 'error',
angular: 'error',
react: 'error',
vue: 'error',
marko: 'error',
},
},
messages: {
wrongPresenceQuery: 'Use `getBy*` queries rather than `queryBy*` for checking element is present',
wrongAbsenceQuery: 'Use `queryBy*` queries rather than `getBy*` for checking element is NOT present',
},
schema: [
{
type: 'object',
additionalProperties: false,
properties: {
presence: {
type: 'boolean',
},
absence: {
type: 'boolean',
},
},
},
],
type: 'suggestion',
},
defaultOptions: [
{
presence: true,
absence: true,
},
],
create(context, [{ absence = true, presence = true }], helpers) {
return {
'CallExpression Identifier'(node) {
const expectCallNode = (0, node_utils_1.findClosestCallNode)(node, 'expect');
const withinCallNode = (0, node_utils_1.findClosestCallNode)(node, 'within');
if (!expectCallNode || !(0, node_utils_1.isMemberExpression)(expectCallNode.parent)) {
return;
}
if (!helpers.isSyncQuery(node)) {
return;
}
const isPresenceQuery = helpers.isGetQueryVariant(node);
const expectStatement = expectCallNode.parent;
const isPresenceAssert = helpers.isPresenceAssert(expectStatement);
const isAbsenceAssert = helpers.isAbsenceAssert(expectStatement);
if (!isPresenceAssert && !isAbsenceAssert) {
return;
}
if (presence &&
(withinCallNode || isPresenceAssert) &&
!isPresenceQuery) {
context.report({ node, messageId: 'wrongPresenceQuery' });
}
else if (!withinCallNode &&
absence &&
isAbsenceAssert &&
isPresenceQuery) {
context.report({ node, messageId: 'wrongAbsenceQuery' });
}
},
};
},
});

View File

@@ -0,0 +1,120 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'prefer-query-by-disappearance';
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'problem',
docs: {
description: 'Suggest using `queryBy*` queries when waiting for disappearance',
recommendedConfig: {
dom: 'error',
angular: 'error',
react: 'error',
vue: 'error',
marko: 'error',
},
},
messages: {
preferQueryByDisappearance: 'Prefer using queryBy* when waiting for disappearance',
},
schema: [],
},
defaultOptions: [],
create(context, _, helpers) {
function isWaitForElementToBeRemoved(node) {
const identifierNode = (0, node_utils_1.getPropertyIdentifierNode)(node);
if (!identifierNode) {
return false;
}
return helpers.isAsyncUtil(identifierNode, ['waitForElementToBeRemoved']);
}
function reportExpression(node) {
const argumentProperty = (0, node_utils_1.isMemberExpression)(node)
? (0, node_utils_1.getPropertyIdentifierNode)(node.property)
: (0, node_utils_1.getPropertyIdentifierNode)(node);
if (!argumentProperty) {
return false;
}
if (helpers.isGetQueryVariant(argumentProperty) ||
helpers.isFindQueryVariant(argumentProperty)) {
context.report({
node: argumentProperty,
messageId: 'preferQueryByDisappearance',
});
return true;
}
return false;
}
function checkNonCallbackViolation(node) {
if (!(0, node_utils_1.isCallExpression)(node)) {
return false;
}
if (!(0, node_utils_1.isMemberExpression)(node.callee) &&
!(0, node_utils_1.getPropertyIdentifierNode)(node.callee)) {
return false;
}
return reportExpression(node.callee);
}
function isReturnViolation(node) {
if (!(0, node_utils_1.isReturnStatement)(node) || !(0, node_utils_1.isCallExpression)(node.argument)) {
return false;
}
return reportExpression(node.argument.callee);
}
function isNonReturnViolation(node) {
if (!(0, node_utils_1.isExpressionStatement)(node) || !(0, node_utils_1.isCallExpression)(node.expression)) {
return false;
}
if (!(0, node_utils_1.isMemberExpression)(node.expression.callee) &&
!(0, node_utils_1.getPropertyIdentifierNode)(node.expression.callee)) {
return false;
}
return reportExpression(node.expression.callee);
}
function isStatementViolation(statement) {
return isReturnViolation(statement) || isNonReturnViolation(statement);
}
function checkFunctionExpressionViolation(node) {
if (!(0, node_utils_1.isFunctionExpression)(node)) {
return false;
}
return node.body.body.some((statement) => isStatementViolation(statement));
}
function isArrowFunctionBodyViolation(node) {
if (!(0, node_utils_1.isArrowFunctionExpression)(node) || !(0, node_utils_1.isBlockStatement)(node.body)) {
return false;
}
return node.body.body.some((statement) => isStatementViolation(statement));
}
function isArrowFunctionImplicitReturnViolation(node) {
if (!(0, node_utils_1.isArrowFunctionExpression)(node) || !(0, node_utils_1.isCallExpression)(node.body)) {
return false;
}
if (!(0, node_utils_1.isMemberExpression)(node.body.callee) &&
!(0, node_utils_1.getPropertyIdentifierNode)(node.body.callee)) {
return false;
}
return reportExpression(node.body.callee);
}
function checkArrowFunctionViolation(node) {
return (isArrowFunctionBodyViolation(node) ||
isArrowFunctionImplicitReturnViolation(node));
}
function check(node) {
if (!isWaitForElementToBeRemoved(node)) {
return;
}
const argumentNode = node.arguments[0];
checkNonCallbackViolation(argumentNode);
checkArrowFunctionViolation(argumentNode);
checkFunctionExpressionViolation(argumentNode);
}
return {
CallExpression: check,
};
},
});

View File

@@ -0,0 +1,83 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'prefer-query-matchers';
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
docs: {
description: 'Ensure the configured `get*`/`query*` query is used with the corresponding matchers',
recommendedConfig: {
dom: false,
angular: false,
react: false,
vue: false,
marko: false,
},
},
messages: {
wrongQueryForMatcher: 'Use `{{ query }}By*` queries for {{ matcher }}',
},
schema: [
{
type: 'object',
additionalProperties: false,
properties: {
validEntries: {
type: 'array',
items: {
type: 'object',
properties: {
query: {
type: 'string',
enum: ['get', 'query'],
},
matcher: {
type: 'string',
},
},
},
},
},
},
],
type: 'suggestion',
},
defaultOptions: [
{
validEntries: [],
},
],
create(context, [{ validEntries }], helpers) {
return {
'CallExpression Identifier'(node) {
const expectCallNode = (0, node_utils_1.findClosestCallNode)(node, 'expect');
if (!expectCallNode || !(0, node_utils_1.isMemberExpression)(expectCallNode.parent)) {
return;
}
if (!helpers.isSyncQuery(node)) {
return;
}
const isGetBy = helpers.isGetQueryVariant(node);
const expectStatement = expectCallNode.parent;
for (const entry of validEntries) {
const { query, matcher } = entry;
const isMatchingAssertForThisEntry = helpers.isMatchingAssert(expectStatement, matcher);
if (!isMatchingAssertForThisEntry) {
continue;
}
const actualQuery = isGetBy ? 'get' : 'query';
if (query !== actualQuery) {
context.report({
node,
messageId: 'wrongQueryForMatcher',
data: { query, matcher },
});
}
}
},
};
},
});

View File

@@ -0,0 +1,132 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const utils_1 = require("@typescript-eslint/utils");
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'prefer-screen-queries';
const ALLOWED_RENDER_PROPERTIES_FOR_DESTRUCTURING = [
'container',
'baseElement',
];
function usesContainerOrBaseElement(node) {
const secondArgument = node.arguments[1];
return ((0, node_utils_1.isObjectExpression)(secondArgument) &&
secondArgument.properties.some((property) => (0, node_utils_1.isProperty)(property) &&
utils_1.ASTUtils.isIdentifier(property.key) &&
ALLOWED_RENDER_PROPERTIES_FOR_DESTRUCTURING.includes(property.key.name)));
}
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'suggestion',
docs: {
description: 'Suggest using `screen` while querying',
recommendedConfig: {
dom: 'error',
angular: 'error',
react: 'error',
vue: 'error',
marko: 'error',
},
},
messages: {
preferScreenQueries: 'Avoid destructuring queries from `render` result, use `screen.{{ name }}` instead',
},
schema: [],
},
defaultOptions: [],
create(context, _, helpers) {
const renderWrapperNames = [];
function detectRenderWrapper(node) {
const innerFunction = (0, node_utils_1.getInnermostReturningFunction)(context, node);
if (innerFunction) {
renderWrapperNames.push((0, node_utils_1.getFunctionName)(innerFunction));
}
}
function isReportableRender(node) {
return (helpers.isRenderUtil(node) || renderWrapperNames.includes(node.name));
}
function reportInvalidUsage(node) {
context.report({
node,
messageId: 'preferScreenQueries',
data: {
name: node.name,
},
});
}
function saveSafeDestructuredQueries(node) {
if ((0, node_utils_1.isObjectPattern)(node.id)) {
for (const property of node.id.properties) {
if ((0, node_utils_1.isProperty)(property) &&
utils_1.ASTUtils.isIdentifier(property.key) &&
helpers.isBuiltInQuery(property.key)) {
safeDestructuredQueries.push(property.key.name);
}
}
}
}
function isIdentifierAllowed(name) {
return ['screen', ...withinDeclaredVariables].includes(name);
}
const safeDestructuredQueries = [];
const withinDeclaredVariables = [];
return {
VariableDeclarator(node) {
if (!(0, node_utils_1.isCallExpression)(node.init) ||
!utils_1.ASTUtils.isIdentifier(node.init.callee)) {
return;
}
const isComingFromValidRender = isReportableRender(node.init.callee);
if (!isComingFromValidRender) {
saveSafeDestructuredQueries(node);
}
const isWithinFunction = node.init.callee.name === 'within';
const usesRenderOptions = isComingFromValidRender && usesContainerOrBaseElement(node.init);
if (!isWithinFunction && !usesRenderOptions) {
return;
}
if ((0, node_utils_1.isObjectPattern)(node.id)) {
saveSafeDestructuredQueries(node);
}
else if (utils_1.ASTUtils.isIdentifier(node.id)) {
withinDeclaredVariables.push(node.id.name);
}
},
CallExpression(node) {
const identifierNode = (0, node_utils_1.getDeepestIdentifierNode)(node);
if (!identifierNode) {
return;
}
if (helpers.isRenderUtil(identifierNode)) {
detectRenderWrapper(identifierNode);
}
if (!helpers.isBuiltInQuery(identifierNode)) {
return;
}
if (!(0, node_utils_1.isMemberExpression)(identifierNode.parent)) {
const isSafeDestructuredQuery = safeDestructuredQueries.some((queryName) => queryName === identifierNode.name);
if (isSafeDestructuredQuery) {
return;
}
reportInvalidUsage(identifierNode);
return;
}
const memberExpressionNode = identifierNode.parent;
if ((0, node_utils_1.isCallExpression)(memberExpressionNode.object) &&
utils_1.ASTUtils.isIdentifier(memberExpressionNode.object.callee) &&
memberExpressionNode.object.callee.name !== 'within' &&
isReportableRender(memberExpressionNode.object.callee) &&
!usesContainerOrBaseElement(memberExpressionNode.object)) {
reportInvalidUsage(identifierNode);
return;
}
if (utils_1.ASTUtils.isIdentifier(memberExpressionNode.object) &&
!isIdentifierAllowed(memberExpressionNode.object.name)) {
reportInvalidUsage(identifierNode);
}
},
};
},
});

View File

@@ -0,0 +1,147 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.MAPPING_TO_USER_EVENT = exports.UserEventMethods = exports.RULE_NAME = void 0;
const utils_1 = require("@typescript-eslint/utils");
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'prefer-user-event';
exports.UserEventMethods = [
'click',
'dblClick',
'type',
'upload',
'clear',
'selectOptions',
'deselectOptions',
'tab',
'hover',
'unhover',
'paste',
];
exports.MAPPING_TO_USER_EVENT = {
click: ['click', 'type', 'selectOptions', 'deselectOptions'],
change: ['upload', 'type', 'clear', 'selectOptions', 'deselectOptions'],
dblClick: ['dblClick'],
input: ['type', 'upload', 'selectOptions', 'deselectOptions', 'paste'],
keyDown: ['type', 'tab'],
keyPress: ['type'],
keyUp: ['type', 'tab'],
mouseDown: ['click', 'dblClick', 'selectOptions', 'deselectOptions'],
mouseEnter: ['hover', 'selectOptions', 'deselectOptions'],
mouseLeave: ['unhover'],
mouseMove: ['hover', 'unhover', 'selectOptions', 'deselectOptions'],
mouseOut: ['unhover'],
mouseOver: ['hover', 'selectOptions', 'deselectOptions'],
mouseUp: ['click', 'dblClick', 'selectOptions', 'deselectOptions'],
paste: ['paste'],
pointerDown: ['click', 'dblClick', 'selectOptions', 'deselectOptions'],
pointerEnter: ['hover', 'selectOptions', 'deselectOptions'],
pointerLeave: ['unhover'],
pointerMove: ['hover', 'unhover', 'selectOptions', 'deselectOptions'],
pointerOut: ['unhover'],
pointerOver: ['hover', 'selectOptions', 'deselectOptions'],
pointerUp: ['click', 'dblClick', 'selectOptions', 'deselectOptions'],
};
function buildErrorMessage(fireEventMethod) {
const userEventMethods = exports.MAPPING_TO_USER_EVENT[fireEventMethod].map((methodName) => `userEvent.${methodName}`);
return userEventMethods.join(', ').replace(/, ([a-zA-Z.]+)$/, ', or $1');
}
const fireEventMappedMethods = Object.keys(exports.MAPPING_TO_USER_EVENT);
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'suggestion',
docs: {
description: 'Suggest using `userEvent` over `fireEvent` for simulating user interactions',
recommendedConfig: {
dom: false,
angular: false,
react: false,
vue: false,
marko: false,
},
},
messages: {
preferUserEvent: 'Prefer using {{userEventMethods}} over fireEvent.{{fireEventMethod}}',
},
schema: [
{
type: 'object',
properties: {
allowedMethods: { type: 'array' },
},
},
],
},
defaultOptions: [{ allowedMethods: [] }],
create(context, [options], helpers) {
const { allowedMethods } = options;
const createEventVariables = {};
const isfireEventMethodAllowed = (methodName) => !fireEventMappedMethods.includes(methodName) ||
allowedMethods.includes(methodName);
const getFireEventMethodName = (callExpressionNode, node) => {
if (!utils_1.ASTUtils.isIdentifier(callExpressionNode.callee) &&
!(0, node_utils_1.isMemberExpression)(callExpressionNode.callee)) {
return node.name;
}
const secondArgument = callExpressionNode.arguments[1];
if (utils_1.ASTUtils.isIdentifier(secondArgument) &&
createEventVariables[secondArgument.name] !== undefined) {
return createEventVariables[secondArgument.name];
}
if (!(0, node_utils_1.isCallExpression)(secondArgument) ||
!helpers.isCreateEventUtil(secondArgument)) {
return node.name;
}
if (utils_1.ASTUtils.isIdentifier(secondArgument.callee)) {
return secondArgument.arguments[0]
.value;
}
return secondArgument.callee
.property.name;
};
return {
'CallExpression Identifier'(node) {
if (!helpers.isFireEventMethod(node)) {
return;
}
const closestCallExpression = (0, node_utils_1.findClosestCallExpressionNode)(node, true);
if (!closestCallExpression) {
return;
}
const fireEventMethodName = getFireEventMethodName(closestCallExpression, node);
if (!fireEventMethodName ||
isfireEventMethodAllowed(fireEventMethodName)) {
return;
}
context.report({
node: closestCallExpression.callee,
messageId: 'preferUserEvent',
data: {
userEventMethods: buildErrorMessage(fireEventMethodName),
fireEventMethod: fireEventMethodName,
},
});
},
VariableDeclarator(node) {
if (!(0, node_utils_1.isCallExpression)(node.init) ||
!helpers.isCreateEventUtil(node.init) ||
!utils_1.ASTUtils.isIdentifier(node.id)) {
return;
}
let fireEventMethodName = '';
if ((0, node_utils_1.isMemberExpression)(node.init.callee) &&
utils_1.ASTUtils.isIdentifier(node.init.callee.property)) {
fireEventMethodName = node.init.callee.property.name;
}
else if (node.init.arguments.length > 0) {
fireEventMethodName = node.init.arguments[0]
.value;
}
if (!isfireEventMethodAllowed(fireEventMethodName)) {
createEventVariables[node.id.name] = fireEventMethodName;
}
},
};
},
});

View File

@@ -0,0 +1,146 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const utils_1 = require("@typescript-eslint/utils");
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'prefer-wait-for';
const DEPRECATED_METHODS = ['wait', 'waitForElement', 'waitForDomChange'];
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'suggestion',
docs: {
description: 'Use `waitFor` instead of deprecated wait methods',
recommendedConfig: {
dom: false,
angular: false,
react: false,
vue: false,
marko: false,
},
},
messages: {
preferWaitForMethod: '`{{ methodName }}` is deprecated in favour of `waitFor`',
preferWaitForImport: 'import `waitFor` instead of deprecated async utils',
preferWaitForRequire: 'require `waitFor` instead of deprecated async utils',
},
fixable: 'code',
schema: [],
},
defaultOptions: [],
create(context, _, helpers) {
let addWaitFor = false;
const reportRequire = (node) => {
context.report({
node,
messageId: 'preferWaitForRequire',
fix(fixer) {
const excludedImports = [...DEPRECATED_METHODS, 'waitFor'];
const newAllRequired = node.properties
.filter((s) => (0, node_utils_1.isProperty)(s) &&
utils_1.ASTUtils.isIdentifier(s.key) &&
!excludedImports.includes(s.key.name))
.map((s) => s.key.name);
newAllRequired.push('waitFor');
return fixer.replaceText(node, `{ ${newAllRequired.join(',')} }`);
},
});
};
const reportImport = (node) => {
context.report({
node,
messageId: 'preferWaitForImport',
fix(fixer) {
const excludedImports = [...DEPRECATED_METHODS, 'waitFor'];
const newImports = node.specifiers
.map((specifier) => (0, node_utils_1.isImportSpecifier)(specifier) &&
!excludedImports.includes(specifier.imported.name) &&
specifier.imported.name)
.filter(Boolean);
newImports.push('waitFor');
const newNode = `import { ${newImports.join(',')} } from '${node.source.value}';`;
return fixer.replaceText(node, newNode);
},
});
};
const reportWait = (node) => {
context.report({
node,
messageId: 'preferWaitForMethod',
data: {
methodName: node.name,
},
fix(fixer) {
const callExpressionNode = (0, node_utils_1.findClosestCallExpressionNode)(node);
if (!callExpressionNode) {
return null;
}
const [arg] = callExpressionNode.arguments;
const fixers = [];
if (arg) {
fixers.push(fixer.replaceText(node, 'waitFor'));
if (node.name === 'waitForDomChange') {
fixers.push(fixer.insertTextBefore(arg, '() => {}, '));
}
}
else {
let methodReplacement = 'waitFor(() => {})';
if ((0, node_utils_1.isMemberExpression)(node.parent) &&
utils_1.ASTUtils.isIdentifier(node.parent.object)) {
methodReplacement = `${node.parent.object.name}.${methodReplacement}`;
}
const newText = methodReplacement;
fixers.push(fixer.replaceText(callExpressionNode, newText));
}
return fixers;
},
});
};
return {
'CallExpression > MemberExpression'(node) {
const isDeprecatedMethod = utils_1.ASTUtils.isIdentifier(node.property) &&
DEPRECATED_METHODS.includes(node.property.name);
if (!isDeprecatedMethod) {
return;
}
if (!helpers.isNodeComingFromTestingLibrary(node)) {
return;
}
addWaitFor = true;
reportWait(node.property);
},
'CallExpression > Identifier'(node) {
if (!DEPRECATED_METHODS.includes(node.name)) {
return;
}
if (!helpers.isNodeComingFromTestingLibrary(node)) {
return;
}
addWaitFor = true;
reportWait(node);
},
'Program:exit'() {
var _a;
if (!addWaitFor) {
return;
}
const testingLibraryNode = (_a = helpers.getCustomModuleImportNode()) !== null && _a !== void 0 ? _a : helpers.getTestingLibraryImportNode();
if ((0, node_utils_1.isCallExpression)(testingLibraryNode)) {
const parent = testingLibraryNode.parent;
if (!(0, node_utils_1.isObjectPattern)(parent.id)) {
return;
}
reportRequire(parent.id);
}
else if (testingLibraryNode) {
if (testingLibraryNode.specifiers.length === 1 &&
(0, node_utils_1.isImportNamespaceSpecifier)(testingLibraryNode.specifiers[0])) {
return;
}
reportImport(testingLibraryNode);
}
},
};
},
});

View File

@@ -0,0 +1,83 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const utils_1 = require("@typescript-eslint/utils");
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'render-result-naming-convention';
const ALLOWED_VAR_NAMES = ['view', 'utils'];
const ALLOWED_VAR_NAMES_TEXT = ALLOWED_VAR_NAMES.map((name) => `\`${name}\``)
.join(', ')
.replace(/, ([^,]*)$/, ', or $1');
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'suggestion',
docs: {
description: 'Enforce a valid naming for return value from `render`',
recommendedConfig: {
dom: false,
angular: 'error',
react: 'error',
vue: 'error',
marko: 'error',
},
},
messages: {
renderResultNamingConvention: `\`{{ renderResultName }}\` is not a recommended name for \`render\` returned value. Instead, you should destructure it, or name it using one of: ${ALLOWED_VAR_NAMES_TEXT}`,
},
schema: [],
},
defaultOptions: [],
create(context, _, helpers) {
const renderWrapperNames = [];
function detectRenderWrapper(node) {
const innerFunction = (0, node_utils_1.getInnermostReturningFunction)(context, node);
if (innerFunction) {
renderWrapperNames.push((0, node_utils_1.getFunctionName)(innerFunction));
}
}
return {
CallExpression(node) {
const callExpressionIdentifier = (0, node_utils_1.getDeepestIdentifierNode)(node);
if (!callExpressionIdentifier) {
return;
}
if (helpers.isRenderUtil(callExpressionIdentifier)) {
detectRenderWrapper(callExpressionIdentifier);
}
},
VariableDeclarator(node) {
if (!node.init) {
return;
}
const initIdentifierNode = (0, node_utils_1.getDeepestIdentifierNode)(node.init);
if (!initIdentifierNode) {
return;
}
if (!helpers.isRenderVariableDeclarator(node) &&
!renderWrapperNames.includes(initIdentifierNode.name)) {
return;
}
if ((0, node_utils_1.isObjectPattern)(node.id)) {
return;
}
const renderResultName = utils_1.ASTUtils.isIdentifier(node.id) && node.id.name;
if (!renderResultName) {
return;
}
const isAllowedRenderResultName = ALLOWED_VAR_NAMES.includes(renderResultName);
if (isAllowedRenderResultName) {
return;
}
context.report({
node,
messageId: 'renderResultNamingConvention',
data: {
renderResultName,
},
});
},
};
},
});

View File

@@ -0,0 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.importDefault = void 0;
const interopRequireDefault = (obj) => (obj === null || obj === void 0 ? void 0 : obj.__esModule) ? obj : { default: obj };
const importDefault = (moduleName) => interopRequireDefault(require(moduleName)).default;
exports.importDefault = importDefault;

View File

@@ -0,0 +1,132 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ABSENCE_MATCHERS = exports.PRESENCE_MATCHERS = exports.ALL_RETURNING_NODES = exports.METHODS_RETURNING_NODES = exports.PROPERTIES_RETURNING_NODES = exports.LIBRARY_MODULES = exports.TESTING_FRAMEWORK_SETUP_HOOKS = exports.EVENTS_SIMULATORS = exports.DEBUG_UTILS = exports.ASYNC_UTILS = exports.ALL_QUERIES_COMBINATIONS = exports.ASYNC_QUERIES_COMBINATIONS = exports.SYNC_QUERIES_COMBINATIONS = exports.ALL_QUERIES_METHODS = exports.ALL_QUERIES_VARIANTS = exports.ASYNC_QUERIES_VARIANTS = exports.SYNC_QUERIES_VARIANTS = exports.getDocsUrl = exports.combineQueries = void 0;
__exportStar(require("./file-import"), exports);
__exportStar(require("./types"), exports);
const combineQueries = (variants, methods) => {
const combinedQueries = [];
variants.forEach((variant) => {
const variantPrefix = variant.replace('By', '');
methods.forEach((method) => {
combinedQueries.push(`${variantPrefix}${method}`);
});
});
return combinedQueries;
};
exports.combineQueries = combineQueries;
const getDocsUrl = (ruleName) => `https://github.com/testing-library/eslint-plugin-testing-library/tree/main/docs/rules/${ruleName}.md`;
exports.getDocsUrl = getDocsUrl;
const LIBRARY_MODULES = [
'@testing-library/dom',
'@testing-library/angular',
'@testing-library/react',
'@testing-library/preact',
'@testing-library/vue',
'@testing-library/svelte',
'@marko/testing-library',
];
exports.LIBRARY_MODULES = LIBRARY_MODULES;
const SYNC_QUERIES_VARIANTS = ['getBy', 'getAllBy', 'queryBy', 'queryAllBy'];
exports.SYNC_QUERIES_VARIANTS = SYNC_QUERIES_VARIANTS;
const ASYNC_QUERIES_VARIANTS = ['findBy', 'findAllBy'];
exports.ASYNC_QUERIES_VARIANTS = ASYNC_QUERIES_VARIANTS;
const ALL_QUERIES_VARIANTS = [
...SYNC_QUERIES_VARIANTS,
...ASYNC_QUERIES_VARIANTS,
];
exports.ALL_QUERIES_VARIANTS = ALL_QUERIES_VARIANTS;
const ALL_QUERIES_METHODS = [
'ByLabelText',
'ByPlaceholderText',
'ByText',
'ByAltText',
'ByTitle',
'ByDisplayValue',
'ByRole',
'ByTestId',
];
exports.ALL_QUERIES_METHODS = ALL_QUERIES_METHODS;
const SYNC_QUERIES_COMBINATIONS = combineQueries(SYNC_QUERIES_VARIANTS, ALL_QUERIES_METHODS);
exports.SYNC_QUERIES_COMBINATIONS = SYNC_QUERIES_COMBINATIONS;
const ASYNC_QUERIES_COMBINATIONS = combineQueries(ASYNC_QUERIES_VARIANTS, ALL_QUERIES_METHODS);
exports.ASYNC_QUERIES_COMBINATIONS = ASYNC_QUERIES_COMBINATIONS;
const ALL_QUERIES_COMBINATIONS = [
...SYNC_QUERIES_COMBINATIONS,
...ASYNC_QUERIES_COMBINATIONS,
];
exports.ALL_QUERIES_COMBINATIONS = ALL_QUERIES_COMBINATIONS;
const ASYNC_UTILS = [
'waitFor',
'waitForElementToBeRemoved',
'wait',
'waitForElement',
'waitForDomChange',
];
exports.ASYNC_UTILS = ASYNC_UTILS;
const DEBUG_UTILS = [
'debug',
'logTestingPlaygroundURL',
'prettyDOM',
'logRoles',
'logDOM',
'prettyFormat',
];
exports.DEBUG_UTILS = DEBUG_UTILS;
const EVENTS_SIMULATORS = ['fireEvent', 'userEvent'];
exports.EVENTS_SIMULATORS = EVENTS_SIMULATORS;
const TESTING_FRAMEWORK_SETUP_HOOKS = ['beforeEach', 'beforeAll'];
exports.TESTING_FRAMEWORK_SETUP_HOOKS = TESTING_FRAMEWORK_SETUP_HOOKS;
const PROPERTIES_RETURNING_NODES = [
'activeElement',
'children',
'childElementCount',
'firstChild',
'firstElementChild',
'fullscreenElement',
'lastChild',
'lastElementChild',
'nextElementSibling',
'nextSibling',
'parentElement',
'parentNode',
'pointerLockElement',
'previousElementSibling',
'previousSibling',
'rootNode',
'scripts',
];
exports.PROPERTIES_RETURNING_NODES = PROPERTIES_RETURNING_NODES;
const METHODS_RETURNING_NODES = [
'closest',
'getElementById',
'getElementsByClassName',
'getElementsByName',
'getElementsByTagName',
'getElementsByTagNameNS',
'querySelector',
'querySelectorAll',
];
exports.METHODS_RETURNING_NODES = METHODS_RETURNING_NODES;
const ALL_RETURNING_NODES = [
...PROPERTIES_RETURNING_NODES,
...METHODS_RETURNING_NODES,
];
exports.ALL_RETURNING_NODES = ALL_RETURNING_NODES;
const PRESENCE_MATCHERS = ['toBeInTheDocument', 'toBeTruthy', 'toBeDefined'];
exports.PRESENCE_MATCHERS = PRESENCE_MATCHERS;
const ABSENCE_MATCHERS = ['toBeNull', 'toBeFalsy'];
exports.ABSENCE_MATCHERS = ABSENCE_MATCHERS;

View File

@@ -0,0 +1,10 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SUPPORTED_TESTING_FRAMEWORKS = void 0;
exports.SUPPORTED_TESTING_FRAMEWORKS = [
'dom',
'angular',
'react',
'vue',
'marko',
];