Skip to content

Commit 21451b8

Browse files
committed
feat: add new rule await-fire-event
1 parent 5e4157f commit 21451b8

File tree

6 files changed

+210
-6
lines changed

6 files changed

+210
-6
lines changed

README.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -115,12 +115,13 @@ To enable this configuration use the `extends` property in your
115115

116116
## Supported Rules
117117

118-
| Rule | Description | Recommended | Frameworks | Fixable |
119-
| -------------------------------------------------------- | --------------------------------------------- | ---------------- | -------------------------------- | ------------ |
120-
| [await-async-query](docs/rules/await-async-query.md) | Enforce async queries to have proper `await` | ![recommended][] | | |
121-
| [no-await-sync-query](docs/rules/no-await-sync-query.md) | Disallow unnecessary `await` for sync queries | ![recommended][] | | |
122-
| [no-debug](docs/rules/no-debug.md) | Disallow the use of `debug` | | ![angular][] ![react][] ![vue][] | |
123-
| [no-dom-import](docs/rules/no-dom-import.md) | Disallow importing from DOM Testing Library | | ![angular][] ![react][] ![vue][] | ![fixable][] |
118+
| Rule | Description | Recommended | Frameworks | Fixable |
119+
| -------------------------------------------------------- | ---------------------------------------------- | ---------------- | -------------------------------- | ------------ |
120+
| [await-async-query](docs/rules/await-async-query.md) | Enforce async queries to have proper `await` | ![recommended][] | | |
121+
| [await-fire-event](docs/rules/await-fire-event.md) | Enforce async fire event methods to be awaited | | ![vue][] | |
122+
| [no-await-sync-query](docs/rules/no-await-sync-query.md) | Disallow unnecessary `await` for sync queries | ![recommended][] | | |
123+
| [no-debug](docs/rules/no-debug.md) | Disallow the use of `debug` | | ![angular][] ![react][] ![vue][] | |
124+
| [no-dom-import](docs/rules/no-dom-import.md) | Disallow importing from DOM Testing Library | | ![angular][] ![react][] ![vue][] | ![fixable][] |
124125

125126
[recommended]: https://img.shields.io/badge/recommended-lightgrey?style=flat-square
126127
[fixable]: https://img.shields.io/badge/fixable-success?style=flat-square

