Skip to content

Commit b4b5394

Browse files
skovyMichaelDeBoey
authored andcommitted
feat(await-async-event): add basic fixer (#656)
1 parent b4ce9bb commit b4b5394

File tree

3 files changed

+159
-17
lines changed

3 files changed

+159
-17
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ To enable this configuration use the `extends` property in your
206206

207207
| Name | Description | 🔧 | Included in configurations |
208208
| ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------- | --- | ---------------------------------------------------------------------------------- |
209-
| [`await-async-event`](./docs/rules/await-async-event.md) | Enforce promises from async event methods are handled | | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] ![marko-badge][] |
209+
| [`await-async-event`](./docs/rules/await-async-event.md) | Enforce promises from async event methods are handled | 🔧 | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] ![marko-badge][] |
210210
| [`await-async-query`](./docs/rules/await-async-query.md) | Enforce promises from async queries to be handled | | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] ![marko-badge][] |
211211
| [`await-async-utils`](./docs/rules/await-async-utils.md) | Enforce promises from async utils to be awaited properly | | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] ![marko-badge][] |
212212
| [`consistent-data-testid`](./docs/rules/consistent-data-testid.md) | Ensures consistent usage of `data-testid` | | |

lib/rules/await-async-event.ts

+37-15
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { ASTUtils, TSESTree } from '@typescript-eslint/utils';
1+
import { ASTUtils, TSESLint, TSESTree } from '@typescript-eslint/utils';
22

