diff --git a/.github/ISSUE_TEMPLATE/propose_new_rule.yml b/.github/ISSUE_TEMPLATE/propose_new_rule.yml index 8205d7d4..6af708cd 100644 --- a/.github/ISSUE_TEMPLATE/propose_new_rule.yml +++ b/.github/ISSUE_TEMPLATE/propose_new_rule.yml @@ -7,7 +7,7 @@ body: attributes: label: Name for new rule description: Suggest a name for the new rule that follows the [rule naming conventions](https://github.com/testing-library/eslint-plugin-testing-library/blob/main/CONTRIBUTING.md#rule-naming-conventions). - placeholder: prefer-wait-for + placeholder: prefer-find-by validations: required: true diff --git a/README.md b/README.md index e0355281..5b02e35d 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ +

eslint-plugin-testing-library

ESLint plugin to follow best practices and anticipate common mistakes when writing tests with Testing Library

@@ -49,8 +50,9 @@ $ yarn add --dev eslint-plugin-testing-library 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) +- [Migration guide for v4](docs/migration-guides/v4.md) +- [Migration guide for v5](docs/migration-guides/v5.md) +- [Migration guide for v6](docs/migration-guides/v6.md) ## Usage @@ -67,8 +69,8 @@ Then configure the rules you want to use within `rules` property of your `.eslin ```js module.exports = { rules: { - 'testing-library/await-async-query': 'error', - 'testing-library/no-await-sync-query': 'error', + 'testing-library/await-async-queries': 'error', + 'testing-library/no-await-sync-queries': 'error', 'testing-library/no-debugging-utils': 'warn', 'testing-library/no-dom-import': 'off', }, @@ -201,38 +203,37 @@ module.exports = { 💼 Configurations enabled in.\ +⚠️ Configurations set to warn 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][] | | +| Name                            | Description | 💼 | ⚠️ | 🔧 | +| :------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------- | :------------------------------------------------------------------ | :-- | +| [await-async-events](docs/rules/await-async-events.md) | Enforce promises from async event methods are handled | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-vue][] | | 🔧 | +| [await-async-queries](docs/rules/await-async-queries.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][] | | | +| [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 | ![badge-angular][] ![badge-dom][] ![badge-react][] | | | +| [no-await-sync-queries](docs/rules/no-await-sync-queries.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 | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-vue][] | | 🔧 | +| [no-manual-cleanup](docs/rules/no-manual-cleanup.md) | Disallow the use of `cleanup` | ![badge-react][] ![badge-vue][] | | | +| [no-node-access](docs/rules/no-node-access.md) | Disallow direct Node access | ![badge-angular][] ![badge-dom][] ![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-lifecycle](docs/rules/no-render-in-lifecycle.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-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 | | | | +| [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][] | | | diff --git a/docs/migration-guides/v6.md b/docs/migration-guides/v6.md new file mode 100644 index 00000000..05557e2b --- /dev/null +++ b/docs/migration-guides/v6.md @@ -0,0 +1,30 @@ +# Guide: migrating to v6 + +If you are not on v5 yet, we recommend first following the [v5 migration guide](docs/migration-guides/v5.md). + +## Overview + +- `prefer-wait-for` was removed +- `no-wait-for-empty-callback` was removed +- `await-fire-event` is now called `await-async-events` with support for an `eventModule` option with `userEvent` and/or `fireEvent` +- `await-async-events` is now enabled by default for `fireEvent` in Vue and Marko shared configs +- `await-async-events` is now enabled by default for `userEvent` in all shared configs +- `await-async-query` is now called `await-async-queries` +- `no-await-async-query` is now called `no-await-async-queries` +- `no-render-in-setup` is now called `no-render-in-lifecycle` +- `no-await-sync-events` is now enabled by default in React, Angular, and DOM shared configs +- `no-manual-cleanup` is now enabled by default in React and Vue shared configs +- `no-global-regexp-flag-in-query` is now enabled by default in all shared configs +- `no-node-access` is now enabled by default in DOM shared config +- `no-debugging-utils` now reports all debugging utility methods by default +- `no-debugging-utils` now defaults to `warn` instead of `error` in all shared configs + +## Steps to upgrade + +- Removing `testing-library/prefer-wait-for` if you were referencing it manually somewhere +- Removing `testing-library/no-wait-for-empty-callback` if you were referencing it manually somewhere +- Renaming `testing-library/await-fire-event` to `testing-library/await-async-events` if you were referencing it manually somewhere +- Renaming `testing-library/await-async-query` to `testing-library/await-async-queries` if you were referencing it manually somewhere +- Renaming `testing-library/no-await-async-query` to `testing-library/no-await-async-queries` if you were referencing it manually somewhere +- Renaming `testing-library/no-render-in-setup` to `testing-library/no-render-in-lifecycle` if you were referencing it manually somewhere +- Being aware of new rules enabled or changed above in shared configs which can lead to newly reported errors diff --git a/docs/rules/await-async-events.md b/docs/rules/await-async-events.md new file mode 100644 index 00000000..8f21ce33 --- /dev/null +++ b/docs/rules/await-async-events.md @@ -0,0 +1,149 @@ +# Enforce promises from async event methods are handled (`testing-library/await-async-events`) + +💼 This rule is enabled in the following configs: `angular`, `dom`, `marko`, `react`, `vue`. + +🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). + + + +Ensure that promises returned by `userEvent` (v14+) async methods or `fireEvent` (only Vue and Marko) async methods are handled properly. + +## Rule Details + +This rule aims to prevent users from forgetting to handle promise returned from async event +methods. + +> ⚠️ `fireEvent` methods are async only on following Testing Library packages: +> +> - `@testing-library/vue` (supported by this plugin) +> - `@testing-library/svelte` (not supported yet by this plugin) +> - `@marko/testing-library` (supported by this plugin) + +Examples of **incorrect** code for this rule: + +```js +fireEvent.click(getByText('Click me')); + +fireEvent.focus(getByLabelText('username')); +fireEvent.blur(getByLabelText('username')); + +// wrap a fireEvent method within a function... +function triggerEvent() { + return fireEvent.click(button); +} +triggerEvent(); // ...but not handling promise from it is incorrect too +``` + +```js +userEvent.click(getByText('Click me')); +userEvent.tripleClick(getByText('Click me')); +userEvent.keyboard('foo'); + +// wrap a userEvent method within a function... +function triggerEvent() { + return userEvent.click(button); +} +triggerEvent(); // ...but not handling promise from it is incorrect too +``` + +Examples of **correct** code for this rule: + +```js +// `await` operator is correct +await fireEvent.focus(getByLabelText('username')); +await fireEvent.blur(getByLabelText('username')); + +// `then` method is correct +fireEvent.click(getByText('Click me')).then(() => { + // ... +}); + +// return the promise within a function is correct too! +const clickMeArrowFn = () => fireEvent.click(getByText('Click me')); + +// wrap a fireEvent method within a function... +function triggerEvent() { + return fireEvent.click(button); +} +await triggerEvent(); // ...and handling promise from it is correct also + +// using `Promise.all` or `Promise.allSettled` with an array of promises is valid +await Promise.all([ + fireEvent.focus(getByLabelText('username')), + fireEvent.blur(getByLabelText('username')), +]); +``` + +```js +// `await` operator is correct +await userEvent.click(getByText('Click me')); +await userEvent.tripleClick(getByText('Click me')); + +// `then` method is correct +userEvent.keyboard('foo').then(() => { + // ... +}); + +// return the promise within a function is correct too! +const clickMeArrowFn = () => userEvent.click(getByText('Click me')); + +// wrap a userEvent method within a function... +function triggerEvent() { + return userEvent.click(button); +} +await triggerEvent(); // ...and handling promise from it is correct also + +// using `Promise.all` or `Promise.allSettled` with an array of promises is valid +await Promise.all([ + userEvent.click(getByText('Click me')); + userEvent.tripleClick(getByText('Click me')); +]); +``` + +## Options + +- `eventModule`: `string` or `string[]`. Which event module should be linted for async event methods. Defaults to `userEvent` which should be used after v14. `fireEvent` should only be used with frameworks that have async fire event methods. + +## Example + +```json +{ + "testing-library/await-async-events": [ + 2, + { + "eventModule": "userEvent" + } + ] +} +``` + +```json +{ + "testing-library/await-async-events": [ + 2, + { + "eventModule": "fireEvent" + } + ] +} +``` + +```json +{ + "testing-library/await-async-events": [ + 2, + { + "eventModule": ["fireEvent", "userEvent"] + } + ] +} +``` + +## When Not To Use It + +- `userEvent` is below v14, before all event methods are async +- `fireEvent` methods are sync for most Testing Library packages. If you are not using Testing Library package with async events, you shouldn't use this rule. + +## Further Reading + +- [Vue Testing Library fireEvent](https://testing-library.com/docs/vue-testing-library/api#fireevent) diff --git a/docs/rules/await-async-query.md b/docs/rules/await-async-queries.md similarity index 99% rename from docs/rules/await-async-query.md rename to docs/rules/await-async-queries.md index 6852fc24..520b6968 100644 --- a/docs/rules/await-async-query.md +++ b/docs/rules/await-async-queries.md @@ -1,4 +1,4 @@ -# Enforce promises from async queries to be handled (`testing-library/await-async-query`) +# Enforce promises from async queries to be handled (`testing-library/await-async-queries`) 💼 This rule is enabled in the following configs: `angular`, `dom`, `marko`, `react`, `vue`. diff --git a/docs/rules/await-async-utils.md b/docs/rules/await-async-utils.md index 86119f64..b5433d83 100644 --- a/docs/rules/await-async-utils.md +++ b/docs/rules/await-async-utils.md @@ -10,11 +10,8 @@ Ensure that promises returned by async utils are handled properly. Testing library provides several utilities for dealing with asynchronous code. These are useful to wait for an element until certain criteria or situation happens. The available async utils are: -- `waitFor` _(introduced since dom-testing-library v7)_ +- `waitFor` - `waitForElementToBeRemoved` -- `wait` _(**deprecated** since dom-testing-library v7)_ -- `waitForElement` _(**deprecated** since dom-testing-library v7)_ -- `waitForDomChange` _(**deprecated** since dom-testing-library v7)_ This rule aims to prevent users from forgetting to handle the returned promise from async utils, which could lead to diff --git a/docs/rules/await-fire-event.md b/docs/rules/await-fire-event.md deleted file mode 100644 index 5c1002ef..00000000 --- a/docs/rules/await-fire-event.md +++ /dev/null @@ -1,84 +0,0 @@ -# Enforce promises from `fireEvent` methods to be handled (`testing-library/await-fire-event`) - -💼 This rule is enabled in the following configs: `marko`, `vue`. - - - -Ensure that promises returned by `fireEvent` methods are handled -properly. - -## Rule Details - -This rule aims to prevent users from forgetting to handle promise returned from `fireEvent` -methods. -The promise will be considered as handled when: - -- using the `await` operator -- wrapped within `Promise.all` or `Promise.allSettled` methods -- chaining the `then` method -- chaining `resolves` or `rejects` from jest -- chaining `toResolve()` or `toReject()` from [jest-extended](https://github.com/jest-community/jest-extended#promise) -- it's returned from a function (in this case, that particular function will be analyzed by this rule too) - -> ⚠️ `fireEvent` methods are async only on following Testing Library packages: -> -> - `@testing-library/vue` (supported by this plugin) -> - `@testing-library/svelte` (not supported yet by this plugin) -> - `@marko/testing-library` (supported by this plugin) - -Examples of **incorrect** code for this rule: - -```js -fireEvent.click(getByText('Click me')); - -fireEvent.focus(getByLabelText('username')); -fireEvent.blur(getByLabelText('username')); - -// wrap a fireEvent method within a function... -function triggerEvent() { - return fireEvent.click(button); -} -triggerEvent(); // ...but not handling promise from it is incorrect too -``` - -Examples of **correct** code for this rule: - -```js -// `await` operator is correct -await fireEvent.focus(getByLabelText('username')); -await fireEvent.blur(getByLabelText('username')); - -// `then` method is correct -fireEvent.click(getByText('Click me')).then(() => { - // ... -}); - -// return the promise within a function is correct too! -const clickMeArrowFn = () => fireEvent.click(getByText('Click me')); - -// wrap a fireEvent method within a function... -function triggerEvent() { - return fireEvent.click(button); -} -await triggerEvent(); // ...and handling promise from it is correct also - -// using `Promise.all` or `Promise.allSettled` with an array of promises is valid -await Promise.all([ - fireEvent.focus(getByLabelText('username')), - fireEvent.blur(getByLabelText('username')), -]); - -// Using jest resolves or rejects -expect(fireEvent.focus(getByLabelText('username'))).resolves.toBeUndefined(); - -// Using jest-extended a toResolve/toReject matcher is also correct -expect(waitFor(() => getByLabelText('email'))).toResolve(); -``` - -## When Not To Use It - -`fireEvent` methods are not async on all Testing Library packages. If you are not using Testing Library package with async fire event, you shouldn't use this rule. - -## Further Reading - -- [Vue Testing Library fireEvent](https://testing-library.com/docs/vue-testing-library/api#fireevent) diff --git a/docs/rules/no-await-sync-events.md b/docs/rules/no-await-sync-events.md index 9d5dfedd..4c9e0519 100644 --- a/docs/rules/no-await-sync-events.md +++ b/docs/rules/no-await-sync-events.md @@ -1,5 +1,7 @@ # Disallow unnecessary `await` for sync events (`testing-library/no-await-sync-events`) +💼 This rule is enabled in the following configs: `angular`, `dom`, `react`. + Ensure that sync simulated events are not awaited unnecessarily. @@ -107,5 +109,4 @@ module.exports = { ## Notes - Since `user-event` v14 all its methods are async, so you should disable reporting them by setting the `eventModules` to just `"fire-event"` so `user-event` methods are not reported. -- There is another rule `await-fire-event`, which is only in Vue Testing - Library. Please do not confuse with this rule. +- There is another rule `await-async-events`, which is for awaiting async events for `user-event` v14 or `fire-event` only in Vue Testing Library. Please do not confuse with this rule. diff --git a/docs/rules/no-await-sync-query.md b/docs/rules/no-await-sync-queries.md similarity index 98% rename from docs/rules/no-await-sync-query.md rename to docs/rules/no-await-sync-queries.md index 79046767..a4538d71 100644 --- a/docs/rules/no-await-sync-query.md +++ b/docs/rules/no-await-sync-queries.md @@ -1,4 +1,4 @@ -# Disallow unnecessary `await` for sync queries (`testing-library/no-await-sync-query`) +# Disallow unnecessary `await` for sync queries (`testing-library/no-await-sync-queries`) 💼 This rule is enabled in the following configs: `angular`, `dom`, `marko`, `react`, `vue`. diff --git a/docs/rules/no-debugging-utils.md b/docs/rules/no-debugging-utils.md index 6f244a45..cb91690e 100644 --- a/docs/rules/no-debugging-utils.md +++ b/docs/rules/no-debugging-utils.md @@ -1,6 +1,6 @@ # Disallow the use of debugging utilities like `debug` (`testing-library/no-debugging-utils`) -💼 This rule is enabled in the following configs: `angular`, `marko`, `react`, `vue`. +⚠️ This rule _warns_ in the following configs: `angular`, `marko`, `react`, `vue`. @@ -17,7 +17,7 @@ This rule supports disallowing the following debugging utilities: - `logDOM` - `prettyFormat` -By default, only `debug` and `logTestingPlaygroundURL` are disallowed. +By default, all are disallowed. Examples of **incorrect** code for this rule: diff --git a/docs/rules/no-global-regexp-flag-in-query.md b/docs/rules/no-global-regexp-flag-in-query.md index 41469460..75e5a18a 100644 --- a/docs/rules/no-global-regexp-flag-in-query.md +++ b/docs/rules/no-global-regexp-flag-in-query.md @@ -1,5 +1,7 @@ # Disallow the use of the global RegExp flag (/g) in queries (`testing-library/no-global-regexp-flag-in-query`) +💼 This rule is enabled in the following configs: `angular`, `dom`, `marko`, `react`, `vue`. + 🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). diff --git a/docs/rules/no-manual-cleanup.md b/docs/rules/no-manual-cleanup.md index ebd8d427..7c0b0115 100644 --- a/docs/rules/no-manual-cleanup.md +++ b/docs/rules/no-manual-cleanup.md @@ -1,5 +1,7 @@ # Disallow the use of `cleanup` (`testing-library/no-manual-cleanup`) +💼 This rule is enabled in the following configs: `react`, `vue`. + `cleanup` is performed automatically if the testing framework you're using supports the `afterEach` global (like mocha, Jest, and Jasmine). In this case, it's unnecessary to do manual cleanups after each test unless you skip the auto-cleanup with environment variables such as `RTL_SKIP_AUTO_CLEANUP` for React. diff --git a/docs/rules/no-node-access.md b/docs/rules/no-node-access.md index 4f90c681..91c0727a 100644 --- a/docs/rules/no-node-access.md +++ b/docs/rules/no-node-access.md @@ -1,6 +1,6 @@ # Disallow direct Node access (`testing-library/no-node-access`) -💼 This rule is enabled in the following configs: `angular`, `marko`, `react`, `vue`. +💼 This rule is enabled in the following configs: `angular`, `dom`, `marko`, `react`, `vue`. diff --git a/docs/rules/no-render-in-setup.md b/docs/rules/no-render-in-lifecycle.md similarity index 94% rename from docs/rules/no-render-in-setup.md rename to docs/rules/no-render-in-lifecycle.md index 69838a55..7c610feb 100644 --- a/docs/rules/no-render-in-setup.md +++ b/docs/rules/no-render-in-lifecycle.md @@ -1,4 +1,4 @@ -# Disallow the use of `render` in testing frameworks setup functions (`testing-library/no-render-in-setup`) +# Disallow the use of `render` in testing frameworks setup functions (`testing-library/no-render-in-lifecycle`) 💼 This rule is enabled in the following configs: `angular`, `marko`, `react`, `vue`. @@ -87,5 +87,5 @@ it('Should have foo and bar', () => { If you would like to allow the use of `render` (or a custom render function) in _either_ `beforeAll` or `beforeEach`, this can be configured using the option `allowTestingFrameworkSetupHook`. This may be useful if you have configured your tests to [skip auto cleanup](https://testing-library.com/docs/react-testing-library/setup#skipping-auto-cleanup). `allowTestingFrameworkSetupHook` is an enum that accepts either `"beforeAll"` or `"beforeEach"`. ``` - "testing-library/no-render-in-setup": ["error", {"allowTestingFrameworkSetupHook": "beforeAll"}], + "testing-library/no-render-in-lifecycle": ["error", {"allowTestingFrameworkSetupHook": "beforeAll"}], ``` diff --git a/docs/rules/no-wait-for-empty-callback.md b/docs/rules/no-wait-for-empty-callback.md deleted file mode 100644 index b92fc3a2..00000000 --- a/docs/rules/no-wait-for-empty-callback.md +++ /dev/null @@ -1,45 +0,0 @@ -# Disallow empty callbacks for `waitFor` and `waitForElementToBeRemoved` (`testing-library/no-wait-for-empty-callback`) - -💼 This rule is enabled in the following configs: `angular`, `dom`, `marko`, `react`, `vue`. - - - -## Rule Details - -This rule aims to ensure the correct usage of `waitFor` and `waitForElementToBeRemoved`, in the way that they're intended to be used. -If an empty callback is passed, these methods will just wait next tick of the event loop before proceeding, and that's not consistent with the philosophy of the library. -**Instead, insert an assertion in that callback function.** - -Examples of **incorrect** code for this rule: - -```js -const foo = async () => { - await waitFor(() => {}); - await waitFor(function () {}); - await waitFor(noop); - - await waitForElementToBeRemoved(() => {}); - await waitForElementToBeRemoved(function () {}); - await waitForElementToBeRemoved(noop); -}; -``` - -Examples of **correct** code for this rule: - -```js -const foo = async () => { - await waitFor(() => { - screen.getByText(/submit/i); - }); - - const submit = screen.getByText(/submit/i); - await waitForElementToBeRemoved(() => submit); - // or - await waitForElementToBeRemoved(submit); -}; -``` - -## Further Reading - -- [dom-testing-library v7 release](https://github.com/testing-library/dom-testing-library/releases/tag/v7.0.0) -- [inspiration for this rule](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library#passing-an-empty-callback-to-waitfor) diff --git a/docs/rules/no-wait-for-snapshot.md b/docs/rules/no-wait-for-snapshot.md index 470d07bc..e29fb7fa 100644 --- a/docs/rules/no-wait-for-snapshot.md +++ b/docs/rules/no-wait-for-snapshot.md @@ -29,14 +29,6 @@ const bar = async () => { await waitFor(() => expect(container).toMatchInlineSnapshot()); // ... }; - -const baz = async () => { - // ... - await wait(() => { - expect(container).toMatchSnapshot(); - }); - // ... -}; ``` Examples of **correct** code for this rule: diff --git a/docs/rules/prefer-find-by.md b/docs/rules/prefer-find-by.md index 257af7a2..1cc5b217 100644 --- a/docs/rules/prefer-find-by.md +++ b/docs/rules/prefer-find-by.md @@ -10,7 +10,7 @@ ## Rule details -This rule aims to use `findBy*` or `findAllBy*` queries to wait for elements, rather than using `waitFor`, or the deprecated methods `waitForElement` and `wait`. +This rule aims to use `findBy*` or `findAllBy*` queries to wait for elements, rather than using `waitFor`. This rule analyzes those cases where `waitFor` is used with just one query method, in the form of an arrow function with only one statement (that is, without a block of statements). Given the callback could be more complex, this rule does not consider function callbacks or arrow functions with blocks of code. Examples of **incorrect** code for this rule diff --git a/docs/rules/prefer-wait-for.md b/docs/rules/prefer-wait-for.md deleted file mode 100644 index a3b59bc6..00000000 --- a/docs/rules/prefer-wait-for.md +++ /dev/null @@ -1,78 +0,0 @@ -# Use `waitFor` instead of deprecated wait methods (`testing-library/prefer-wait-for`) - -🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). - - - -`dom-testing-library` v7 released a new async util called `waitFor` which satisfies the use cases of `wait`, `waitForElement`, and `waitForDomChange` making them deprecated. - -## Rule Details - -This rule aims to use `waitFor` async util rather than previous deprecated ones. - -Deprecated `wait` async utils are: - -- `wait` -- `waitForElement` -- `waitForDomChange` - -> This rule will auto fix deprecated async utils for you, including the necessary empty callback for `waitFor`. This means `wait();` will be replaced with `waitFor(() => {});` - -Examples of **incorrect** code for this rule: - -```js -import { wait, waitForElement, waitForDomChange } from '@testing-library/dom'; -// this also works for const { wait, waitForElement, waitForDomChange } = require ('@testing-library/dom') - -const foo = async () => { - await wait(); - await wait(() => {}); - await waitForElement(() => {}); - await waitForDomChange(); - await waitForDomChange(mutationObserverOptions); - await waitForDomChange({ timeout: 100 }); -}; - -import * as tl from '@testing-library/dom'; -// this also works for const tl = require('@testing-library/dom') -const foo = async () => { - await tl.wait(); - await tl.wait(() => {}); - await tl.waitForElement(() => {}); - await tl.waitForDomChange(); - await tl.waitForDomChange(mutationObserverOptions); - await tl.waitForDomChange({ timeout: 100 }); -}; -``` - -Examples of **correct** code for this rule: - -```js -import { waitFor, waitForElementToBeRemoved } from '@testing-library/dom'; -// this also works for const { waitFor, waitForElementToBeRemoved } = require('@testing-library/dom') -const foo = async () => { - // new waitFor method - await waitFor(() => {}); - - // previous waitForElementToBeRemoved is not deprecated - await waitForElementToBeRemoved(() => {}); -}; - -import * as tl from '@testing-library/dom'; -// this also works for const tl = require('@testing-library/dom') -const foo = async () => { - // new waitFor method - await tl.waitFor(() => {}); - - // previous waitForElementToBeRemoved is not deprecated - await tl.waitForElementToBeRemoved(() => {}); -}; -``` - -## When Not To Use It - -When using dom-testing-library (or any other Testing Library relying on dom-testing-library) prior to v7. - -## Further Reading - -- [dom-testing-library v7 release](https://github.com/testing-library/dom-testing-library/releases/tag/v7.0.0) diff --git a/lib/configs/angular.ts b/lib/configs/angular.ts index 6f4388b8..8defb66b 100644 --- a/lib/configs/angular.ts +++ b/lib/configs/angular.ts @@ -5,16 +5,21 @@ export = { plugins: ['testing-library'], rules: { - 'testing-library/await-async-query': 'error', + 'testing-library/await-async-events': [ + 'error', + { eventModule: 'userEvent' }, + ], + 'testing-library/await-async-queries': 'error', 'testing-library/await-async-utils': 'error', - 'testing-library/no-await-sync-query': 'error', + 'testing-library/no-await-sync-events': 'error', + 'testing-library/no-await-sync-queries': 'error', 'testing-library/no-container': 'error', - 'testing-library/no-debugging-utils': 'error', + 'testing-library/no-debugging-utils': 'warn', 'testing-library/no-dom-import': ['error', 'angular'], + 'testing-library/no-global-regexp-flag-in-query': 'error', '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-render-in-lifecycle': 'error', 'testing-library/no-wait-for-multiple-assertions': 'error', 'testing-library/no-wait-for-side-effects': 'error', 'testing-library/no-wait-for-snapshot': 'error', diff --git a/lib/configs/dom.ts b/lib/configs/dom.ts index 83add602..3e5830ac 100644 --- a/lib/configs/dom.ts +++ b/lib/configs/dom.ts @@ -5,11 +5,17 @@ export = { plugins: ['testing-library'], rules: { - 'testing-library/await-async-query': 'error', + 'testing-library/await-async-events': [ + 'error', + { eventModule: 'userEvent' }, + ], + 'testing-library/await-async-queries': 'error', 'testing-library/await-async-utils': 'error', - 'testing-library/no-await-sync-query': 'error', + 'testing-library/no-await-sync-events': 'error', + 'testing-library/no-await-sync-queries': 'error', + 'testing-library/no-global-regexp-flag-in-query': 'error', + 'testing-library/no-node-access': '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', diff --git a/lib/configs/marko.ts b/lib/configs/marko.ts index d5dc1311..066c3498 100644 --- a/lib/configs/marko.ts +++ b/lib/configs/marko.ts @@ -5,18 +5,21 @@ export = { plugins: ['testing-library'], rules: { - 'testing-library/await-async-query': 'error', + 'testing-library/await-async-events': [ + 'error', + { eventModule: ['fireEvent', 'userEvent'] }, + ], + 'testing-library/await-async-queries': 'error', 'testing-library/await-async-utils': 'error', - 'testing-library/await-fire-event': 'error', - 'testing-library/no-await-sync-query': 'error', + 'testing-library/no-await-sync-queries': 'error', 'testing-library/no-container': 'error', - 'testing-library/no-debugging-utils': 'error', + 'testing-library/no-debugging-utils': 'warn', 'testing-library/no-dom-import': ['error', 'marko'], + 'testing-library/no-global-regexp-flag-in-query': 'error', 'testing-library/no-node-access': 'error', 'testing-library/no-promise-in-fire-event': 'error', - 'testing-library/no-render-in-setup': 'error', + 'testing-library/no-render-in-lifecycle': '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', diff --git a/lib/configs/react.ts b/lib/configs/react.ts index 066d9fd7..538b4fc9 100644 --- a/lib/configs/react.ts +++ b/lib/configs/react.ts @@ -5,17 +5,23 @@ export = { plugins: ['testing-library'], rules: { - 'testing-library/await-async-query': 'error', + 'testing-library/await-async-events': [ + 'error', + { eventModule: 'userEvent' }, + ], + 'testing-library/await-async-queries': 'error', 'testing-library/await-async-utils': 'error', - 'testing-library/no-await-sync-query': 'error', + 'testing-library/no-await-sync-events': 'error', + 'testing-library/no-await-sync-queries': 'error', 'testing-library/no-container': 'error', - 'testing-library/no-debugging-utils': 'error', + 'testing-library/no-debugging-utils': 'warn', 'testing-library/no-dom-import': ['error', 'react'], + 'testing-library/no-global-regexp-flag-in-query': 'error', + 'testing-library/no-manual-cleanup': 'error', 'testing-library/no-node-access': 'error', 'testing-library/no-promise-in-fire-event': 'error', - 'testing-library/no-render-in-setup': 'error', + 'testing-library/no-render-in-lifecycle': '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', diff --git a/lib/configs/vue.ts b/lib/configs/vue.ts index 2a3b2d2b..fdf8bfb7 100644 --- a/lib/configs/vue.ts +++ b/lib/configs/vue.ts @@ -5,17 +5,21 @@ export = { plugins: ['testing-library'], rules: { - 'testing-library/await-async-query': 'error', + 'testing-library/await-async-events': [ + 'error', + { eventModule: ['fireEvent', 'userEvent'] }, + ], + 'testing-library/await-async-queries': 'error', 'testing-library/await-async-utils': 'error', - 'testing-library/await-fire-event': 'error', - 'testing-library/no-await-sync-query': 'error', + 'testing-library/no-await-sync-queries': 'error', 'testing-library/no-container': 'error', - 'testing-library/no-debugging-utils': 'error', + 'testing-library/no-debugging-utils': 'warn', 'testing-library/no-dom-import': ['error', 'vue'], + 'testing-library/no-global-regexp-flag-in-query': 'error', + 'testing-library/no-manual-cleanup': 'error', '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-render-in-lifecycle': 'error', 'testing-library/no-wait-for-multiple-assertions': 'error', 'testing-library/no-wait-for-side-effects': 'error', 'testing-library/no-wait-for-snapshot': 'error', diff --git a/lib/node-utils/index.ts b/lib/node-utils/index.ts index 361aa709..0b41bd4a 100644 --- a/lib/node-utils/index.ts +++ b/lib/node-utils/index.ts @@ -13,6 +13,8 @@ import { isBlockStatement, isCallExpression, isExpressionStatement, + isFunctionExpression, + isFunctionDeclaration, isImportDeclaration, isImportNamespaceSpecifier, isImportSpecifier, @@ -95,6 +97,28 @@ export function findClosestVariableDeclaratorNode( return findClosestVariableDeclaratorNode(node.parent); } +export function findClosestFunctionExpressionNode( + node: TSESTree.Node | undefined +): + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionExpression + | TSESTree.FunctionDeclaration + | null { + if (!node) { + return null; + } + + if ( + isArrowFunctionExpression(node) || + isFunctionExpression(node) || + isFunctionDeclaration(node) + ) { + return node; + } + + return findClosestFunctionExpressionNode(node.parent); +} + /** * TODO: remove this one in favor of {@link findClosestCallExpressionNode} */ @@ -235,7 +259,7 @@ export function isPromiseHandled(nodeIdentifier: TSESTree.Identifier): boolean { } export function getVariableReferences( - context: TSESLint.RuleContext, + context: TSESLint.RuleContext, node: TSESTree.Node ): TSESLint.Scope.Reference[] { if (ASTUtils.isVariableDeclarator(node)) { diff --git a/lib/node-utils/is-node-of-type.ts b/lib/node-utils/is-node-of-type.ts index 8f74a9a5..afa2b3fc 100644 --- a/lib/node-utils/is-node-of-type.ts +++ b/lib/node-utils/is-node-of-type.ts @@ -59,3 +59,6 @@ export const isReturnStatement = ASTUtils.isNodeOfType( export const isFunctionExpression = ASTUtils.isNodeOfType( AST_NODE_TYPES.FunctionExpression ); +export const isFunctionDeclaration = ASTUtils.isNodeOfType( + AST_NODE_TYPES.FunctionDeclaration +); diff --git a/lib/rules/await-async-events.ts b/lib/rules/await-async-events.ts new file mode 100644 index 00000000..53c62659 --- /dev/null +++ b/lib/rules/await-async-events.ts @@ -0,0 +1,227 @@ +import { ASTUtils, TSESLint, TSESTree } from '@typescript-eslint/utils'; + +import { createTestingLibraryRule } from '../create-testing-library-rule'; +import { + findClosestCallExpressionNode, + findClosestFunctionExpressionNode, + getFunctionName, + getInnermostReturningFunction, + getVariableReferences, + isMemberExpression, + isPromiseHandled, +} from '../node-utils'; +import { EVENTS_SIMULATORS } from '../utils'; + +export const RULE_NAME = 'await-async-events'; +export type MessageIds = 'awaitAsyncEvent' | 'awaitAsyncEventWrapper'; +const FIRE_EVENT_NAME = 'fireEvent'; +const USER_EVENT_NAME = 'userEvent'; +type EventModules = (typeof EVENTS_SIMULATORS)[number]; +export type Options = [ + { + eventModule: EventModules | EventModules[]; + } +]; + +export default createTestingLibraryRule({ + name: RULE_NAME, + meta: { + type: 'problem', + docs: { + description: 'Enforce promises from async event methods are handled', + recommendedConfig: { + dom: ['error', { eventModule: 'userEvent' }], + angular: ['error', { eventModule: 'userEvent' }], + react: ['error', { eventModule: 'userEvent' }], + vue: ['error', { eventModule: ['fireEvent', 'userEvent'] }], + marko: ['error', { eventModule: ['fireEvent', 'userEvent'] }], + }, + }, + messages: { + awaitAsyncEvent: + 'Promise returned from async event method `{{ name }}` must be handled', + awaitAsyncEventWrapper: + 'Promise returned from `{{ name }}` wrapper over async event method must be handled', + }, + fixable: 'code', + schema: [ + { + type: 'object', + default: {}, + additionalProperties: false, + properties: { + eventModule: { + default: USER_EVENT_NAME, + oneOf: [ + { + type: 'string', + enum: EVENTS_SIMULATORS, + }, + { + type: 'array', + items: { + type: 'string', + enum: EVENTS_SIMULATORS, + }, + }, + ], + }, + }, + }, + ], + }, + defaultOptions: [ + { + eventModule: USER_EVENT_NAME, + }, + ], + + create(context, [options], helpers) { + const functionWrappersNames: string[] = []; + + function reportUnhandledNode({ + node, + closestCallExpression, + messageId = 'awaitAsyncEvent', + fix, + }: { + node: TSESTree.Identifier; + closestCallExpression: TSESTree.CallExpression; + messageId?: MessageIds; + fix?: TSESLint.ReportFixFunction; + }): void { + if (!isPromiseHandled(node)) { + context.report({ + node: closestCallExpression.callee, + messageId, + data: { name: node.name }, + fix, + }); + } + } + + function detectEventMethodWrapper(node: TSESTree.Identifier): void { + const innerFunction = getInnermostReturningFunction(context, node); + + if (innerFunction) { + functionWrappersNames.push(getFunctionName(innerFunction)); + } + } + + const eventModules = + typeof options.eventModule === 'string' + ? [options.eventModule] + : options.eventModule; + const isFireEventEnabled = eventModules.includes(FIRE_EVENT_NAME); + const isUserEventEnabled = eventModules.includes(USER_EVENT_NAME); + + return { + 'CallExpression Identifier'(node: TSESTree.Identifier) { + if ( + (isFireEventEnabled && helpers.isFireEventMethod(node)) || + (isUserEventEnabled && helpers.isUserEventMethod(node)) + ) { + detectEventMethodWrapper(node); + + const closestCallExpression = findClosestCallExpressionNode( + node, + true + ); + + if (!closestCallExpression?.parent) { + return; + } + + const references = getVariableReferences( + context, + closestCallExpression.parent + ); + + if (references.length === 0) { + reportUnhandledNode({ + node, + closestCallExpression, + fix: (fixer) => { + if (isMemberExpression(node.parent)) { + const functionExpression = + findClosestFunctionExpressionNode(node); + + if (functionExpression) { + const memberExpressionFixer = fixer.insertTextBefore( + node.parent, + 'await ' + ); + + if (functionExpression.async) { + return memberExpressionFixer; + } else { + // Mutate the actual node so if other nodes exist in this + // function expression body they don't also try to fix it. + functionExpression.async = true; + + return [ + memberExpressionFixer, + fixer.insertTextBefore(functionExpression, 'async '), + ]; + } + } + } + + return null; + }, + }); + } else { + for (const reference of references) { + if (ASTUtils.isIdentifier(reference.identifier)) { + reportUnhandledNode({ + node: reference.identifier, + closestCallExpression, + }); + } + } + } + } else if (functionWrappersNames.includes(node.name)) { + // report promise returned from function wrapping fire event method + // previously detected + const closestCallExpression = findClosestCallExpressionNode( + node, + true + ); + + if (!closestCallExpression) { + return; + } + + reportUnhandledNode({ + node, + closestCallExpression, + messageId: 'awaitAsyncEventWrapper', + fix: (fixer) => { + const functionExpression = + findClosestFunctionExpressionNode(node); + + if (functionExpression) { + const nodeFixer = fixer.insertTextBefore(node, 'await '); + + if (functionExpression.async) { + return nodeFixer; + } else { + // Mutate the actual node so if other nodes exist in this + // function expression body they don't also try to fix it. + functionExpression.async = true; + + return [ + nodeFixer, + fixer.insertTextBefore(functionExpression, 'async '), + ]; + } + } + + return null; + }, + }); + } + }, + }; + }, +}); diff --git a/lib/rules/await-async-query.ts b/lib/rules/await-async-queries.ts similarity index 98% rename from lib/rules/await-async-query.ts rename to lib/rules/await-async-queries.ts index 8ca4e716..6da6b0d2 100644 --- a/lib/rules/await-async-query.ts +++ b/lib/rules/await-async-queries.ts @@ -10,7 +10,7 @@ import { isPromiseHandled, } from '../node-utils'; -export const RULE_NAME = 'await-async-query'; +export const RULE_NAME = 'await-async-queries'; export type MessageIds = 'asyncQueryWrapper' | 'awaitAsyncQuery'; type Options = []; diff --git a/lib/rules/await-fire-event.ts b/lib/rules/await-fire-event.ts deleted file mode 100644 index 7d8c23a3..00000000 --- a/lib/rules/await-fire-event.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { ASTUtils, TSESTree } from '@typescript-eslint/utils'; - -import { createTestingLibraryRule } from '../create-testing-library-rule'; -import { - findClosestCallExpressionNode, - getFunctionName, - getInnermostReturningFunction, - getVariableReferences, - isPromiseHandled, -} from '../node-utils'; - -export const RULE_NAME = 'await-fire-event'; -export type MessageIds = 'awaitFireEvent' | 'fireEventWrapper'; -type Options = []; - -export default createTestingLibraryRule({ - name: 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: string[] = []; - - function reportUnhandledNode( - node: TSESTree.Identifier, - closestCallExpressionNode: TSESTree.CallExpression, - messageId: MessageIds = 'awaitFireEvent' - ): void { - if (!isPromiseHandled(node)) { - context.report({ - node: closestCallExpressionNode.callee, - messageId, - data: { name: node.name }, - }); - } - } - - function detectFireEventMethodWrapper(node: TSESTree.Identifier): void { - const innerFunction = getInnermostReturningFunction(context, node); - - if (innerFunction) { - functionWrappersNames.push(getFunctionName(innerFunction)); - } - } - - return { - 'CallExpression Identifier'(node: TSESTree.Identifier) { - if (helpers.isFireEventMethod(node)) { - detectFireEventMethodWrapper(node); - - const closestCallExpression = findClosestCallExpressionNode( - node, - true - ); - - if (!closestCallExpression?.parent) { - return; - } - - const references = getVariableReferences( - context, - closestCallExpression.parent - ); - - if (references.length === 0) { - reportUnhandledNode(node, closestCallExpression); - } else { - for (const reference of references) { - if (ASTUtils.isIdentifier(reference.identifier)) { - reportUnhandledNode( - reference.identifier, - closestCallExpression - ); - } - } - } - } else if (functionWrappersNames.includes(node.name)) { - // report promise returned from function wrapping fire event method - // previously detected - const closestCallExpression = findClosestCallExpressionNode( - node, - true - ); - - if (!closestCallExpression) { - return; - } - - reportUnhandledNode(node, closestCallExpression, 'fireEventWrapper'); - } - }, - }; - }, -}); diff --git a/lib/rules/no-await-sync-events.ts b/lib/rules/no-await-sync-events.ts index ee5f3cac..e9fa9b9d 100644 --- a/lib/rules/no-await-sync-events.ts +++ b/lib/rules/no-await-sync-events.ts @@ -25,9 +25,9 @@ export default createTestingLibraryRule({ docs: { description: 'Disallow unnecessary `await` for sync events', recommendedConfig: { - dom: false, - angular: false, - react: false, + dom: 'error', + angular: 'error', + react: 'error', vue: false, marko: false, }, diff --git a/lib/rules/no-await-sync-query.ts b/lib/rules/no-await-sync-queries.ts similarity index 95% rename from lib/rules/no-await-sync-query.ts rename to lib/rules/no-await-sync-queries.ts index 70c86e8b..c6f4a70c 100644 --- a/lib/rules/no-await-sync-query.ts +++ b/lib/rules/no-await-sync-queries.ts @@ -3,7 +3,7 @@ import { TSESTree } from '@typescript-eslint/utils'; import { createTestingLibraryRule } from '../create-testing-library-rule'; import { getDeepestIdentifierNode } from '../node-utils'; -export const RULE_NAME = 'no-await-sync-query'; +export const RULE_NAME = 'no-await-sync-queries'; export type MessageIds = 'noAwaitSyncQuery'; type Options = []; diff --git a/lib/rules/no-debugging-utils.ts b/lib/rules/no-debugging-utils.ts index d90d652a..d9ed3040 100644 --- a/lib/rules/no-debugging-utils.ts +++ b/lib/rules/no-debugging-utils.ts @@ -13,14 +13,22 @@ import { } from '../node-utils'; import { DEBUG_UTILS } from '../utils'; -type DebugUtilsToCheckFor = Partial< - Record<(typeof DEBUG_UTILS)[number], boolean> ->; +type DebugUtilsToCheckForConfig = Record<(typeof DEBUG_UTILS)[number], boolean>; +type DebugUtilsToCheckFor = Partial; export const RULE_NAME = 'no-debugging-utils'; export type MessageIds = 'noDebug'; type Options = [{ utilsToCheckFor?: DebugUtilsToCheckFor }]; +const defaultUtilsToCheckFor: DebugUtilsToCheckForConfig = { + debug: true, + logTestingPlaygroundURL: true, + prettyDOM: true, + logRoles: true, + logDOM: true, + prettyFormat: true, +}; + export default createTestingLibraryRule({ name: RULE_NAME, meta: { @@ -29,10 +37,10 @@ export default createTestingLibraryRule({ description: 'Disallow the use of debugging utilities like `debug`', recommendedConfig: { dom: false, - angular: 'error', - react: 'error', - vue: 'error', - marko: 'error', + angular: 'warn', + react: 'warn', + vue: 'warn', + marko: 'warn', }, }, messages: { @@ -60,9 +68,7 @@ export default createTestingLibraryRule({ }, ], }, - defaultOptions: [ - { utilsToCheckFor: { debug: true, logTestingPlaygroundURL: true } }, - ], + defaultOptions: [{ utilsToCheckFor: defaultUtilsToCheckFor }], create(context, [{ utilsToCheckFor = {} }], helpers) { const suspiciousDebugVariableNames: string[] = []; diff --git a/lib/rules/no-global-regexp-flag-in-query.ts b/lib/rules/no-global-regexp-flag-in-query.ts index de2f108d..f69894d6 100644 --- a/lib/rules/no-global-regexp-flag-in-query.ts +++ b/lib/rules/no-global-regexp-flag-in-query.ts @@ -21,11 +21,11 @@ export default createTestingLibraryRule({ docs: { description: 'Disallow the use of the global RegExp flag (/g) in queries', recommendedConfig: { - dom: false, - angular: false, - react: false, - vue: false, - marko: false, + dom: 'error', + angular: 'error', + react: 'error', + vue: 'error', + marko: 'error', }, }, messages: { diff --git a/lib/rules/no-manual-cleanup.ts b/lib/rules/no-manual-cleanup.ts index 4f8cb4c2..833fa147 100644 --- a/lib/rules/no-manual-cleanup.ts +++ b/lib/rules/no-manual-cleanup.ts @@ -28,8 +28,8 @@ export default createTestingLibraryRule({ recommendedConfig: { dom: false, angular: false, - react: false, - vue: false, + react: 'error', + vue: 'error', marko: false, }, }, diff --git a/lib/rules/no-node-access.ts b/lib/rules/no-node-access.ts index 1beb3f2c..78ffdc31 100644 --- a/lib/rules/no-node-access.ts +++ b/lib/rules/no-node-access.ts @@ -14,7 +14,7 @@ export default createTestingLibraryRule({ docs: { description: 'Disallow direct Node access', recommendedConfig: { - dom: false, + dom: 'error', angular: 'error', react: 'error', vue: 'error', diff --git a/lib/rules/no-render-in-setup.ts b/lib/rules/no-render-in-lifecycle.ts similarity index 98% rename from lib/rules/no-render-in-setup.ts rename to lib/rules/no-render-in-lifecycle.ts index f40c7d1e..4496522c 100644 --- a/lib/rules/no-render-in-setup.ts +++ b/lib/rules/no-render-in-lifecycle.ts @@ -9,7 +9,7 @@ import { } from '../node-utils'; import { TESTING_FRAMEWORK_SETUP_HOOKS } from '../utils'; -export const RULE_NAME = 'no-render-in-setup'; +export const RULE_NAME = 'no-render-in-lifecycle'; export type MessageIds = 'noRenderInSetup'; type Options = [ { diff --git a/lib/rules/no-wait-for-empty-callback.ts b/lib/rules/no-wait-for-empty-callback.ts deleted file mode 100644 index 4ef3f851..00000000 --- a/lib/rules/no-wait-for-empty-callback.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { ASTUtils, TSESTree } from '@typescript-eslint/utils'; - -import { createTestingLibraryRule } from '../create-testing-library-rule'; -import { - getPropertyIdentifierNode, - isCallExpression, - isEmptyFunction, -} from '../node-utils'; - -export const RULE_NAME = 'no-wait-for-empty-callback'; -export type MessageIds = 'noWaitForEmptyCallback'; -type Options = []; - -export default createTestingLibraryRule({ - name: 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: [], - - // trimmed down implementation of https://github.com/eslint/eslint/blob/master/lib/rules/no-empty-function.js - create(context, _, helpers) { - function isValidWaitFor(node: TSESTree.Node): boolean { - const parentCallExpression = node.parent as TSESTree.CallExpression; - const parentIdentifier = getPropertyIdentifierNode(parentCallExpression); - - if (!parentIdentifier) { - return false; - } - - return helpers.isAsyncUtil(parentIdentifier, [ - 'waitFor', - 'waitForElementToBeRemoved', - ]); - } - - function reportIfEmpty( - node: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression - ) { - if (!isValidWaitFor(node)) { - return; - } - - if ( - isEmptyFunction(node) && - isCallExpression(node.parent) && - ASTUtils.isIdentifier(node.parent.callee) - ) { - context.report({ - node, - loc: node.body.loc.start, - messageId: 'noWaitForEmptyCallback', - data: { - methodName: node.parent.callee.name, - }, - }); - } - } - - function reportNoop(node: TSESTree.Identifier) { - if (!isValidWaitFor(node)) { - return; - } - - context.report({ - node, - loc: node.loc.start, - messageId: 'noWaitForEmptyCallback', - data: { - methodName: - isCallExpression(node.parent) && - ASTUtils.isIdentifier(node.parent.callee) && - node.parent.callee.name, - }, - }); - } - - return { - 'CallExpression > ArrowFunctionExpression': reportIfEmpty, - 'CallExpression > FunctionExpression': reportIfEmpty, - 'CallExpression > Identifier[name="noop"]': reportNoop, - }; - }, -}); diff --git a/lib/rules/prefer-find-by.ts b/lib/rules/prefer-find-by.ts index 6c096cdb..f9d951e0 100644 --- a/lib/rules/prefer-find-by.ts +++ b/lib/rules/prefer-find-by.ts @@ -14,8 +14,6 @@ export const RULE_NAME = 'prefer-find-by'; export type MessageIds = 'preferFindBy'; type Options = []; -export const WAIT_METHODS = ['waitFor', 'waitForElement', 'wait'] as const; - export function getFindByQueryVariant( queryMethod: string ): 'findAllBy' | 'findBy' { @@ -63,7 +61,7 @@ export default createTestingLibraryRule({ }, messages: { preferFindBy: - 'Prefer `{{queryVariant}}{{queryMethod}}` query over using `{{waitForMethodName}}` + `{{prevQuery}}`', + 'Prefer `{{queryVariant}}{{queryMethod}}` query over using `waitFor` + `{{prevQuery}}`', }, fixable: 'code', schema: [], @@ -75,12 +73,11 @@ export default createTestingLibraryRule({ /** * Reports the invalid usage of wait* plus getBy/QueryBy methods and automatically fixes the scenario - * @param node - The CallExpresion node that contains the wait* method + * @param node - The CallExpression node that contains the waitFor method * @param replacementParams - Object with info for error message and autofix: * @param replacementParams.queryVariant - The variant method used to query: findBy/findAllBy. * @param replacementParams.prevQuery - The query originally used inside `waitFor` * @param replacementParams.queryMethod - Suffix string to build the query method (the query-part that comes after the "By"): LabelText, Placeholder, Text, Role, Title, etc. - * @param replacementParams.waitForMethodName - wait for method used: waitFor/wait/waitForElement * @param replacementParams.fix - Function that applies the fix to correct the code */ function reportInvalidUsage( @@ -89,12 +86,10 @@ export default createTestingLibraryRule({ queryVariant: 'findAllBy' | 'findBy'; queryMethod: string; prevQuery: string; - waitForMethodName: string; fix: TSESLint.ReportFixFunction; } ) { - const { queryMethod, queryVariant, prevQuery, waitForMethodName, fix } = - replacementParams; + const { queryMethod, queryVariant, prevQuery, fix } = replacementParams; context.report({ node, messageId: 'preferFindBy', @@ -102,7 +97,6 @@ export default createTestingLibraryRule({ queryVariant, queryMethod, prevQuery, - waitForMethodName, }, fix, }); @@ -336,7 +330,7 @@ export default createTestingLibraryRule({ 'AwaitExpression > CallExpression'(node: TSESTree.CallExpression) { if ( !ASTUtils.isIdentifier(node.callee) || - !helpers.isAsyncUtil(node.callee, WAIT_METHODS) + !helpers.isAsyncUtil(node.callee, ['waitFor']) ) { return; } @@ -350,8 +344,6 @@ export default createTestingLibraryRule({ return; } - const waitForMethodName = node.callee.name; - // ensure here it's one of the sync methods that we are calling if (isScreenSyncQuery(argument)) { const caller = getCaller(argument); @@ -386,7 +378,6 @@ export default createTestingLibraryRule({ queryMethod, queryVariant, prevQuery: fullQueryMethod, - waitForMethodName, fix(fixer) { const property = ( (argument.body as TSESTree.CallExpression) @@ -423,7 +414,6 @@ export default createTestingLibraryRule({ queryMethod, queryVariant, prevQuery: fullQueryMethod, - waitForMethodName, fix(fixer) { // we know from above callee is an Identifier if ( diff --git a/lib/rules/prefer-wait-for.ts b/lib/rules/prefer-wait-for.ts deleted file mode 100644 index 30cb93b7..00000000 --- a/lib/rules/prefer-wait-for.ts +++ /dev/null @@ -1,214 +0,0 @@ -import { TSESTree, ASTUtils } from '@typescript-eslint/utils'; - -import { createTestingLibraryRule } from '../create-testing-library-rule'; -import { - isImportSpecifier, - isMemberExpression, - findClosestCallExpressionNode, - isCallExpression, - isImportNamespaceSpecifier, - isObjectPattern, - isProperty, -} from '../node-utils'; - -export const RULE_NAME = 'prefer-wait-for'; -export type MessageIds = - | 'preferWaitForImport' - | 'preferWaitForMethod' - | 'preferWaitForRequire'; -type Options = []; - -const DEPRECATED_METHODS = ['wait', 'waitForElement', 'waitForDomChange']; - -export default createTestingLibraryRule({ - name: 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: TSESTree.ObjectPattern) => { - context.report({ - node, - messageId: 'preferWaitForRequire', - fix(fixer) { - const excludedImports = [...DEPRECATED_METHODS, 'waitFor']; - - const newAllRequired = node.properties - .filter( - (s) => - isProperty(s) && - ASTUtils.isIdentifier(s.key) && - !excludedImports.includes(s.key.name) - ) - .map( - (s) => ((s as TSESTree.Property).key as TSESTree.Identifier).name - ); - - newAllRequired.push('waitFor'); - - return fixer.replaceText(node, `{ ${newAllRequired.join(',')} }`); - }, - }); - }; - - const reportImport = (node: TSESTree.ImportDeclaration) => { - context.report({ - node, - messageId: 'preferWaitForImport', - fix(fixer) { - const excludedImports = [...DEPRECATED_METHODS, 'waitFor']; - - // get all import names excluding all testing library `wait*` utils... - const newImports = node.specifiers - .map( - (specifier) => - isImportSpecifier(specifier) && - !excludedImports.includes(specifier.imported.name) && - specifier.imported.name - ) - .filter(Boolean) as string[]; - - // ... and append `waitFor` - newImports.push('waitFor'); - - // build new node with new imports and previous source value - const newNode = `import { ${newImports.join(',')} } from '${ - node.source.value - }';`; - - return fixer.replaceText(node, newNode); - }, - }); - }; - - const reportWait = (node: TSESTree.Identifier | TSESTree.JSXIdentifier) => { - context.report({ - node, - messageId: 'preferWaitForMethod', - data: { - methodName: node.name, - }, - fix(fixer) { - const callExpressionNode = findClosestCallExpressionNode(node); - if (!callExpressionNode) { - return null; - } - const [arg] = callExpressionNode.arguments; - const fixers = []; - - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (arg) { - // if method been fixed already had a callback - // then we just replace the method name. - fixers.push(fixer.replaceText(node, 'waitFor')); - - if (node.name === 'waitForDomChange') { - // if method been fixed is `waitForDomChange` - // then the arg received was options object so we need to insert - // empty callback before. - fixers.push(fixer.insertTextBefore(arg, '() => {}, ')); - } - } else { - // if wait method been fixed didn't have any callback - // then we replace the method name and include an empty callback. - let methodReplacement = 'waitFor(() => {})'; - - // if wait method used like `foo.wait()` then we need to keep the - // member expression to get `foo.waitFor(() => {})` - if ( - isMemberExpression(node.parent) && - 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: TSESTree.MemberExpression) { - const isDeprecatedMethod = - ASTUtils.isIdentifier(node.property) && - DEPRECATED_METHODS.includes(node.property.name); - if (!isDeprecatedMethod) { - // the method does not match a deprecated method - return; - } - if (!helpers.isNodeComingFromTestingLibrary(node)) { - // the method does not match from the imported elements from TL (even from custom) - return; - } - addWaitFor = true; - reportWait(node.property as TSESTree.Identifier); // compiler is not picking up correctly, it should have inferred it is an identifier - }, - 'CallExpression > Identifier'(node: TSESTree.Identifier) { - if (!DEPRECATED_METHODS.includes(node.name)) { - return; - } - - if (!helpers.isNodeComingFromTestingLibrary(node)) { - return; - } - addWaitFor = true; - reportWait(node); - }, - 'Program:exit'() { - if (!addWaitFor) { - return; - } - // now that all usages of deprecated methods were replaced, remove the extra imports - const testingLibraryNode = - helpers.getCustomModuleImportNode() ?? - helpers.getTestingLibraryImportNode(); - if (isCallExpression(testingLibraryNode)) { - const parent = - testingLibraryNode.parent as TSESTree.VariableDeclarator; - if (!isObjectPattern(parent.id)) { - // if there is no destructuring, there is nothing to replace - return; - } - reportRequire(parent.id); - } else if (testingLibraryNode) { - if ( - testingLibraryNode.specifiers.length === 1 && - isImportNamespaceSpecifier(testingLibraryNode.specifiers[0]) - ) { - // if we import everything, there is nothing to replace - return; - } - reportImport(testingLibraryNode); - } - }, - }; - }, -}); diff --git a/lib/utils/index.ts b/lib/utils/index.ts index adaef168..7ed659f7 100644 --- a/lib/utils/index.ts +++ b/lib/utils/index.ts @@ -59,13 +59,7 @@ const ALL_QUERIES_COMBINATIONS = [ ...ASYNC_QUERIES_COMBINATIONS, ]; -const ASYNC_UTILS = [ - 'waitFor', - 'waitForElementToBeRemoved', - 'wait', - 'waitForElement', - 'waitForDomChange', -] as const; +const ASYNC_UTILS = ['waitFor', 'waitForElementToBeRemoved'] as const; const DEBUG_UTILS = [ 'debug', diff --git a/tests/index.test.ts b/tests/index.test.ts index b3b535b2..c69e0c6a 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -3,7 +3,7 @@ import { resolve } from 'path'; import plugin from '../lib'; -const numberOfRules = 28; +const numberOfRules = 26; const ruleNames = Object.keys(plugin.rules); // eslint-disable-next-line jest/expect-expect diff --git a/tests/lib/rules/await-async-events.test.ts b/tests/lib/rules/await-async-events.test.ts new file mode 100644 index 00000000..fe133c0c --- /dev/null +++ b/tests/lib/rules/await-async-events.test.ts @@ -0,0 +1,991 @@ +import rule, { + Options, + RULE_NAME, +} from '../../../lib/rules/await-async-events'; +import { createRuleTester } from '../test-utils'; + +const ruleTester = createRuleTester(); + +const FIRE_EVENT_ASYNC_FUNCTIONS = [ + 'click', + 'change', + 'focus', + 'blur', + 'keyDown', +] as const; +const USER_EVENT_ASYNC_FUNCTIONS = [ + 'click', + 'dblClick', + 'tripleClick', + 'hover', + 'unhover', + 'tab', + 'keyboard', + 'copy', + 'cut', + 'paste', + 'pointer', + 'clear', + 'deselectOptions', + 'selectOptions', + 'type', + 'upload', +] as const; +const FIRE_EVENT_ASYNC_FRAMEWORKS = [ + '@testing-library/vue', + '@marko/testing-library', +] as const; +const USER_EVENT_ASYNC_FRAMEWORKS = ['@testing-library/user-event'] as const; + +ruleTester.run(RULE_NAME, rule, { + valid: [ + ...FIRE_EVENT_ASYNC_FRAMEWORKS.flatMap((testingFramework) => [ + ...FIRE_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + code: ` + import { fireEvent } from '${testingFramework}' + test('event method not called is valid', () => { + fireEvent.${eventMethod} + }) + `, + options: [{ eventModule: 'fireEvent' }] as const, + })), + ...FIRE_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + code: ` + import { fireEvent } from '${testingFramework}' + test('await promise from event method is valid', async () => { + await fireEvent.${eventMethod}(getByLabelText('username')) + }) + `, + options: [{ eventModule: 'fireEvent' }] as const, + })), + ...FIRE_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + code: ` + import { fireEvent } from '${testingFramework}' + test('await several promises from event methods is valid', async () => { + await fireEvent.${eventMethod}(getByLabelText('username')) + await fireEvent.${eventMethod}(getByLabelText('username')) + }) + `, + options: [{ eventModule: 'fireEvent' }] as const, + })), + ...FIRE_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + code: ` + import { fireEvent } from '${testingFramework}' + test('await promise kept in a var from event method is valid', async () => { + const promise = fireEvent.${eventMethod}(getByLabelText('username')) + await promise + }) + `, + options: [{ eventModule: 'fireEvent' }] as const, + })), + ...FIRE_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + code: ` + import { fireEvent } from '${testingFramework}' + test('chain then method to promise from event method is valid', async (done) => { + fireEvent.${eventMethod}(getByLabelText('username')) + .then(() => { done() }) + }) + `, + options: [{ eventModule: 'fireEvent' }] as const, + })), + ...FIRE_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + code: ` + import { fireEvent } from '${testingFramework}' + test('chain then method to several promises from event methods is valid', async (done) => { + fireEvent.${eventMethod}(getByLabelText('username')).then(() => { + fireEvent.${eventMethod}(getByLabelText('username')).then(() => { done() }) + }) + }) + `, + options: [{ eventModule: 'fireEvent' }] as const, + })), + { + code: ` + import { fireEvent } from '${testingFramework}' + test('event methods wrapped with Promise.all are valid', async () => { + await Promise.all([ + fireEvent.${FIRE_EVENT_ASYNC_FUNCTIONS[0]}(getByText('Click me')), + fireEvent.${FIRE_EVENT_ASYNC_FUNCTIONS[1]}(getByText('Click me')), + ]) + }) + `, + options: [{ eventModule: 'fireEvent' }] as const, + }, + ...FIRE_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + code: ` + import { fireEvent } from '${testingFramework}' + test('return promise from event methods is valid', () => { + function triggerEvent() { + doSomething() + return fireEvent.${eventMethod}(getByLabelText('username')) + } + }) + `, + options: [{ eventModule: 'fireEvent' }] as const, + })), + ...FIRE_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + code: ` + import { fireEvent } from '${testingFramework}' + test('await promise returned from function wrapping event method is valid', () => { + function triggerEvent() { + doSomething() + return fireEvent.${eventMethod}(getByLabelText('username')) + } + + await triggerEvent() + }) + `, + options: [{ eventModule: 'fireEvent' }] as const, + })), + ...FIRE_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` + import { fireEvent } from 'somewhere-else' + test('unhandled promise from event not related to TL is valid', async () => { + fireEvent.${eventMethod}(getByLabelText('username')) + }) + `, + options: [{ eventModule: 'fireEvent' }] as const, + })), + ...FIRE_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` + import { fireEvent } from 'test-utils' + test('await promise from event method imported from custom module is valid', async () => { + await fireEvent.${eventMethod}(getByLabelText('username')) + }) + `, + options: [{ eventModule: 'fireEvent' }] as const, + })), + { + // edge case for coverage: + // valid use case without call expression + // so there is no innermost function scope found + code: ` + import { fireEvent } from 'test-utils' + test('edge case for innermost function without call expression', async () => { + function triggerEvent() { + doSomething() + return fireEvent.focus(getByLabelText('username')) + } + + const reassignedFunction = triggerEvent + }) + `, + options: [{ eventModule: 'fireEvent' }] as const, + }, + ]), + + ...USER_EVENT_ASYNC_FRAMEWORKS.flatMap((testingFramework) => [ + ...USER_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + code: ` + import userEvent from '${testingFramework}' + test('event method not called is valid', () => { + userEvent.${eventMethod} + }) + `, + options: [{ eventModule: 'userEvent' }] as const, + })), + ...USER_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + code: ` + import userEvent from '${testingFramework}' + test('await promise from event method is valid', async () => { + await userEvent.${eventMethod}(getByLabelText('username')) + }) + `, + options: [{ eventModule: 'userEvent' }] as const, + })), + ...USER_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + code: ` + import userEvent from '${testingFramework}' + test('await several promises from event methods is valid', async () => { + await userEvent.${eventMethod}(getByLabelText('username')) + await userEvent.${eventMethod}(getByLabelText('username')) + }) + `, + options: [{ eventModule: 'userEvent' }] as const, + })), + ...USER_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + code: ` + import userEvent from '${testingFramework}' + test('await promise kept in a var from event method is valid', async () => { + const promise = userEvent.${eventMethod}(getByLabelText('username')) + await promise + }) + `, + options: [{ eventModule: 'userEvent' }] as const, + })), + ...USER_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + code: ` + import userEvent from '${testingFramework}' + test('chain then method to promise from event method is valid', async (done) => { + userEvent.${eventMethod}(getByLabelText('username')) + .then(() => { done() }) + }) + `, + options: [{ eventModule: 'userEvent' }] as const, + })), + ...USER_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + code: ` + import userEvent from '${testingFramework}' + test('chain then method to several promises from event methods is valid', async (done) => { + userEvent.${eventMethod}(getByLabelText('username')).then(() => { + userEvent.${eventMethod}(getByLabelText('username')).then(() => { done() }) + }) + }) + `, + options: [{ eventModule: 'userEvent' }] as const, + })), + { + code: ` + import userEvent from '${testingFramework}' + test('event methods wrapped with Promise.all are valid', async () => { + await Promise.all([ + userEvent.${USER_EVENT_ASYNC_FUNCTIONS[0]}(getByText('Click me')), + userEvent.${USER_EVENT_ASYNC_FUNCTIONS[1]}(getByText('Click me')), + ]) + }) + `, + options: [{ eventModule: 'userEvent' }] as const, + }, + ...USER_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + code: ` + import userEvent from '${testingFramework}' + test('return promise from event methods is valid', () => { + function triggerEvent() { + doSomething() + return userEvent.${eventMethod}(getByLabelText('username')) + } + }) + `, + options: [{ eventModule: 'userEvent' }] as const, + })), + ...USER_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + code: ` + import userEvent from '${testingFramework}' + test('await promise returned from function wrapping event method is valid', () => { + function triggerEvent() { + doSomething() + return userEvent.${eventMethod}(getByLabelText('username')) + } + + await triggerEvent() + }) + `, + options: [{ eventModule: 'userEvent' }] as const, + })), + ...USER_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` + import userEvent from 'somewhere-else' + test('unhandled promise from event not related to TL is valid', async () => { + userEvent.${eventMethod}(getByLabelText('username')) + }) + `, + options: [{ eventModule: 'userEvent' }] as const, + })), + ...USER_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` + import userEvent from 'test-utils' + test('await promise from event method imported from custom module is valid', async () => { + await userEvent.${eventMethod}(getByLabelText('username')) + }) + `, + options: [{ eventModule: 'userEvent' }] as const, + })), + ...USER_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + code: ` + import userEvent from '${testingFramework}' + test('await promise from userEvent relying on default options', async () => { + await userEvent.${eventMethod}(getByLabelText('username')) + }) + `, + })), + { + // edge case for coverage: + // valid use case without call expression + // so there is no innermost function scope found + code: ` + import userEvent from 'test-utils' + test('edge case for innermost function without call expression', async () => { + function triggerEvent() { + doSomething() + return userEvent.focus(getByLabelText('username')) + } + + const reassignedFunction = triggerEvent + }) + `, + options: [{ eventModule: 'userEvent' }] as const, + }, + { + code: ` + import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}' + import { fireEvent } from '${FIRE_EVENT_ASYNC_FRAMEWORKS[0]}' + test('await promises from multiple event modules', async () => { + await fireEvent.click(getByLabelText('username')) + await userEvent.click(getByLabelText('username')) + }) + `, + options: [{ eventModule: ['userEvent', 'fireEvent'] }] as Options, + }, + ]), + ], + + invalid: [ + ...FIRE_EVENT_ASYNC_FRAMEWORKS.flatMap((testingFramework) => [ + ...FIRE_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + code: ` + import { fireEvent } from '${testingFramework}' + test('unhandled promise from event method is invalid', () => { + fireEvent.${eventMethod}(getByLabelText('username')) + }) + `, + errors: [ + { + line: 4, + column: 9, + endColumn: 19 + eventMethod.length, + messageId: 'awaitAsyncEvent', + data: { name: eventMethod }, + }, + ], + options: [{ eventModule: 'fireEvent' }], + output: ` + import { fireEvent } from '${testingFramework}' + test('unhandled promise from event method is invalid', async () => { + await fireEvent.${eventMethod}(getByLabelText('username')) + }) + `, + } as const) + ), + ...FIRE_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + code: ` + import { fireEvent } from '${testingFramework}' + + fireEvent.${eventMethod}(getByLabelText('username')) + `, + errors: [ + { + line: 4, + column: 7, + endColumn: 17 + eventMethod.length, + messageId: 'awaitAsyncEvent', + data: { name: eventMethod }, + }, + ], + options: [{ eventModule: 'fireEvent' }], + output: ` + import { fireEvent } from '${testingFramework}' + + fireEvent.${eventMethod}(getByLabelText('username')) + `, + } as const) + ), + ...FIRE_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + code: ` + import { fireEvent } from '${testingFramework}' + + function run() { + fireEvent.${eventMethod}(getByLabelText('username')) + } + + test('should handle external function', run) + `, + errors: [ + { + line: 5, + column: 9, + endColumn: 19 + eventMethod.length, + messageId: 'awaitAsyncEvent', + data: { name: eventMethod }, + }, + ], + options: [{ eventModule: 'fireEvent' }], + output: ` + import { fireEvent } from '${testingFramework}' + + async function run() { + await fireEvent.${eventMethod}(getByLabelText('username')) + } + + test('should handle external function', run) + `, + } as const) + ), + ...FIRE_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + code: ` + import { fireEvent as testingLibraryFireEvent } from '${testingFramework}' + test('unhandled promise from aliased event method is invalid', async () => { + testingLibraryFireEvent.${eventMethod}(getByLabelText('username')) + }) + `, + errors: [ + { + line: 4, + column: 9, + endColumn: 33 + eventMethod.length, + messageId: 'awaitAsyncEvent', + data: { name: eventMethod }, + }, + ], + options: [{ eventModule: 'fireEvent' }], + output: ` + import { fireEvent as testingLibraryFireEvent } from '${testingFramework}' + test('unhandled promise from aliased event method is invalid', async () => { + await testingLibraryFireEvent.${eventMethod}(getByLabelText('username')) + }) + `, + } as const) + ), + ...FIRE_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + code: ` + import * as testingLibrary from '${testingFramework}' + test('unhandled promise from wildcard imported event method is invalid', async () => { + testingLibrary.fireEvent.${eventMethod}(getByLabelText('username')) + }) + `, + errors: [ + { + line: 4, + column: 9, + endColumn: 34 + eventMethod.length, + messageId: 'awaitAsyncEvent', + data: { name: eventMethod }, + }, + ], + options: [{ eventModule: 'fireEvent' }], + output: ` + import * as testingLibrary from '${testingFramework}' + test('unhandled promise from wildcard imported event method is invalid', async () => { + await testingLibrary.fireEvent.${eventMethod}(getByLabelText('username')) + }) + `, + } as const) + ), + ...FIRE_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + code: ` + import { fireEvent } from '${testingFramework}' + test('several unhandled promises from event methods is invalid', async function() { + fireEvent.${eventMethod}(getByLabelText('username')) + fireEvent.${eventMethod}(getByLabelText('username')) + }) + `, + errors: [ + { + line: 4, + column: 9, + messageId: 'awaitAsyncEvent', + data: { name: eventMethod }, + }, + { + line: 5, + column: 9, + messageId: 'awaitAsyncEvent', + data: { name: eventMethod }, + }, + ], + options: [{ eventModule: 'fireEvent' }], + output: ` + import { fireEvent } from '${testingFramework}' + test('several unhandled promises from event methods is invalid', async function() { + await fireEvent.${eventMethod}(getByLabelText('username')) + await fireEvent.${eventMethod}(getByLabelText('username')) + }) + `, + } as const) + ), + ...FIRE_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` + import { fireEvent } from '${testingFramework}' + test('unhandled promise from event method with aggressive reporting opted-out is invalid', function() { + fireEvent.${eventMethod}(getByLabelText('username')) + }) + `, + errors: [ + { + line: 4, + column: 9, + messageId: 'awaitAsyncEvent', + data: { name: eventMethod }, + }, + ], + options: [{ eventModule: 'fireEvent' }], + output: ` + import { fireEvent } from '${testingFramework}' + test('unhandled promise from event method with aggressive reporting opted-out is invalid', async function() { + await fireEvent.${eventMethod}(getByLabelText('username')) + }) + `, + } as const) + ), + ...FIRE_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` + import { fireEvent } from 'test-utils' + test( + 'unhandled promise from event method imported from custom module with aggressive reporting opted-out is invalid', + () => { + fireEvent.${eventMethod}(getByLabelText('username')) + }) + `, + errors: [ + { + line: 6, + column: 9, + messageId: 'awaitAsyncEvent', + data: { name: eventMethod }, + }, + ], + options: [{ eventModule: 'fireEvent' }], + output: ` + import { fireEvent } from 'test-utils' + test( + 'unhandled promise from event method imported from custom module with aggressive reporting opted-out is invalid', + async () => { + await fireEvent.${eventMethod}(getByLabelText('username')) + }) + `, + } as const) + ), + ...FIRE_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` + import { fireEvent } from '${testingFramework}' + test( + 'unhandled promise from event method imported from default module with aggressive reporting opted-out is invalid', + () => { + fireEvent.${eventMethod}(getByLabelText('username')) + }) + `, + errors: [ + { + line: 6, + column: 9, + messageId: 'awaitAsyncEvent', + data: { name: eventMethod }, + }, + ], + options: [{ eventModule: 'fireEvent' }], + output: ` + import { fireEvent } from '${testingFramework}' + test( + 'unhandled promise from event method imported from default module with aggressive reporting opted-out is invalid', + async () => { + await fireEvent.${eventMethod}(getByLabelText('username')) + }) + `, + } as const) + ), + + ...FIRE_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + code: ` + import { fireEvent } from '${testingFramework}' + test( + 'unhandled promise from event method kept in a var is invalid', + () => { + const promise = fireEvent.${eventMethod}(getByLabelText('username')) + }) + `, + errors: [ + { + line: 6, + column: 25, + messageId: 'awaitAsyncEvent', + data: { name: eventMethod }, + }, + ], + options: [{ eventModule: 'fireEvent' }], + output: ` + import { fireEvent } from '${testingFramework}' + test( + 'unhandled promise from event method kept in a var is invalid', + async () => { + const promise = await fireEvent.${eventMethod}(getByLabelText('username')) + }) + `, + } as const) + ), + ...FIRE_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + code: ` + import { fireEvent } from '${testingFramework}' + test('unhandled promise returned from function wrapping event method is invalid', () => { + function triggerEvent() { + doSomething() + return fireEvent.${eventMethod}(getByLabelText('username')) + } + + triggerEvent() + }) + `, + errors: [ + { + line: 9, + column: 9, + messageId: 'awaitAsyncEventWrapper', + data: { name: 'triggerEvent' }, + }, + ], + options: [{ eventModule: 'fireEvent' }], + output: ` + import { fireEvent } from '${testingFramework}' + test('unhandled promise returned from function wrapping event method is invalid', async () => { + function triggerEvent() { + doSomething() + return fireEvent.${eventMethod}(getByLabelText('username')) + } + + await triggerEvent() + }) + `, + } as const) + ), + ...FIRE_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + code: ` + import { fireEvent } from '${testingFramework}' + + function triggerEvent() { + doSomething() + return fireEvent.${eventMethod}(getByLabelText('username')) + } + + triggerEvent() + `, + errors: [ + { + line: 9, + column: 7, + messageId: 'awaitAsyncEventWrapper', + data: { name: 'triggerEvent' }, + }, + ], + options: [{ eventModule: 'fireEvent' }], + output: ` + import { fireEvent } from '${testingFramework}' + + function triggerEvent() { + doSomething() + return fireEvent.${eventMethod}(getByLabelText('username')) + } + + triggerEvent() + `, + } as const) + ), + ]), + ...USER_EVENT_ASYNC_FRAMEWORKS.flatMap((testingFramework) => [ + ...USER_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + code: ` + import userEvent from '${testingFramework}' + test('unhandled promise from event method is invalid', () => { + userEvent.${eventMethod}(getByLabelText('username')) + }) + `, + errors: [ + { + line: 4, + column: 9, + endColumn: 19 + eventMethod.length, + messageId: 'awaitAsyncEvent', + data: { name: eventMethod }, + }, + ], + options: [{ eventModule: 'userEvent' }], + output: ` + import userEvent from '${testingFramework}' + test('unhandled promise from event method is invalid', async () => { + await userEvent.${eventMethod}(getByLabelText('username')) + }) + `, + } as const) + ), + ...USER_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + code: ` + import userEvent from '${testingFramework}' + + userEvent.${eventMethod}(getByLabelText('username')) + `, + errors: [ + { + line: 4, + column: 7, + endColumn: 17 + eventMethod.length, + messageId: 'awaitAsyncEvent', + data: { name: eventMethod }, + }, + ], + options: [{ eventModule: 'userEvent' }], + output: ` + import userEvent from '${testingFramework}' + + userEvent.${eventMethod}(getByLabelText('username')) + `, + } as const) + ), + ...USER_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + code: ` + import testingLibraryUserEvent from '${testingFramework}' + test('unhandled promise imported from alternate name event method is invalid', () => { + testingLibraryUserEvent.${eventMethod}(getByLabelText('username')) + }) + `, + errors: [ + { + line: 4, + column: 9, + endColumn: 33 + eventMethod.length, + messageId: 'awaitAsyncEvent', + data: { name: eventMethod }, + }, + ], + options: [{ eventModule: 'userEvent' }], + output: ` + import testingLibraryUserEvent from '${testingFramework}' + test('unhandled promise imported from alternate name event method is invalid', async () => { + await testingLibraryUserEvent.${eventMethod}(getByLabelText('username')) + }) + `, + } as const) + ), + ...USER_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + code: ` + import userEvent from '${testingFramework}' + test('several unhandled promises from event methods is invalid', () => { + userEvent.${eventMethod}(getByLabelText('username')) + userEvent.${eventMethod}(getByLabelText('username')) + }) + `, + errors: [ + { + line: 4, + column: 9, + messageId: 'awaitAsyncEvent', + data: { name: eventMethod }, + }, + { + line: 5, + column: 9, + messageId: 'awaitAsyncEvent', + data: { name: eventMethod }, + }, + ], + options: [{ eventModule: 'userEvent' }], + output: ` + import userEvent from '${testingFramework}' + test('several unhandled promises from event methods is invalid', async () => { + await userEvent.${eventMethod}(getByLabelText('username')) + await userEvent.${eventMethod}(getByLabelText('username')) + }) + `, + } as const) + ), + ...USER_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + code: ` + import userEvent from '${testingFramework}' + test( + 'unhandled promise from event method kept in a var is invalid', + () => { + const promise = userEvent.${eventMethod}(getByLabelText('username')) + }) + `, + errors: [ + { + line: 6, + column: 25, + messageId: 'awaitAsyncEvent', + data: { name: eventMethod }, + }, + ], + options: [{ eventModule: 'userEvent' }], + output: ` + import userEvent from '${testingFramework}' + test( + 'unhandled promise from event method kept in a var is invalid', + async () => { + const promise = await userEvent.${eventMethod}(getByLabelText('username')) + }) + `, + } as const) + ), + ...USER_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + code: ` + import userEvent from '${testingFramework}' + test('unhandled promise returned from function wrapping event method is invalid', function() { + function triggerEvent() { + doSomething() + return userEvent.${eventMethod}(getByLabelText('username')) + } + + triggerEvent() + }) + `, + errors: [ + { + line: 9, + column: 9, + messageId: 'awaitAsyncEventWrapper', + data: { name: 'triggerEvent' }, + }, + ], + options: [{ eventModule: 'userEvent' }], + output: ` + import userEvent from '${testingFramework}' + test('unhandled promise returned from function wrapping event method is invalid', async function() { + function triggerEvent() { + doSomething() + return userEvent.${eventMethod}(getByLabelText('username')) + } + + await triggerEvent() + }) + `, + } as const) + ), + ...USER_EVENT_ASYNC_FUNCTIONS.map( + (eventMethod) => + ({ + code: ` + import userEvent from '${testingFramework}' + + function triggerEvent() { + doSomething() + return userEvent.${eventMethod}(getByLabelText('username')) + } + + triggerEvent() + `, + errors: [ + { + line: 9, + column: 7, + messageId: 'awaitAsyncEventWrapper', + data: { name: 'triggerEvent' }, + }, + ], + options: [{ eventModule: 'userEvent' }], + output: ` + import userEvent from '${testingFramework}' + + function triggerEvent() { + doSomething() + return userEvent.${eventMethod}(getByLabelText('username')) + } + + triggerEvent() + `, + } as const) + ), + ]), + { + code: ` + import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}' + import { fireEvent } from '${FIRE_EVENT_ASYNC_FRAMEWORKS[0]}' + test('unhandled promises from multiple event modules', () => { + fireEvent.click(getByLabelText('username')) + userEvent.click(getByLabelText('username')) + }) + `, + errors: [ + { + line: 5, + column: 9, + messageId: 'awaitAsyncEvent', + data: { name: 'click' }, + }, + { + line: 6, + column: 9, + messageId: 'awaitAsyncEvent', + data: { name: 'click' }, + }, + ], + options: [{ eventModule: ['userEvent', 'fireEvent'] }] as Options, + output: ` + import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}' + import { fireEvent } from '${FIRE_EVENT_ASYNC_FRAMEWORKS[0]}' + test('unhandled promises from multiple event modules', async () => { + await fireEvent.click(getByLabelText('username')) + await userEvent.click(getByLabelText('username')) + }) + `, + }, + { + code: ` + import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}' + import { fireEvent } from '${FIRE_EVENT_ASYNC_FRAMEWORKS[0]}' + test('unhandled promise from userEvent relying on default options', async function() { + fireEvent.click(getByLabelText('username')) + userEvent.click(getByLabelText('username')) + }) + `, + errors: [ + { + line: 6, + column: 9, + messageId: 'awaitAsyncEvent', + data: { name: 'click' }, + }, + ], + output: ` + import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}' + import { fireEvent } from '${FIRE_EVENT_ASYNC_FRAMEWORKS[0]}' + test('unhandled promise from userEvent relying on default options', async function() { + fireEvent.click(getByLabelText('username')) + await userEvent.click(getByLabelText('username')) + }) + `, + }, + ], +}); diff --git a/tests/lib/rules/await-async-query.test.ts b/tests/lib/rules/await-async-queries.test.ts similarity index 99% rename from tests/lib/rules/await-async-query.test.ts rename to tests/lib/rules/await-async-queries.test.ts index 4a588f38..799e5e70 100644 --- a/tests/lib/rules/await-async-query.test.ts +++ b/tests/lib/rules/await-async-queries.test.ts @@ -1,6 +1,6 @@ import { TSESLint } from '@typescript-eslint/utils'; -import rule, { RULE_NAME } from '../../../lib/rules/await-async-query'; +import rule, { RULE_NAME } from '../../../lib/rules/await-async-queries'; import { ASYNC_QUERIES_COMBINATIONS, ASYNC_QUERIES_VARIANTS, diff --git a/tests/lib/rules/await-fire-event.test.ts b/tests/lib/rules/await-fire-event.test.ts deleted file mode 100644 index fe3aed93..00000000 --- a/tests/lib/rules/await-fire-event.test.ts +++ /dev/null @@ -1,385 +0,0 @@ -import rule, { RULE_NAME } from '../../../lib/rules/await-fire-event'; -import { createRuleTester } from '../test-utils'; - -const ruleTester = createRuleTester(); - -const COMMON_FIRE_EVENT_METHODS: string[] = [ - 'click', - 'change', - 'focus', - 'blur', - 'keyDown', -]; -const SUPPORTED_TESTING_FRAMEWORKS = [ - '@testing-library/vue', - '@marko/testing-library', -]; - -ruleTester.run(RULE_NAME, rule, { - valid: SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => [ - ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ - code: ` - import { fireEvent } from '${testingFramework}' - test('fire event method not called is valid', () => { - fireEvent.${fireEventMethod} - }) - `, - })), - ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ - code: ` - import { fireEvent } from '${testingFramework}' - test('await promise from fire event method is valid', async () => { - await fireEvent.${fireEventMethod}(getByLabelText('username')) - }) - `, - })), - ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ - code: ` - import { fireEvent } from '${testingFramework}' - test('promise .resolves from fire event method is valid', async () => { - expect(fireEvent.${fireEventMethod}(getByLabelText('username'))).resolves.toBe("bar") - }) - `, - })), - ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ - code: ` - import { fireEvent } from '${testingFramework}' - test('wrapped promise .resolves from fire event method is valid', async () => { - expect(wrapper(fireEvent.${fireEventMethod}(getByLabelText('username')))).resolves.toBe("bar") - }) - `, - })), - ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ - code: ` - import { fireEvent } from '${testingFramework}' - test('promise .toResolve() from fire event method is valid', async () => { - expect(fireEvent.${fireEventMethod}(getByLabelText('username'))).toResolve() - }) - `, - })), - ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ - code: ` - import { fireEvent } from '${testingFramework}' - test('promise .toResolve() from fire event method is valid', async () => { - expect(wrapper(fireEvent.${fireEventMethod}(getByLabelText('username')))).toResolve() - }) - `, - })), - ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ - code: ` - import { fireEvent } from '${testingFramework}' - test('await several promises from fire event methods is valid', async () => { - await fireEvent.${fireEventMethod}(getByLabelText('username')) - await fireEvent.${fireEventMethod}(getByLabelText('username')) - }) - `, - })), - ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ - code: ` - import { fireEvent } from '${testingFramework}' - test('await promise kept in a var from fire event method is valid', async () => { - const promise = fireEvent.${fireEventMethod}(getByLabelText('username')) - await promise - }) - `, - })), - ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ - code: ` - import { fireEvent } from '${testingFramework}' - test('chain then method to promise from fire event method is valid', async (done) => { - fireEvent.${fireEventMethod}(getByLabelText('username')) - .then(() => { done() }) - }) - `, - })), - ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ - code: ` - import { fireEvent } from '${testingFramework}' - test('chain then method to several promises from fire event methods is valid', async (done) => { - fireEvent.${fireEventMethod}(getByLabelText('username')).then(() => { - fireEvent.${fireEventMethod}(getByLabelText('username')).then(() => { done() }) - }) - }) - `, - })), - { - code: ` - import { fireEvent } from '${testingFramework}' - test('fireEvent methods wrapped with Promise.all are valid', async () => { - await Promise.all([ - fireEvent.blur(getByText('Click me')), - fireEvent.click(getByText('Click me')), - ]) - }) - `, - }, - ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ - code: ` - import { fireEvent } from '${testingFramework}' - test('return promise from fire event methods is valid', () => { - function triggerEvent() { - doSomething() - return fireEvent.${fireEventMethod}(getByLabelText('username')) - } - }) - `, - })), - ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ - code: ` - import { fireEvent } from '${testingFramework}' - test('await promise returned from function wrapping fire event method is valid', () => { - function triggerEvent() { - doSomething() - return fireEvent.${fireEventMethod}(getByLabelText('username')) - } - - await triggerEvent() - }) - `, - })), - ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` - import { fireEvent } from 'somewhere-else' - test('unhandled promise from fire event not related to TL is valid', async () => { - fireEvent.${fireEventMethod}(getByLabelText('username')) - }) - `, - })), - ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` - import { fireEvent } from 'test-utils' - test('await promise from fire event method imported from custom module is valid', async () => { - await fireEvent.${fireEventMethod}(getByLabelText('username')) - }) - `, - })), - - { - // edge case for coverage: - // valid use case without call expression - // so there is no innermost function scope found - code: ` - import { fireEvent } from 'test-utils' - test('edge case for innermost function without call expression', async () => { - function triggerEvent() { - doSomething() - return fireEvent.focus(getByLabelText('username')) - } - - const reassignedFunction = triggerEvent - }) - `, - }, - ]), - - invalid: SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => [ - ...COMMON_FIRE_EVENT_METHODS.map( - (fireEventMethod) => - ({ - code: ` - import { fireEvent } from '${testingFramework}' - test('unhandled promise from fire event method is invalid', async () => { - fireEvent.${fireEventMethod}(getByLabelText('username')) - }) - `, - errors: [ - { - line: 4, - column: 9, - endColumn: 19 + fireEventMethod.length, - messageId: 'awaitFireEvent', - data: { name: fireEventMethod }, - }, - ], - } as const) - ), - ...COMMON_FIRE_EVENT_METHODS.map( - (fireEventMethod) => - ({ - code: ` - import { fireEvent as testingLibraryFireEvent } from '${testingFramework}' - test('unhandled promise from aliased fire event method is invalid', async () => { - testingLibraryFireEvent.${fireEventMethod}(getByLabelText('username')) - }) - `, - errors: [ - { - line: 4, - column: 9, - endColumn: 33 + fireEventMethod.length, - messageId: 'awaitFireEvent', - data: { name: fireEventMethod }, - }, - ], - } as const) - ), - ...COMMON_FIRE_EVENT_METHODS.map( - (fireEventMethod) => - ({ - code: ` - import * as testingLibrary from '${testingFramework}' - test('unhandled promise from wildcard imported fire event method is invalid', async () => { - testingLibrary.fireEvent.${fireEventMethod}(getByLabelText('username')) - }) - `, - errors: [ - { - line: 4, - column: 9, - endColumn: 34 + fireEventMethod.length, - messageId: 'awaitFireEvent', - data: { name: fireEventMethod }, - }, - ], - } as const) - ), - ...COMMON_FIRE_EVENT_METHODS.map( - (fireEventMethod) => - ({ - code: ` - import { fireEvent } from '${testingFramework}' - test('several unhandled promises from fire event methods is invalid', async () => { - fireEvent.${fireEventMethod}(getByLabelText('username')) - fireEvent.${fireEventMethod}(getByLabelText('username')) - }) - `, - errors: [ - { - line: 4, - column: 9, - messageId: 'awaitFireEvent', - data: { name: fireEventMethod }, - }, - { - line: 5, - column: 9, - messageId: 'awaitFireEvent', - data: { name: fireEventMethod }, - }, - ], - } as const) - ), - ...COMMON_FIRE_EVENT_METHODS.map( - (fireEventMethod) => - ({ - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` - import { fireEvent } from '${testingFramework}' - test('unhandled promise from fire event method with aggressive reporting opted-out is invalid', async () => { - fireEvent.${fireEventMethod}(getByLabelText('username')) - }) - `, - errors: [ - { - line: 4, - column: 9, - messageId: 'awaitFireEvent', - data: { name: fireEventMethod }, - }, - ], - } as const) - ), - ...COMMON_FIRE_EVENT_METHODS.map( - (fireEventMethod) => - ({ - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` - import { fireEvent } from 'test-utils' - test( - 'unhandled promise from fire event method imported from custom module with aggressive reporting opted-out is invalid', - () => { - fireEvent.${fireEventMethod}(getByLabelText('username')) - }) - `, - errors: [ - { - line: 6, - column: 9, - messageId: 'awaitFireEvent', - data: { name: fireEventMethod }, - }, - ], - } as const) - ), - ...COMMON_FIRE_EVENT_METHODS.map( - (fireEventMethod) => - ({ - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` - import { fireEvent } from '${testingFramework}' - test( - 'unhandled promise from fire event method imported from default module with aggressive reporting opted-out is invalid', - () => { - fireEvent.${fireEventMethod}(getByLabelText('username')) - }) - `, - errors: [ - { - line: 6, - column: 9, - messageId: 'awaitFireEvent', - data: { name: fireEventMethod }, - }, - ], - } as const) - ), - - ...COMMON_FIRE_EVENT_METHODS.map( - (fireEventMethod) => - ({ - code: ` - import { fireEvent } from '${testingFramework}' - test( - 'unhandled promise from fire event method kept in a var is invalid', - () => { - const promise = fireEvent.${fireEventMethod}(getByLabelText('username')) - }) - `, - errors: [ - { - line: 6, - column: 25, - messageId: 'awaitFireEvent', - data: { name: fireEventMethod }, - }, - ], - } as const) - ), - ...COMMON_FIRE_EVENT_METHODS.map( - (fireEventMethod) => - ({ - code: ` - import { fireEvent } from '${testingFramework}' - test('unhandled promise returned from function wrapping fire event method is invalid', () => { - function triggerEvent() { - doSomething() - return fireEvent.${fireEventMethod}(getByLabelText('username')) - } - - triggerEvent() - }) - `, - errors: [ - { - line: 9, - column: 9, - messageId: 'fireEventWrapper', - data: { name: 'triggerEvent' }, - }, - ], - } as const) - ), - ]), -}); diff --git a/tests/lib/rules/no-await-sync-query.test.ts b/tests/lib/rules/no-await-sync-queries.test.ts similarity index 99% rename from tests/lib/rules/no-await-sync-query.test.ts rename to tests/lib/rules/no-await-sync-queries.test.ts index 9538a979..3b088ad0 100644 --- a/tests/lib/rules/no-await-sync-query.test.ts +++ b/tests/lib/rules/no-await-sync-queries.test.ts @@ -1,4 +1,4 @@ -import rule, { RULE_NAME } from '../../../lib/rules/no-await-sync-query'; +import rule, { RULE_NAME } from '../../../lib/rules/no-await-sync-queries'; import { SYNC_QUERIES_COMBINATIONS, ASYNC_QUERIES_COMBINATIONS, diff --git a/tests/lib/rules/no-debugging-utils.test.ts b/tests/lib/rules/no-debugging-utils.test.ts index 3879d9e2..b31861a7 100644 --- a/tests/lib/rules/no-debugging-utils.test.ts +++ b/tests/lib/rules/no-debugging-utils.test.ts @@ -448,7 +448,6 @@ ruleTester.run(RULE_NAME, rule, { import { screen } from '@testing-library/dom' screen.logTestingPlaygroundURL() `, - options: [{ utilsToCheckFor: { logTestingPlaygroundURL: true } }], errors: [ { line: 3, @@ -462,7 +461,6 @@ ruleTester.run(RULE_NAME, rule, { import { logRoles } from '@testing-library/dom' logRoles(document.createElement('nav')) `, - options: [{ utilsToCheckFor: { logRoles: true } }], errors: [ { line: 3, @@ -476,7 +474,6 @@ ruleTester.run(RULE_NAME, rule, { import { screen } from '@testing-library/dom' screen.logTestingPlaygroundURL() `, - options: [{ utilsToCheckFor: { logRoles: true } }], errors: [ { line: 3, @@ -490,7 +487,6 @@ ruleTester.run(RULE_NAME, rule, { import { screen } from '@testing-library/dom' screen.logTestingPlaygroundURL() `, - options: [{ utilsToCheckFor: { debug: false } }], errors: [ { line: 3, diff --git a/tests/lib/rules/no-render-in-setup.test.ts b/tests/lib/rules/no-render-in-lifecycle.test.ts similarity index 99% rename from tests/lib/rules/no-render-in-setup.test.ts rename to tests/lib/rules/no-render-in-lifecycle.test.ts index 0e66802e..9665afcc 100644 --- a/tests/lib/rules/no-render-in-setup.test.ts +++ b/tests/lib/rules/no-render-in-lifecycle.test.ts @@ -1,4 +1,4 @@ -import rule, { RULE_NAME } from '../../../lib/rules/no-render-in-setup'; +import rule, { RULE_NAME } from '../../../lib/rules/no-render-in-lifecycle'; import { TESTING_FRAMEWORK_SETUP_HOOKS } from '../../../lib/utils'; import { createRuleTester } from '../test-utils'; diff --git a/tests/lib/rules/no-wait-for-empty-callback.test.ts b/tests/lib/rules/no-wait-for-empty-callback.test.ts deleted file mode 100644 index ed65bad6..00000000 --- a/tests/lib/rules/no-wait-for-empty-callback.test.ts +++ /dev/null @@ -1,242 +0,0 @@ -import rule, { RULE_NAME } from '../../../lib/rules/no-wait-for-empty-callback'; -import { createRuleTester } from '../test-utils'; - -const ruleTester = createRuleTester(); - -const ALL_WAIT_METHODS = ['waitFor', 'waitForElementToBeRemoved']; -const SUPPORTED_TESTING_FRAMEWORKS = [ - '@testing-library/dom', - '@testing-library/angular', - '@testing-library/react', - '@testing-library/vue', - '@marko/testing-library', -]; - -ruleTester.run(RULE_NAME, rule, { - valid: [ - ...ALL_WAIT_METHODS.map((m) => ({ - code: `${m}(() => { - screen.getByText(/submit/i) - })`, - })), - ...ALL_WAIT_METHODS.map((m) => ({ - code: `${m}(function() { - screen.getByText(/submit/i) - })`, - })), - { - code: `waitForElementToBeRemoved(someNode)`, - }, - { - code: `waitForElementToBeRemoved(() => someNode)`, - }, - { - code: `waitSomethingElse(() => {})`, - }, - { - code: `wait(() => {})`, - }, - { - code: `wait(noop)`, - }, - { - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` - import { waitFor } from 'somewhere-else' - waitFor(() => {}) - `, - }, - ...SUPPORTED_TESTING_FRAMEWORKS.map((testingFramework) => ({ - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` - import { waitFor as renamedWaitFor } from '${testingFramework}' - import { waitFor } from 'somewhere-else' - waitFor(() => {}) - `, - })), - ], - - invalid: [ - ...ALL_WAIT_METHODS.map( - (m) => - ({ - code: `${m}(() => {})`, - errors: [ - { - line: 1, - column: 8 + m.length, - messageId: 'noWaitForEmptyCallback', - data: { - methodName: m, - }, - }, - ], - } as const) - ), - ...ALL_WAIT_METHODS.map( - (m) => - ({ - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` - import { ${m} } from 'test-utils'; - ${m}(() => {}); - `, - errors: [ - { - line: 3, - column: 16 + m.length, - messageId: 'noWaitForEmptyCallback', - data: { - methodName: m, - }, - }, - ], - } as const) - ), - ...SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => - ALL_WAIT_METHODS.map( - (m) => - ({ - code: ` - import { ${m} } from '${testingFramework}'; - ${m}(() => {}); - `, - errors: [ - { - line: 3, - column: 16 + m.length, - messageId: 'noWaitForEmptyCallback', - data: { - methodName: m, - }, - }, - ], - } as const) - ) - ), - ...ALL_WAIT_METHODS.map( - (m) => - ({ - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` - import { ${m} as renamedAsyncUtil } from 'test-utils'; - renamedAsyncUtil(() => {}); - `, - errors: [ - { - line: 3, - column: 32, - messageId: 'noWaitForEmptyCallback', - data: { - methodName: 'renamedAsyncUtil', - }, - }, - ], - } as const) - ), - ...ALL_WAIT_METHODS.map( - (m) => - ({ - code: `${m}((a, b) => {})`, - errors: [ - { - line: 1, - column: 12 + m.length, - messageId: 'noWaitForEmptyCallback', - data: { - methodName: m, - }, - }, - ], - } as const) - ), - ...ALL_WAIT_METHODS.map( - (m) => - ({ - code: `${m}(() => { /* I'm empty anyway */ })`, - errors: [ - { - line: 1, - column: 8 + m.length, - messageId: 'noWaitForEmptyCallback', - data: { - methodName: m, - }, - }, - ], - } as const) - ), - - ...ALL_WAIT_METHODS.map( - (m) => - ({ - code: `${m}(function() { - - })`, - errors: [ - { - line: 1, - column: 13 + m.length, - messageId: 'noWaitForEmptyCallback', - data: { - methodName: m, - }, - }, - ], - } as const) - ), - ...ALL_WAIT_METHODS.map( - (m) => - ({ - code: `${m}(function(a) { - - })`, - errors: [ - { - line: 1, - column: 14 + m.length, - messageId: 'noWaitForEmptyCallback', - data: { - methodName: m, - }, - }, - ], - } as const) - ), - ...ALL_WAIT_METHODS.map( - (m) => - ({ - code: `${m}(function() { - // another empty callback - })`, - errors: [ - { - line: 1, - column: 13 + m.length, - messageId: 'noWaitForEmptyCallback', - data: { - methodName: m, - }, - }, - ], - } as const) - ), - - ...ALL_WAIT_METHODS.map( - (m) => - ({ - code: `${m}(noop)`, - errors: [ - { - line: 1, - column: 2 + m.length, - messageId: 'noWaitForEmptyCallback', - data: { - methodName: m, - }, - }, - ], - } as const) - ), - ], -}); diff --git a/tests/lib/rules/prefer-find-by.test.ts b/tests/lib/rules/prefer-find-by.test.ts index 97fec6d5..22a2eb4e 100644 --- a/tests/lib/rules/prefer-find-by.test.ts +++ b/tests/lib/rules/prefer-find-by.test.ts @@ -1,7 +1,6 @@ import { TSESLint } from '@typescript-eslint/utils'; import rule, { - WAIT_METHODS, RULE_NAME, getFindByQueryVariant, MessageIds, @@ -31,14 +30,8 @@ function createScenario< | TSESLint.InvalidTestCase | TSESLint.ValidTestCase<[]> >(callback: (waitMethod: string, queryMethod: string) => T) { - return WAIT_METHODS.reduce( - (acc: T[], waitMethod) => - acc.concat( - SYNC_QUERIES_COMBINATIONS.map((queryMethod) => - callback(waitMethod, queryMethod) - ) - ), - [] + return SYNC_QUERIES_COMBINATIONS.map((queryMethod) => + callback('waitFor', queryMethod) ); } @@ -153,7 +146,6 @@ ruleTester.run(RULE_NAME, rule, { import {waitFor} from '${testingFramework}'; it('tests', async () => { await waitFor(); - await wait(); }) `, }, @@ -204,67 +196,59 @@ ruleTester.run(RULE_NAME, rule, { `, })), // // this scenario verifies it works when the render function is defined in another scope - ...WAIT_METHODS.map( - (waitMethod: string) => - ({ - code: ` - import {${waitMethod}} from '${testingFramework}'; + { + code: ` + import { waitFor } from '${testingFramework}'; const { getByText, queryByLabelText, findAllByRole } = customRender() it('tests', async () => { - const submitButton = await ${waitMethod}(() => getByText('baz', { name: 'button' })) + const submitButton = await waitFor(() => getByText('baz', { name: 'button' })) }) `, - errors: [ - { - messageId: 'preferFindBy', - data: { - queryVariant: 'findBy', - queryMethod: 'Text', - prevQuery: 'getByText', - waitForMethodName: waitMethod, - }, - }, - ], - output: ` - import {${waitMethod}} from '${testingFramework}'; + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: 'findBy', + queryMethod: 'Text', + prevQuery: 'getByText', + }, + }, + ], + output: ` + import { waitFor } from '${testingFramework}'; const { getByText, queryByLabelText, findAllByRole, findByText } = customRender() it('tests', async () => { const submitButton = await findByText('baz', { name: 'button' }) }) `, - } as const) - ), + }, // // this scenario verifies when findBy* were already defined (because it was used elsewhere) - ...WAIT_METHODS.map( - (waitMethod: string) => - ({ - code: ` - import {${waitMethod}} from '${testingFramework}'; + { + code: ` + import { waitFor } from '${testingFramework}'; const { getAllByRole, findAllByRole } = customRender() it('tests', async () => { - const submitButton = await ${waitMethod}(() => getAllByRole('baz', { name: 'button' })) + const submitButton = await waitFor(() => getAllByRole('baz', { name: 'button' })) }) `, - errors: [ - { - messageId: 'preferFindBy', - data: { - queryVariant: 'findAllBy', - queryMethod: 'Role', - prevQuery: 'getAllByRole', - waitForMethodName: waitMethod, - }, - }, - ], - output: ` - import {${waitMethod}} from '${testingFramework}'; + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: 'findAllBy', + queryMethod: 'Role', + prevQuery: 'getAllByRole', + }, + }, + ], + output: ` + import { waitFor } from '${testingFramework}'; const { getAllByRole, findAllByRole } = customRender() it('tests', async () => { const submitButton = await findAllByRole('baz', { name: 'button' }) }) `, - } as const) - ), + }, // invalid code, as we need findBy* to be defined somewhere, but required for getting 100% coverage { code: `const submitButton = await waitFor(() => getByText('baz', { name: 'button' }))`, @@ -304,67 +288,59 @@ ruleTester.run(RULE_NAME, rule, { `, }, // custom query triggers the error but there is no fix - so output is the same - ...WAIT_METHODS.map( - (waitMethod: string) => - ({ - code: ` - import {${waitMethod},render} from '${testingFramework}'; + { + code: ` + import { waitFor, render} from '${testingFramework}'; it('tests', async () => { const { getByCustomQuery } = render() - const submitButton = await ${waitMethod}(() => getByCustomQuery('baz')) + const submitButton = await waitFor(() => getByCustomQuery('baz')) }) `, - errors: [ - { - messageId: 'preferFindBy', - data: { - queryVariant: 'findBy', - queryMethod: 'CustomQuery', - prevQuery: 'getByCustomQuery', - waitForMethodName: waitMethod, - }, - }, - ], - output: ` - import {${waitMethod},render} from '${testingFramework}'; + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: 'findBy', + queryMethod: 'CustomQuery', + prevQuery: 'getByCustomQuery', + }, + }, + ], + output: ` + import { waitFor, render} from '${testingFramework}'; it('tests', async () => { const { getByCustomQuery } = render() - const submitButton = await ${waitMethod}(() => getByCustomQuery('baz')) + const submitButton = await waitFor(() => getByCustomQuery('baz')) }) `, - } as const) - ), + }, // custom query triggers the error but there is no fix - so output is the same - ...WAIT_METHODS.map( - (waitMethod: string) => - ({ - code: ` - import {${waitMethod},render,screen} from '${testingFramework}'; + { + code: ` + import {waitFor,render,screen} from '${testingFramework}'; it('tests', async () => { const { getByCustomQuery } = render() - const submitButton = await ${waitMethod}(() => screen.getByCustomQuery('baz')) + const submitButton = await waitFor(() => screen.getByCustomQuery('baz')) }) `, - errors: [ - { - messageId: 'preferFindBy', - data: { - queryVariant: 'findBy', - queryMethod: 'CustomQuery', - prevQuery: 'getByCustomQuery', - waitForMethodName: waitMethod, - }, - }, - ], - output: ` - import {${waitMethod},render,screen} from '${testingFramework}'; + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: 'findBy', + queryMethod: 'CustomQuery', + prevQuery: 'getByCustomQuery', + }, + }, + ], + output: ` + import {waitFor,render,screen} from '${testingFramework}'; it('tests', async () => { const { getByCustomQuery } = render() - const submitButton = await ${waitMethod}(() => screen.getByCustomQuery('baz')) + const submitButton = await waitFor(() => screen.getByCustomQuery('baz')) }) `, - } as const) - ), + }, // presence matchers ...createScenario((waitMethod: string, queryMethod: string) => ({ code: ` diff --git a/tests/lib/rules/prefer-wait-for.test.ts b/tests/lib/rules/prefer-wait-for.test.ts deleted file mode 100644 index d9dec019..00000000 --- a/tests/lib/rules/prefer-wait-for.test.ts +++ /dev/null @@ -1,2258 +0,0 @@ -import rule, { RULE_NAME } from '../../../lib/rules/prefer-wait-for'; -import { LIBRARY_MODULES } from '../../../lib/utils'; -import { createRuleTester } from '../test-utils'; - -const ruleTester = createRuleTester(); - -ruleTester.run(RULE_NAME, rule, { - valid: [ - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `import { waitFor, render } from '${libraryModule}'; - - async () => { - await waitFor(() => {}); - }`, - })), - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `const { waitFor, render } = require('${libraryModule}'); - - async () => { - await waitFor(() => {}); - }`, - })), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `import { waitFor, render } from 'test-utils'; - - async () => { - await waitFor(() => {}); - }`, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `const { waitFor, render } = require('test-utils'); - - async () => { - await waitFor(() => {}); - }`, - }, - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `import { waitForElementToBeRemoved, render } from '${libraryModule}'; - - async () => { - await waitForElementToBeRemoved(() => {}); - }`, - })), - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `const { waitForElementToBeRemoved, render } = require('${libraryModule}'); - - async () => { - await waitForElementToBeRemoved(() => {}); - }`, - })), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `import { waitForElementToBeRemoved, render } from 'test-utils'; - - async () => { - await waitForElementToBeRemoved(() => {}); - }`, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `const { waitForElementToBeRemoved, render } = require('test-utils'); - - async () => { - await waitForElementToBeRemoved(() => {}); - }`, - }, - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `import * as testingLibrary from '${libraryModule}'; - - async () => { - await testingLibrary.waitForElementToBeRemoved(() => {}); - }`, - })), - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `const testingLibrary = require('${libraryModule}'); - - async () => { - await testingLibrary.waitForElementToBeRemoved(() => {}); - }`, - })), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `import * as testingLibrary from 'test-utils'; - - async () => { - await testingLibrary.waitForElementToBeRemoved(() => {}); - }`, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `const testingLibrary = require('test-utils'); - - async () => { - await testingLibrary.waitForElementToBeRemoved(() => {}); - }`, - }, - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `import { render } from '${libraryModule}'; - import { waitForSomethingElse } from 'other-module'; - - async () => { - await waitForSomethingElse(() => {}); - }`, - })), - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `const { render } = require('${libraryModule}'); - const { waitForSomethingElse } = require('other-module'); - - async () => { - await waitForSomethingElse(() => {}); - }`, - })), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `import { render } from 'test-utils'; - import { waitForSomethingElse } from 'other-module'; - - async () => { - await waitForSomethingElse(() => {}); - }`, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `const { render } = require('test-utils'); - const { waitForSomethingElse } = require('other-module'); - - async () => { - await waitForSomethingElse(() => {}); - }`, - }, - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `import * as testingLibrary from '${libraryModule}'; - - async () => { - await testingLibrary.waitFor(() => {}, { timeout: 500 }); - }`, - })), - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `const testingLibrary = require('${libraryModule}'); - - async () => { - await testingLibrary.waitFor(() => {}, { timeout: 500 }); - }`, - })), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `import * as testingLibrary from 'test-utils'; - - async () => { - await testingLibrary.waitFor(() => {}, { timeout: 500 }); - }`, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `const testingLibrary = require('test-utils'); - - async () => { - await testingLibrary.waitFor(() => {}, { timeout: 500 }); - }`, - }, - { - code: `import { wait } from 'imNoTestingLibrary'; - - async () => { - await wait(); - }`, - }, - { - code: `const { wait } = require('imNoTestingLibrary'); - - async () => { - await wait(); - }`, - }, - { - code: `import * as foo from 'imNoTestingLibrary'; - - async () => { - await foo.wait(); - }`, - }, - { - code: `const foo = require('imNoTestingLibrary'); - - async () => { - await foo.wait(); - }`, - }, - { - code: `import * as foo from 'imNoTestingLibrary'; - cy.wait(); - `, - }, - { - code: `const foo = require('imNoTestingLibrary'); - cy.wait(); - `, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` - // case: aggressive reporting disabled - method named same as invalid method - // but not coming from Testing Library is valid - import { wait as testingLibraryWait } from 'test-utils' - import { wait } from 'somewhere-else' - - async () => { - await wait(); - } - `, - }, - { - // https://github.com/testing-library/eslint-plugin-testing-library/issues/145 - code: `import * as foo from 'imNoTestingLibrary'; - async function wait(): Promise { - // doesn't matter - } - - function callsWait(): void { - await wait(); - } - `, - }, - { - // https://github.com/testing-library/eslint-plugin-testing-library/issues/145 - code: `const foo = require('imNoTestingLibrary'); - async function wait(): Promise { - // doesn't matter - } - - function callsWait(): void { - await wait(); - } - `, - }, - ], - - invalid: [ - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `import { wait, render } from '${libraryModule}'; - - async () => { - await wait(); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { render,waitFor } from '${libraryModule}'; - - async () => { - await waitFor(() => {}); - }`, - } as const) - ), - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `const { wait, render } = require('${libraryModule}'); - - async () => { - await wait(); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `const { render,waitFor } = require('${libraryModule}'); - - async () => { - await waitFor(() => {}); - }`, - } as const) - ), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `import { wait, render } from 'test-utils'; - - async () => { - await wait(); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { render,waitFor } from 'test-utils'; - - async () => { - await waitFor(() => {}); - }`, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `const { wait, render } = require('test-utils'); - - async () => { - await wait(); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `const { render,waitFor } = require('test-utils'); - - async () => { - await waitFor(() => {}); - }`, - }, - // namespaced wait should be fixed but not its import - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `import * as testingLibrary from '${libraryModule}'; - - async () => { - await testingLibrary.wait(); - }`, - errors: [ - { - messageId: 'preferWaitForMethod', - line: 4, - column: 30, - }, - ], - output: `import * as testingLibrary from '${libraryModule}'; - - async () => { - await testingLibrary.waitFor(() => {}); - }`, - } as const) - ), - // namespaced wait should be fixed but not its import - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `const testingLibrary = require('${libraryModule}'); - - async () => { - await testingLibrary.wait(); - }`, - errors: [ - { - messageId: 'preferWaitForMethod', - line: 4, - column: 30, - }, - ], - output: `const testingLibrary = require('${libraryModule}'); - - async () => { - await testingLibrary.waitFor(() => {}); - }`, - } as const) - ), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `import * as testingLibrary from 'test-utils'; - - async () => { - await testingLibrary.wait(); - }`, - errors: [ - { - messageId: 'preferWaitForMethod', - line: 4, - column: 30, - }, - ], - output: `import * as testingLibrary from 'test-utils'; - - async () => { - await testingLibrary.waitFor(() => {}); - }`, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `const testingLibrary = require('test-utils'); - - async () => { - await testingLibrary.wait(); - }`, - errors: [ - { - messageId: 'preferWaitForMethod', - line: 4, - column: 30, - }, - ], - output: `const testingLibrary = require('test-utils'); - - async () => { - await testingLibrary.waitFor(() => {}); - }`, - }, - // namespaced waitForDomChange should be fixed but not its import - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `import * as testingLibrary from '${libraryModule}'; - - async () => { - await testingLibrary.waitForDomChange({ timeout: 500 }); - }`, - errors: [ - { - messageId: 'preferWaitForMethod', - line: 4, - column: 30, - }, - ], - output: `import * as testingLibrary from '${libraryModule}'; - - async () => { - await testingLibrary.waitFor(() => {}, { timeout: 500 }); - }`, - } as const) - ), - // namespaced waitForDomChange should be fixed but not its import - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `const testingLibrary = require('${libraryModule}'); - - async () => { - await testingLibrary.waitForDomChange({ timeout: 500 }); - }`, - errors: [ - { - messageId: 'preferWaitForMethod', - line: 4, - column: 30, - }, - ], - output: `const testingLibrary = require('${libraryModule}'); - - async () => { - await testingLibrary.waitFor(() => {}, { timeout: 500 }); - }`, - } as const) - ), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `import * as testingLibrary from 'test-utils'; - - async () => { - await testingLibrary.waitForDomChange({ timeout: 500 }); - }`, - errors: [ - { - messageId: 'preferWaitForMethod', - line: 4, - column: 30, - }, - ], - output: `import * as testingLibrary from 'test-utils'; - - async () => { - await testingLibrary.waitFor(() => {}, { timeout: 500 }); - }`, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `const testingLibrary = require('test-utils'); - - async () => { - await testingLibrary.waitForDomChange({ timeout: 500 }); - }`, - errors: [ - { - messageId: 'preferWaitForMethod', - line: 4, - column: 30, - }, - ], - output: `const testingLibrary = require('test-utils'); - - async () => { - await testingLibrary.waitFor(() => {}, { timeout: 500 }); - }`, - }, - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `import { render, wait } from '${libraryModule}' - - async () => { - await wait(() => {}); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { render,waitFor } from '${libraryModule}'; - - async () => { - await waitFor(() => {}); - }`, - } as const) - ), - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `const { render, wait } = require('${libraryModule}'); - - async () => { - await wait(() => {}); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `const { render,waitFor } = require('${libraryModule}'); - - async () => { - await waitFor(() => {}); - }`, - } as const) - ), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `import { render, wait } from 'test-utils' - - async () => { - await wait(() => {}); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { render,waitFor } from 'test-utils'; - - async () => { - await waitFor(() => {}); - }`, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `const { render, wait } = require('test-utils'); - - async () => { - await wait(() => {}); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `const { render,waitFor } = require('test-utils'); - - async () => { - await waitFor(() => {}); - }`, - }, - // this import doesn't have trailing semicolon but fixer adds it - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `import { render, wait, screen } from "${libraryModule}"; - - async () => { - await wait(function cb() { - doSomething(); - }); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { render,screen,waitFor } from '${libraryModule}'; - - async () => { - await waitFor(function cb() { - doSomething(); - }); - }`, - } as const) - ), - // this import doesn't have trailing semicolon but fixer adds it - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `import { render, wait, screen } from "${libraryModule}"; - - async () => { - await wait(function cb() { - doSomething(); - }); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { render,screen,waitFor } from '${libraryModule}'; - - async () => { - await waitFor(function cb() { - doSomething(); - }); - }`, - } as const) - ), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `import { render, wait, screen } from "test-utils"; - - async () => { - await wait(function cb() { - doSomething(); - }); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { render,screen,waitFor } from 'test-utils'; - - async () => { - await waitFor(function cb() { - doSomething(); - }); - }`, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `const { render, wait, screen } = require('test-utils'); - - async () => { - await wait(function cb() { - doSomething(); - }); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `const { render,screen,waitFor } = require('test-utils'); - - async () => { - await waitFor(function cb() { - doSomething(); - }); - }`, - }, - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `import { render, waitForElement, screen } from '${libraryModule}' - - async () => { - await waitForElement(() => {}); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { render,screen,waitFor } from '${libraryModule}'; - - async () => { - await waitFor(() => {}); - }`, - } as const) - ), - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `const { render, waitForElement, screen } = require('${libraryModule}'); - - async () => { - await waitForElement(() => {}); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `const { render,screen,waitFor } = require('${libraryModule}'); - - async () => { - await waitFor(() => {}); - }`, - } as const) - ), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `import { render, waitForElement, screen } from 'test-utils' - - async () => { - await waitForElement(() => {}); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { render,screen,waitFor } from 'test-utils'; - - async () => { - await waitFor(() => {}); - }`, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `const { render, waitForElement, screen } = require('test-utils'); - - async () => { - await waitForElement(() => {}); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `const { render,screen,waitFor } = require('test-utils'); - - async () => { - await waitFor(() => {}); - }`, - }, - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `import { waitForElement } from '${libraryModule}'; - - async () => { - await waitForElement(function cb() { - doSomething(); - }); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { waitFor } from '${libraryModule}'; - - async () => { - await waitFor(function cb() { - doSomething(); - }); - }`, - } as const) - ), - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `const { waitForElement } = require('${libraryModule}'); - - async () => { - await waitForElement(function cb() { - doSomething(); - }); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `const { waitFor } = require('${libraryModule}'); - - async () => { - await waitFor(function cb() { - doSomething(); - }); - }`, - } as const) - ), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `import { waitForElement } from 'test-utils'; - - async () => { - await waitForElement(function cb() { - doSomething(); - }); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { waitFor } from 'test-utils'; - - async () => { - await waitFor(function cb() { - doSomething(); - }); - }`, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `const { waitForElement } = require('test-utils'); - - async () => { - await waitForElement(function cb() { - doSomething(); - }); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `const { waitFor } = require('test-utils'); - - async () => { - await waitFor(function cb() { - doSomething(); - }); - }`, - }, - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `import { waitForDomChange } from '${libraryModule}'; - - async () => { - await waitForDomChange(); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { waitFor } from '${libraryModule}'; - - async () => { - await waitFor(() => {}); - }`, - } as const) - ), - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `const { waitForDomChange } = require('${libraryModule}'); - - async () => { - await waitForDomChange(); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `const { waitFor } = require('${libraryModule}'); - - async () => { - await waitFor(() => {}); - }`, - } as const) - ), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `import { waitForDomChange } from 'test-utils'; - - async () => { - await waitForDomChange(); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { waitFor } from 'test-utils'; - - async () => { - await waitFor(() => {}); - }`, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `const { waitForDomChange } = require('test-utils'); - - async () => { - await waitForDomChange(); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `const { waitFor } = require('test-utils'); - - async () => { - await waitFor(() => {}); - }`, - }, - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `import { waitForDomChange } from '${libraryModule}'; - - async () => { - await waitForDomChange(mutationObserverOptions); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { waitFor } from '${libraryModule}'; - - async () => { - await waitFor(() => {}, mutationObserverOptions); - }`, - } as const) - ), - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `const { waitForDomChange } = require('${libraryModule}'); - - async () => { - await waitForDomChange(mutationObserverOptions); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `const { waitFor } = require('${libraryModule}'); - - async () => { - await waitFor(() => {}, mutationObserverOptions); - }`, - } as const) - ), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `import { waitForDomChange } from 'test-utils'; - - async () => { - await waitForDomChange(mutationObserverOptions); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { waitFor } from 'test-utils'; - - async () => { - await waitFor(() => {}, mutationObserverOptions); - }`, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `const { waitForDomChange } = require('test-utils'); - - async () => { - await waitForDomChange(mutationObserverOptions); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `const { waitFor } = require('test-utils'); - - async () => { - await waitFor(() => {}, mutationObserverOptions); - }`, - }, - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `import { waitForDomChange } from '${libraryModule}'; - - async () => { - await waitForDomChange({ timeout: 5000 }); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { waitFor } from '${libraryModule}'; - - async () => { - await waitFor(() => {}, { timeout: 5000 }); - }`, - } as const) - ), - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `const { waitForDomChange } = require('${libraryModule}'); - - async () => { - await waitForDomChange({ timeout: 5000 }); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `const { waitFor } = require('${libraryModule}'); - - async () => { - await waitFor(() => {}, { timeout: 5000 }); - }`, - } as const) - ), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `import { waitForDomChange } from 'test-utils'; - - async () => { - await waitForDomChange({ timeout: 5000 }); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { waitFor } from 'test-utils'; - - async () => { - await waitFor(() => {}, { timeout: 5000 }); - }`, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `const { waitForDomChange } = require('test-utils'); - - async () => { - await waitForDomChange({ timeout: 5000 }); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `const { waitFor } = require('test-utils'); - - async () => { - await waitFor(() => {}, { timeout: 5000 }); - }`, - }, - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `import { waitForDomChange, wait, waitForElement } from '${libraryModule}'; - import userEvent from '@testing-library/user-event'; - - async () => { - await waitForDomChange({ timeout: 5000 }); - await waitForElement(); - await wait(); - await wait(() => { doSomething() }); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 5, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 6, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 7, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 8, - column: 15, - }, - ], - output: `import { waitFor } from '${libraryModule}'; - import userEvent from '@testing-library/user-event'; - - async () => { - await waitFor(() => {}, { timeout: 5000 }); - await waitFor(() => {}); - await waitFor(() => {}); - await waitFor(() => { doSomething() }); - }`, - } as const) - ), - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `const { waitForDomChange, wait, waitForElement } = require('${libraryModule}'); - const userEvent = require('@testing-library/user-event'); - - async () => { - await waitForDomChange({ timeout: 5000 }); - await waitForElement(); - await wait(); - await wait(() => { doSomething() }); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 5, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 6, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 7, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 8, - column: 15, - }, - ], - output: `const { waitFor } = require('${libraryModule}'); - const userEvent = require('@testing-library/user-event'); - - async () => { - await waitFor(() => {}, { timeout: 5000 }); - await waitFor(() => {}); - await waitFor(() => {}); - await waitFor(() => { doSomething() }); - }`, - } as const) - ), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `import { waitForDomChange, wait, waitForElement } from 'test-utils'; - import userEvent from '@testing-library/user-event'; - - async () => { - await waitForDomChange({ timeout: 5000 }); - await waitForElement(); - await wait(); - await wait(() => { doSomething() }); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 5, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 6, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 7, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 8, - column: 15, - }, - ], - output: `import { waitFor } from 'test-utils'; - import userEvent from '@testing-library/user-event'; - - async () => { - await waitFor(() => {}, { timeout: 5000 }); - await waitFor(() => {}); - await waitFor(() => {}); - await waitFor(() => { doSomething() }); - }`, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `const { waitForDomChange, wait, waitForElement } = require('test-utils'); - const userEvent = require('@testing-library/user-event'); - - async () => { - await waitForDomChange({ timeout: 5000 }); - await waitForElement(); - await wait(); - await wait(() => { doSomething() }); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 5, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 6, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 7, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 8, - column: 15, - }, - ], - output: `const { waitFor } = require('test-utils'); - const userEvent = require('@testing-library/user-event'); - - async () => { - await waitFor(() => {}, { timeout: 5000 }); - await waitFor(() => {}); - await waitFor(() => {}); - await waitFor(() => { doSomething() }); - }`, - }, - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `import { render, waitForDomChange, wait, waitForElement } from '${libraryModule}'; - - async () => { - await waitForDomChange({ timeout: 5000 }); - await waitForElement(); - await wait(); - await wait(() => { doSomething() }); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 5, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 6, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 7, - column: 15, - }, - ], - output: `import { render,waitFor } from '${libraryModule}'; - - async () => { - await waitFor(() => {}, { timeout: 5000 }); - await waitFor(() => {}); - await waitFor(() => {}); - await waitFor(() => { doSomething() }); - }`, - } as const) - ), - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `const { render, waitForDomChange, wait, waitForElement } = require('${libraryModule}'); - - async () => { - await waitForDomChange({ timeout: 5000 }); - await waitForElement(); - await wait(); - await wait(() => { doSomething() }); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 5, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 6, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 7, - column: 15, - }, - ], - output: `const { render,waitFor } = require('${libraryModule}'); - - async () => { - await waitFor(() => {}, { timeout: 5000 }); - await waitFor(() => {}); - await waitFor(() => {}); - await waitFor(() => { doSomething() }); - }`, - } as const) - ), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `import { render, waitForDomChange, wait, waitForElement } from 'test-utils'; - - async () => { - await waitForDomChange({ timeout: 5000 }); - await waitForElement(); - await wait(); - await wait(() => { doSomething() }); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 5, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 6, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 7, - column: 15, - }, - ], - output: `import { render,waitFor } from 'test-utils'; - - async () => { - await waitFor(() => {}, { timeout: 5000 }); - await waitFor(() => {}); - await waitFor(() => {}); - await waitFor(() => { doSomething() }); - }`, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `const { render, waitForDomChange, wait, waitForElement } = require('test-utils'); - - async () => { - await waitForDomChange({ timeout: 5000 }); - await waitForElement(); - await wait(); - await wait(() => { doSomething() }); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 5, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 6, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 7, - column: 15, - }, - ], - output: `const { render,waitFor } = require('test-utils'); - - async () => { - await waitFor(() => {}, { timeout: 5000 }); - await waitFor(() => {}); - await waitFor(() => {}); - await waitFor(() => { doSomething() }); - }`, - }, - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `import { waitForDomChange, wait, render, waitForElement } from '${libraryModule}'; - - async () => { - await waitForDomChange({ timeout: 5000 }); - await waitForElement(); - await wait(); - await wait(() => { doSomething() }); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 5, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 6, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 7, - column: 15, - }, - ], - output: `import { render,waitFor } from '${libraryModule}'; - - async () => { - await waitFor(() => {}, { timeout: 5000 }); - await waitFor(() => {}); - await waitFor(() => {}); - await waitFor(() => { doSomething() }); - }`, - } as const) - ), - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `const { waitForDomChange, wait, render, waitForElement } = require('${libraryModule}'); - - async () => { - await waitForDomChange({ timeout: 5000 }); - await waitForElement(); - await wait(); - await wait(() => { doSomething() }); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 5, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 6, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 7, - column: 15, - }, - ], - output: `const { render,waitFor } = require('${libraryModule}'); - - async () => { - await waitFor(() => {}, { timeout: 5000 }); - await waitFor(() => {}); - await waitFor(() => {}); - await waitFor(() => { doSomething() }); - }`, - } as const) - ), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `import { waitForDomChange, wait, render, waitForElement } from 'test-utils'; - - async () => { - await waitForDomChange({ timeout: 5000 }); - await waitForElement(); - await wait(); - await wait(() => { doSomething() }); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 5, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 6, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 7, - column: 15, - }, - ], - output: `import { render,waitFor } from 'test-utils'; - - async () => { - await waitFor(() => {}, { timeout: 5000 }); - await waitFor(() => {}); - await waitFor(() => {}); - await waitFor(() => { doSomething() }); - }`, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `const { waitForDomChange, wait, render, waitForElement } = require('test-utils'); - - async () => { - await waitForDomChange({ timeout: 5000 }); - await waitForElement(); - await wait(); - await wait(() => { doSomething() }); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 5, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 6, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 7, - column: 15, - }, - ], - output: `const { render,waitFor } = require('test-utils'); - - async () => { - await waitFor(() => {}, { timeout: 5000 }); - await waitFor(() => {}); - await waitFor(() => {}); - await waitFor(() => { doSomething() }); - }`, - }, - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `import { - waitForDomChange, - wait, - render, - waitForElement, - } from '${libraryModule}'; - - async () => { - await waitForDomChange({ timeout: 5000 }); - await waitForElement(); - await wait(); - await wait(() => { doSomething() }); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 9, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 10, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 11, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 12, - column: 15, - }, - ], - output: `import { render,waitFor } from '${libraryModule}'; - - async () => { - await waitFor(() => {}, { timeout: 5000 }); - await waitFor(() => {}); - await waitFor(() => {}); - await waitFor(() => { doSomething() }); - }`, - } as const) - ), - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - code: `const { - waitForDomChange, - wait, - render, - waitForElement, - } = require('${libraryModule}'); - - async () => { - await waitForDomChange({ timeout: 5000 }); - await waitForElement(); - await wait(); - await wait(() => { doSomething() }); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 9, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 10, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 11, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 12, - column: 15, - }, - ], - output: `const { render,waitFor } = require('${libraryModule}'); - - async () => { - await waitFor(() => {}, { timeout: 5000 }); - await waitFor(() => {}); - await waitFor(() => {}); - await waitFor(() => { doSomething() }); - }`, - } as const) - ), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `import { - waitForDomChange, - wait, - render, - waitForElement, - } from 'test-utils'; - - async () => { - await waitForDomChange({ timeout: 5000 }); - await waitForElement(); - await wait(); - await wait(() => { doSomething() }); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 9, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 10, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 11, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 12, - column: 15, - }, - ], - output: `import { render,waitFor } from 'test-utils'; - - async () => { - await waitFor(() => {}, { timeout: 5000 }); - await waitFor(() => {}); - await waitFor(() => {}); - await waitFor(() => { doSomething() }); - }`, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `const { - waitForDomChange, - wait, - render, - waitForElement, - } = require('test-utils'); - - async () => { - await waitForDomChange({ timeout: 5000 }); - await waitForElement(); - await wait(); - await wait(() => { doSomething() }); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 9, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 10, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 11, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 12, - column: 15, - }, - ], - output: `const { render,waitFor } = require('test-utils'); - - async () => { - await waitFor(() => {}, { timeout: 5000 }); - await waitFor(() => {}); - await waitFor(() => {}); - await waitFor(() => { doSomething() }); - }`, - }, - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - // if already importing waitFor then it's not imported twice - code: `import { wait, waitFor, render } from '${libraryModule}'; - - async () => { - await wait(); - await waitFor(someCallback); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { render,waitFor } from '${libraryModule}'; - - async () => { - await waitFor(() => {}); - await waitFor(someCallback); - }`, - } as const) - ), - ...LIBRARY_MODULES.map( - (libraryModule) => - ({ - // if already importing waitFor then it's not imported twice - code: `const { wait, waitFor, render } = require('${libraryModule}'); - - async () => { - await wait(); - await waitFor(someCallback); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `const { render,waitFor } = require('${libraryModule}'); - - async () => { - await waitFor(() => {}); - await waitFor(someCallback); - }`, - } as const) - ), - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - // if already importing waitFor then it's not imported twice - code: `import { wait, waitFor, render } from 'test-utils'; - - async () => { - await wait(); - await waitFor(someCallback); - }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { render,waitFor } from 'test-utils'; - - async () => { - await waitFor(() => {}); - await waitFor(someCallback); - }`, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - // if already importing waitFor then it's not imported twice - code: `const { wait, waitFor, render } = require('test-utils'); - - async () => { - await wait(); - await waitFor(someCallback); - }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `const { render,waitFor } = require('test-utils'); - - async () => { - await waitFor(() => {}); - await waitFor(someCallback); - }`, - }, - ], -});