diff --git a/README.md b/README.md
index 885625a7..41ad630a 100644
--- a/README.md
+++ b/README.md
@@ -99,9 +99,9 @@ Assuming you are using the same pattern for your test files as [Jest by default]
};
```
-#### ESLint Cascading and Hierachy
+#### ESLint Cascading and Hierarchy
-Another approach for customizing ESLint config by paths is through [ESLint Cascading and Hierachy](https://eslint.org/docs/user-guide/configuring/configuration-files#cascading-and-hierarchy). This is useful if all your tests are placed under the same folder, so you can place there another `.eslintrc` where you enable `eslint-plugin-testing-library` for applying it only to the files under such folder, rather than enabling it on your global `.eslintrc` which would apply to your whole project.
+Another approach for customizing ESLint config by paths is through [ESLint Cascading and Hierarchy](https://eslint.org/docs/user-guide/configuring/configuration-files#cascading-and-hierarchy). This is useful if all your tests are placed under the same folder, so you can place there another `.eslintrc` where you enable `eslint-plugin-testing-library` for applying it only to the files under such folder, rather than enabling it on your global `.eslintrc` which would apply to your whole project.
## Shareable configurations
@@ -228,17 +228,17 @@ To enable this configuration use the `extends` property in your
In v4 this plugin introduced a new feature called "Aggressive Reporting", which intends to detect Testing Library utils usages even if they don't come directly from a Testing Library package (i.e. [using a custom utility file to re-export everything from Testing Library](https://testing-library.com/docs/react-testing-library/setup/#custom-render)). You can [read more about this feature here](docs/migrating-to-v4-guide.md#aggressive-reporting).
-If you are looking to restricting this feature, please refer to the [Shared Settings section](#shared-settings) to do so. It's not possible to switch this mechanism entirely off yet, but there will be a new option in the Shared Settings in the future to be able to achieve this.
+If you are looking to restricting or switching off this feature, please refer to the [Shared Settings section](#shared-settings) to do so.
## Shared Settings
-There are some configuration options available that will be shared across all the plugin rules. This is achieved using [ESLint Shared Settings](https://eslint.org/docs/user-guide/configuring/configuration-files#adding-shared-settings). These Shared Settings are meant to be used if you need to restrict the Aggressive Reporting mechanism, which is an out of the box advanced feature to lint Testing Library usages in a simpler way for most of the users. **So please before configuring any of these settings**, read more about [the advantages of `eslint-plugin-testing-library` Aggressive Reporting mechanism](docs/migrating-to-v4-guide.md#aggressive-reporting), and [how it's affected by these settings](docs/migrating-to-v4-guide.md#shared-settings).
+There are some configuration options available that will be shared across all the plugin rules. This is achieved using [ESLint Shared Settings](https://eslint.org/docs/user-guide/configuring/configuration-files#adding-shared-settings). These Shared Settings are meant to be used if you need to restrict or switch off the Aggressive Reporting, which is an out of the box advanced feature to lint Testing Library usages in a simpler way for most of the users. **So please before configuring any of these settings**, read more about [the advantages of `eslint-plugin-testing-library` Aggressive Reporting feature](docs/migrating-to-v4-guide.md#aggressive-reporting), and [how it's affected by these settings](docs/migrating-to-v4-guide.md#shared-settings).
If you are sure about configuring the settings, these are the options available:
### `testing-library/utils-module`
-The name of your custom utility file from where you re-export everything from Testing Library package.
+The name of your custom utility file from where you re-export everything from the Testing Library package, or `"off"` to switch related Aggressive Reporting mechanism off. Relates to [Aggressive Imports Reporting](docs/migrating-to-v4-guide.md#imports).
```json
// .eslintrc
@@ -249,11 +249,11 @@ The name of your custom utility file from where you re-export everything from Te
}
```
-[You can find more details here](docs/migrating-to-v4-guide.md#testing-libraryutils-module).
+[You can find more details about the `utils-module` setting here](docs/migrating-to-v4-guide.md#testing-libraryutils-module).
### `testing-library/custom-renders`
-A list of function names that are valid as Testing Library custom renders. Relates to [Aggressive Reporting - Renders](docs/migrating-to-v4-guide.md#renders)
+A list of function names that are valid as Testing Library custom renders, or `"off"` to switch related Aggressive Reporting mechanism off. Relates to [Aggressive Renders Reporting](docs/migrating-to-v4-guide.md#renders).
```json
// .eslintrc
@@ -264,11 +264,41 @@ A list of function names that are valid as Testing Library custom renders. Relat
}
```
-[You can find more details here](docs/migrating-to-v4-guide.md#testing-librarycustom-renders).
+[You can find more details about the `custom-renders` setting here](docs/migrating-to-v4-guide.md#testing-librarycustom-renders).
+
+### `testing-library/custom-queries`
+
+A list of query names/patterns that are valid as Testing Library custom queries, or `"off"` to switch related Aggressive Reporting mechanism off. Relates to [Aggressive Reporting - Queries](docs/migrating-to-v4-guide.md#queries)
+
+```json
+// .eslintrc
+{
+ "settings": {
+ "testing-library/custom-queries": ["ByIcon", "getByComplexText"]
+ }
+}
+```
+
+[You can find more details about the `custom-queries` setting here](docs/migrating-to-v4-guide.md#testing-librarycustom-queries).
+
+### Switching all Aggressive Reporting mechanisms off
+
+Since each Shared Setting is related to one Aggressive Reporting mechanism, and they accept `"off"` to opt out of that mechanism, you can switch the entire feature off by doing:
+
+```json
+// .eslintrc
+{
+ "settings": {
+ "testing-library/utils-module": "off",
+ "testing-library/custom-renders": "off",
+ "testing-library/custom-queries": "off"
+ }
+}
+```
## Troubleshooting
-### There are errors reported in non-testing files
+### Errors reported in non-testing files
If you find ESLint errors related to `eslint-plugin-testing-library` in files other than testing, this could be caused by [Aggressive Reporting](#aggressive-reporting).
@@ -276,14 +306,18 @@ You can avoid this by:
1. [running `eslint-plugin-testing-library` only against testing files](#run-the-plugin-only-against-test-files)
2. [limiting the scope of Aggressive Reporting through Shared Settings](#shared-settings)
+3. [switching Aggressive Reporting feature off](#switching-all-aggressive-reporting-mechanisms-off)
If you think the error you are getting is not related to this at all, please [fill a new issue](https://github.com/testing-library/eslint-plugin-testing-library/issues/new/choose) with as many details as possible.
-### There are false positives in testing files
+### False positives in testing files
If you are getting false positive ESLint errors in your testing files, this could be caused by [Aggressive Reporting](#aggressive-reporting).
-You can avoid this by [limiting the scope of Aggressive Reporting through Shared Settings](#shared-settings)
+You can avoid this by:
+
+1. [limiting the scope of Aggressive Reporting through Shared Settings](#shared-settings)
+2. [switching Aggressive Reporting feature off](#switching-all-aggressive-reporting-mechanisms-off)
If you think the error you are getting is not related to this at all, please [fill a new issue](https://github.com/testing-library/eslint-plugin-testing-library/issues/new/choose) with as many details as possible.
diff --git a/docs/migrating-to-v4-guide.md b/docs/migrating-to-v4-guide.md
index 050de6cc..4d89d6c8 100644
--- a/docs/migrating-to-v4-guide.md
+++ b/docs/migrating-to-v4-guide.md
@@ -98,7 +98,7 @@ So what is this Aggressive Reporting introduced on v4? Until v3, `eslint-plugin-
- users can [re-export Testing Library utils from a custom module](https://testing-library.com/docs/react-testing-library/setup/#configuring-jest-with-test-utils), so they won't be imported from a Testing Library package but a custom one.
- users can [add their own Custom Queries](https://testing-library.com/docs/react-testing-library/setup/#add-custom-queries), so it's possible to use other queries than built-in ones.
-These customization mechanisms make impossible for `eslint-plugin-testing-library` to figure out if some utils are related to Testing Library or not. Here you have some examples illustrating it:
+These customization possibilities make it impossible for `eslint-plugin-testing-library` to figure out if some utils are related to Testing Library or not. Here you have some examples illustrating it:
```javascript
import { render, screen } from '@testing-library/react';
@@ -129,7 +129,7 @@ const el = findByIcon('profile');
How can the `eslint-plugin-testing-library` be aware of this? Until v3, the plugin offered some options to indicate some of these custom things, so the plugin would check them when reporting usages. This can lead to false negatives though since the users might not be aware of the necessity of indicating such custom utils or just forget about doing so.
-Instead, in `eslint-plugin-testing-library` v4 we have opted-in a more **aggressive reporting** mechanism which, by default, will assume any method named following the same patterns as Testing Library utils has to be reported too:
+Instead, in `eslint-plugin-testing-library` v4 we have opted-in into a more **aggressive reporting** feature which, by default, will assume any method named following the same patterns as Testing Library utils has to be reported too:
```javascript
// importing from Custom Module
@@ -145,31 +145,32 @@ const wrapper = renderWithRedux();
const el = findByIcon('profile');
```
-There are 3 behaviors then that can be aggressively reported: imports, renders, and queries. This new Aggressive Reporting mechanism will just work fine out of the box and won't create false positives for most of the users. However, it's possible to do some tweaks to disable some of these behaviors using the new [Shared Settings](#shared-settings). We recommend you to keep reading this section to know more about these Aggressive Reporting behaviors and then check the Shared Settings if you think you'd still need it for some particular reason.
+There are 3 mechanisms that can be aggressively reported: imports, renders, and queries. This new Aggressive Reporting feature will work fine out of the box and won't create false positives for most of the users. However, it's possible to restrict or switch off these mechanisms using [Shared Settings](#shared-settings).
+We recommend you to keep reading this section to know more about these Aggressive Reporting mechanisms and afterwards check the Shared Settings if you think you'd still need them for some particular reason.
_You can find the motivation behind this behavior on [this issue comment](https://github.com/testing-library/eslint-plugin-testing-library/issues/222#issuecomment-679592434)._
### Imports
-By default, `eslint-plugin-testing-library` v4 won't check from which module are the utils imported. This means it doesn't matter if you are importing the utils from `@testing-library/*`, `test-utils` or `whatever`.
+By default, `eslint-plugin-testing-library` v4 won't check from which module the utils are imported. This means it doesn't matter if you are importing the utils from `@testing-library/*`, `test-utils` or `whatever`: they'll be assumed as Testing Library related if matching Testing Library utils patterns.
-There is a new Shared Setting to restrict this scope though: [`utils-module`](#testing-libraryutils-module). By using this setting, only utils imported from `@testing-library/*` packages, or the custom one indicated in this setting would be reported.
+There is a Shared Setting property to restrict or switch off this mechanism though: [`utils-module`](#testing-libraryutils-module). By using this setting, only utils imported from `@testing-library/*` packages + those indicated in this setting (if any) will be reported.
### Renders
By default, `eslint-plugin-testing-library` v4 will assume that all methods which names contain "render" should be reported. This means it doesn't matter if you are rendering your elements for testing using `render`, `customRender` or `renderWithRedux`.
-There is a new Shared Setting to restrict this scope though: [`custom-renders`](#testing-librarycustom-renders). By using this setting, only methods strictly named `render` or as one of the indicated Custom Renders would be reported.
+There is a Shared Setting property to restrict or switch off this mechanism though: [`custom-renders`](#testing-librarycustom-renders). By using this setting, only methods strictly named `render` + those indicated in this setting (if any) will be reported.
### Queries
`eslint-plugin-testing-library` v4 will assume that all methods named following the pattern `get(All)By*`, `query(All)By*`, or `find(All)By*` are queries to be reported. This means it doesn't matter if you are using a built-in query (`getByText`), or a custom one (`getByIcon`): if it matches this pattern, it will be assumed as a potential query to be reported.
-There is no way to restrict this behavior for now.
+There is a Shared Setting property to restrict or switch off this mechanism though: [`custom-queries`](#testing-librarycustom-queries). By using this setting, only [built-in queries](https://testing-library.com/docs/queries/about) + those indicated in this setting (if any) will be reported.
## Shared Settings
-ESLint has a setting feature which allows configuring data that must be shared across all its rules: [Shared Settings](https://eslint.org/docs/user-guide/configuring/configuration-files#adding-shared-settings). Since `eslint-plugin-testing-library` v4 we are using this Shared Settings to config global things for the plugin.
+ESLint provides a way of configuring data that must be shared across all its rules: [Shared Settings](https://eslint.org/docs/user-guide/configuring/configuration-files#adding-shared-settings). Since `eslint-plugin-testing-library` v4 we are using this Shared Settings to config global things for the plugin.
To avoid collision with settings from other ESLint plugins, all the properties for this one are prefixed with `testing-library/`.
@@ -177,7 +178,9 @@ To avoid collision with settings from other ESLint plugins, all the properties f
### `testing-library/utils-module`
-The name of your custom utility file from where you re-export everything from Testing Library package. Relates to [Aggressive Reporting - Imports](#imports).
+Relates to the [Aggressive Imports Reporting mechanism](#imports). This setting accepts any string value.
+
+If you pass a string other than `"off"` to this option, it will represent your custom utility file from where you re-export everything from Testing Library package.
```json
// .eslintrc
@@ -188,7 +191,7 @@ The name of your custom utility file from where you re-export everything from Te
}
```
-Enabling this setting, you'll restrict the errors reported by the plugin to only those utils being imported from this custom utility file, or some `@testing-library/*` package. The previous setting example would cause:
+Configuring this setting like that, you'll restrict the errors reported by the plugin to only those utils being imported from this custom utility file, or some `@testing-library/*` package. The previous setting example would cause:
```javascript
import { waitFor } from '@testing-library/react';
@@ -220,9 +223,22 @@ test('testing-library/utils-module setting example', () => {
});
```
+You can also set this setting to `"off"` to entirely opt-out Aggressive Imports Reporting mechanism, so only utils coming from Testing Library packages are reported.
+
+```json
+// .eslintrc
+{
+ "settings": {
+ "testing-library/utils-module": "off"
+ }
+}
+```
+
### `testing-library/custom-renders`
-A list of function names that are valid as Testing Library custom renders. Relates to [Aggressive Reporting - Renders](#renders)
+Relates to the [Aggressive Renders Reporting mechanism](#renders). This setting accepts an array of strings or `"off"`.
+
+If you pass an array of strings to this option, it will represent a list of function names that are valid as Testing Library custom renders.
```json
// .eslintrc
@@ -233,7 +249,7 @@ A list of function names that are valid as Testing Library custom renders. Relat
}
```
-Enabling this setting, you'll restrict the errors reported by the plugin related to `render` somehow to only those functions sharing a name with one of the elements of that list, or built-in `render`. The previous setting example would cause:
+Configuring this setting like that, you'll restrict the errors reported by the plugin related to `render` somehow to only those functions sharing a name with one of the elements of that list, or built-in `render`. The previous setting example would cause:
```javascript
import {
@@ -269,3 +285,70 @@ test('testing-library/custom-renders setting example', () => {
const invalidUsage = setupB();
});
```
+
+You can also set this setting to `"off"` to entirely opt-out Aggressive Renders Reporting mechanism, so only methods named `render` are reported as Testing Library render util.
+
+```json
+// .eslintrc
+{
+ "settings": {
+ "testing-library/custom-renders": "off"
+ }
+}
+```
+
+### `testing-library/custom-queries`
+
+Relates to the [Aggressive Queries Reporting mechanism](#queries). This setting accepts an array of strings or `"off"`.
+
+If you pass an array of strings to this option, it will represent a list of query names/variants that are the only valid Testing Library custom queries.
+
+Each string passed to this list of custom queries can be:
+
+- **pattern query (recommended)**: a custom query variant (suffix starting with "By") to be reported, so all query combinations around it are reported. For instance: `"ByIcon"` would report all `getByIcon()`, `getAllByIcon()`, `queryByIcon()` and `findByIcon()`.
+- **strict query**: a specific custom query name to be reported, so only that very exact query would be reported but not any related variant. For instance: `"getByIcon"` would make the plugin to report `getByIcon()` but not `getAllByIcon()`, `queryByIcon()` or `findByIcon()`.
+
+```json
+// .eslintrc
+{
+ "settings": {
+ "testing-library/custom-queries": ["ByIcon", "getByComplexText"]
+ }
+}
+```
+
+Configuring this setting like that, you'll restrict the errors reported by the plugin related to the queries to only those custom queries matching name or pattern from that list, or [built-in queries](https://testing-library.com/docs/queries/about). The previous setting example would cause:
+
+```javascript
+// ✅ this would be reported since `getByText` is a built-in Testing Library query
+getByText('foo');
+
+// ✅ this would be reported since `findAllByRole` is a built-in Testing Library query
+findAllByRole('foo');
+
+// ✅ this would be reported since `getByIcon` is a custom query matching "ByIcon" setting
+getByIcon('foo');
+
+// ✅ this would be reported since `findAllByIcon` is a custom query matching "ByIcon" setting
+findAllByIcon('foo');
+
+// ✅ this would be reported since `getByComplexText` is a custom query matching "getByComplexText" etting
+getByComplexText('foo');
+
+// ❌ this would NOT be reported since `getAllByComplexText` is a custom query but not matching any setting
+getAllByComplexText('foo');
+
+// ❌ this would NOT be reported since `findBySomethingElse` is a custom query but not matching any setting
+findBySomethingElse('foo');
+```
+
+You can also set this setting to `"off"` to entirely opt-out Aggressive Queries Reporting mechanism, so only built-in queries are reported.
+
+```json
+// .eslintrc
+{
+ "settings": {
+ "testing-library/custom-queries": "off"
+ }
+}
+```
diff --git a/lib/detect-testing-library-utils.ts b/lib/detect-testing-library-utils.ts
index f4324e1a..cf1483b4 100644
--- a/lib/detect-testing-library-utils.ts
+++ b/lib/detect-testing-library-utils.ts
@@ -27,9 +27,12 @@ import {
PRESENCE_MATCHERS,
} from './utils';
+const SETTING_OPTION_OFF = 'off' as const;
+
export type TestingLibrarySettings = {
- 'testing-library/utils-module'?: string;
- 'testing-library/custom-renders'?: string[];
+ 'testing-library/utils-module'?: string | typeof SETTING_OPTION_OFF;
+ 'testing-library/custom-renders'?: string[] | typeof SETTING_OPTION_OFF;
+ 'testing-library/custom-queries'?: string[] | typeof SETTING_OPTION_OFF;
};
export type TestingLibraryContext<
@@ -137,9 +140,12 @@ export function detectTestingLibraryUtils<
let importedUserEventLibraryNode: ImportModuleNode | null = null;
// Init options based on shared ESLint settings
- const customModule = context.settings['testing-library/utils-module'];
- const customRenders =
- context.settings['testing-library/custom-renders'] ?? [];
+ const customModuleSetting =
+ context.settings['testing-library/utils-module'];
+ const customRendersSetting =
+ context.settings['testing-library/custom-renders'];
+ const customQueriesSetting =
+ context.settings['testing-library/custom-queries'];
/**
* Small method to extract common checks to determine whether a node is
@@ -200,7 +206,7 @@ export function detectTestingLibraryUtils<
* opted-out in favour to report only those utils coming from Testing
* Library package or custom module set up on settings.
*/
- const isAggressiveModuleReportingEnabled = () => !customModule;
+ const isAggressiveModuleReportingEnabled = () => !customModuleSetting;
/**
* Determines whether aggressive render reporting is enabled or not.
@@ -211,8 +217,61 @@ export function detectTestingLibraryUtils<
* reporting mechanism is opted-out in favour to report only `render` or
* names set up on custom renders setting.
*/
- const isAggressiveRenderReportingEnabled = () =>
- !Array.isArray(customRenders) || customRenders.length === 0;
+ const isAggressiveRenderReportingEnabled = (): boolean => {
+ const isSwitchedOff = customRendersSetting === SETTING_OPTION_OFF;
+ const hasCustomOptions =
+ Array.isArray(customRendersSetting) && customRendersSetting.length > 0;
+
+ return !isSwitchedOff && !hasCustomOptions;
+ };
+
+ /**
+ * Determines whether Aggressive Reporting for queries is enabled or not.
+ *
+ * This Aggressive Reporting mechanism is considered as enabled when custom-queries setting is not set,
+ * so the plugin needs to report both built-in and custom queries.
+ * Otherwise, this Aggressive Reporting mechanism is opted-out in favour of reporting only built-in queries + those
+ * indicated in custom-queries setting.
+ */
+ const isAggressiveQueryReportingEnabled = (): boolean => {
+ const isSwitchedOff = customQueriesSetting === SETTING_OPTION_OFF;
+ const hasCustomOptions =
+ Array.isArray(customQueriesSetting) && customQueriesSetting.length > 0;
+
+ return !isSwitchedOff && !hasCustomOptions;
+ };
+
+ const getCustomModule = (): string | undefined => {
+ if (
+ !isAggressiveModuleReportingEnabled() &&
+ customModuleSetting !== SETTING_OPTION_OFF
+ ) {
+ return customModuleSetting;
+ }
+ return undefined;
+ };
+
+ const getCustomRenders = (): string[] => {
+ if (
+ !isAggressiveRenderReportingEnabled() &&
+ customRendersSetting !== SETTING_OPTION_OFF
+ ) {
+ return customRendersSetting as string[];
+ }
+
+ return [];
+ };
+
+ const getCustomQueries = (): string[] => {
+ if (
+ !isAggressiveQueryReportingEnabled() &&
+ customQueriesSetting !== SETTING_OPTION_OFF
+ ) {
+ return customQueriesSetting as string[];
+ }
+
+ return [];
+ };
// Helpers for Testing Library detection.
const getTestingLibraryImportNode: GetTestingLibraryImportNodeFn = () => {
@@ -256,25 +315,50 @@ export function detectTestingLibraryUtils<
);
};
+ /**
+ * Determines whether a given node is a reportable query,
+ * either a built-in or a custom one.
+ *
+ * Depending on Aggressive Query Reporting setting, custom queries will be
+ * reportable or not.
+ */
+ const isQuery: IsQueryFn = (node) => {
+ const hasQueryPattern = /^(get|query|find)(All)?By.+$/.test(node.name);
+ if (!hasQueryPattern) {
+ return false;
+ }
+
+ if (isAggressiveQueryReportingEnabled()) {
+ return true;
+ }
+
+ const customQueries = getCustomQueries();
+ const isBuiltInQuery = ALL_QUERIES_COMBINATIONS.includes(node.name);
+ const isReportableCustomQuery = customQueries.some((pattern) =>
+ new RegExp(pattern).test(node.name)
+ );
+ return isBuiltInQuery || isReportableCustomQuery;
+ };
+
/**
* Determines whether a given node is `get*` query variant or not.
*/
const isGetQueryVariant: IsGetQueryVariantFn = (node) => {
- return /^get(All)?By.+$/.test(node.name);
+ return isQuery(node) && node.name.startsWith('get');
};
/**
* Determines whether a given node is `query*` query variant or not.
*/
const isQueryQueryVariant: IsQueryQueryVariantFn = (node) => {
- return /^query(All)?By.+$/.test(node.name);
+ return isQuery(node) && node.name.startsWith('query');
};
/**
* Determines whether a given node is `find*` query variant or not.
*/
const isFindQueryVariant: IsFindQueryVariantFn = (node) => {
- return /^find(All)?By.+$/.test(node.name);
+ return isQuery(node) && node.name.startsWith('find');
};
/**
@@ -291,20 +375,12 @@ export function detectTestingLibraryUtils<
return isFindQueryVariant(node);
};
- /**
- * Determines whether a given node is a valid query,
- * either built-in or custom
- */
- const isQuery: IsQueryFn = (node) => {
- return isSyncQuery(node) || isAsyncQuery(node);
- };
-
const isCustomQuery: IsCustomQueryFn = (node) => {
return isQuery(node) && !ALL_QUERIES_COMBINATIONS.includes(node.name);
};
const isBuiltInQuery = (node: TSESTree.Identifier): boolean => {
- return ALL_QUERIES_COMBINATIONS.includes(node.name);
+ return isQuery(node) && ALL_QUERIES_COMBINATIONS.includes(node.name);
};
/**
@@ -486,18 +562,20 @@ export function detectTestingLibraryUtils<
return identifierNodeName.toLowerCase().includes(RENDER_NAME);
}
- return [RENDER_NAME, ...customRenders].some((validRenderName) => {
- let isMatch = false;
+ return [RENDER_NAME, ...getCustomRenders()].some(
+ (validRenderName) => {
+ let isMatch = false;
- if (validRenderName === identifierNodeName) {
- isMatch = true;
- }
+ if (validRenderName === identifierNodeName) {
+ isMatch = true;
+ }
- if (!!originalNodeName && validRenderName === originalNodeName) {
- isMatch = true;
+ if (!!originalNodeName && validRenderName === originalNodeName) {
+ isMatch = true;
+ }
+ return isMatch;
}
- return isMatch;
- });
+ );
}
);
};
@@ -747,6 +825,7 @@ export function detectTestingLibraryUtils<
// check only if custom module import not found yet so we avoid
// to override importedCustomModuleNode after it's found
+ const customModule = getCustomModule();
if (
customModule &&
!importedCustomModuleNode &&
@@ -784,6 +863,7 @@ export function detectTestingLibraryUtils<
importedTestingLibraryNode = callExpression;
}
+ const customModule = getCustomModule();
if (
!importedCustomModuleNode &&
args.some(
diff --git a/tests/create-testing-library-rule.test.ts b/tests/create-testing-library-rule.test.ts
index 93a67506..c3af7151 100644
--- a/tests/create-testing-library-rule.test.ts
+++ b/tests/create-testing-library-rule.test.ts
@@ -70,6 +70,16 @@ ruleTester.run(RULE_NAME, rule, {
import { foo } from 'custom-module-forced-report'
`,
},
+ {
+ settings: {
+ 'testing-library/utils-module': 'off',
+ },
+ code: `
+ // case: aggressive import switched off - imported from non-built-in module
+ import 'report-me';
+ require('report-me');
+ `,
+ },
// Test Cases for user-event imports
{
@@ -141,6 +151,39 @@ ruleTester.run(RULE_NAME, rule, {
const utils = render()
`,
},
+ {
+ settings: { 'testing-library/utils-module': 'test-utils' },
+ code: `
+ // case: aggressive module disabled and render coming from non-related module
+ import * as somethingElse from '@somewhere/else'
+ import { render } from '@testing-library/react'
+
+ // somethingElse.render is not coming from any module related to TL
+ const utils = somethingElse.render()
+ `,
+ },
+ {
+ settings: {
+ 'testing-library/custom-renders': ['customRender', 'renderWithRedux'],
+ },
+ code: `
+ // case: aggressive render disabled - method not matching custom-renders
+ import { renderWithProviders } from '@somewhere/else'
+
+ const utils = renderWithProviders()
+ `,
+ },
+ {
+ settings: {
+ 'testing-library/custom-renders': 'off',
+ },
+ code: `
+ // case: aggressive render switched off
+ import { renderWithProviders } from '@somewhere/else'
+
+ const utils = renderWithProviders()
+ `,
+ },
// Test Cases for presence/absence assertions
// cases: asserts not related to presence/absence
@@ -252,6 +295,40 @@ ruleTester.run(RULE_NAME, rule, {
within(container).findByRole('button')
`,
},
+ {
+ settings: {
+ 'testing-library/custom-queries': ['ByComplexText', 'findByIcon'],
+ },
+ code: `// case: custom "queryBy*" query not reported (custom-queries not matching)
+ queryByIcon('search')`,
+ },
+ {
+ settings: {
+ 'testing-library/custom-queries': ['ByComplexText', 'queryByIcon'],
+ },
+ code: `// case: custom "getBy*" query not reported (custom-queries not matching)
+ getByIcon('search')`,
+ },
+ {
+ settings: {
+ 'testing-library/custom-queries': ['ByComplexText', 'getByIcon'],
+ },
+ code: `// case: custom "findBy*" query not reported (custom-queries not matching)
+ findByIcon('search')`,
+ },
+ {
+ settings: {
+ 'testing-library/custom-queries': 'off',
+ },
+ code: `// case: custom queries not reported (aggressive queries switched off)
+ getByIcon('search');
+ queryByIcon('search');
+ findByIcon('search');
+ getAllByIcon('search');
+ queryAllByIcon('search');
+ findAllByIcon('search');
+ `,
+ },
// Test Cases for async utils
{
@@ -286,25 +363,41 @@ ruleTester.run(RULE_NAME, rule, {
{
settings: {
'testing-library/utils-module': 'test-utils',
+ 'testing-library/custom-renders': ['customRender'],
+ 'testing-library/custom-queries': ['ByIcon', 'ByComplexText'],
},
code: `
- // case: matching custom settings
+ // case: not matching any of the custom settings
+ import { renderWithRedux } from 'test-utils'
import { render } from 'other-utils'
import { somethingElse } from 'another-module'
const foo = require('bar')
- const utils = render();
+ const utils = render()
+ renderWithRedux()
+ getBySomethingElse('foo')
+ queryBySomethingElse('foo')
+ findBySomethingElse('foo')
`,
},
{
- settings: { 'testing-library/utils-module': 'test-utils' },
+ settings: {
+ 'testing-library/utils-module': 'off',
+ 'testing-library/custom-renders': 'off',
+ 'testing-library/custom-queries': 'off',
+ },
code: `
- // case: aggressive module disabled and render coming from non-related module
- import * as somethingElse from '@somewhere/else'
- import { render } from '@testing-library/react'
+ // case: all settings switched off + only custom utils used
+ import { renderWithRedux } from 'test-utils'
+ import { render } from 'other-utils'
+ import { somethingElse } from 'another-module'
+ const foo = require('bar')
- // somethingElse.render is not coming from any module related to TL
- const utils = somethingElse.render()
+ const utils = render()
+ renderWithRedux()
+ getBySomethingElse('foo')
+ queryBySomethingElse('foo')
+ findBySomethingElse('foo')
`,
},
@@ -596,6 +689,20 @@ ruleTester.run(RULE_NAME, rule, {
`,
errors: [{ line: 5, column: 25, messageId: 'renderError' }],
},
+ {
+ settings: {
+ 'testing-library/utils-module': 'test-utils',
+ },
+ code: `
+ // case: matching all custom settings
+ import { render } from 'test-utils'
+ import { somethingElse } from 'another-module'
+ const foo = require('bar')
+
+ const utils = render();
+ `,
+ errors: [{ line: 7, column: 21, messageId: 'renderError' }],
+ },
// Test Cases for presence/absence assertions
{
@@ -727,6 +834,36 @@ ruleTester.run(RULE_NAME, rule, {
`,
errors: [{ line: 3, column: 25, messageId: 'findByError' }],
},
+ {
+ settings: {
+ 'testing-library/custom-queries': ['ByIcon'],
+ },
+ code: `
+ // case: built-in "queryBy*" query reported (aggressive reporting disabled)
+ queryByRole('button')
+ `,
+ errors: [{ line: 3, column: 7, messageId: 'queryByError' }],
+ },
+ {
+ settings: {
+ 'testing-library/custom-queries': ['ByIcon'],
+ },
+ code: `
+ // case: built-in "queryBy*" query reported (aggressive reporting disabled)
+ within(container).queryByRole('button')
+ `,
+ errors: [{ line: 3, column: 25, messageId: 'queryByError' }],
+ },
+ {
+ settings: {
+ 'testing-library/custom-queries': ['ByIcon'],
+ },
+ code: `
+ // case: built-in "findBy*" query reported (aggressive reporting disabled)
+ findByRole('button')
+ `,
+ errors: [{ line: 3, column: 7, messageId: 'findByError' }],
+ },
{
code: `
// case: custom "queryBy*" query reported without import (aggressive reporting)
@@ -755,6 +892,50 @@ ruleTester.run(RULE_NAME, rule, {
`,
errors: [{ line: 3, column: 25, messageId: 'customQueryError' }],
},
+ {
+ settings: {
+ 'testing-library/custom-queries': ['queryByIcon', 'ByComplexText'],
+ },
+ code: `
+ // case: custom "queryBy*" query reported without import (custom-queries set)
+ queryByIcon('search')
+ `,
+ errors: [{ line: 3, column: 7, messageId: 'customQueryError' }],
+ },
+ {
+ settings: {
+ 'testing-library/custom-queries': ['ByIcon', 'ByComplexText'],
+ },
+ code: `
+ // case: custom "queryBy*" query reported without import using within (custom-queries set)
+ within(container).queryByIcon('search')
+ `,
+ errors: [{ line: 3, column: 25, messageId: 'customQueryError' }],
+ },
+ {
+ settings: {
+ 'testing-library/custom-queries': [
+ 'queryByIcon',
+ 'ByComplexText',
+ 'findByIcon',
+ ],
+ },
+ code: `
+ // case: custom "findBy*" query reported without import (custom-queries set)
+ findByIcon('search')
+ `,
+ errors: [{ line: 3, column: 7, messageId: 'customQueryError' }],
+ },
+ {
+ settings: {
+ 'testing-library/custom-queries': ['ByIcon', 'ByComplexText'],
+ },
+ code: `
+ // case: custom "findBy*" query reported without import using within (custom-queries set)
+ within(container).findByIcon('search')
+ `,
+ errors: [{ line: 3, column: 25, messageId: 'customQueryError' }],
+ },
{
settings: {
'testing-library/utils-module': 'test-utils',
@@ -806,39 +987,77 @@ ruleTester.run(RULE_NAME, rule, {
settings: {
'testing-library/custom-renders': ['customRender', 'renderWithRedux'],
'testing-library/utils-module': 'test-utils',
+ 'testing-library/custom-queries': ['ByIcon', 'findByComplexText'],
},
code: `
// case: aggressive reporting disabled - matching all custom settings
import { renderWithRedux, waitFor, screen } from 'test-utils'
+ import { findByComplexText } from 'custom-queries'
- const { getByRole } = renderWithRedux()
+ const { getByRole, getAllByIcon } = renderWithRedux()
const el = getByRole('button')
+ const iconButtons = getAllByIcon('search')
waitFor(() => {})
+ findByComplexText('foo')
+
`,
errors: [
- { line: 5, column: 29, messageId: 'renderError' },
- { line: 6, column: 18, messageId: 'getByError' },
+ { line: 6, column: 43, messageId: 'renderError' },
+ { line: 7, column: 18, messageId: 'getByError' },
+ { line: 8, column: 27, messageId: 'customQueryError' },
{
- line: 7,
+ line: 9,
column: 7,
messageId: 'asyncUtilError',
data: { utilName: 'waitFor' },
},
+ { line: 10, column: 7, messageId: 'customQueryError' },
],
},
{
settings: {
- 'testing-library/utils-module': 'test-utils',
+ 'testing-library/utils-module': 'off',
+ 'testing-library/custom-renders': 'off',
+ 'testing-library/custom-queries': 'off',
},
code: `
- // case: matching all custom settings
- import { render } from 'test-utils'
- import { somethingElse } from 'another-module'
- const foo = require('bar')
+ // case: built-in utils reported when all aggressive reporting completely switched off
+ import { render, screen, waitFor } from '@testing-library/react';
+ import userEvent from '@testing-library/user-event'
const utils = render();
+ const el = utils.getByText('foo');
+ screen.findByRole('button');
+ waitFor();
+ userEvent.click(el);
`,
- errors: [{ line: 7, column: 21, messageId: 'renderError' }],
+ errors: [
+ {
+ line: 6,
+ column: 21,
+ messageId: 'renderError',
+ },
+ {
+ line: 7,
+ column: 24,
+ messageId: 'getByError',
+ },
+ {
+ line: 8,
+ column: 14,
+ messageId: 'findByError',
+ },
+ {
+ line: 9,
+ column: 7,
+ messageId: 'asyncUtilError',
+ },
+ {
+ line: 10,
+ column: 17,
+ messageId: 'userEventError',
+ },
+ ],
},
],
});
diff --git a/tests/lib/rules/await-async-query.test.ts b/tests/lib/rules/await-async-query.test.ts
index fa56dfd7..32d17459 100644
--- a/tests/lib/rules/await-async-query.test.ts
+++ b/tests/lib/rules/await-async-query.test.ts
@@ -219,7 +219,7 @@ ruleTester.run(RULE_NAME, rule, {
// non-matching query is valid
`
- test('An valid example test', async () => {
+ test('A valid example test', async () => {
const example = findText("my example")
})
`,
@@ -231,12 +231,36 @@ ruleTester.run(RULE_NAME, rule, {
return somethingElse(element)
}
- test('An valid example test', async () => {
+ test('A valid example test', async () => {
// findButton doesn't match async query pattern
const button = findButton()
})
`,
+ // unhandled promise from custom query not matching custom-queries setting is valid
+ {
+ settings: {
+ 'testing-library/custom-queries': ['queryByIcon', 'ByComplexText'],
+ },
+ code: `
+ test('A valid example test', () => {
+ const element = findByIcon('search')
+ })
+ `,
+ },
+
+ // unhandled promise from custom query with aggressive query switched off is valid
+ {
+ settings: {
+ 'testing-library/custom-queries': 'off',
+ },
+ code: `
+ test('A valid example test', () => {
+ const element = findByIcon('search')
+ })
+ `,
+ },
+
// edge case for coverage
// return non-matching query and other than Identifier or CallExpression
`
@@ -245,7 +269,7 @@ ruleTester.run(RULE_NAME, rule, {
return element ? findSomethingElse(element) : null
}
- test('An valid example test', async () => {
+ test('A valid example test', async () => {
someSetup()
})
`,
@@ -365,7 +389,7 @@ ruleTester.run(RULE_NAME, rule, {
const element = queryWrapper()
})
- test("An valid example test", async () => {
+ test("An invalid example test", async () => {
const element = await queryWrapper()
})
`,
@@ -387,7 +411,7 @@ ruleTester.run(RULE_NAME, rule, {
const element = queryWrapper()
})
- test("An valid example test", async () => {
+ test("An invalid example test", async () => {
const element = await queryWrapper()
})
`,
@@ -405,12 +429,25 @@ ruleTester.run(RULE_NAME, rule, {
const element = queryWrapper()
})
- test("An valid example test", async () => {
+ test("An invalid example test", async () => {
const element = await queryWrapper()
})
`,
errors: [{ messageId: 'asyncQueryWrapper', line: 5, column: 27 }],
} as const)
),
+
+ // unhandled promise from custom query matching custom-queries setting is invalid
+ {
+ settings: {
+ 'testing-library/custom-queries': ['ByIcon', 'getByComplexText'],
+ },
+ code: `
+ test('An invalid example test', () => {
+ const element = findByIcon('search')
+ })
+ `,
+ errors: [{ messageId: 'awaitAsyncQuery', line: 3, column: 25 }],
+ },
],
});
diff --git a/tests/lib/rules/no-await-sync-query.test.ts b/tests/lib/rules/no-await-sync-query.test.ts
index a3a27751..a6655d03 100644
--- a/tests/lib/rules/no-await-sync-query.test.ts
+++ b/tests/lib/rules/no-await-sync-query.test.ts
@@ -39,6 +39,19 @@ ruleTester.run(RULE_NAME, rule, {
});
}
`,
+
+ // awaited custom sync query not matching custom-queries setting is valid
+ {
+ settings: {
+ 'testing-library/custom-queries': ['queryByIcon', 'ByComplexText'],
+ },
+ code: `
+ test('A valid example test', async () => {
+ const element = await getByIcon('search')
+ })
+ `,
+ },
+
// sync queries without await inside assert are valid
...SYNC_QUERIES_COMBINATIONS.map((query) => ({
code: `() => {
@@ -233,5 +246,18 @@ ruleTester.run(RULE_NAME, rule, {
`,
errors: [{ messageId: 'noAwaitSyncQuery', line: 4, column: 38 }],
},
+
+ // awaited custom sync query matching custom-queries setting is invalid
+ {
+ settings: {
+ 'testing-library/custom-queries': ['queryByIcon', 'ByComplexText'],
+ },
+ code: `
+ test('A valid example test', async () => {
+ const element = await queryByIcon('search')
+ })
+ `,
+ errors: [{ messageId: 'noAwaitSyncQuery', line: 3, column: 31 }],
+ },
],
});
diff --git a/tests/lib/rules/prefer-screen-queries.test.ts b/tests/lib/rules/prefer-screen-queries.test.ts
index 8a8dcc1f..b6eeff11 100644
--- a/tests/lib/rules/prefer-screen-queries.test.ts
+++ b/tests/lib/rules/prefer-screen-queries.test.ts
@@ -65,6 +65,19 @@ ruleTester.run(RULE_NAME, rule, {
})
`
),
+ ...CUSTOM_QUERY_COMBINATIONS.map((query) => ({
+ settings: {
+ 'testing-library/custom-queries': [query, 'ByComplexText'],
+ },
+ code: `
+ import { render } from '@testing-library/react'
+
+ test("custom queries + custom-queries setting, since they can't be used through screen", () => {
+ const { ${query} } = render(foo)
+ ${query}('bar')
+ })
+ `,
+ })),
{
code: `
const screen = render(baz);
diff --git a/tests/lib/rules/render-result-naming-convention.test.ts b/tests/lib/rules/render-result-naming-convention.test.ts
index 9043ca5f..0bf82ed6 100644
--- a/tests/lib/rules/render-result-naming-convention.test.ts
+++ b/tests/lib/rules/render-result-naming-convention.test.ts
@@ -162,13 +162,37 @@ ruleTester.run(RULE_NAME, rule, {
};
test(
- 'both render and module aggressive reporting disabled - should not report render result called "wrapper" from nont-related renamed custom render wrapped in a function',
+ 'both render and module aggressive reporting disabled - should not report render result called "wrapper" from non-related renamed custom render wrapped in a function',
async () => {
const wrapper = setup();
await wrapper.findByRole('button');
});
`,
},
+ {
+ settings: {
+ 'testing-library/utils-module': 'off',
+ 'testing-library/custom-renders': 'off',
+ },
+ code: `
+ import { customRender as myRender } from 'test-utils';
+ import { render } from 'non-related'
+
+ const setup = () => {
+ return render();
+ };
+
+ test(
+ 'both render and module aggressive reporting switched off - should not report render result called "wrapper"',
+ async () => {
+ const wrapper1 = render();
+ const wrapper2 = myRender();
+ const wrapper3 = setup();
+
+ await wrapper1.findByRole('button');
+ });
+ `,
+ },
],
invalid: [
{