33
import { createTestingLibraryRule } from '../create-testing-library-rule';
44
import {
55
findClosestCallExpressionNode,
66
getFunctionName,
77
getInnermostReturningFunction,
88
getVariableReferences,
9+
isMemberExpression,
910
isPromiseHandled,
1011
} from '../node-utils';
1112
import { EVENTS_SIMULATORS } from '../utils';
@@ -41,6 +42,7 @@ export default createTestingLibraryRule<Options, MessageIds>({
4142
awaitAsyncEventWrapper:
4243
'Promise returned from `{{ name }}` wrapper over async event method must be handled',
4344
},
45+
fixable: 'code',
4446
schema: [
4547
{
4648
type: 'object',
@@ -76,16 +78,23 @@ export default createTestingLibraryRule<Options, MessageIds>({
7678
create(context, [options], helpers) {
7779
const functionWrappersNames: string[] = [];
7880

79-
function reportUnhandledNode(
80-
node: TSESTree.Identifier,
81-
closestCallExpressionNode: TSESTree.CallExpression,
82-
messageId: MessageIds = 'awaitAsyncEvent'
83-
): void {
81+
function reportUnhandledNode({
82+
node,
83+
closestCallExpression,
84+
messageId = 'awaitAsyncEvent',
85+
fix,
86+
}: {
87+
node: TSESTree.Identifier;
88+
closestCallExpression: TSESTree.CallExpression;
89+
messageId?: MessageIds;
90+
fix?: TSESLint.ReportFixFunction;
91+
}): void {
8492
if (!isPromiseHandled(node)) {
8593
context.report({
86-
node: closestCallExpressionNode.callee,
94+
node: closestCallExpression.callee,
8795
messageId,
8896
data: { name: node.name },
97+
fix,
8998
});
9099
}
91100
}
@@ -128,14 +137,24 @@ export default createTestingLibraryRule<Options, MessageIds>({
128137
);
129138

130139
if (references.length === 0) {
131-
reportUnhandledNode(node, closestCallExpression);
140+
reportUnhandledNode({
141+
node,
142+
closestCallExpression,
143+
fix: (fixer) => {
144+
if (isMemberExpression(node.parent)) {
145+
return fixer.insertTextBefore(node.parent, 'await ');
146+
}
147+
148+
return null;
149+
},
150+
});
132151
} else {
133152
for (const reference of references) {
134153
if (ASTUtils.isIdentifier(reference.identifier)) {
135-
reportUnhandledNode(
136-
reference.identifier,
137-
closestCallExpression
138-
);
154+
reportUnhandledNode({
155+
node: reference.identifier,
156+
closestCallExpression,
157+
});
139158
}
140159
}
141160
}
@@ -151,11 +170,14 @@ export default createTestingLibraryRule<Options, MessageIds>({
151170
return;
152171
}
153172

154-
reportUnhandledNode(
173+
reportUnhandledNode({
155174
node,
156175
closestCallExpression,
157-
'awaitAsyncEventWrapper'
158-
);
176+
messageId: 'awaitAsyncEventWrapper',
177+
fix: (fixer) => {
178+
return fixer.insertTextBefore(node, 'await ');
179+
},
180+
});
159181
}
160182
},
161183
};

tests/lib/rules/await-async-event.test.ts

+121-1
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,12 @@ ruleTester.run(RULE_NAME, rule, {
359359
},
360360
],
361361
options: [{ eventModule: 'fireEvent' }],
362+
output: `
363+
import { fireEvent } from '${testingFramework}'
364+
test('unhandled promise from event method is invalid', async () => {
365+
await fireEvent.${eventMethod}(getByLabelText('username'))
366+
})
367+
`,
362368
} as const)
363369
),
364370
...FIRE_EVENT_ASYNC_FUNCTIONS.map(
@@ -380,6 +386,12 @@ ruleTester.run(RULE_NAME, rule, {
380386
},
381387
],
382388
options: [{ eventModule: 'fireEvent' }],
389+
output: `
390+
import { fireEvent as testingLibraryFireEvent } from '${testingFramework}'
391+
test('unhandled promise from aliased event method is invalid', async () => {
392+
await testingLibraryFireEvent.${eventMethod}(getByLabelText('username'))
393+
})
394+
`,
383395
} as const)
384396
),
385397
...FIRE_EVENT_ASYNC_FUNCTIONS.map(
@@ -401,6 +413,12 @@ ruleTester.run(RULE_NAME, rule, {
401413
},
402414
],
403415
options: [{ eventModule: 'fireEvent' }],
416+
output: `
417+
import * as testingLibrary from '${testingFramework}'
418+
test('unhandled promise from wildcard imported event method is invalid', async () => {
419+
await testingLibrary.fireEvent.${eventMethod}(getByLabelText('username'))
420+
})
421+
`,
404422
} as const)
405423
),
406424
...FIRE_EVENT_ASYNC_FUNCTIONS.map(
@@ -428,6 +446,13 @@ ruleTester.run(RULE_NAME, rule, {
428446
},
429447
],
430448
options: [{ eventModule: 'fireEvent' }],
449+
output: `
450+
import { fireEvent } from '${testingFramework}'
451+
test('several unhandled promises from event methods is invalid', async () => {
452+
await fireEvent.${eventMethod}(getByLabelText('username'))
453+
await fireEvent.${eventMethod}(getByLabelText('username'))
454+
})
455+
`,
431456
} as const)
432457
),
433458
...FIRE_EVENT_ASYNC_FUNCTIONS.map(
@@ -451,6 +476,12 @@ ruleTester.run(RULE_NAME, rule, {
451476
},
452477
],
453478
options: [{ eventModule: 'fireEvent' }],
479+
output: `
480+
import { fireEvent } from '${testingFramework}'
481+
test('unhandled promise from event method with aggressive reporting opted-out is invalid', async () => {
482+
await fireEvent.${eventMethod}(getByLabelText('username'))
483+
})
484+
`,
454485
} as const)
455486
),
456487
...FIRE_EVENT_ASYNC_FUNCTIONS.map(
@@ -476,6 +507,14 @@ ruleTester.run(RULE_NAME, rule, {
476507
},
477508
],
478509
options: [{ eventModule: 'fireEvent' }],
510+
output: `
511+
import { fireEvent } from 'test-utils'
512+
test(
513+
'unhandled promise from event method imported from custom module with aggressive reporting opted-out is invalid',
514+
() => {
515+
await fireEvent.${eventMethod}(getByLabelText('username'))
516+
})
517+
`,
479518
} as const)
480519
),
481520
...FIRE_EVENT_ASYNC_FUNCTIONS.map(
@@ -501,6 +540,14 @@ ruleTester.run(RULE_NAME, rule, {
501540
},
502541
],
503542
options: [{ eventModule: 'fireEvent' }],
543+
output: `
544+
import { fireEvent } from '${testingFramework}'
545+
test(
546+
'unhandled promise from event method imported from default module with aggressive reporting opted-out is invalid',
547+
() => {
548+
await fireEvent.${eventMethod}(getByLabelText('username'))
549+
})
550+
`,
504551
} as const)
505552
),
506553

