diff --git a/README.md b/README.md index a2a3b3d26..a14bbd5f5 100644 --- a/README.md +++ b/README.md @@ -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 | ✅ | | 🔧 | | diff --git a/docs/rules/valid-expect.md b/docs/rules/valid-expect.md index b21d46aa2..08388359b 100644 --- a/docs/rules/valid-expect.md +++ b/docs/rules/valid-expect.md @@ -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). + +> [!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. diff --git a/src/rules/__tests__/valid-expect.test.ts b/src/rules/__tests__/valid-expect.test.ts index f55137917..883a5d958 100644 --- a/src/rules/__tests__/valid-expect.test.ts +++ b/src/rules/__tests__/valid-expect.test.ts @@ -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, @@ -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, @@ -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, @@ -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, @@ -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: [ { @@ -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, @@ -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: [ { @@ -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: [ { @@ -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", () => { @@ -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, diff --git a/src/rules/valid-expect.ts b/src/rules/valid-expect.ts index e5b30ea2f..bee809d73 100644 --- a/src/rules/valid-expect.ts +++ b/src/rules/valid-expect.ts @@ -8,6 +8,8 @@ import { ModifierName, createRule, getAccessorValue, + getSourceCode, + isFunction, isSupportedAccessor, parseJestFnCallWithReason, } from './utils'; @@ -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; @@ -127,6 +141,7 @@ export default createRule<[Options], MessageIds>({ promisesWithAsyncAssertionsMustBeAwaited: 'Promises which return async assertions must be awaited{{ orReturned }}', }, + fixable: 'code', type: 'suggestion', schema: [ { @@ -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) {