Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(await-async-event): add basic fixer #656

Merged
merged 1 commit into from
Oct 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ To enable this configuration use the `extends` property in your

| Name | Description | 🔧 | Included in configurations |
| ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------- | --- | ---------------------------------------------------------------------------------- |
| [`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][] |
| [`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][] |
| [`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][] |
| [`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][] |
| [`consistent-data-testid`](./docs/rules/consistent-data-testid.md) | Ensures consistent usage of `data-testid` | | |
Expand Down
52 changes: 37 additions & 15 deletions lib/rules/await-async-event.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { ASTUtils, TSESTree } from '@typescript-eslint/utils';
import { ASTUtils, TSESLint, TSESTree } from '@typescript-eslint/utils';

import { createTestingLibraryRule } from '../create-testing-library-rule';
import {
findClosestCallExpressionNode,
getFunctionName,
getInnermostReturningFunction,
getVariableReferences,
isMemberExpression,
isPromiseHandled,
} from '../node-utils';
import { EVENTS_SIMULATORS } from '../utils';
Expand Down Expand Up @@ -41,6 +42,7 @@ export default createTestingLibraryRule<Options, MessageIds>({
awaitAsyncEventWrapper:
'Promise returned from `{{ name }}` wrapper over async event method must be handled',
},
fixable: 'code',
schema: [
{
type: 'object',
Expand Down Expand Up @@ -76,16 +78,23 @@ export default createTestingLibraryRule<Options, MessageIds>({
create(context, [options], helpers) {
const functionWrappersNames: string[] = [];

function reportUnhandledNode(
node: TSESTree.Identifier,
closestCallExpressionNode: TSESTree.CallExpression,
messageId: MessageIds = 'awaitAsyncEvent'
): void {
function reportUnhandledNode({
node,
closestCallExpression,
messageId = 'awaitAsyncEvent',
fix,
}: {
node: TSESTree.Identifier;
closestCallExpression: TSESTree.CallExpression;
messageId?: MessageIds;
fix?: TSESLint.ReportFixFunction;
}): void {
if (!isPromiseHandled(node)) {
context.report({
node: closestCallExpressionNode.callee,
node: closestCallExpression.callee,
messageId,
data: { name: node.name },
fix,
});
}
}
Expand Down Expand Up @@ -128,14 +137,24 @@ export default createTestingLibraryRule<Options, MessageIds>({
);

if (references.length === 0) {
reportUnhandledNode(node, closestCallExpression);
reportUnhandledNode({
node,
closestCallExpression,
fix: (fixer) => {
if (isMemberExpression(node.parent)) {
return fixer.insertTextBefore(node.parent, 'await ');
}

return null;
},
});
} else {
for (const reference of references) {
if (ASTUtils.isIdentifier(reference.identifier)) {
reportUnhandledNode(
reference.identifier,
closestCallExpression
);
reportUnhandledNode({
node: reference.identifier,
closestCallExpression,
});
}
}
}
Expand All @@ -151,11 +170,14 @@ export default createTestingLibraryRule<Options, MessageIds>({
return;
}

reportUnhandledNode(
reportUnhandledNode({
node,
closestCallExpression,
'awaitAsyncEventWrapper'
);
messageId: 'awaitAsyncEventWrapper',
fix: (fixer) => {
return fixer.insertTextBefore(node, 'await ');
},
});
}
},
};
Expand Down
122 changes: 121 additions & 1 deletion tests/lib/rules/await-async-event.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,12 @@ ruleTester.run(RULE_NAME, rule, {
},
],
options: [{ eventModule: 'fireEvent' }],
output: `
import { fireEvent } from '${testingFramework}'
test('unhandled promise from event method is invalid', async () => {
await fireEvent.${eventMethod}(getByLabelText('username'))
})
`,
} as const)
),
...FIRE_EVENT_ASYNC_FUNCTIONS.map(
Expand All @@ -380,6 +386,12 @@ ruleTester.run(RULE_NAME, rule, {
},
],
options: [{ eventModule: 'fireEvent' }],
output: `
import { fireEvent as testingLibraryFireEvent } from '${testingFramework}'
test('unhandled promise from aliased event method is invalid', async () => {
await testingLibraryFireEvent.${eventMethod}(getByLabelText('username'))
})
`,
} as const)
),
...FIRE_EVENT_ASYNC_FUNCTIONS.map(
Expand All @@ -401,6 +413,12 @@ ruleTester.run(RULE_NAME, rule, {
},
],
options: [{ eventModule: 'fireEvent' }],
output: `
import * as testingLibrary from '${testingFramework}'
test('unhandled promise from wildcard imported event method is invalid', async () => {
await testingLibrary.fireEvent.${eventMethod}(getByLabelText('username'))
})
`,
} as const)
),
...FIRE_EVENT_ASYNC_FUNCTIONS.map(
Expand Down Expand Up @@ -428,6 +446,13 @@ ruleTester.run(RULE_NAME, rule, {
},
],
options: [{ eventModule: 'fireEvent' }],
output: `
import { fireEvent } from '${testingFramework}'
test('several unhandled promises from event methods is invalid', async () => {
await fireEvent.${eventMethod}(getByLabelText('username'))
await fireEvent.${eventMethod}(getByLabelText('username'))
})
`,
} as const)
),
...FIRE_EVENT_ASYNC_FUNCTIONS.map(
Expand All @@ -451,6 +476,12 @@ ruleTester.run(RULE_NAME, rule, {
},
],
options: [{ eventModule: 'fireEvent' }],
output: `
import { fireEvent } from '${testingFramework}'
test('unhandled promise from event method with aggressive reporting opted-out is invalid', async () => {
await fireEvent.${eventMethod}(getByLabelText('username'))
})
`,
} as const)
),
...FIRE_EVENT_ASYNC_FUNCTIONS.map(
Expand All @@ -476,6 +507,14 @@ ruleTester.run(RULE_NAME, rule, {
},
],
options: [{ eventModule: 'fireEvent' }],
output: `
import { fireEvent } from 'test-utils'
test(
'unhandled promise from event method imported from custom module with aggressive reporting opted-out is invalid',
() => {
await fireEvent.${eventMethod}(getByLabelText('username'))
})
`,
} as const)
),
...FIRE_EVENT_ASYNC_FUNCTIONS.map(
Expand All @@ -501,6 +540,14 @@ ruleTester.run(RULE_NAME, rule, {
},
],
options: [{ eventModule: 'fireEvent' }],
output: `
import { fireEvent } from '${testingFramework}'
test(
'unhandled promise from event method imported from default module with aggressive reporting opted-out is invalid',
() => {
await fireEvent.${eventMethod}(getByLabelText('username'))
})
`,
} as const)
),