@@ -524,6 +571,14 @@ ruleTester.run(RULE_NAME, rule, {
524571
},
525572
],
526573
options: [{ eventModule: 'fireEvent' }],
574+
output: `
575+
import { fireEvent } from '${testingFramework}'
576+
test(
577+
'unhandled promise from event method kept in a var is invalid',
578+
() => {
579+
const promise = await fireEvent.${eventMethod}(getByLabelText('username'))
580+
})
581+
`,
527582
} as const)
528583
),
529584
...FIRE_EVENT_ASYNC_FUNCTIONS.map(
@@ -549,6 +604,17 @@ ruleTester.run(RULE_NAME, rule, {
549604
},
550605
],
551606
options: [{ eventModule: 'fireEvent' }],
607+
output: `
608+
import { fireEvent } from '${testingFramework}'
609+
test('unhandled promise returned from function wrapping event method is invalid', () => {
610+
function triggerEvent() {
611+
doSomething()
612+
return fireEvent.${eventMethod}(getByLabelText('username'))
613+
}
614+
615+
await triggerEvent()
616+
})
617+
`,
552618
} as const)
553619
),
554620
]),
@@ -572,6 +638,12 @@ ruleTester.run(RULE_NAME, rule, {
572638
},
573639
],
574640
options: [{ eventModule: 'userEvent' }],
641+
output: `
642+
import userEvent from '${testingFramework}'
643+
test('unhandled promise from event method is invalid', async () => {
644+
await userEvent.${eventMethod}(getByLabelText('username'))
645+
})
646+
`,
575647
} as const)
576648
),
577649
...USER_EVENT_ASYNC_FUNCTIONS.map(
@@ -593,6 +665,12 @@ ruleTester.run(RULE_NAME, rule, {
593665
},
594666
],
595667
options: [{ eventModule: 'userEvent' }],
668+
output: `
669+
import testingLibraryUserEvent from '${testingFramework}'
670+
test('unhandled promise imported from alternate name event method is invalid', async () => {
671+
await testingLibraryUserEvent.${eventMethod}(getByLabelText('username'))
672+
})
673+
`,
596674
} as const)
597675
),
598676
...USER_EVENT_ASYNC_FUNCTIONS.map(
@@ -620,6 +698,13 @@ ruleTester.run(RULE_NAME, rule, {
620698
},
621699
],
622700
options: [{ eventModule: 'userEvent' }],
701+
output: `
702+
import userEvent from '${testingFramework}'
703+
test('several unhandled promises from event methods is invalid', async () => {
704+
await userEvent.${eventMethod}(getByLabelText('username'))
705+
await userEvent.${eventMethod}(getByLabelText('username'))
706+
})
707+
`,
623708
} as const)
624709
),
625710
...USER_EVENT_ASYNC_FUNCTIONS.map(
@@ -642,6 +727,14 @@ ruleTester.run(RULE_NAME, rule, {
642727
},
643728
],
644729
options: [{ eventModule: 'userEvent' }],
730+
output: `
731+
import userEvent from '${testingFramework}'
732+
test(
733+
'unhandled promise from event method kept in a var is invalid',
734+
() => {
735+
const promise = await userEvent.${eventMethod}(getByLabelText('username'))
736+
})
737+
`,
645738
} as const)
646739
),
647740
...USER_EVENT_ASYNC_FUNCTIONS.map(
@@ -667,14 +760,25 @@ ruleTester.run(RULE_NAME, rule, {
667760
},
668761
],
669762
options: [{ eventModule: 'userEvent' }],
763+
output: `
764+
import userEvent from '${testingFramework}'
765+
test('unhandled promise returned from function wrapping event method is invalid', () => {
766+
function triggerEvent() {
767+
doSomething()
768+
return userEvent.${eventMethod}(getByLabelText('username'))
769+
}
770+
771+
await triggerEvent()
772+
})
773+
`,
670774
} as const)
671775
),
672776
]),
673777
{
674778
code: `
675779
import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}'
676780
import { fireEvent } from '${FIRE_EVENT_ASYNC_FRAMEWORKS[0]}'
677-
test('unhandled promises from multiple event modules', async () => {
781+
test('unhandled promises from multiple event modules', async () => {
678782
fireEvent.click(getByLabelText('username'))
679783
userEvent.click(getByLabelText('username'))
680784
})
@@ -694,6 +798,14 @@ ruleTester.run(RULE_NAME, rule, {
694798
},
695799
],
696800
options: [{ eventModule: ['userEvent', 'fireEvent'] }] as Options,
801+
output: `
802+
import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}'
803+
import { fireEvent } from '${FIRE_EVENT_ASYNC_FRAMEWORKS[0]}'
804+
test('unhandled promises from multiple event modules', async () => {
805+
await fireEvent.click(getByLabelText('username'))
806+
await userEvent.click(getByLabelText('username'))
807+
})
808+
`,
697809
},
698810
{
699811
code: `
@@ -712,6 +824,14 @@ ruleTester.run(RULE_NAME, rule, {
712824
data: { name: 'click' },
713825
},
714826
],
827+
output: `
828+
import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}'
829+
import { fireEvent } from '${FIRE_EVENT_ASYNC_FRAMEWORKS[0]}'
830+
test('unhandled promise from userEvent relying on default options', async () => {
831+
fireEvent.click(getByLabelText('username'))
832+
await userEvent.click(getByLabelText('username'))
833+
})
834+
`,
715835
},
716836
],
717837
});

0 commit comments

Comments
 (0)