Skip to content

Commit

Permalink
feat(valid-expect): supporting automatically fixing missing await i…
Browse files Browse the repository at this point in the history
…n some cases (#1574)

* feat: add 'fixable' to valid-expect

* test: add test to valid-expect

* docs: update doc for valid-expect

* docs: added additional note

* fix: docs

* docs: using fancy alerts

* fix: format
  • Loading branch information
y-hsgw authored May 3, 2024
1 parent f47cc3c commit a407098
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 1 deletion.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ set to warn in.\
| [require-to-throw-message](docs/rules/require-to-throw-message.md) | Require a message for `toThrow()` | | | | |
| [require-top-level-describe](docs/rules/require-top-level-describe.md) | Require test cases and hooks to be inside a `describe` block | | | | |
| [valid-describe-callback](docs/rules/valid-describe-callback.md) | Enforce valid `describe()` callback || | | |
| [valid-expect](docs/rules/valid-expect.md) | Enforce valid `expect()` usage || | | |
| [valid-expect](docs/rules/valid-expect.md) | Enforce valid `expect()` usage || | 🔧 | |
| [valid-expect-in-promise](docs/rules/valid-expect-in-promise.md) | Require promises that have expectations in their chain to be valid || | | |
| [valid-title](docs/rules/valid-title.md) | Enforce valid titles || | 🔧 | |

Expand Down
6 changes: 6 additions & 0 deletions docs/rules/valid-expect.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@
💼 This rule is enabled in the ✅ `recommended`
[config](https://github.com/jest-community/eslint-plugin-jest/blob/main/README.md#shareable-configurations).

🔧 This rule is automatically fixable by the
[`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).

<!-- end auto-generated rule header -->

> [!NOTE] Test function will be fixed if it is async and does not have await in
> the async assertion.
Ensure `expect()` is called with a single argument and there is an actual
expectation made.

Expand Down
69 changes: 69 additions & 0 deletions src/rules/__tests__/valid-expect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,8 @@ ruleTester.run('valid-expect', rule, {
// usages in async function
{
code: 'test("valid-expect", async () => { expect(Promise.resolve(2)).resolves.toBeDefined(); });',
output:
'test("valid-expect", async () => { await expect(Promise.resolve(2)).resolves.toBeDefined(); });',
errors: [
{
column: 36,
Expand All @@ -582,6 +584,8 @@ ruleTester.run('valid-expect', rule, {
},
{
code: 'test("valid-expect", async () => { expect(Promise.resolve(2)).resolves.not.toBeDefined(); });',
output:
'test("valid-expect", async () => { await expect(Promise.resolve(2)).resolves.not.toBeDefined(); });',
errors: [
{
column: 36,
Expand Down Expand Up @@ -621,6 +625,12 @@ ruleTester.run('valid-expect', rule, {
expect(Promise.resolve(1)).rejects.toBeDefined();
});
`,
output: dedent`
test("valid-expect", async () => {
await expect(Promise.resolve(2)).resolves.not.toBeDefined();
await expect(Promise.resolve(1)).rejects.toBeDefined();
});
`,
errors: [
{
line: 2,
Expand All @@ -646,6 +656,12 @@ ruleTester.run('valid-expect', rule, {
expect(Promise.resolve(1)).rejects.toBeDefined();
});
`,
output: dedent`
test("valid-expect", async () => {
await expect(Promise.resolve(2)).resolves.not.toBeDefined();
await expect(Promise.resolve(1)).rejects.toBeDefined();
});
`,
errors: [
{
line: 3,
Expand All @@ -667,6 +683,12 @@ ruleTester.run('valid-expect', rule, {
return expect(Promise.resolve(1)).rejects.toBeDefined();
});
`,
output: dedent`
test("valid-expect", async () => {
await expect(Promise.resolve(2)).resolves.not.toBeDefined();
await expect(Promise.resolve(1)).rejects.toBeDefined();
});
`,
options: [{ alwaysAwait: true }],
errors: [
{
Expand All @@ -691,6 +713,12 @@ ruleTester.run('valid-expect', rule, {
return expect(Promise.resolve(1)).rejects.toBeDefined();
});
`,
output: dedent`
test("valid-expect", async () => {
await expect(Promise.resolve(2)).resolves.not.toBeDefined();
return expect(Promise.resolve(1)).rejects.toBeDefined();
});
`,
errors: [
{
line: 2,
Expand All @@ -709,6 +737,12 @@ ruleTester.run('valid-expect', rule, {
return expect(Promise.resolve(1)).rejects.toBeDefined();
});
`,
output: dedent`
test("valid-expect", async () => {
await expect(Promise.resolve(2)).resolves.not.toBeDefined();
await expect(Promise.resolve(1)).rejects.toBeDefined();
});
`,
options: [{ alwaysAwait: true }],
errors: [
{
Expand All @@ -726,6 +760,12 @@ ruleTester.run('valid-expect', rule, {
return expect(Promise.resolve(1)).toReject();
});
`,
output: dedent`
test("valid-expect", async () => {
await expect(Promise.resolve(2)).toResolve();
await expect(Promise.resolve(1)).toReject();
});
`,
options: [{ alwaysAwait: true }],
errors: [
{
Expand Down Expand Up @@ -771,6 +811,27 @@ ruleTester.run('valid-expect', rule, {
},
],
},
{
code: dedent`
test("valid-expect", async () => {
Promise.reject(expect(Promise.resolve(2)).resolves.not.toBeDefined());
});
`,
output: dedent`
test("valid-expect", async () => {
await Promise.reject(expect(Promise.resolve(2)).resolves.not.toBeDefined());
});
`,
errors: [
{
line: 2,
column: 3,
endColumn: 72,
messageId: 'promisesWithAsyncAssertionsMustBeAwaited',
data: { orReturned: ' or returned' },
},
],
},
{
code: dedent`
test("valid-expect", () => {
Expand Down Expand Up @@ -961,6 +1022,14 @@ ruleTester.run('valid-expect', rule, {
});
});
`,
output: dedent`
test("valid-expect", () => {
return expect(functionReturningAPromise()).resolves.toEqual(1).then(async () => {
await expect(Promise.resolve(2)).resolves.toBe(1);
await expect(Promise.resolve(4)).resolves.toBe(4);
});
});
`,
errors: [
{
line: 4,
Expand Down
34 changes: 34 additions & 0 deletions src/rules/valid-expect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
ModifierName,
createRule,
getAccessorValue,
getSourceCode,
isFunction,
isSupportedAccessor,
parseJestFnCallWithReason,
} from './utils';
Expand Down Expand Up @@ -48,6 +50,18 @@ const findPromiseCallExpressionNode = (node: TSESTree.Node) =>
? getPromiseCallExpressionNode(node.parent)
: null;

const findFirstAsyncFunction = ({
parent,
}: TSESTree.Node): TSESTree.Node | null => {
if (!parent) {
return null;
}

return isFunction(parent) && parent.async
? parent
: findFirstAsyncFunction(parent);
};

const getParentIfThenified = (node: TSESTree.Node): TSESTree.Node => {
const grandParentNode = node.parent?.parent;

Expand Down Expand Up @@ -127,6 +141,7 @@ export default createRule<[Options], MessageIds>({
promisesWithAsyncAssertionsMustBeAwaited:
'Promises which return async assertions must be awaited{{ orReturned }}',
},
fixable: 'code',
type: 'suggestion',
schema: [
{
Expand Down Expand Up @@ -339,6 +354,25 @@ export default createRule<[Options], MessageIds>({
? 'asyncMustBeAwaited'
: 'promisesWithAsyncAssertionsMustBeAwaited',
node,
fix(fixer) {
if (!findFirstAsyncFunction(finalNode)) {
return [];
}
const returnStatement =
finalNode.parent?.type === AST_NODE_TYPES.ReturnStatement
? finalNode.parent
: null;

if (alwaysAwait && returnStatement) {
const sourceCodeText =
getSourceCode(context).getText(returnStatement);
const replacedText = sourceCodeText.replace('return', 'await');

return fixer.replaceText(returnStatement, replacedText);
}

return fixer.insertTextBefore(finalNode, 'await ');
},
});

if (isParentArrayExpression) {
Expand Down

0 comments on commit a407098

Please sign in to comment.