docs/rules/await-fire-event.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Enforce async fire event methods to be awaited (await-fire-event)
2+
3+
Ensure that promises returned by `fireEvent` methods are awaited
4+
properly.
5+
6+
## Rule Details
7+
8+
This rule aims to prevent users from forgetting to await `fireEvent`
9+
methods when they are async.
10+
11+
Examples of **incorrect** code for this rule:
12+
13+
```js
14+
fireEvent.click(getByText('Click me'));
15+
16+
fireEvent.focus(getByLabelText('username'));
17+
fireEvent.blur(getByLabelText('username'));
18+
```
19+
20+
Examples of **correct** code for this rule:
21+
22+
```js
23+
// `await` operator is correct
24+
await fireEvent.focus(getByLabelText('username'));
25+
await fireEvent.blur(getByLabelText('username'));
26+
27+
// `then` method is correct
28+
fireEvent.click(getByText('Click me')).then(() => {
29+
// ...
30+
});
31+
32+
// return the promise within a function is correct too!
33+
function clickMeRegularFn() {
34+
return fireEvent.click(getByText('Click me'));
35+
}
36+
const clickMeArrowFn = () => fireEvent.click(getByText('Click me'));
37+
```
38+
39+
## When Not To Use It
40+
41+
`fireEvent` methods are only async in Vue Testing Library so if you are using another Testing Library module, you shouldn't use this rule.
42+
43+
## Further Reading
44+
45+
- [Vue Testing Library fireEvent](https://testing-library.com/docs/vue-testing-library/api#fireevent)

lib/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
const rules = {
44
'await-async-query': require('./rules/await-async-query'),
5+
'await-fire-event': require('./rules/await-fire-event'),
56
'no-await-sync-query': require('./rules/no-await-sync-query'),
67
'no-debug': require('./rules/no-debug'),
78
'no-dom-import': require('./rules/no-dom-import'),
@@ -39,6 +40,7 @@ module.exports = {
3940
plugins: ['testing-library'],
4041
rules: {
4142
...recommendedRules,
43+
'testing-library/await-fire-event': 'error',
4244
'testing-library/no-debug': 'warn',
4345
'testing/library/no-dom-import': ['error', 'vue'],
4446
},

lib/rules/await-fire-event.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
'use strict';
2+
3+
const { getDocsUrl } = require('../utils');
4+
5+
function isAwaited(node) {
6+
return VALID_PARENTS.includes(node.type);
7+
}
8+
9+
function hasThenProperty(node) {
10+
return node.type === 'MemberExpression' && node.property.name === 'then';
11+
}
12+
13+
function isPromiseResolved(node) {
14+
const parent = node.parent.parent;
15+
16+
// fireEvent.click().then(...)
17+
if (parent.type === 'CallExpression') {
18+
return hasThenProperty(parent.parent);
19+
}
20+
}
21+
22+
const VALID_PARENTS = [
23+
'AwaitExpression',
24+
'ArrowFunctionExpression',
25+
'ReturnStatement',
26+
];
27+
28+
module.exports = {
29+
meta: {
30+
type: 'problem',
31+
docs: {
32+
description: 'Enforce async fire event methods to be awaited',
33+
category: 'Best Practices',
34+
recommended: false,
35+
url: getDocsUrl('await-fire-event'),
36+
},
37+
messages: {
38+
awaitFireEvent: 'async `fireEvent.{{ methodName }}` must be awaited',
39+
},
40+
fixable: null,
41+
schema: [],
42+
},
43+
44+
create: function(context) {
45+
return {
46+
'CallExpression > MemberExpression > Identifier[name=fireEvent]'(node) {
47+
const fireEventMethodNode = node.parent.property;
48+
49+
if (
50+
!isAwaited(node.parent.parent.parent) &&
51+
!isPromiseResolved(fireEventMethodNode)
52+
) {
53+
context.report({
54+
node: fireEventMethodNode,
55+
messageId: 'awaitFireEvent',
56+
data: {
57+
methodName: fireEventMethodNode.name,
58+
},
59+
});
60+
}
61+
},
62+
};
63+
},
64+
};

tests/__snapshots__/index.test.js.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ Object {
5353
],
5454
"rules": Object {
5555
"testing-library/await-async-query": "error",
56+
"testing-library/await-fire-event": "error",
5657
"testing-library/no-await-sync-query": "error",
5758
"testing-library/no-debug": "warn",
5859
"testing/library/no-dom-import": Array [

tests/lib/rules/await-fire-event.js

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
'use strict';
2+
3+
const rule = require('../../../lib/rules/await-fire-event');
4+
const RuleTester = require('eslint').RuleTester;
5+
6+
const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2018 } });
7+
ruleTester.run('await-fire-event', rule, {
8+
valid: [
9+
{
10+
code: `fireEvent.click`,
11+
},
12+
{
13+
code: `async () => {
14+
await fireEvent.click(getByText('Click me'))
15+
}
16+
`,
17+
},
18+
{
19+
code: `async () => {
20+
await fireEvent.focus(getByLabelText('username'))
21+
await fireEvent.blur(getByLabelText('username'))
22+
}
23+
`,
24+
},
25+
{
26+
code: `done => {
27+
fireEvent.click(getByText('Click me')).then(() => { done() })
28+
}
29+
`,
30+
},
31+
{
32+
code: `done => {
33+
fireEvent.focus(getByLabelText('username')).then(() => {
34+
fireEvent.blur(getByLabelText('username')).then(() => { done() })
35+
})
36+
}
37+
`,
38+
},
39+
{
40+
code: `() => {
41+
return fireEvent.click(getByText('Click me'))
42+
}
43+
`,
44+
},
45+
{
46+
code: `() => fireEvent.click(getByText('Click me'))
47+
`,
48+
},
49+
{
50+
code: `function clickUtil() {
51+
doSomething()
52+
return fireEvent.click(getByText('Click me'))
53+
}
54+
`,
55+
},
56+
],
57+
58+
invalid: [
59+
{
60+
code: `() => {
61+
fireEvent.click(getByText('Click me'))
62+
}
63+
`,
64+
errors: [
65+
{
66+
column: 19,
67+
messageId: 'awaitFireEvent',
68+
},
69+
],
70+
},
71+
{
72+
code: `() => {
73+
fireEvent.focus(getByLabelText('username'))
74+
fireEvent.blur(getByLabelText('username'))
75+
}
76+
`,
77+
errors: [
78+
{
79+
line: 2,
80+
column: 19,
81+
messageId: 'awaitFireEvent',
82+
},
83+
{
84+
line: 3,
85+
column: 19,
86+
messageId: 'awaitFireEvent',
87+
},
88+
],
89+
},
90+
],
91+
});

0 commit comments

Comments
 (0)