From c4979049b3d62c54cd951ecb2fa8c9b60898f906 Mon Sep 17 00:00:00 2001 From: hainenber Date: Sun, 30 Nov 2025 14:17:42 +0700 Subject: [PATCH 1/5] feat(rule): disallow unnecessary async function wrapper for single `await` call Signed-off-by: hainenber --- README.md | 131 ++++++++-------- .../no-async-wrapper-for-expected-promise.md | 42 ++++++ .../__snapshots__/rules.test.ts.snap | 4 + src/__tests__/rules.test.ts | 2 +- src/index.ts | 1 + ...async-wrapper-for-expected-promise.test.ts | 141 ++++++++++++++++++ .../no-async-wrapper-for-expected-promise.ts | 74 +++++++++ 7 files changed, 329 insertions(+), 66 deletions(-) create mode 100644 docs/rules/no-async-wrapper-for-expected-promise.md create mode 100644 src/rules/__tests__/no-async-wrapper-for-expected-promise.test.ts create mode 100644 src/rules/no-async-wrapper-for-expected-promise.ts diff --git a/README.md b/README.md index 675e0e4d3..75e5cf073 100644 --- a/README.md +++ b/README.md @@ -323,71 +323,72 @@ Automatically fixable by the Manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions). -| Name                              | Description | 💼 | ⚠️ | 🔧 | 💡 | -| :----------------------------------------------------------------------------------- | :------------------------------------------------------------------------ | :-- | :-- | :-- | :-- | -| [consistent-test-it](docs/rules/consistent-test-it.md) | Enforce `test` and `it` usage conventions | | | 🔧 | | -| [expect-expect](docs/rules/expect-expect.md) | Enforce assertion to be made in a test body | | ✅ | | | -| [max-expects](docs/rules/max-expects.md) | Enforces a maximum number assertion calls in a test body | | | | | -| [max-nested-describe](docs/rules/max-nested-describe.md) | Enforces a maximum depth to nested describe calls | | | | | -| [no-alias-methods](docs/rules/no-alias-methods.md) | Disallow alias methods | ✅ | | 🔧 | | -| [no-commented-out-tests](docs/rules/no-commented-out-tests.md) | Disallow commented out tests | | ✅ | | | -| [no-conditional-expect](docs/rules/no-conditional-expect.md) | Disallow calling `expect` conditionally | ✅ | | | | -| [no-conditional-in-test](docs/rules/no-conditional-in-test.md) | Disallow conditional logic in tests | | | | | -| [no-confusing-set-timeout](docs/rules/no-confusing-set-timeout.md) | Disallow confusing usages of jest.setTimeout | | | | | -| [no-deprecated-functions](docs/rules/no-deprecated-functions.md) | Disallow use of deprecated functions | ✅ | | 🔧 | | -| [no-disabled-tests](docs/rules/no-disabled-tests.md) | Disallow disabled tests | | ✅ | | | -| [no-done-callback](docs/rules/no-done-callback.md) | Disallow using a callback in asynchronous tests and hooks | ✅ | | | 💡 | -| [no-duplicate-hooks](docs/rules/no-duplicate-hooks.md) | Disallow duplicate setup and teardown hooks | | | | | -| [no-export](docs/rules/no-export.md) | Disallow using `exports` in files containing tests | ✅ | | | | -| [no-focused-tests](docs/rules/no-focused-tests.md) | Disallow focused tests | ✅ | | | 💡 | -| [no-hooks](docs/rules/no-hooks.md) | Disallow setup and teardown hooks | | | | | -| [no-identical-title](docs/rules/no-identical-title.md) | Disallow identical titles | ✅ | | | | -| [no-interpolation-in-snapshots](docs/rules/no-interpolation-in-snapshots.md) | Disallow string interpolation inside snapshots | ✅ | | | | -| [no-jasmine-globals](docs/rules/no-jasmine-globals.md) | Disallow Jasmine globals | ✅ | | 🔧 | | -| [no-large-snapshots](docs/rules/no-large-snapshots.md) | Disallow large snapshots | | | | | -| [no-mocks-import](docs/rules/no-mocks-import.md) | Disallow manually importing from `__mocks__` | ✅ | | | | -| [no-restricted-jest-methods](docs/rules/no-restricted-jest-methods.md) | Disallow specific `jest.` methods | | | | | -| [no-restricted-matchers](docs/rules/no-restricted-matchers.md) | Disallow specific matchers & modifiers | | | | | -| [no-standalone-expect](docs/rules/no-standalone-expect.md) | Disallow using `expect` outside of `it` or `test` blocks | ✅ | | | | -| [no-test-prefixes](docs/rules/no-test-prefixes.md) | Require using `.only` and `.skip` over `f` and `x` | ✅ | | 🔧 | | -| [no-test-return-statement](docs/rules/no-test-return-statement.md) | Disallow explicitly returning from tests | | | | | -| [no-untyped-mock-factory](docs/rules/no-untyped-mock-factory.md) | Disallow using `jest.mock()` factories without an explicit type parameter | | | 🔧 | | -| [padding-around-after-all-blocks](docs/rules/padding-around-after-all-blocks.md) | Enforce padding around `afterAll` blocks | | | 🔧 | | -| [padding-around-after-each-blocks](docs/rules/padding-around-after-each-blocks.md) | Enforce padding around `afterEach` blocks | | | 🔧 | | -| [padding-around-all](docs/rules/padding-around-all.md) | Enforce padding around Jest functions | | | 🔧 | | -| [padding-around-before-all-blocks](docs/rules/padding-around-before-all-blocks.md) | Enforce padding around `beforeAll` blocks | | | 🔧 | | -| [padding-around-before-each-blocks](docs/rules/padding-around-before-each-blocks.md) | Enforce padding around `beforeEach` blocks | | | 🔧 | | -| [padding-around-describe-blocks](docs/rules/padding-around-describe-blocks.md) | Enforce padding around `describe` blocks | | | 🔧 | | -| [padding-around-expect-groups](docs/rules/padding-around-expect-groups.md) | Enforce padding around `expect` groups | | | 🔧 | | -| [padding-around-test-blocks](docs/rules/padding-around-test-blocks.md) | Enforce padding around `test` and `it` blocks | | | 🔧 | | -| [prefer-called-with](docs/rules/prefer-called-with.md) | Suggest using `toBeCalledWith()` or `toHaveBeenCalledWith()` | | | | | -| [prefer-comparison-matcher](docs/rules/prefer-comparison-matcher.md) | Suggest using the built-in comparison matchers | | | 🔧 | | -| [prefer-each](docs/rules/prefer-each.md) | Prefer using `.each` rather than manual loops | | | | | -| [prefer-ending-with-an-expect](docs/rules/prefer-ending-with-an-expect.md) | Prefer having the last statement in a test be an assertion | | | | | -| [prefer-equality-matcher](docs/rules/prefer-equality-matcher.md) | Suggest using the built-in equality matchers | | | | 💡 | -| [prefer-expect-assertions](docs/rules/prefer-expect-assertions.md) | Suggest using `expect.assertions()` OR `expect.hasAssertions()` | | | | 💡 | -| [prefer-expect-resolves](docs/rules/prefer-expect-resolves.md) | Prefer `await expect(...).resolves` over `expect(await ...)` syntax | | | 🔧 | | -| [prefer-hooks-in-order](docs/rules/prefer-hooks-in-order.md) | Prefer having hooks in a consistent order | | | | | -| [prefer-hooks-on-top](docs/rules/prefer-hooks-on-top.md) | Suggest having hooks before any test cases | | | | | -| [prefer-importing-jest-globals](docs/rules/prefer-importing-jest-globals.md) | Prefer importing Jest globals | | | 🔧 | | -| [prefer-jest-mocked](docs/rules/prefer-jest-mocked.md) | Prefer `jest.mocked()` over `fn as jest.Mock` | | | 🔧 | | -| [prefer-lowercase-title](docs/rules/prefer-lowercase-title.md) | Enforce lowercase test names | | | 🔧 | | -| [prefer-mock-promise-shorthand](docs/rules/prefer-mock-promise-shorthand.md) | Prefer mock resolved/rejected shorthands for promises | | | 🔧 | | -| [prefer-snapshot-hint](docs/rules/prefer-snapshot-hint.md) | Prefer including a hint with external snapshots | | | | | -| [prefer-spy-on](docs/rules/prefer-spy-on.md) | Suggest using `jest.spyOn()` | | | 🔧 | | -| [prefer-strict-equal](docs/rules/prefer-strict-equal.md) | Suggest using `toStrictEqual()` | | | | 💡 | -| [prefer-to-be](docs/rules/prefer-to-be.md) | Suggest using `toBe()` for primitive literals | 🎨 | | 🔧 | | -| [prefer-to-contain](docs/rules/prefer-to-contain.md) | Suggest using `toContain()` | 🎨 | | 🔧 | | -| [prefer-to-have-length](docs/rules/prefer-to-have-length.md) | Suggest using `toHaveLength()` | 🎨 | | 🔧 | | -| [prefer-todo](docs/rules/prefer-todo.md) | Suggest using `test.todo` | | | 🔧 | | -| [require-hook](docs/rules/require-hook.md) | Require setup and teardown code to be within a hook | | | | | -| [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-in-promise](docs/rules/valid-expect-in-promise.md) | Require promises that have expectations in their chain to be valid | ✅ | | | | -| [valid-mock-module-path](docs/rules/valid-mock-module-path.md) | Disallow mocking of non-existing module paths | | | | | -| [valid-title](docs/rules/valid-title.md) | Enforce valid titles | ✅ | | 🔧 | | +| Name                                  | Description | 💼 | ⚠️ | 🔧 | 💡 | +| :------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------ | :-- | :-- | :-- | :-- | +| [consistent-test-it](docs/rules/consistent-test-it.md) | Enforce `test` and `it` usage conventions | | | 🔧 | | +| [expect-expect](docs/rules/expect-expect.md) | Enforce assertion to be made in a test body | | ✅ | | | +| [max-expects](docs/rules/max-expects.md) | Enforces a maximum number assertion calls in a test body | | | | | +| [max-nested-describe](docs/rules/max-nested-describe.md) | Enforces a maximum depth to nested describe calls | | | | | +| [no-alias-methods](docs/rules/no-alias-methods.md) | Disallow alias methods | ✅ | | 🔧 | | +| [no-async-wrapper-for-expected-promise](docs/rules/no-async-wrapper-for-expected-promise.md) | Disallow unnecessary async wrapper for expected promises | ✅ | | 🔧 | | +| [no-commented-out-tests](docs/rules/no-commented-out-tests.md) | Disallow commented out tests | | ✅ | | | +| [no-conditional-expect](docs/rules/no-conditional-expect.md) | Disallow calling `expect` conditionally | ✅ | | | | +| [no-conditional-in-test](docs/rules/no-conditional-in-test.md) | Disallow conditional logic in tests | | | | | +| [no-confusing-set-timeout](docs/rules/no-confusing-set-timeout.md) | Disallow confusing usages of jest.setTimeout | | | | | +| [no-deprecated-functions](docs/rules/no-deprecated-functions.md) | Disallow use of deprecated functions | ✅ | | 🔧 | | +| [no-disabled-tests](docs/rules/no-disabled-tests.md) | Disallow disabled tests | | ✅ | | | +| [no-done-callback](docs/rules/no-done-callback.md) | Disallow using a callback in asynchronous tests and hooks | ✅ | | | 💡 | +| [no-duplicate-hooks](docs/rules/no-duplicate-hooks.md) | Disallow duplicate setup and teardown hooks | | | | | +| [no-export](docs/rules/no-export.md) | Disallow using `exports` in files containing tests | ✅ | | | | +| [no-focused-tests](docs/rules/no-focused-tests.md) | Disallow focused tests | ✅ | | | 💡 | +| [no-hooks](docs/rules/no-hooks.md) | Disallow setup and teardown hooks | | | | | +| [no-identical-title](docs/rules/no-identical-title.md) | Disallow identical titles | ✅ | | | | +| [no-interpolation-in-snapshots](docs/rules/no-interpolation-in-snapshots.md) | Disallow string interpolation inside snapshots | ✅ | | | | +| [no-jasmine-globals](docs/rules/no-jasmine-globals.md) | Disallow Jasmine globals | ✅ | | 🔧 | | +| [no-large-snapshots](docs/rules/no-large-snapshots.md) | Disallow large snapshots | | | | | +| [no-mocks-import](docs/rules/no-mocks-import.md) | Disallow manually importing from `__mocks__` | ✅ | | | | +| [no-restricted-jest-methods](docs/rules/no-restricted-jest-methods.md) | Disallow specific `jest.` methods | | | | | +| [no-restricted-matchers](docs/rules/no-restricted-matchers.md) | Disallow specific matchers & modifiers | | | | | +| [no-standalone-expect](docs/rules/no-standalone-expect.md) | Disallow using `expect` outside of `it` or `test` blocks | ✅ | | | | +| [no-test-prefixes](docs/rules/no-test-prefixes.md) | Require using `.only` and `.skip` over `f` and `x` | ✅ | | 🔧 | | +| [no-test-return-statement](docs/rules/no-test-return-statement.md) | Disallow explicitly returning from tests | | | | | +| [no-untyped-mock-factory](docs/rules/no-untyped-mock-factory.md) | Disallow using `jest.mock()` factories without an explicit type parameter | | | 🔧 | | +| [padding-around-after-all-blocks](docs/rules/padding-around-after-all-blocks.md) | Enforce padding around `afterAll` blocks | | | 🔧 | | +| [padding-around-after-each-blocks](docs/rules/padding-around-after-each-blocks.md) | Enforce padding around `afterEach` blocks | | | 🔧 | | +| [padding-around-all](docs/rules/padding-around-all.md) | Enforce padding around Jest functions | | | 🔧 | | +| [padding-around-before-all-blocks](docs/rules/padding-around-before-all-blocks.md) | Enforce padding around `beforeAll` blocks | | | 🔧 | | +| [padding-around-before-each-blocks](docs/rules/padding-around-before-each-blocks.md) | Enforce padding around `beforeEach` blocks | | | 🔧 | | +| [padding-around-describe-blocks](docs/rules/padding-around-describe-blocks.md) | Enforce padding around `describe` blocks | | | 🔧 | | +| [padding-around-expect-groups](docs/rules/padding-around-expect-groups.md) | Enforce padding around `expect` groups | | | 🔧 | | +| [padding-around-test-blocks](docs/rules/padding-around-test-blocks.md) | Enforce padding around `test` and `it` blocks | | | 🔧 | | +| [prefer-called-with](docs/rules/prefer-called-with.md) | Suggest using `toBeCalledWith()` or `toHaveBeenCalledWith()` | | | | | +| [prefer-comparison-matcher](docs/rules/prefer-comparison-matcher.md) | Suggest using the built-in comparison matchers | | | 🔧 | | +| [prefer-each](docs/rules/prefer-each.md) | Prefer using `.each` rather than manual loops | | | | | +| [prefer-ending-with-an-expect](docs/rules/prefer-ending-with-an-expect.md) | Prefer having the last statement in a test be an assertion | | | | | +| [prefer-equality-matcher](docs/rules/prefer-equality-matcher.md) | Suggest using the built-in equality matchers | | | | 💡 | +| [prefer-expect-assertions](docs/rules/prefer-expect-assertions.md) | Suggest using `expect.assertions()` OR `expect.hasAssertions()` | | | | 💡 | +| [prefer-expect-resolves](docs/rules/prefer-expect-resolves.md) | Prefer `await expect(...).resolves` over `expect(await ...)` syntax | | | 🔧 | | +| [prefer-hooks-in-order](docs/rules/prefer-hooks-in-order.md) | Prefer having hooks in a consistent order | | | | | +| [prefer-hooks-on-top](docs/rules/prefer-hooks-on-top.md) | Suggest having hooks before any test cases | | | | | +| [prefer-importing-jest-globals](docs/rules/prefer-importing-jest-globals.md) | Prefer importing Jest globals | | | 🔧 | | +| [prefer-jest-mocked](docs/rules/prefer-jest-mocked.md) | Prefer `jest.mocked()` over `fn as jest.Mock` | | | 🔧 | | +| [prefer-lowercase-title](docs/rules/prefer-lowercase-title.md) | Enforce lowercase test names | | | 🔧 | | +| [prefer-mock-promise-shorthand](docs/rules/prefer-mock-promise-shorthand.md) | Prefer mock resolved/rejected shorthands for promises | | | 🔧 | | +| [prefer-snapshot-hint](docs/rules/prefer-snapshot-hint.md) | Prefer including a hint with external snapshots | | | | | +| [prefer-spy-on](docs/rules/prefer-spy-on.md) | Suggest using `jest.spyOn()` | | | 🔧 | | +| [prefer-strict-equal](docs/rules/prefer-strict-equal.md) | Suggest using `toStrictEqual()` | | | | 💡 | +| [prefer-to-be](docs/rules/prefer-to-be.md) | Suggest using `toBe()` for primitive literals | 🎨 | | 🔧 | | +| [prefer-to-contain](docs/rules/prefer-to-contain.md) | Suggest using `toContain()` | 🎨 | | 🔧 | | +| [prefer-to-have-length](docs/rules/prefer-to-have-length.md) | Suggest using `toHaveLength()` | 🎨 | | 🔧 | | +| [prefer-todo](docs/rules/prefer-todo.md) | Suggest using `test.todo` | | | 🔧 | | +| [require-hook](docs/rules/require-hook.md) | Require setup and teardown code to be within a hook | | | | | +| [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-in-promise](docs/rules/valid-expect-in-promise.md) | Require promises that have expectations in their chain to be valid | ✅ | | | | +| [valid-mock-module-path](docs/rules/valid-mock-module-path.md) | Disallow mocking of non-existing module paths | | | | | +| [valid-title](docs/rules/valid-title.md) | Enforce valid titles | ✅ | | 🔧 | | ### Requires Type Checking diff --git a/docs/rules/no-async-wrapper-for-expected-promise.md b/docs/rules/no-async-wrapper-for-expected-promise.md new file mode 100644 index 000000000..d7b9754b4 --- /dev/null +++ b/docs/rules/no-async-wrapper-for-expected-promise.md @@ -0,0 +1,42 @@ +# Disallow unnecessary async wrapper for expected promises (`no-async-wrapper-for-expected-promise`) + +💼 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). + + + +`Jest` can handle fulfilled/rejected promisified function call normally but +occassionally, engineers wrap said function in another `async` function that is +excessively verbose and make the tests harder to read. + +## Rule details + +This rule triggers a warning if a single `await` function call is wrapped by an +unnecessary `async` function. + +Examples of **incorrect** code for this rule + +```js +it('wrong1', async () => { + await expect(async () => { + await doSomethingAsync(); + }).rejects.toThrow(); +}); + +it('wrong2', async () => { + await expect(async function () { + await doSomethingAsync(); + }).rejects.toThrow(); +}); +``` + +Examples of **correct** code for this rule + +```js +it('right1', async () => { + await expect(doSomethingAsync()).rejects.toThrow(); +}); +``` diff --git a/src/__tests__/__snapshots__/rules.test.ts.snap b/src/__tests__/__snapshots__/rules.test.ts.snap index 6bf462676..205a6fcf1 100644 --- a/src/__tests__/__snapshots__/rules.test.ts.snap +++ b/src/__tests__/__snapshots__/rules.test.ts.snap @@ -15,6 +15,7 @@ exports[`rules should export configs that refer to actual rules 1`] = ` "jest/max-expects": "error", "jest/max-nested-describe": "error", "jest/no-alias-methods": "error", + "jest/no-async-wrapper-for-expected-promise": "error", "jest/no-commented-out-tests": "error", "jest/no-conditional-expect": "error", "jest/no-conditional-in-test": "error", @@ -108,6 +109,7 @@ exports[`rules should export configs that refer to actual rules 1`] = ` "jest/max-expects": "error", "jest/max-nested-describe": "error", "jest/no-alias-methods": "error", + "jest/no-async-wrapper-for-expected-promise": "error", "jest/no-commented-out-tests": "error", "jest/no-conditional-expect": "error", "jest/no-conditional-in-test": "error", @@ -198,6 +200,7 @@ exports[`rules should export configs that refer to actual rules 1`] = ` "rules": { "jest/expect-expect": "warn", "jest/no-alias-methods": "error", + "jest/no-async-wrapper-for-expected-promise": "error", "jest/no-commented-out-tests": "warn", "jest/no-conditional-expect": "error", "jest/no-deprecated-functions": "error", @@ -259,6 +262,7 @@ exports[`rules should export configs that refer to actual rules 1`] = ` "rules": { "jest/expect-expect": "warn", "jest/no-alias-methods": "error", + "jest/no-async-wrapper-for-expected-promise": "error", "jest/no-commented-out-tests": "warn", "jest/no-conditional-expect": "error", "jest/no-deprecated-functions": "error", diff --git a/src/__tests__/rules.test.ts b/src/__tests__/rules.test.ts index a1a9af556..9ca3b1e0e 100644 --- a/src/__tests__/rules.test.ts +++ b/src/__tests__/rules.test.ts @@ -2,7 +2,7 @@ import { existsSync } from 'fs'; import { resolve } from 'path'; import plugin from '../'; -const numberOfRules = 64; +const numberOfRules = 65; const ruleNames = Object.keys(plugin.rules); const deprecatedRules = Object.entries(plugin.rules) .filter(([, rule]) => rule.meta.deprecated) diff --git a/src/index.ts b/src/index.ts index e32f97b52..888c874be 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,6 +33,7 @@ const rules = Object.fromEntries( const recommendedRules = { 'jest/expect-expect': 'warn', 'jest/no-alias-methods': 'error', + 'jest/no-async-wrapper-for-expected-promise': 'error', 'jest/no-commented-out-tests': 'warn', 'jest/no-conditional-expect': 'error', 'jest/no-deprecated-functions': 'error', diff --git a/src/rules/__tests__/no-async-wrapper-for-expected-promise.test.ts b/src/rules/__tests__/no-async-wrapper-for-expected-promise.test.ts new file mode 100644 index 000000000..ce54547df --- /dev/null +++ b/src/rules/__tests__/no-async-wrapper-for-expected-promise.test.ts @@ -0,0 +1,141 @@ +import dedent from 'dedent'; +import rule from '../no-async-wrapper-for-expected-promise'; +import { FlatCompatRuleTester as RuleTester, espreeParser } from './test-utils'; + +const ruleTester = new RuleTester({ + parser: espreeParser, + parserOptions: { + ecmaVersion: 2017, + }, +}); + +ruleTester.run('no-async-wrapper-for-expected-promise', rule, { + valid: [ + 'expect.hasAssertions()', + dedent` + it('pass', async () => { + expect(); + }) + `, + dedent` + it('pass', async () => { + await expect(doSomethingAsync()).rejects.toThrow(); + }) + `, + dedent` + it('pass', async () => { + await expect(doSomethingAsync(1, 2)).resolves.toBe(1); + }) + `, + dedent` + it('pass', async () => { + await expect(async () => { + await doSomethingAsync(); + await doSomethingTwiceAsync(1, 2); + }).rejects.toThrow(); + }) + `, + { + code: dedent` + import { expect as pleaseExpect } from '@jest/globals'; + it('pass', async () => { + await pleaseExpect(doSomethingAsync()).rejects.toThrow(); + }) + `, + parserOptions: { sourceType: 'module' }, + }, + dedent` + it('pass', async () => { + await expect(async () => { + doSomethingSync(); + }).rejects.toThrow(); + }) + `, + ], + invalid: [ + { + code: dedent` + it('should be fix', async () => { + await expect(async () => { + await doSomethingAsync(); + }).rejects.toThrow(); + }) + `, + output: dedent` + it('should be fix', async () => { + await expect(doSomethingAsync()).rejects.toThrow(); + }) + `, + errors: [ + { + endColumn: 6, + column: 18, + messageId: 'noAsyncWrapperForExpectedPromise', + }, + ], + }, + { + code: dedent` + it('should be fix', async () => { + await expect(async function () { + await doSomethingAsync(); + }).rejects.toThrow(); + }) + `, + output: dedent` + it('should be fix', async () => { + await expect(doSomethingAsync()).rejects.toThrow(); + }) + `, + errors: [ + { + endColumn: 6, + column: 18, + messageId: 'noAsyncWrapperForExpectedPromise', + }, + ], + }, + { + code: dedent` + it('should be fix', async () => { + await expect(async () => { + await doSomethingAsync(1, 2); + }).rejects.toThrow(); + }) + `, + output: dedent` + it('should be fix', async () => { + await expect(doSomethingAsync(1, 2)).rejects.toThrow(); + }) + `, + errors: [ + { + endColumn: 6, + column: 18, + messageId: 'noAsyncWrapperForExpectedPromise', + }, + ], + }, + { + code: dedent` + it('should be fix', async () => { + await expect(async function () { + await doSomethingAsync(1, 2); + }).rejects.toThrow(); + }) + `, + output: dedent` + it('should be fix', async () => { + await expect(doSomethingAsync(1, 2)).rejects.toThrow(); + }) + `, + errors: [ + { + endColumn: 6, + column: 18, + messageId: 'noAsyncWrapperForExpectedPromise', + }, + ], + }, + ], +}); diff --git a/src/rules/no-async-wrapper-for-expected-promise.ts b/src/rules/no-async-wrapper-for-expected-promise.ts new file mode 100644 index 000000000..27a2a0b96 --- /dev/null +++ b/src/rules/no-async-wrapper-for-expected-promise.ts @@ -0,0 +1,74 @@ +import { AST_NODE_TYPES, type TSESTree } from '@typescript-eslint/utils'; +import { createRule, parseJestFnCall } from './utils'; + +export default createRule({ + name: __filename, + meta: { + docs: { + description: + 'Disallow unnecessary async function wrapper for expected promises', + }, + fixable: 'code', + messages: { + noAsyncWrapperForExpectedPromise: + 'Rejected/resolved promises should not be wrapped in async function', + }, + schema: [], + type: 'suggestion', + }, + defaultOptions: [], + create(context) { + return { + CallExpression(node: TSESTree.CallExpression) { + const jestFnCall = parseJestFnCall(node, context); + + if (jestFnCall?.type !== 'expect') { + return; + } + + const { parent } = jestFnCall.head.node; + + if (parent?.type !== AST_NODE_TYPES.CallExpression) { + return; + } + + const [awaitNode] = parent.arguments; + + if ( + (awaitNode?.type !== AST_NODE_TYPES.ArrowFunctionExpression && + awaitNode?.type !== AST_NODE_TYPES.FunctionExpression) || + !awaitNode?.async || + awaitNode.body.type !== AST_NODE_TYPES.BlockStatement || + awaitNode.body.body.length !== 1 + ) { + return; + } + + const [callback] = awaitNode.body.body; + + if ( + callback.type === AST_NODE_TYPES.ExpressionStatement && + callback.expression.type === AST_NODE_TYPES.AwaitExpression && + callback.expression.argument.type === AST_NODE_TYPES.CallExpression + ) { + const innerAsyncFuncCall = callback.expression.argument; + + context.report({ + node: awaitNode, + messageId: 'noAsyncWrapperForExpectedPromise', + fix(fixer) { + const { sourceCode } = context; + + return [ + fixer.replaceTextRange( + [awaitNode.range[0], awaitNode.range[1]], + sourceCode.getText(innerAsyncFuncCall), + ), + ]; + }, + }); + } + }, + }; + }, +}); From c7e1d1e6c59baf325751fa46962832a203a6212b Mon Sep 17 00:00:00 2001 From: hainenber Date: Sun, 30 Nov 2025 14:24:39 +0700 Subject: [PATCH 2/5] chore: regenerate docs for changed error message Signed-off-by: hainenber --- README.md | 2 +- docs/rules/no-async-wrapper-for-expected-promise.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 75e5cf073..fcb808cbc 100644 --- a/README.md +++ b/README.md @@ -330,7 +330,7 @@ Manually fixable by | [max-expects](docs/rules/max-expects.md) | Enforces a maximum number assertion calls in a test body | | | | | | [max-nested-describe](docs/rules/max-nested-describe.md) | Enforces a maximum depth to nested describe calls | | | | | | [no-alias-methods](docs/rules/no-alias-methods.md) | Disallow alias methods | ✅ | | 🔧 | | -| [no-async-wrapper-for-expected-promise](docs/rules/no-async-wrapper-for-expected-promise.md) | Disallow unnecessary async wrapper for expected promises | ✅ | | 🔧 | | +| [no-async-wrapper-for-expected-promise](docs/rules/no-async-wrapper-for-expected-promise.md) | Disallow unnecessary async function wrapper for expected promises | ✅ | | 🔧 | | | [no-commented-out-tests](docs/rules/no-commented-out-tests.md) | Disallow commented out tests | | ✅ | | | | [no-conditional-expect](docs/rules/no-conditional-expect.md) | Disallow calling `expect` conditionally | ✅ | | | | | [no-conditional-in-test](docs/rules/no-conditional-in-test.md) | Disallow conditional logic in tests | | | | | diff --git a/docs/rules/no-async-wrapper-for-expected-promise.md b/docs/rules/no-async-wrapper-for-expected-promise.md index d7b9754b4..fc6119f72 100644 --- a/docs/rules/no-async-wrapper-for-expected-promise.md +++ b/docs/rules/no-async-wrapper-for-expected-promise.md @@ -1,4 +1,4 @@ -# Disallow unnecessary async wrapper for expected promises (`no-async-wrapper-for-expected-promise`) +# Disallow unnecessary async function wrapper for expected promises (`no-async-wrapper-for-expected-promise`) 💼 This rule is enabled in the ✅ `recommended` [config](https://github.com/jest-community/eslint-plugin-jest/blob/main/README.md#shareable-configurations). From b23a00ba2b967de30848e66444a381530e5b0e62 Mon Sep 17 00:00:00 2001 From: hainenber Date: Sun, 30 Nov 2025 14:26:38 +0700 Subject: [PATCH 3/5] chore: remove from recommended config Signed-off-by: hainenber --- README.md | 2 +- docs/rules/no-async-wrapper-for-expected-promise.md | 3 --- src/__tests__/__snapshots__/rules.test.ts.snap | 2 -- src/index.ts | 1 - 4 files changed, 1 insertion(+), 7 deletions(-) diff --git a/README.md b/README.md index fcb808cbc..ed8c29ba1 100644 --- a/README.md +++ b/README.md @@ -330,7 +330,7 @@ Manually fixable by | [max-expects](docs/rules/max-expects.md) | Enforces a maximum number assertion calls in a test body | | | | | | [max-nested-describe](docs/rules/max-nested-describe.md) | Enforces a maximum depth to nested describe calls | | | | | | [no-alias-methods](docs/rules/no-alias-methods.md) | Disallow alias methods | ✅ | | 🔧 | | -| [no-async-wrapper-for-expected-promise](docs/rules/no-async-wrapper-for-expected-promise.md) | Disallow unnecessary async function wrapper for expected promises | ✅ | | 🔧 | | +| [no-async-wrapper-for-expected-promise](docs/rules/no-async-wrapper-for-expected-promise.md) | Disallow unnecessary async function wrapper for expected promises | | | 🔧 | | | [no-commented-out-tests](docs/rules/no-commented-out-tests.md) | Disallow commented out tests | | ✅ | | | | [no-conditional-expect](docs/rules/no-conditional-expect.md) | Disallow calling `expect` conditionally | ✅ | | | | | [no-conditional-in-test](docs/rules/no-conditional-in-test.md) | Disallow conditional logic in tests | | | | | diff --git a/docs/rules/no-async-wrapper-for-expected-promise.md b/docs/rules/no-async-wrapper-for-expected-promise.md index fc6119f72..7e7483557 100644 --- a/docs/rules/no-async-wrapper-for-expected-promise.md +++ b/docs/rules/no-async-wrapper-for-expected-promise.md @@ -1,8 +1,5 @@ # Disallow unnecessary async function wrapper for expected promises (`no-async-wrapper-for-expected-promise`) -💼 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). diff --git a/src/__tests__/__snapshots__/rules.test.ts.snap b/src/__tests__/__snapshots__/rules.test.ts.snap index 205a6fcf1..439f1fe52 100644 --- a/src/__tests__/__snapshots__/rules.test.ts.snap +++ b/src/__tests__/__snapshots__/rules.test.ts.snap @@ -200,7 +200,6 @@ exports[`rules should export configs that refer to actual rules 1`] = ` "rules": { "jest/expect-expect": "warn", "jest/no-alias-methods": "error", - "jest/no-async-wrapper-for-expected-promise": "error", "jest/no-commented-out-tests": "warn", "jest/no-conditional-expect": "error", "jest/no-deprecated-functions": "error", @@ -262,7 +261,6 @@ exports[`rules should export configs that refer to actual rules 1`] = ` "rules": { "jest/expect-expect": "warn", "jest/no-alias-methods": "error", - "jest/no-async-wrapper-for-expected-promise": "error", "jest/no-commented-out-tests": "warn", "jest/no-conditional-expect": "error", "jest/no-deprecated-functions": "error", diff --git a/src/index.ts b/src/index.ts index 888c874be..e32f97b52 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,7 +33,6 @@ const rules = Object.fromEntries( const recommendedRules = { 'jest/expect-expect': 'warn', 'jest/no-alias-methods': 'error', - 'jest/no-async-wrapper-for-expected-promise': 'error', 'jest/no-commented-out-tests': 'warn', 'jest/no-conditional-expect': 'error', 'jest/no-deprecated-functions': 'error', From 36e4235836debce270b07d7bfeadf672121c4f47 Mon Sep 17 00:00:00 2001 From: hainenber Date: Mon, 1 Dec 2025 09:16:51 +0700 Subject: [PATCH 4/5] chore: correct indentation for test code samples + use isFunction utility + rephrase error message Signed-off-by: hainenber --- ...async-wrapper-for-expected-promise.test.ts | 114 +++++++++--------- .../no-async-wrapper-for-expected-promise.ts | 9 +- 2 files changed, 61 insertions(+), 62 deletions(-) diff --git a/src/rules/__tests__/no-async-wrapper-for-expected-promise.test.ts b/src/rules/__tests__/no-async-wrapper-for-expected-promise.test.ts index ce54547df..664776d87 100644 --- a/src/rules/__tests__/no-async-wrapper-for-expected-promise.test.ts +++ b/src/rules/__tests__/no-async-wrapper-for-expected-promise.test.ts @@ -13,63 +13,63 @@ ruleTester.run('no-async-wrapper-for-expected-promise', rule, { valid: [ 'expect.hasAssertions()', dedent` - it('pass', async () => { - expect(); - }) - `, + it('pass', async () => { + expect(); + }) + `, dedent` - it('pass', async () => { - await expect(doSomethingAsync()).rejects.toThrow(); - }) - `, + it('pass', async () => { + await expect(doSomethingAsync()).rejects.toThrow(); + }) + `, dedent` - it('pass', async () => { - await expect(doSomethingAsync(1, 2)).resolves.toBe(1); - }) - `, + it('pass', async () => { + await expect(doSomethingAsync(1, 2)).resolves.toBe(1); + }) + `, dedent` - it('pass', async () => { - await expect(async () => { - await doSomethingAsync(); - await doSomethingTwiceAsync(1, 2); - }).rejects.toThrow(); - }) - `, + it('pass', async () => { + await expect(async () => { + await doSomethingAsync(); + await doSomethingTwiceAsync(1, 2); + }).rejects.toThrow(); + }) + `, { code: dedent` import { expect as pleaseExpect } from '@jest/globals'; it('pass', async () => { - await pleaseExpect(doSomethingAsync()).rejects.toThrow(); + await pleaseExpect(doSomethingAsync()).rejects.toThrow(); }) - `, + `, parserOptions: { sourceType: 'module' }, }, dedent` - it('pass', async () => { - await expect(async () => { - doSomethingSync(); - }).rejects.toThrow(); - }) + it('pass', async () => { + await expect(async () => { + doSomethingSync(); + }).rejects.toThrow(); + }) `, ], invalid: [ { code: dedent` it('should be fix', async () => { - await expect(async () => { - await doSomethingAsync(); - }).rejects.toThrow(); + await expect(async () => { + await doSomethingAsync(); + }).rejects.toThrow(); }) - `, + `, output: dedent` it('should be fix', async () => { - await expect(doSomethingAsync()).rejects.toThrow(); + await expect(doSomethingAsync()).rejects.toThrow(); }) - `, + `, errors: [ { - endColumn: 6, - column: 18, + endColumn: 4, + column: 16, messageId: 'noAsyncWrapperForExpectedPromise', }, ], @@ -77,20 +77,20 @@ ruleTester.run('no-async-wrapper-for-expected-promise', rule, { { code: dedent` it('should be fix', async () => { - await expect(async function () { - await doSomethingAsync(); - }).rejects.toThrow(); + await expect(async function () { + await doSomethingAsync(); + }).rejects.toThrow(); }) - `, + `, output: dedent` it('should be fix', async () => { - await expect(doSomethingAsync()).rejects.toThrow(); + await expect(doSomethingAsync()).rejects.toThrow(); }) `, errors: [ { - endColumn: 6, - column: 18, + endColumn: 4, + column: 16, messageId: 'noAsyncWrapperForExpectedPromise', }, ], @@ -98,20 +98,20 @@ ruleTester.run('no-async-wrapper-for-expected-promise', rule, { { code: dedent` it('should be fix', async () => { - await expect(async () => { - await doSomethingAsync(1, 2); - }).rejects.toThrow(); + await expect(async () => { + await doSomethingAsync(1, 2); + }).rejects.toThrow(); }) - `, + `, output: dedent` it('should be fix', async () => { - await expect(doSomethingAsync(1, 2)).rejects.toThrow(); + await expect(doSomethingAsync(1, 2)).rejects.toThrow(); }) - `, + `, errors: [ { - endColumn: 6, - column: 18, + endColumn: 4, + column: 16, messageId: 'noAsyncWrapperForExpectedPromise', }, ], @@ -119,20 +119,20 @@ ruleTester.run('no-async-wrapper-for-expected-promise', rule, { { code: dedent` it('should be fix', async () => { - await expect(async function () { - await doSomethingAsync(1, 2); - }).rejects.toThrow(); + await expect(async function () { + await doSomethingAsync(1, 2); + }).rejects.toThrow(); }) - `, + `, output: dedent` it('should be fix', async () => { - await expect(doSomethingAsync(1, 2)).rejects.toThrow(); + await expect(doSomethingAsync(1, 2)).rejects.toThrow(); }) - `, + `, errors: [ { - endColumn: 6, - column: 18, + endColumn: 4, + column: 16, messageId: 'noAsyncWrapperForExpectedPromise', }, ], diff --git a/src/rules/no-async-wrapper-for-expected-promise.ts b/src/rules/no-async-wrapper-for-expected-promise.ts index 27a2a0b96..51cd35738 100644 --- a/src/rules/no-async-wrapper-for-expected-promise.ts +++ b/src/rules/no-async-wrapper-for-expected-promise.ts @@ -1,5 +1,5 @@ import { AST_NODE_TYPES, type TSESTree } from '@typescript-eslint/utils'; -import { createRule, parseJestFnCall } from './utils'; +import { createRule, isFunction, parseJestFnCall } from './utils'; export default createRule({ name: __filename, @@ -10,8 +10,7 @@ export default createRule({ }, fixable: 'code', messages: { - noAsyncWrapperForExpectedPromise: - 'Rejected/resolved promises should not be wrapped in async function', + noAsyncWrapperForExpectedPromise: 'Unnecessary async function wrapper', }, schema: [], type: 'suggestion', @@ -35,8 +34,8 @@ export default createRule({ const [awaitNode] = parent.arguments; if ( - (awaitNode?.type !== AST_NODE_TYPES.ArrowFunctionExpression && - awaitNode?.type !== AST_NODE_TYPES.FunctionExpression) || + !awaitNode || + !isFunction(awaitNode) || !awaitNode?.async || awaitNode.body.type !== AST_NODE_TYPES.BlockStatement || awaitNode.body.body.length !== 1 From 0bf81a69f63563147c85520f3ad65ce29d5fb5c6 Mon Sep 17 00:00:00 2001 From: hainenber Date: Mon, 1 Dec 2025 21:13:51 +0700 Subject: [PATCH 5/5] chore: add valid test case Signed-off-by: hainenber --- .../no-async-wrapper-for-expected-promise.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/rules/__tests__/no-async-wrapper-for-expected-promise.test.ts b/src/rules/__tests__/no-async-wrapper-for-expected-promise.test.ts index 664776d87..3c0b74927 100644 --- a/src/rules/__tests__/no-async-wrapper-for-expected-promise.test.ts +++ b/src/rules/__tests__/no-async-wrapper-for-expected-promise.test.ts @@ -51,6 +51,14 @@ ruleTester.run('no-async-wrapper-for-expected-promise', rule, { }).rejects.toThrow(); }) `, + dedent` + it('pass', async () => { + await expect(async () => { + const a = 1; + await doSomethingSync(a); + }).rejects.toThrow(); + }) + `, ], invalid: [ {