Expand All @@ -524,6 +571,14 @@ ruleTester.run(RULE_NAME, rule, {
},
],
options: [{ eventModule: 'fireEvent' }],
output: `
import { fireEvent } from '${testingFramework}'
test(
'unhandled promise from event method kept in a var is invalid',
() => {
const promise = await fireEvent.${eventMethod}(getByLabelText('username'))
})
`,
} as const)
),
...FIRE_EVENT_ASYNC_FUNCTIONS.map(
Expand All @@ -549,6 +604,17 @@ ruleTester.run(RULE_NAME, rule, {
},
],
options: [{ eventModule: 'fireEvent' }],
output: `
import { fireEvent } from '${testingFramework}'
test('unhandled promise returned from function wrapping event method is invalid', () => {
function triggerEvent() {
doSomething()
return fireEvent.${eventMethod}(getByLabelText('username'))
}

await triggerEvent()
})
`,
} as const)
),
]),
Expand All @@ -572,6 +638,12 @@ ruleTester.run(RULE_NAME, rule, {
},
],
options: [{ eventModule: 'userEvent' }],
output: `
import userEvent from '${testingFramework}'
test('unhandled promise from event method is invalid', async () => {
await userEvent.${eventMethod}(getByLabelText('username'))
})
`,
} as const)
),
...USER_EVENT_ASYNC_FUNCTIONS.map(
Expand All @@ -593,6 +665,12 @@ ruleTester.run(RULE_NAME, rule, {
},
],
options: [{ eventModule: 'userEvent' }],
output: `
import testingLibraryUserEvent from '${testingFramework}'
test('unhandled promise imported from alternate name event method is invalid', async () => {
await testingLibraryUserEvent.${eventMethod}(getByLabelText('username'))
})
`,
} as const)
),
...USER_EVENT_ASYNC_FUNCTIONS.map(
Expand Down Expand Up @@ -620,6 +698,13 @@ ruleTester.run(RULE_NAME, rule, {
},
],
options: [{ eventModule: 'userEvent' }],
output: `
import userEvent from '${testingFramework}'
test('several unhandled promises from event methods is invalid', async () => {
await userEvent.${eventMethod}(getByLabelText('username'))
await userEvent.${eventMethod}(getByLabelText('username'))
})
`,
} as const)
),
...USER_EVENT_ASYNC_FUNCTIONS.map(
Expand All @@ -642,6 +727,14 @@ ruleTester.run(RULE_NAME, rule, {
},
],
options: [{ eventModule: 'userEvent' }],
output: `
import userEvent from '${testingFramework}'
test(
'unhandled promise from event method kept in a var is invalid',
() => {
const promise = await userEvent.${eventMethod}(getByLabelText('username'))
})
`,
} as const)
),
...USER_EVENT_ASYNC_FUNCTIONS.map(
Expand All @@ -667,14 +760,25 @@ ruleTester.run(RULE_NAME, rule, {
},
],
options: [{ eventModule: 'userEvent' }],
output: `
import userEvent from '${testingFramework}'
test('unhandled promise returned from function wrapping event method is invalid', () => {
function triggerEvent() {
doSomething()
return userEvent.${eventMethod}(getByLabelText('username'))
}

await triggerEvent()
})
`,
} as const)
),
]),
{
code: `
import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}'
import { fireEvent } from '${FIRE_EVENT_ASYNC_FRAMEWORKS[0]}'
test('unhandled promises from multiple event modules', async () => {
test('unhandled promises from multiple event modules', async () => {
fireEvent.click(getByLabelText('username'))
userEvent.click(getByLabelText('username'))
})
Expand All @@ -694,6 +798,14 @@ ruleTester.run(RULE_NAME, rule, {
},
],
options: [{ eventModule: ['userEvent', 'fireEvent'] }] as Options,
output: `
import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}'
import { fireEvent } from '${FIRE_EVENT_ASYNC_FRAMEWORKS[0]}'
test('unhandled promises from multiple event modules', async () => {
await fireEvent.click(getByLabelText('username'))
await userEvent.click(getByLabelText('username'))
})
`,
},
{
code: `
Expand All @@ -712,6 +824,14 @@ ruleTester.run(RULE_NAME, rule, {
data: { name: 'click' },
},
],
output: `
import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}'
import { fireEvent } from '${FIRE_EVENT_ASYNC_FRAMEWORKS[0]}'
test('unhandled promise from userEvent relying on default options', async () => {
fireEvent.click(getByLabelText('username'))
await userEvent.click(getByLabelText('username'))
})
`,
},
],
});