diff --git a/README.md b/README.md index ad167abf..197d055d 100644 --- a/README.md +++ b/README.md @@ -338,6 +338,7 @@ module.exports = [ | [no-node-access](docs/rules/no-node-access.md) | Disallow direct Node access | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-svelte][] ![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-svelte][] ![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-svelte][] ![badge-vue][] | | | +| [no-test-id-queries](docs/rules/no-test-id-queries.md) | Ensure no `data-testid` queries are used | | | | | [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-svelte][] ![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-svelte][] ![badge-vue][] | | | diff --git a/docs/rules/no-test-id-queries.md b/docs/rules/no-test-id-queries.md new file mode 100644 index 00000000..958b6bf7 --- /dev/null +++ b/docs/rules/no-test-id-queries.md @@ -0,0 +1,32 @@ +# Ensure no `data-testid` queries are used (`testing-library/no-test-id-queries`) + + + +## Rule Details + +This rule aims to reduce the usage of `*ByTestId` queries in your tests. + +When using `*ByTestId` queries, you are coupling your tests to the implementation details of your components, and not to how they behave and being used. + +Prefer using queries that are more related to the user experience, like `getByRole`, `getByLabelText`, etc. + +Example of **incorrect** code for this rule: + +```js +const button = queryByTestId('my-button'); +const input = screen.queryByTestId('my-input'); +``` + +Examples of **correct** code for this rule: + +```js +const button = screen.getByRole('button'); +const input = screen.getByRole('textbox'); +``` + +## Further Reading + +- [about `getByTestId`](https://testing-library.com/docs/queries/bytestid) +- [about `getByRole`](https://testing-library.com/docs/queries/byrole) +- [about `getByLabelText`](https://testing-library.com/docs/queries/bylabeltext) +- [Common mistakes with React Testing Library - Not querying by text](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library#not-querying-by-text) diff --git a/lib/rules/no-test-id-queries.ts b/lib/rules/no-test-id-queries.ts new file mode 100644 index 00000000..7420f8d8 --- /dev/null +++ b/lib/rules/no-test-id-queries.ts @@ -0,0 +1,47 @@ +import { TSESTree } from '@typescript-eslint/utils'; + +import { createTestingLibraryRule } from '../create-testing-library-rule'; +import { ALL_QUERIES_VARIANTS } from '../utils'; + +export const RULE_NAME = 'no-test-id-queries'; +export type MessageIds = 'noTestIdQueries'; +type Options = []; + +const QUERIES_REGEX = `/^(${ALL_QUERIES_VARIANTS.join('|')})TestId$/`; + +export default createTestingLibraryRule({ + name: RULE_NAME, + meta: { + type: 'problem', + docs: { + description: 'Ensure no `data-testid` queries are used', + recommendedConfig: { + dom: false, + angular: false, + react: false, + vue: false, + svelte: false, + marko: false, + }, + }, + messages: { + noTestIdQueries: + 'Using `data-testid` queries is not recommended. Use a more descriptive query instead.', + }, + schema: [], + }, + defaultOptions: [], + + create(context) { + return { + [`CallExpression[callee.property.name=${QUERIES_REGEX}], CallExpression[callee.name=${QUERIES_REGEX}]`]( + node: TSESTree.CallExpression + ) { + context.report({ + node, + messageId: 'noTestIdQueries', + }); + }, + }; + }, +}); diff --git a/tests/index.test.ts b/tests/index.test.ts index 0695abde..03cdbe2b 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 = 27; +const numberOfRules = 28; const ruleNames = Object.keys(plugin.rules); // eslint-disable-next-line jest/expect-expect diff --git a/tests/lib/rules/no-test-id-queries.test.ts b/tests/lib/rules/no-test-id-queries.test.ts new file mode 100644 index 00000000..23ba3335 --- /dev/null +++ b/tests/lib/rules/no-test-id-queries.test.ts @@ -0,0 +1,86 @@ +import rule, { RULE_NAME } from '../../../lib/rules/no-test-id-queries'; +import { createRuleTester } from '../test-utils'; + +const ruleTester = createRuleTester(); + +const SUPPORTED_TESTING_FRAMEWORKS = [ + '@testing-library/dom', + '@testing-library/angular', + '@testing-library/react', + '@testing-library/vue', + '@marko/testing-library', +]; + +const QUERIES = [ + 'getByTestId', + 'queryByTestId', + 'getAllByTestId', + 'queryAllByTestId', + 'findByTestId', + 'findAllByTestId', +]; + +ruleTester.run(RULE_NAME, rule, { + valid: [ + ` + import { render } from '@testing-library/react'; + + test('test', async () => { + const { getByRole } = render(); + + expect(getByRole('button')).toBeInTheDocument(); + }); + `, + + ` + import { render } from '@testing-library/react'; + + test('test', async () => { + render(); + + expect(getTestId('button')).toBeInTheDocument(); + }); + `, + ], + + invalid: SUPPORTED_TESTING_FRAMEWORKS.flatMap((framework) => + QUERIES.flatMap((query) => [ + { + code: ` + import { render } from '${framework}'; + + test('test', async () => { + const { ${query} } = render(); + + expect(${query}('my-test-id')).toBeInTheDocument(); + }); + `, + errors: [ + { + messageId: 'noTestIdQueries', + line: 7, + column: 14, + }, + ], + }, + { + code: ` + import { render, screen } from '${framework}'; + + test('test', async () => { + render(); + + expect(screen.${query}('my-test-id')).toBeInTheDocument(); + }); + `, + errors: [ + { + messageId: 'noTestIdQueries', + line: 7, + column: 14, + }, + ], + }, + ]) + ), +});