Skip to content

Commit 5aee16c

Browse files
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
1 parent 3ee3591 commit 5aee16c

File tree

10 files changed

+232
-5
lines changed

10 files changed

+232
-5
lines changed
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
import { fireEvent, userEvent } from "@testing-library/react-native";
12
// Save without formatting: [⌘ + K] > [S]
23

34
// This should trigger an error breaking eslint-testing-library rule:
45
// testing-library/no-await-sync-events
6+
// @bam.tech/await-user-event
57

6-
it("a test", () => {
8+
it("a test", async () => {
79
await fireEvent();
10+
userEvent.press(button);
811
});

example-app/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"devDependencies": {
1212
"@bam.tech/eslint-plugin": "*",
1313
"@bam.tech/typescript-config": "*",
14+
"@testing-library/react-native": "^12.3.1",
1415
"@types/jest": "^29.5.2",
1516
"@types/react": "^18.2.14",
1617
"@typescript-eslint/eslint-plugin": "^5.61.0",

packages/eslint-plugin/README.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,15 @@ This plugin exports some custom rules that you can optionally use in your projec
102102

103103
<!-- begin auto-generated rules list -->
104104

105-
| Name | Description |
106-
| :------------------------------------------------------------------------------------------------------------------------------------------------ | :----------------------------------------------------- |
107-
| [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 |
105+
💼 Configurations enabled in.\
106+
🧪 Set in the `tests` configuration.\
107+
🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).
108+
109+
| Name | Description | 💼 | 🔧 |
110+
| :------------------------------------------------------------------------------------------------------------------------------------------------ | :----------------------------------------------------- | :-- | :-- |
111+
| [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 | 🧪 | 🔧 |
112+
| [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. | | 🔧 |
113+
| [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 | | |
108114

109115
<!-- end auto-generated rules list -->
110116

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Enforces awaiting userEvent calls (`@bam.tech/await-user-event`)
2+
3+
💼 This rule is enabled in the 🧪 `tests` config.
4+
5+
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
6+
7+
<!-- end auto-generated rule header -->
8+
9+
Makes sure calls to `userEvent` APIs are awaited
10+
11+
## Rule details
12+
13+
Examples of **incorrect** code for this rule:
14+
15+
```jsx
16+
userEvent.press(button);
17+
```
18+
19+
Examples of **correct** code for this rule:
20+
21+
```jsx
22+
await userEvent.press(button);
23+
```
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Enforces usage of userEvent over fireEvent in tests (`@bam.tech/prefer-user-event`)
2+
3+
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
4+
5+
<!-- end auto-generated rule header -->
6+
7+
Enforces the usage of `userEvent.type` over `fireEvent.changeText` and `userEvent.press` over `fireEvent.press`
8+
9+
## Rule details
10+
11+
Examples of **incorrect** code for this rule:
12+
13+
```jsx
14+
fireEvent.press(button);
15+
```
16+
17+
```jsx
18+
fireEvent.changeText(input, "text");
19+
```
20+
21+
Examples of **correct** code for this rule:
22+
23+
```jsx
24+
await userEvent.press(button);
25+
```
26+
27+
```jsx
28+
await userEvent.type(input, "text");
29+
```

packages/eslint-plugin/lib/configs/tests.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,6 @@ module.exports = defineConfig({
4545
"testing-library/prefer-presence-queries": "error",
4646
"testing-library/no-wait-for-side-effects": "error",
4747
"testing-library/prefer-screen-queries": "error",
48+
"@bam.tech/await-user-event": "error",
4849
},
4950
});
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* @fileoverview Makes sure userEvent.press and userEvent.type are awaited
3+
* @author Pierre Zimmermann
4+
*/
5+
"use strict";
6+
7+
//------------------------------------------------------------------------------
8+
// Rule Definition
9+
//------------------------------------------------------------------------------
10+
11+
/** @type {import('eslint').Rule.RuleModule} */
12+
module.exports = {
13+
meta: {
14+
type: "problem",
15+
docs: {
16+
description: "Enforces awaiting userEvent calls",
17+
category: "Possible Errors",
18+
recommended: true,
19+
url: "https://github.com/bamlab/react-native-project-config/tree/main/packages/eslint-plugin/docs/rules/await-user-event.md",
20+
},
21+
messages: {
22+
missingAwait: "userEvent calls should be preceded by 'await'.",
23+
},
24+
schema: [],
25+
fixable: "code",
26+
},
27+
28+
create(context) {
29+
return {
30+
CallExpression(node) {
31+
if (
32+
node.callee.type === "MemberExpression" &&
33+
node.callee.object.name === "userEvent"
34+
) {
35+
// Check if the parent is not an AwaitExpression
36+
if (node.parent.type !== "AwaitExpression") {
37+
context.report({
38+
node,
39+
messageId: "missingAwait",
40+
fix(fixer) {
41+
return fixer.insertTextBefore(node, "await ");
42+
},
43+
});
44+
}
45+
}
46+
},
47+
};
48+
},
49+
};
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/**
2+
* @fileoverview Forces usage of userEvent.press over fireEvent.press and userEvent.type over fireEvent.changeText
3+
* @author Pierre Zimmermann
4+
*/
5+
"use strict";
6+
7+
//------------------------------------------------------------------------------
8+
// Rule Definition
9+
//------------------------------------------------------------------------------
10+
11+
/** @type {import('eslint').Rule.RuleModule} */
12+
module.exports = {
13+
meta: {
14+
type: "problem",
15+
docs: {
16+
description: "Enforces usage of userEvent over fireEvent in tests.",
17+
category: "Possible Errors",
18+
recommended: true,
19+
url: "https://github.com/bamlab/react-native-project-config/tree/main/packages/eslint-plugin/docs/rules/prefer-user-event.md",
20+
},
21+
messages: {
22+
replacePress: "Replace `fireEvent.press` with `await userEvent.press.`",
23+
replaceChangeText: "Replace `fireEvent.changeText` with `await userEvent.type.`",
24+
},
25+
fixable: "code",
26+
schema: [],
27+
},
28+
29+
create(context) {
30+
return {
31+
MemberExpression: (node) => {
32+
if (node.object.name === "fireEvent") {
33+
if (node.property.name === "press") {
34+
context.report({
35+
node: node.property,
36+
messageId: "replacePress",
37+
fix(fixer) {
38+
return [
39+
fixer.replaceText(node.object, "await userEvent"),
40+
fixer.replaceText(node.property, "press"),
41+
];
42+
},
43+
});
44+
} else if (node.property.name === "changeText") {
45+
context.report({
46+
node: node.property,
47+
messageId: "replaceChangeText",
48+
fix(fixer) {
49+
return [
50+
fixer.replaceText(node.object, "await userEvent"),
51+
fixer.replaceText(node.property, "type"),
52+
];
53+
},
54+
});
55+
}
56+
}
57+
},
58+
};
59+
},
60+
};

packages/eslint-plugin/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"lint:js": "eslint .",
2222
"lint:eslint-docs": "npm-run-all \"update:eslint-docs -- --check\"",
2323
"test": "mocha tests --recursive",
24-
"update:eslint-docs": "eslint-doc-generator"
24+
"update:eslint-docs": "eslint-doc-generator --config-emoji tests,🧪"
2525
},
2626
"peerDependencies": {
2727
"@typescript-eslint/eslint-plugin": "^5.61.0",

yarn.lock

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1751,6 +1751,13 @@
17511751
dependencies:
17521752
"@sinclair/typebox" "^0.27.8"
17531753

1754+
"@jest/schemas@^29.6.3":
1755+
version "29.6.3"
1756+
resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03"
1757+
integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==
1758+
dependencies:
1759+
"@sinclair/typebox" "^0.27.8"
1760+
17541761
"@jest/source-map@^29.6.0":
17551762
version "29.6.0"
17561763
resolved "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.0.tgz"
@@ -2672,6 +2679,15 @@
26722679
dependencies:
26732680
"@sinonjs/commons" "^3.0.0"
26742681

2682+
"@testing-library/react-native@^12.3.1":
2683+
version "12.3.1"
2684+
resolved "https://registry.yarnpkg.com/@testing-library/react-native/-/react-native-12.3.1.tgz#7ac584711f214c7a1702fa4f637a9b3b22f0d093"
2685+
integrity sha512-nSd+trdQv8gbTSiAbjROVW9p7VZ6xhoy3qKy0q6vdnbzJCQlkKN2SzhZe92/evgu/aJZj575dajxuE37EcHA/Q==
2686+
dependencies:
2687+
jest-matcher-utils "^29.7.0"
2688+
pretty-format "^29.7.0"
2689+
redent "^3.0.0"
2690+
26752691
"@tootallnate/once@2":
26762692
version "2.0.0"
26772693
resolved "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz"
@@ -4620,6 +4636,11 @@ diff-sequences@^29.4.3:
46204636
resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz"
46214637
integrity sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==
46224638

4639+
diff-sequences@^29.6.3:
4640+
version "29.6.3"
4641+
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921"
4642+
integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==
4643+
46234644
46244645
version "5.0.0"
46254646
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:
70247045
jest-get-type "^29.4.3"
70257046
pretty-format "^29.6.0"
70267047

7048+
jest-diff@^29.7.0:
7049+
version "29.7.0"
7050+
resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.7.0.tgz#017934a66ebb7ecf6f205e84699be10afd70458a"
7051+
integrity sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==
7052+
dependencies:
7053+
chalk "^4.0.0"
7054+
diff-sequences "^29.6.3"
7055+
jest-get-type "^29.6.3"
7056+
pretty-format "^29.7.0"
7057+
70277058
jest-docblock@^29.4.3:
70287059
version "29.4.3"
70297060
resolved "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.4.3.tgz"
@@ -7059,6 +7090,11 @@ jest-get-type@^29.4.3:
70597090
resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.3.tgz"
70607091
integrity sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==
70617092

7093+
jest-get-type@^29.6.3:
7094+
version "29.6.3"
7095+
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.6.3.tgz#36f499fdcea197c1045a127319c0481723908fd1"
7096+
integrity sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==
7097+
70627098
jest-haste-map@^29.6.0:
70637099
version "29.6.0"
70647100
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:
70967132
jest-get-type "^29.4.3"
70977133
pretty-format "^29.6.0"
70987134

7135+
jest-matcher-utils@^29.7.0:
7136+
version "29.7.0"
7137+
resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz#ae8fec79ff249fd592ce80e3ee474e83a6c44f12"
7138+
integrity sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==
7139+
dependencies:
7140+
chalk "^4.0.0"
7141+
jest-diff "^29.7.0"
7142+
jest-get-type "^29.6.3"
7143+
pretty-format "^29.7.0"
7144+
70997145
jest-message-util@^29.6.0:
71007146
version "29.6.0"
71017147
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:
96689714
ansi-styles "^5.0.0"
96699715
react-is "^18.0.0"
96709716

9717+
pretty-format@^29.7.0:
9718+
version "29.7.0"
9719+
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812"
9720+
integrity sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==
9721+
dependencies:
9722+
"@jest/schemas" "^29.6.3"
9723+
ansi-styles "^5.0.0"
9724+
react-is "^18.0.0"
9725+
96719726
proc-log@^2.0.0, proc-log@^2.0.1:
96729727
version "2.0.1"
96739728
resolved "https://registry.npmjs.org/proc-log/-/proc-log-2.0.1.tgz"

0 commit comments

Comments
 (0)