From 5aee16c901be989608596ea9028a560876935e85 Mon Sep 17 00:00:00 2001 From: Pierre Zimmermann <64224599+pierrezimmermannbam@users.noreply.github.com> Date: Thu, 26 Oct 2023 14:57:18 +0200 Subject: [PATCH] feat: Custom test rules (#93) * feat: add new custom rule await-user-event * feat: add new custom rule prefer-user-event * feat: add rule await-user-event to the tests config * chore: add RNTL as dev dep to the example app * fix: update docs that somehow were not correctly generated before rebase * fix: make rule break again in breaking example * fix: config emoji for test config * feat: update messages for eslint rule prefer-user-event --- .../break-testing-library-rules.test.tsx | 5 +- example-app/package.json | 1 + packages/eslint-plugin/README.md | 12 +++- .../docs/rules/await-user-event.md | 23 +++++++ .../docs/rules/prefer-user-event.md | 29 +++++++++ packages/eslint-plugin/lib/configs/tests.js | 1 + .../lib/rules/await-user-event.js | 49 +++++++++++++++ .../lib/rules/prefer-user-event.js | 60 +++++++++++++++++++ packages/eslint-plugin/package.json | 2 +- yarn.lock | 55 +++++++++++++++++ 10 files changed, 232 insertions(+), 5 deletions(-) create mode 100644 packages/eslint-plugin/docs/rules/await-user-event.md create mode 100644 packages/eslint-plugin/docs/rules/prefer-user-event.md create mode 100644 packages/eslint-plugin/lib/rules/await-user-event.js create mode 100644 packages/eslint-plugin/lib/rules/prefer-user-event.js diff --git a/example-app/eslint-breaking-examples/break-testing-library-rules.test.tsx b/example-app/eslint-breaking-examples/break-testing-library-rules.test.tsx index 7911a27..2a7130f 100644 --- a/example-app/eslint-breaking-examples/break-testing-library-rules.test.tsx +++ b/example-app/eslint-breaking-examples/break-testing-library-rules.test.tsx @@ -1,8 +1,11 @@ +import { fireEvent, userEvent } from "@testing-library/react-native"; // Save without formatting: [โŒ˜ + K] > [S] // This should trigger an error breaking eslint-testing-library rule: // testing-library/no-await-sync-events +// @bam.tech/await-user-event -it("a test", () => { +it("a test", async () => { await fireEvent(); + userEvent.press(button); }); diff --git a/example-app/package.json b/example-app/package.json index 6b1027e..3859a22 100644 --- a/example-app/package.json +++ b/example-app/package.json @@ -11,6 +11,7 @@ "devDependencies": { "@bam.tech/eslint-plugin": "*", "@bam.tech/typescript-config": "*", + "@testing-library/react-native": "^12.3.1", "@types/jest": "^29.5.2", "@types/react": "^18.2.14", "@typescript-eslint/eslint-plugin": "^5.61.0", diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index bfb213d..7aef838 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -102,9 +102,15 @@ This plugin exports some custom rules that you can optionally use in your projec -| Name | Description | -| :------------------------------------------------------------------------------------------------------------------------------------------------ | :----------------------------------------------------- | -| [require-named-effect](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/require-named-effect.md) | Enforces the use of named functions inside a useEffect | +๐Ÿ’ผ Configurations enabled in.\ +๐Ÿงช Set in the `tests` configuration.\ +๐Ÿ”ง Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix). + +| Name | Description | ๐Ÿ’ผ | ๐Ÿ”ง | +| :------------------------------------------------------------------------------------------------------------------------------------------------ | :----------------------------------------------------- | :-- | :-- | +| [await-user-event](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/await-user-event.md) | Enforces awaiting userEvent calls | ๐Ÿงช | ๐Ÿ”ง | +| [prefer-user-event](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/prefer-user-event.md) | Enforces usage of userEvent over fireEvent in tests. | | ๐Ÿ”ง | +| [require-named-effect](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/require-named-effect.md) | Enforces the use of named functions inside a useEffect | | | diff --git a/packages/eslint-plugin/docs/rules/await-user-event.md b/packages/eslint-plugin/docs/rules/await-user-event.md new file mode 100644 index 0000000..03ef9e3 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/await-user-event.md @@ -0,0 +1,23 @@ +# Enforces awaiting userEvent calls (`@bam.tech/await-user-event`) + +๐Ÿ’ผ This rule is enabled in the ๐Ÿงช `tests` config. + +๐Ÿ”ง This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). + + + +Makes sure calls to `userEvent` APIs are awaited + +## Rule details + +Examples of **incorrect** code for this rule: + +```jsx +userEvent.press(button); +``` + +Examples of **correct** code for this rule: + +```jsx +await userEvent.press(button); +``` diff --git a/packages/eslint-plugin/docs/rules/prefer-user-event.md b/packages/eslint-plugin/docs/rules/prefer-user-event.md new file mode 100644 index 0000000..272f3b0 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/prefer-user-event.md @@ -0,0 +1,29 @@ +# Enforces usage of userEvent over fireEvent in tests (`@bam.tech/prefer-user-event`) + +๐Ÿ”ง This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). + + + +Enforces the usage of `userEvent.type` over `fireEvent.changeText` and `userEvent.press` over `fireEvent.press` + +## Rule details + +Examples of **incorrect** code for this rule: + +```jsx +fireEvent.press(button); +``` + +```jsx +fireEvent.changeText(input, "text"); +``` + +Examples of **correct** code for this rule: + +```jsx +await userEvent.press(button); +``` + +```jsx +await userEvent.type(input, "text"); +``` diff --git a/packages/eslint-plugin/lib/configs/tests.js b/packages/eslint-plugin/lib/configs/tests.js index 5516eb4..40e7e68 100644 --- a/packages/eslint-plugin/lib/configs/tests.js +++ b/packages/eslint-plugin/lib/configs/tests.js @@ -45,5 +45,6 @@ module.exports = defineConfig({ "testing-library/prefer-presence-queries": "error", "testing-library/no-wait-for-side-effects": "error", "testing-library/prefer-screen-queries": "error", + "@bam.tech/await-user-event": "error", }, }); diff --git a/packages/eslint-plugin/lib/rules/await-user-event.js b/packages/eslint-plugin/lib/rules/await-user-event.js new file mode 100644 index 0000000..fea9633 --- /dev/null +++ b/packages/eslint-plugin/lib/rules/await-user-event.js @@ -0,0 +1,49 @@ +/** + * @fileoverview Makes sure userEvent.press and userEvent.type are awaited + * @author Pierre Zimmermann + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +/** @type {import('eslint').Rule.RuleModule} */ +module.exports = { + meta: { + type: "problem", + docs: { + description: "Enforces awaiting userEvent calls", + category: "Possible Errors", + recommended: true, + url: "https://github.com/bamlab/react-native-project-config/tree/main/packages/eslint-plugin/docs/rules/await-user-event.md", + }, + messages: { + missingAwait: "userEvent calls should be preceded by 'await'.", + }, + schema: [], + fixable: "code", + }, + + create(context) { + return { + CallExpression(node) { + if ( + node.callee.type === "MemberExpression" && + node.callee.object.name === "userEvent" + ) { + // Check if the parent is not an AwaitExpression + if (node.parent.type !== "AwaitExpression") { + context.report({ + node, + messageId: "missingAwait", + fix(fixer) { + return fixer.insertTextBefore(node, "await "); + }, + }); + } + } + }, + }; + }, +}; diff --git a/packages/eslint-plugin/lib/rules/prefer-user-event.js b/packages/eslint-plugin/lib/rules/prefer-user-event.js new file mode 100644 index 0000000..b7e96bb --- /dev/null +++ b/packages/eslint-plugin/lib/rules/prefer-user-event.js @@ -0,0 +1,60 @@ +/** + * @fileoverview Forces usage of userEvent.press over fireEvent.press and userEvent.type over fireEvent.changeText + * @author Pierre Zimmermann + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +/** @type {import('eslint').Rule.RuleModule} */ +module.exports = { + meta: { + type: "problem", + docs: { + description: "Enforces usage of userEvent over fireEvent in tests.", + category: "Possible Errors", + recommended: true, + url: "https://github.com/bamlab/react-native-project-config/tree/main/packages/eslint-plugin/docs/rules/prefer-user-event.md", + }, + messages: { + replacePress: "Replace `fireEvent.press` with `await userEvent.press.`", + replaceChangeText: "Replace `fireEvent.changeText` with `await userEvent.type.`", + }, + fixable: "code", + schema: [], + }, + + create(context) { + return { + MemberExpression: (node) => { + if (node.object.name === "fireEvent") { + if (node.property.name === "press") { + context.report({ + node: node.property, + messageId: "replacePress", + fix(fixer) { + return [ + fixer.replaceText(node.object, "await userEvent"), + fixer.replaceText(node.property, "press"), + ]; + }, + }); + } else if (node.property.name === "changeText") { + context.report({ + node: node.property, + messageId: "replaceChangeText", + fix(fixer) { + return [ + fixer.replaceText(node.object, "await userEvent"), + fixer.replaceText(node.property, "type"), + ]; + }, + }); + } + } + }, + }; + }, +}; diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index c0441ea..6410221 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -21,7 +21,7 @@ "lint:js": "eslint .", "lint:eslint-docs": "npm-run-all \"update:eslint-docs -- --check\"", "test": "mocha tests --recursive", - "update:eslint-docs": "eslint-doc-generator" + "update:eslint-docs": "eslint-doc-generator --config-emoji tests,๐Ÿงช" }, "peerDependencies": { "@typescript-eslint/eslint-plugin": "^5.61.0", diff --git a/yarn.lock b/yarn.lock index 71b4fac..68f9888 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1751,6 +1751,13 @@ dependencies: "@sinclair/typebox" "^0.27.8" +"@jest/schemas@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03" + integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA== + dependencies: + "@sinclair/typebox" "^0.27.8" + "@jest/source-map@^29.6.0": version "29.6.0" resolved "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.0.tgz" @@ -2672,6 +2679,15 @@ dependencies: "@sinonjs/commons" "^3.0.0" +"@testing-library/react-native@^12.3.1": + version "12.3.1" + resolved "https://registry.yarnpkg.com/@testing-library/react-native/-/react-native-12.3.1.tgz#7ac584711f214c7a1702fa4f637a9b3b22f0d093" + integrity sha512-nSd+trdQv8gbTSiAbjROVW9p7VZ6xhoy3qKy0q6vdnbzJCQlkKN2SzhZe92/evgu/aJZj575dajxuE37EcHA/Q== + dependencies: + jest-matcher-utils "^29.7.0" + pretty-format "^29.7.0" + redent "^3.0.0" + "@tootallnate/once@2": version "2.0.0" resolved "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz" @@ -4620,6 +4636,11 @@ diff-sequences@^29.4.3: resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz" integrity sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA== +diff-sequences@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" + integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== + diff@5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz" @@ -7024,6 +7045,16 @@ jest-diff@^29.2.1, jest-diff@^29.6.0: jest-get-type "^29.4.3" pretty-format "^29.6.0" +jest-diff@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.7.0.tgz#017934a66ebb7ecf6f205e84699be10afd70458a" + integrity sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw== + dependencies: + chalk "^4.0.0" + diff-sequences "^29.6.3" + jest-get-type "^29.6.3" + pretty-format "^29.7.0" + jest-docblock@^29.4.3: version "29.4.3" resolved "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.4.3.tgz" @@ -7059,6 +7090,11 @@ jest-get-type@^29.4.3: resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.3.tgz" integrity sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg== +jest-get-type@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.6.3.tgz#36f499fdcea197c1045a127319c0481723908fd1" + integrity sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw== + jest-haste-map@^29.6.0: version "29.6.0" resolved "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.6.0.tgz" @@ -7096,6 +7132,16 @@ jest-matcher-utils@^29.6.0: jest-get-type "^29.4.3" pretty-format "^29.6.0" +jest-matcher-utils@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz#ae8fec79ff249fd592ce80e3ee474e83a6c44f12" + integrity sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g== + dependencies: + chalk "^4.0.0" + jest-diff "^29.7.0" + jest-get-type "^29.6.3" + pretty-format "^29.7.0" + jest-message-util@^29.6.0: version "29.6.0" resolved "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.6.0.tgz" @@ -9668,6 +9714,15 @@ pretty-format@^29.0.0, pretty-format@^29.6.0: ansi-styles "^5.0.0" react-is "^18.0.0" +pretty-format@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812" + integrity sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ== + dependencies: + "@jest/schemas" "^29.6.3" + ansi-styles "^5.0.0" + react-is "^18.0.0" + proc-log@^2.0.0, proc-log@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/proc-log/-/proc-log-2.0.1.tgz"