diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 88d8f44..1fedf1d 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -48,6 +48,7 @@ body: - NgRx - Jest - Vitest + - 'React Testing Library' - Cypress - Playwright - Storybook diff --git a/.github/ISSUE_TEMPLATE/change-request.yml b/.github/ISSUE_TEMPLATE/change-request.yml index 7ce4cbd..3750684 100644 --- a/.github/ISSUE_TEMPLATE/change-request.yml +++ b/.github/ISSUE_TEMPLATE/change-request.yml @@ -46,6 +46,7 @@ body: - NgRx - Jest - Vitest + - 'React Testing Library' - Cypress - Playwright - Storybook diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml index d60d659..151bb1e 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.yml +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -46,6 +46,7 @@ body: - NgRx - Jest - Vitest + - 'React Testing Library' - Cypress - Playwright - Storybook diff --git a/README.md b/README.md index 7968085..f9c8d85 100644 --- a/README.md +++ b/README.md @@ -9,19 +9,20 @@ Recommended ESLint presets by [Code PushUp](https://github.com/code-pushup/cli/t ## ⚙️ Configs -| Stack | Config | Description | -| :-------------------------------------------------: | :--------------------------------- | :------------------------------------------------------------------ | -| ![javascript](./docs/icons/material/javascript.png) | [javascript](./docs/javascript.md) | Default config, suitable for any **JavaScript/TypeScript** project. | -| ![typescript](./docs/icons/material/typescript.png) | [typescript](./docs/typescript.md) | Config for strict **TypeScript** projects. | -| ![nodejs](./docs/icons/material/nodejs.png) | [node](./docs/node.md) | Config for **Node.js** projects. | -| ![angular](./docs/icons/material/angular.png) | [angular](./docs/angular.md) | Config for **Angular** projects. | -| ![ngrx](./docs/icons/other/ngrx.png) | [ngrx](./docs/ngrx.md) | Config for **Angular** projects using **NgRx** library. | -| ![graphql](./docs/icons/material/graphql.png) | [graphql](./docs/graphql.md) | Config for **GraphQL servers** implemented in Node.js. | -| ![jest](./docs/icons/material/jest.png) | [jest](./docs/jest.md) | Config for projects using **Jest** for testing. | -| ![vitest](./docs/icons/material/vitest.png) | [vitest](./docs/vitest.md) | Config for projects using **Vitest** for testing. | -| ![cypress](./docs/icons/material/cypress.png) | [cypress](./docs/cypress.md) | Config for projects using **Cypress** for testing. | -| ![playwright](./docs/icons/material/playwright.png) | [playwright](./docs/playwright.md) | Config for projects using **Playwright** for testing. | -| ![storybook](./docs/icons/material/storybook.png) | [storybook](./docs/storybook.md) | Config for projects using **Storybook** for UI components. | +| Stack | Config | Description | +| :--------------------------------------------------------: | :------------------------------------------------------- | :------------------------------------------------------------------ | +| ![javascript](./docs/icons/material/javascript.png) | [javascript](./docs/javascript.md) | Default config, suitable for any **JavaScript/TypeScript** project. | +| ![typescript](./docs/icons/material/typescript.png) | [typescript](./docs/typescript.md) | Config for strict **TypeScript** projects. | +| ![nodejs](./docs/icons/material/nodejs.png) | [node](./docs/node.md) | Config for **Node.js** projects. | +| ![angular](./docs/icons/material/angular.png) | [angular](./docs/angular.md) | Config for **Angular** projects. | +| ![ngrx](./docs/icons/other/ngrx.png) | [ngrx](./docs/ngrx.md) | Config for **Angular** projects using **NgRx** library. | +| ![graphql](./docs/icons/material/graphql.png) | [graphql](./docs/graphql.md) | Config for **GraphQL servers** implemented in Node.js. | +| ![jest](./docs/icons/material/jest.png) | [jest](./docs/jest.md) | Config for projects using **Jest** for testing. | +| ![vitest](./docs/icons/material/vitest.png) | [vitest](./docs/vitest.md) | Config for projects using **Vitest** for testing. | +| ![cypress](./docs/icons/material/cypress.png) | [cypress](./docs/cypress.md) | Config for projects using **Cypress** for testing. | +| ![playwright](./docs/icons/material/playwright.png) | [playwright](./docs/playwright.md) | Config for projects using **Playwright** for testing. | +| ![storybook](./docs/icons/material/storybook.png) | [storybook](./docs/storybook.md) | Config for projects using **Storybook** for UI components. | +| ![testing-library](./docs/icons/other/testing-library.png) | [react-testing-library](./docs/react-testing-library.md) | Config for projects using **React Testing Library** for testing. | Some configs extend other configs, as illustrated below. So, for example, extending `angular` config implicitly extends `typescript` and `javascript` configs as well. @@ -71,29 +72,30 @@ Depending on your tech stack, you may wish to extend other configs as well ([lis All peer dependencies used by `@code-pushup/eslint-config` are listed below, along with their supported versions. Only the default config's dependencies are required, others are optional. -| | NPM package | Version | Required | -| :-------------------------------------------------: | :--------------------------------------------------------------------------------------------------- | :--------: | :------: | -| ![eslint](./docs/icons/material/eslint.png) | [eslint](https://www.npmjs.com/package/eslint) | `^9.0.0` | ✅ | -| ![eslint](./docs/icons/material/eslint.png) | [@eslint/js](https://www.npmjs.com/package/@eslint/js) | `^9.0.0` | ✅ | -| ![lambda](./docs/icons/icons8/lambda.png) | [eslint-plugin-functional](https://www.npmjs.com/package/eslint-plugin-functional) | `^7.0.0` | ✅ | -| ![import](./docs/icons/icons8/import.png) | [eslint-plugin-import](https://www.npmjs.com/package/eslint-plugin-import) | `^2.31.0` | ✅ | -| ![import](./docs/icons/icons8/import.png) | [eslint-import-resolver-typescript](https://www.npmjs.com/package/eslint-import-resolver-typescript) | `^3.0.0` | | -| ![promise](./docs/icons/icons8/promise.png) | [eslint-plugin-promise](https://www.npmjs.com/package/eslint-plugin-promise) | `>=6.4.0` | ✅ | -| ![sonar](./docs/icons/other/sonar.png) | [eslint-plugin-sonarjs](https://www.npmjs.com/package/eslint-plugin-sonarjs) | `^1.0.4` | ✅ | -| ![unicorn](./docs/icons/icons8/unicorn.png) | [eslint-plugin-unicorn](https://www.npmjs.com/package/eslint-plugin-unicorn) | `>=50.0.0` | ✅ | -| ![global](./docs/icons/icons8/global.png) | [globals](https://www.npmjs.com/package/globals) | `>=14.0.0` | ✅ | -| ![typescript](./docs/icons/material/typescript.png) | [typescript-eslint](https://www.npmjs.com/package/typescript-eslint) | `^8.0.0` | ✅ | -| ![graphql](./docs/icons/material/graphql.png) | [@graphql-eslint/eslint-plugin](https://www.npmjs.com/package/@graphql-eslint/eslint-plugin) | `^3.0.0` | | -| ![ngrx](./docs/icons/other/ngrx.png) | [@ngrx/eslint-plugin](https://www.npmjs.com/package/@ngrx/eslint-plugin) | `^18.0.0` | | -| ![angular](./docs/icons/material/angular.png) | [angular-eslint](https://www.npmjs.com/package/angular-eslint) | `^18.0.0` | | -| ![cypress](./docs/icons/material/cypress.png) | [eslint-plugin-cypress](https://www.npmjs.com/package/eslint-plugin-cypress) | `>=3.3.0` | | -| ![jest](./docs/icons/material/jest.png) | [eslint-plugin-jest](https://www.npmjs.com/package/eslint-plugin-jest) | `^28.8.0` | | -| ![test](./docs/icons/icons8/test.png) | [eslint-plugin-jest-formatting](https://www.npmjs.com/package/eslint-plugin-jest-formatting) | `^3.0.0` | | -| ![nodejs](./docs/icons/material/nodejs.png) | [eslint-plugin-n](https://www.npmjs.com/package/eslint-plugin-n) | `>=17.0.0` | | -| ![playwright](./docs/icons/material/playwright.png) | [eslint-plugin-playwright](https://www.npmjs.com/package/eslint-plugin-playwright) | `^2.1.0` | | -| ![rxjs](./docs/icons/other/rxjs.png) | [eslint-plugin-rxjs-x](https://www.npmjs.com/package/eslint-plugin-rxjs-x) | `>=0.6.0` | | -| ![storybook](./docs/icons/material/storybook.png) | [eslint-plugin-storybook](https://www.npmjs.com/package/eslint-plugin-storybook) | `>=0.10.0` | | -| ![vitest](./docs/icons/material/vitest.png) | [eslint-plugin-vitest](https://www.npmjs.com/package/eslint-plugin-vitest) | `>=0.5.0` | | +| | NPM package | Version | Required | +| :--------------------------------------------------------: | :--------------------------------------------------------------------------------------------------- | :--------: | :------: | +| ![eslint](./docs/icons/material/eslint.png) | [eslint](https://www.npmjs.com/package/eslint) | `^9.0.0` | ✅ | +| ![eslint](./docs/icons/material/eslint.png) | [@eslint/js](https://www.npmjs.com/package/@eslint/js) | `^9.0.0` | ✅ | +| ![lambda](./docs/icons/icons8/lambda.png) | [eslint-plugin-functional](https://www.npmjs.com/package/eslint-plugin-functional) | `^7.0.0` | ✅ | +| ![import](./docs/icons/icons8/import.png) | [eslint-plugin-import](https://www.npmjs.com/package/eslint-plugin-import) | `^2.31.0` | ✅ | +| ![import](./docs/icons/icons8/import.png) | [eslint-import-resolver-typescript](https://www.npmjs.com/package/eslint-import-resolver-typescript) | `^3.0.0` | | +| ![promise](./docs/icons/icons8/promise.png) | [eslint-plugin-promise](https://www.npmjs.com/package/eslint-plugin-promise) | `>=6.4.0` | ✅ | +| ![sonar](./docs/icons/other/sonar.png) | [eslint-plugin-sonarjs](https://www.npmjs.com/package/eslint-plugin-sonarjs) | `^1.0.4` | ✅ | +| ![unicorn](./docs/icons/icons8/unicorn.png) | [eslint-plugin-unicorn](https://www.npmjs.com/package/eslint-plugin-unicorn) | `>=50.0.0` | ✅ | +| ![global](./docs/icons/icons8/global.png) | [globals](https://www.npmjs.com/package/globals) | `>=14.0.0` | ✅ | +| ![typescript](./docs/icons/material/typescript.png) | [typescript-eslint](https://www.npmjs.com/package/typescript-eslint) | `^8.0.0` | ✅ | +| ![graphql](./docs/icons/material/graphql.png) | [@graphql-eslint/eslint-plugin](https://www.npmjs.com/package/@graphql-eslint/eslint-plugin) | `^3.0.0` | | +| ![ngrx](./docs/icons/other/ngrx.png) | [@ngrx/eslint-plugin](https://www.npmjs.com/package/@ngrx/eslint-plugin) | `^18.0.0` | | +| ![angular](./docs/icons/material/angular.png) | [angular-eslint](https://www.npmjs.com/package/angular-eslint) | `^18.0.0` | | +| ![cypress](./docs/icons/material/cypress.png) | [eslint-plugin-cypress](https://www.npmjs.com/package/eslint-plugin-cypress) | `>=3.3.0` | | +| ![jest](./docs/icons/material/jest.png) | [eslint-plugin-jest](https://www.npmjs.com/package/eslint-plugin-jest) | `^28.8.0` | | +| ![test](./docs/icons/icons8/test.png) | [eslint-plugin-jest-formatting](https://www.npmjs.com/package/eslint-plugin-jest-formatting) | `^3.0.0` | | +| ![nodejs](./docs/icons/material/nodejs.png) | [eslint-plugin-n](https://www.npmjs.com/package/eslint-plugin-n) | `>=17.0.0` | | +| ![playwright](./docs/icons/material/playwright.png) | [eslint-plugin-playwright](https://www.npmjs.com/package/eslint-plugin-playwright) | `^2.1.0` | | +| ![rxjs](./docs/icons/other/rxjs.png) | [eslint-plugin-rxjs-x](https://www.npmjs.com/package/eslint-plugin-rxjs-x) | `>=0.6.0` | | +| ![storybook](./docs/icons/material/storybook.png) | [eslint-plugin-storybook](https://www.npmjs.com/package/eslint-plugin-storybook) | `>=0.10.0` | | +| ![testing-library](./docs/icons/other/testing-library.png) | [eslint-plugin-testing-library](https://www.npmjs.com/package/eslint-plugin-testing-library) | `^7.1.1` | | +| ![vitest](./docs/icons/material/vitest.png) | [eslint-plugin-vitest](https://www.npmjs.com/package/eslint-plugin-vitest) | `>=0.5.0` | | ### 🧪 Test overrides diff --git a/docs/icons/other/testing-library.png b/docs/icons/other/testing-library.png new file mode 100644 index 0000000..f21d252 Binary files /dev/null and b/docs/icons/other/testing-library.png differ diff --git a/docs/react-testing-library.md b/docs/react-testing-library.md new file mode 100644 index 0000000..7bb6df7 --- /dev/null +++ b/docs/react-testing-library.md @@ -0,0 +1,60 @@ +# `react-testing-library` config + +Config for projects using **React Testing Library** for testing. + +## 🏗️ Setup + +1. If you haven't already, make sure to [install `@code-pushup/eslint-config` and its required peer dependencies](../README.md#🏗️-setup). +2. Since this plugin requires additional peer dependencies, you have to install them as well: + + ```sh + npm install -D eslint-plugin-testing-library + ``` + +3. Add to your `eslint.config.js` file: + + ```js + import react-testing-library from '@code-pushup/eslint-config/react-testing-library.js'; + import tseslint from 'typescript-eslint'; + + export default tseslint.config(...react-testing-library); + ``` + +## 📏 Rules (25) + +> 🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).
💡 Manually fixable by [editor suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions). + +### 🚨 Errors (9) + +| Plugin | Rule | Options | Autofix | Overrides | +| :----------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :--------------------------------------------------------------------------------------------------------------------------------------- | :-----: | :-------: | +| [![testing-library](./icons/other/testing-library.png)](undefined) | [await-async-events](https://github.com/testing-library/eslint-plugin-testing-library/tree/main/docs/rules/await-async-events.md)
Enforce promises from async event methods are handled |
eventModule: userEvent
{
  "eventModule": "userEvent"
}
| 🔧 | | +| [![testing-library](./icons/other/testing-library.png)](undefined) | [await-async-queries](https://github.com/testing-library/eslint-plugin-testing-library/tree/main/docs/rules/await-async-queries.md)
Enforce promises from async queries to be handled | | | | +| [![testing-library](./icons/other/testing-library.png)](undefined) | [await-async-utils](https://github.com/testing-library/eslint-plugin-testing-library/tree/main/docs/rules/await-async-utils.md)
Enforce promises from async utils to be awaited properly | | | | +| [![testing-library](./icons/other/testing-library.png)](undefined) | [no-dom-import](https://github.com/testing-library/eslint-plugin-testing-library/tree/main/docs/rules/no-dom-import.md)
Disallow importing from DOM Testing Library |
react
"react"
| 🔧 | | +| [![testing-library](./icons/other/testing-library.png)](undefined) | [no-global-regexp-flag-in-query](https://github.com/testing-library/eslint-plugin-testing-library/tree/main/docs/rules/no-global-regexp-flag-in-query.md)
Disallow the use of the global RegExp flag (/g) in queries | | 🔧 | | +| [![testing-library](./icons/other/testing-library.png)](undefined) | [no-promise-in-fire-event](https://github.com/testing-library/eslint-plugin-testing-library/tree/main/docs/rules/no-promise-in-fire-event.md)
Disallow the use of promises passed to a `fireEvent` method | | | | +| [![testing-library](./icons/other/testing-library.png)](undefined) | [no-unnecessary-act](https://github.com/testing-library/eslint-plugin-testing-library/tree/main/docs/rules/no-unnecessary-act.md)
Disallow wrapping Testing Library utils or empty callbacks in `act` | | | | +| [![testing-library](./icons/other/testing-library.png)](undefined) | [no-wait-for-side-effects](https://github.com/testing-library/eslint-plugin-testing-library/tree/main/docs/rules/no-wait-for-side-effects.md)
Disallow the use of side effects in `waitFor` | | | | +| [![testing-library](./icons/other/testing-library.png)](undefined) | [no-wait-for-snapshot](https://github.com/testing-library/eslint-plugin-testing-library/tree/main/docs/rules/no-wait-for-snapshot.md)
Ensures no snapshot is generated inside of a `waitFor` call | | | | + +### ⚠️ Warnings (16) + +| Plugin | Rule | Options | Autofix | Overrides | +| :----------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------ | :-----: | :-------: | +| [![testing-library](./icons/other/testing-library.png)](undefined) | [no-await-sync-events](https://github.com/testing-library/eslint-plugin-testing-library/tree/main/docs/rules/no-await-sync-events.md)
Disallow unnecessary `await` for sync events | | | | +| [![testing-library](./icons/other/testing-library.png)](undefined) | [no-await-sync-queries](https://github.com/testing-library/eslint-plugin-testing-library/tree/main/docs/rules/no-await-sync-queries.md)
Disallow unnecessary `await` for sync queries | | | | +| [![testing-library](./icons/other/testing-library.png)](undefined) | [no-container](https://github.com/testing-library/eslint-plugin-testing-library/tree/main/docs/rules/no-container.md)
Disallow the use of `container` methods | | | | +| [![testing-library](./icons/other/testing-library.png)](undefined) | [no-debugging-utils](https://github.com/testing-library/eslint-plugin-testing-library/tree/main/docs/rules/no-debugging-utils.md)
Disallow the use of debugging utilities like `debug` | | | | +| [![testing-library](./icons/other/testing-library.png)](undefined) | [no-manual-cleanup](https://github.com/testing-library/eslint-plugin-testing-library/tree/main/docs/rules/no-manual-cleanup.md)
Disallow the use of `cleanup` | | | | +| [![testing-library](./icons/other/testing-library.png)](undefined) | [no-node-access](https://github.com/testing-library/eslint-plugin-testing-library/tree/main/docs/rules/no-node-access.md)
Disallow direct Node access | | | | +| [![testing-library](./icons/other/testing-library.png)](undefined) | [no-render-in-lifecycle](https://github.com/testing-library/eslint-plugin-testing-library/tree/main/docs/rules/no-render-in-lifecycle.md)
Disallow the use of `render` in testing frameworks setup functions | | | | +| [![testing-library](./icons/other/testing-library.png)](undefined) | [no-wait-for-multiple-assertions](https://github.com/testing-library/eslint-plugin-testing-library/tree/main/docs/rules/no-wait-for-multiple-assertions.md)
Disallow the use of multiple `expect` calls inside `waitFor` | | | | +| [![testing-library](./icons/other/testing-library.png)](undefined) | [prefer-explicit-assert](https://github.com/testing-library/eslint-plugin-testing-library/tree/main/docs/rules/prefer-explicit-assert.md)
Suggest using explicit assertions rather than standalone queries | | | | +| [![testing-library](./icons/other/testing-library.png)](undefined) | [prefer-find-by](https://github.com/testing-library/eslint-plugin-testing-library/tree/main/docs/rules/prefer-find-by.md)
Suggest using `find(All)By*` query instead of `waitFor` + `get(All)By*` to wait for elements | | 🔧 | | +| [![testing-library](./icons/other/testing-library.png)](undefined) | [prefer-presence-queries](https://github.com/testing-library/eslint-plugin-testing-library/tree/main/docs/rules/prefer-presence-queries.md)
Ensure appropriate `get*`/`query*` queries are used with their respective matchers | | | | +| [![testing-library](./icons/other/testing-library.png)](undefined) | [prefer-query-by-disappearance](https://github.com/testing-library/eslint-plugin-testing-library/tree/main/docs/rules/prefer-query-by-disappearance.md)
Suggest using `queryBy*` queries when waiting for disappearance | | | | +| [![testing-library](./icons/other/testing-library.png)](undefined) | [prefer-query-matchers](https://github.com/testing-library/eslint-plugin-testing-library/tree/main/docs/rules/prefer-query-matchers.md)
Ensure the configured `get*`/`query*` query is used with the corresponding matchers | | | | +| [![testing-library](./icons/other/testing-library.png)](undefined) | [prefer-screen-queries](https://github.com/testing-library/eslint-plugin-testing-library/tree/main/docs/rules/prefer-screen-queries.md)
Suggest using `screen` while querying | | | | +| [![testing-library](./icons/other/testing-library.png)](undefined) | [prefer-user-event](https://github.com/testing-library/eslint-plugin-testing-library/tree/main/docs/rules/prefer-user-event.md)
Suggest using `userEvent` over `fireEvent` for simulating user interactions | | | | +| [![testing-library](./icons/other/testing-library.png)](undefined) | [render-result-naming-convention](https://github.com/testing-library/eslint-plugin-testing-library/tree/main/docs/rules/render-result-naming-convention.md)
Enforce a valid naming for return value from `render` | | | | diff --git a/package-lock.json b/package-lock.json index 1337af0..a7fa660 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "eslint-plugin-rxjs-x": "^0.6.1", "eslint-plugin-sonarjs": "^1.0.4", "eslint-plugin-storybook": "^0.10.0", + "eslint-plugin-testing-library": "^7.1.1", "eslint-plugin-unicorn": "^50.0.0", "eslint-plugin-vitest": "^0.5.4", "globals": "^15.12.0", @@ -54,10 +55,12 @@ "eslint-plugin-jest": "^28.8.0", "eslint-plugin-jest-formatting": "^3.0.0", "eslint-plugin-n": ">=17.0.0", + "eslint-plugin-playwright": "^2.1.0", "eslint-plugin-promise": ">=6.4.0", "eslint-plugin-rxjs-x": ">=0.6.0", "eslint-plugin-sonarjs": "^1.0.4", "eslint-plugin-storybook": ">=0.10.0", + "eslint-plugin-testing-library": "^7.1.1", "eslint-plugin-unicorn": ">=50.0.0", "eslint-plugin-vitest": ">=0.5.0", "globals": ">=14.0.0", @@ -88,12 +91,18 @@ "eslint-plugin-n": { "optional": true }, + "eslint-plugin-playwright": { + "optional": true + }, "eslint-plugin-rxjs-x": { "optional": true }, "eslint-plugin-storybook": { "optional": true }, + "eslint-plugin-testing-library": { + "optional": true + }, "eslint-plugin-vitest": { "optional": true } @@ -2682,13 +2691,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.14.0.tgz", - "integrity": "sha512-aBbBrnW9ARIDn92Zbo7rguLnqQ/pOrUguVpbUwzOhkFg2npFDwTgPGqFqE0H5feXcOoJOfX3SxlJaKEVtq54dw==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.19.0.tgz", + "integrity": "sha512-hkoJiKQS3GQ13TSMEiuNmSCvhz7ujyqD1x3ShbaETATHrck+9RaDdUbt+osXaUuns9OFwrDTTrjtwsU8gJyyRA==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.14.0", - "@typescript-eslint/visitor-keys": "8.14.0" + "@typescript-eslint/types": "8.19.0", + "@typescript-eslint/visitor-keys": "8.19.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2699,10 +2709,11 @@ } }, "node_modules/@typescript-eslint/scope-manager/node_modules/@typescript-eslint/types": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.14.0.tgz", - "integrity": "sha512-yjeB9fnO/opvLJFAsPNYlKPnEM8+z4og09Pk504dkqonT02AyL5Z9SSqlE0XqezS93v6CXn49VHvB2G7XSsl0g==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.19.0.tgz", + "integrity": "sha512-8XQ4Ss7G9WX8oaYvD4OOLCjIQYgRQxO+qCiR2V2s2GxI9AUpo7riNwo6jDhKtTcaJjT8PY54j2Yb33kWtSJsmA==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -2829,6 +2840,86 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.19.0.tgz", + "integrity": "sha512-WW9PpDaLIFW9LCbucMSdYUuGeFUz1OkWYS/5fwZwTA+l2RwlWFdJvReQqMUMBw4yJWJOfqd7An9uwut2Oj8sLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.19.0", + "@typescript-eslint/visitor-keys": "8.19.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/types": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.19.0.tgz", + "integrity": "sha512-8XQ4Ss7G9WX8oaYvD4OOLCjIQYgRQxO+qCiR2V2s2GxI9AUpo7riNwo6jDhKtTcaJjT8PY54j2Yb33kWtSJsmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@typescript-eslint/utils": { "version": "8.13.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.13.0.tgz", @@ -2950,13 +3041,14 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.14.0.tgz", - "integrity": "sha512-vG0XZo8AdTH9OE6VFRwAZldNc7qtJ/6NLGWak+BtENuEUXGZgFpihILPiBvKXvJ2nFu27XNGC6rKiwuaoMbYzQ==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.19.0.tgz", + "integrity": "sha512-mCFtBbFBJDCNCWUl5y6sZSCHXw1DEFEk3c/M3nRK2a4XUB8StGFtmcEMizdjKuBzB6e/smJAAWYug3VrdLMr1w==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.14.0", - "eslint-visitor-keys": "^3.4.3" + "@typescript-eslint/types": "8.19.0", + "eslint-visitor-keys": "^4.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2967,10 +3059,11 @@ } }, "node_modules/@typescript-eslint/visitor-keys/node_modules/@typescript-eslint/types": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.14.0.tgz", - "integrity": "sha512-yjeB9fnO/opvLJFAsPNYlKPnEM8+z4og09Pk504dkqonT02AyL5Z9SSqlE0XqezS93v6CXn49VHvB2G7XSsl0g==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.19.0.tgz", + "integrity": "sha512-8XQ4Ss7G9WX8oaYvD4OOLCjIQYgRQxO+qCiR2V2s2GxI9AUpo7riNwo6jDhKtTcaJjT8PY54j2Yb33kWtSJsmA==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -2979,6 +3072,19 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@vitest/expect": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.4.tgz", @@ -5657,6 +5763,62 @@ "eslint": ">=6" } }, + "node_modules/eslint-plugin-testing-library": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-7.1.1.tgz", + "integrity": "sha512-nszC833aZPwB6tik1nMkbFqmtgIXTT0sfJEYs0zMBKMlkQ4to2079yUV96SvmLh00ovSBJI4pgcBC1TiIP8mXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "^8.15.0", + "@typescript-eslint/utils": "^8.15.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0", + "pnpm": "^9.14.0" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-testing-library/node_modules/@typescript-eslint/types": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.19.0.tgz", + "integrity": "sha512-8XQ4Ss7G9WX8oaYvD4OOLCjIQYgRQxO+qCiR2V2s2GxI9AUpo7riNwo6jDhKtTcaJjT8PY54j2Yb33kWtSJsmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-testing-library/node_modules/@typescript-eslint/utils": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.19.0.tgz", + "integrity": "sha512-PTBG+0oEMPH9jCZlfg07LCB2nYI0I317yyvXGfxnvGvw4SHIOuRnQ3kadyyXY6tGdChusIHIbM5zfIbp4M6tCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.19.0", + "@typescript-eslint/types": "8.19.0", + "@typescript-eslint/typescript-estree": "8.19.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, "node_modules/eslint-plugin-unicorn": { "version": "50.0.0", "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-50.0.0.tgz", diff --git a/package.json b/package.json index c5b8182..1352374 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "eslint-plugin-rxjs-x": ">=0.6.0", "eslint-plugin-sonarjs": "^1.0.4", "eslint-plugin-storybook": ">=0.10.0", + "eslint-plugin-testing-library": "^7.1.1", "eslint-plugin-unicorn": ">=50.0.0", "eslint-plugin-vitest": ">=0.5.0", "globals": ">=14.0.0", @@ -94,6 +95,9 @@ "eslint-plugin-storybook": { "optional": true }, + "eslint-plugin-testing-library": { + "optional": true + }, "eslint-plugin-vitest": { "optional": true } @@ -120,6 +124,7 @@ "eslint-plugin-rxjs-x": "^0.6.1", "eslint-plugin-sonarjs": "^1.0.4", "eslint-plugin-storybook": "^0.10.0", + "eslint-plugin-testing-library": "^7.1.1", "eslint-plugin-unicorn": "^50.0.0", "eslint-plugin-vitest": "^0.5.4", "globals": "^15.12.0", diff --git a/scripts/helpers/configs.js b/scripts/helpers/configs.js index 4ff9101..787efe2 100644 --- a/scripts/helpers/configs.js +++ b/scripts/helpers/configs.js @@ -15,6 +15,7 @@ const configDescriptions = { cypress: md`Config for projects using ${md.bold('Cypress')} for testing.`, playwright: md`Config for projects using ${md.bold('Playwright')} for testing.`, storybook: md`Config for projects using ${md.bold('Storybook')} for UI components.`, + 'react-testing-library': md`Config for projects using ${md.bold('React Testing Library')} for testing.`, }; /** @type {(keyof typeof configDescriptions)[]} */ @@ -34,6 +35,7 @@ const configIcons = { cypress: 'material/cypress', playwright: 'material/playwright', storybook: 'material/storybook', + 'react-testing-library': 'other/testing-library', }; /** @type {Partial>} */ @@ -44,6 +46,7 @@ const configPatterns = { cypress: '*.cy.ts', playwright: '*.spec.ts', storybook: '*.stories.ts', + 'react-testing-library': '*.spec.tsx', }; /** @type {Partial>} */ @@ -53,7 +56,13 @@ const configExtraPatterns = { }; /** @type {(keyof typeof configDescriptions)[]} */ -const testConfigs = ['jest', 'vitest', 'cypress', 'playwright']; +const testConfigs = [ + 'jest', + 'vitest', + 'cypress', + 'playwright', + 'react-testing-library', +]; const tsConfigDocsReference = md`Refer to ${md.link('./typescript.md#🏗️-setup', "step 3 in TypeScript config's setup docs")} for how to set up tsconfig properly.`; diff --git a/scripts/helpers/plugins.js b/scripts/helpers/plugins.js index 0b6fef8..ad84ac9 100644 --- a/scripts/helpers/plugins.js +++ b/scripts/helpers/plugins.js @@ -24,6 +24,7 @@ const pluginIcons = { 'rxjs-x': 'other/rxjs', sonarjs: 'other/sonar', storybook: 'material/storybook', + 'testing-library': 'other/testing-library', unicorn: 'icons8/unicorn', vitest: 'material/vitest', }; @@ -51,6 +52,8 @@ const pluginDocsUrls = { playwright: 'https://github.com/playwright-community/eslint-plugin-playwright#readme', promise: 'https://github.com/eslint-community/eslint-plugin-promise#readme', + 'react-testing-library': + 'https://github.com/testing-library/eslint-plugin-testing-library#readme', react: 'https://github.com/jsx-eslint/eslint-plugin-react#readme', 'react-hooks': 'https://github.com/facebook/react/tree/main/packages/eslint-plugin-react-hooks#readme', diff --git a/scripts/helpers/types.d.ts b/scripts/helpers/types.d.ts index b5b26bc..2118152 100644 --- a/scripts/helpers/types.d.ts +++ b/scripts/helpers/types.d.ts @@ -55,4 +55,5 @@ export type Icon = | 'other/ngrx' | 'other/rx-angular' | 'other/rxjs' - | 'other/sonar'; + | 'other/sonar' + | 'other/testing-library'; diff --git a/src/configs/react-testing-library.js b/src/configs/react-testing-library.js new file mode 100644 index 0000000..dcf50ce --- /dev/null +++ b/src/configs/react-testing-library.js @@ -0,0 +1,49 @@ +// @ts-check + +import rtl from 'eslint-plugin-testing-library'; +import tseslint from 'typescript-eslint'; +import { UNIT_TEST_FILE_PATTERNS } from '../lib/patterns.js'; + +export default tseslint.config({ + files: UNIT_TEST_FILE_PATTERNS, + extends: [ + rtl.configs['flat/react'], + { + name: 'code-pushup/react-testing-library/customized', + rules: { + 'testing-library/no-await-sync-events': 'warn', + 'testing-library/no-await-sync-queries': 'warn', + 'testing-library/no-container': 'warn', + 'testing-library/no-manual-cleanup': 'warn', + 'testing-library/no-node-access': 'warn', + 'testing-library/no-render-in-lifecycle': 'warn', + 'testing-library/no-wait-for-multiple-assertions': 'warn', + 'testing-library/prefer-find-by': 'warn', + 'testing-library/prefer-presence-queries': 'warn', + 'testing-library/prefer-query-by-disappearance': 'warn', + 'testing-library/prefer-query-matchers': 'warn', + 'testing-library/prefer-screen-queries': 'warn', + 'testing-library/render-result-naming-convention': 'warn', + }, + }, + { + name: 'code-pushup/react-testing-library/additional', + rules: { + 'testing-library/prefer-explicit-assert': 'warn', + 'testing-library/prefer-query-matchers': [ + 'warn', + { + validEntries: [ + { matcher: 'toBeVisible', query: 'get' }, + { matcher: 'toHaveTextContent', query: 'get' }, + { matcher: 'toBeEnabled', query: 'get' }, + { matcher: 'toBeDisabled', query: 'get' }, + { matcher: 'toBeChecked', query: 'get' }, + ], + }, + ], + 'testing-library/prefer-user-event': 'warn', + }, + }, + ], +}); diff --git a/tests/configs/react-testing-library.spec.js b/tests/configs/react-testing-library.spec.js new file mode 100644 index 0000000..f761a71 --- /dev/null +++ b/tests/configs/react-testing-library.spec.js @@ -0,0 +1,67 @@ +// @ts-check + +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { createLintUtils } from '../helpers/lint-utils'; + +describe('react-testing-library config', () => { + const { setup, teardown, loadConfig } = createLintUtils( + 'react-testing-library', + '*.spec.tsx', + ); + + beforeAll(setup); + + afterAll(teardown); + + it('should not include react-testing-library rules for non-test file', async () => { + const config = await loadConfig('components/Button.tsx'); + expect(Object.keys(config?.rules ?? {}).join(',')).not.toContain( + 'testing-library/', + ); + }); + + it('should include react-testing-library rules for test file', async () => { + const config = await loadConfig('components/Button.test.tsx'); + expect(Object.keys(config.rules ?? {}).join(',')).toContain( + 'testing-library/', + ); + }); + + it('should have rule from extended recommended react-testing-library config', async () => { + const config = await loadConfig(); + expect(config.rules).toHaveProperty('testing-library/await-async-events'); + }); + + it('should have explicitly added rule', async () => { + const config = await loadConfig(); + expect(config.rules).toHaveProperty('testing-library/prefer-user-event'); + }); + + it('should have customized severity level for rule from extended config', async () => { + const config = await loadConfig(); + expect(config.rules?.['testing-library/no-await-sync-queries']).toEqual([ + 1, + ]); + }); + + it('should have customized rule', async () => { + const config = await loadConfig(); + expect(config.rules).toHaveProperty( + 'testing-library/prefer-query-matchers', + ); + expect( + config.rules?.['testing-library/prefer-query-matchers'], + ).toStrictEqual([ + 1, + { + validEntries: [ + { matcher: 'toBeVisible', query: 'get' }, + { matcher: 'toHaveTextContent', query: 'get' }, + { matcher: 'toBeEnabled', query: 'get' }, + { matcher: 'toBeDisabled', query: 'get' }, + { matcher: 'toBeChecked', query: 'get' }, + ], + }, + ]); + }); +}); diff --git a/tests/helpers/lint-utils.js b/tests/helpers/lint-utils.js index 67ed5ac..9c8c49a 100644 --- a/tests/helpers/lint-utils.js +++ b/tests/helpers/lint-utils.js @@ -16,8 +16,9 @@ export function createLintUtils( defaultFilePath = '*.ts', filesToCreate = [], ) { - const cwd = path.join(process.cwd(), 'tmp', configName); + const camelCaseConfig = kebabToCamelCase(configName); + const cwd = path.join(process.cwd(), 'tmp', configName); const eslint = new ESLint({ cwd }); const setup = async () => { @@ -25,10 +26,10 @@ export function createLintUtils( await fs.mkdir(cwd, { recursive: true }); await fs.writeFile( path.join(cwd, 'eslint.config.js'), - `import ${configName} from '@code-pushup/eslint-config/${configName}.js' + `import ${camelCaseConfig} from '@code-pushup/eslint-config/${configName}.js' export default [ - ...${configName}, + ...${camelCaseConfig}, { languageOptions: { parserOptions: { @@ -116,3 +117,19 @@ export default [ getEnabledRuleIds, }; } + +/** + * Transforms kebab-case into camelCase + * @param {string} text + * @returns {string} + */ +export function kebabToCamelCase(text) { + return text + .split('-') + .map((word, index) => + index > 0 + ? word.charAt(0).toUpperCase() + word.slice(1).toLowerCase() + : word.toLowerCase(), + ) + .join(''); +} diff --git a/tests/helpers/lint-utils.spec.js b/tests/helpers/lint-utils.spec.js new file mode 100644 index 0000000..8c9de75 --- /dev/null +++ b/tests/helpers/lint-utils.spec.js @@ -0,0 +1,14 @@ +// @ts-check + +import { describe, expect, it } from 'vitest'; +import { kebabToCamelCase } from './lint-utils'; + +describe('kebabToCamelCase', () => { + it('should transform kebak-case to camelCase', async () => { + expect(kebabToCamelCase('kebab-case')).toBe('kebabCase'); + }); + + it('should capitalize subsequent words', async () => { + expect(kebabToCamelCase('ke-bAB-CAse')).toBe('keBabCase'); + }); +});