Skip to content

fix(expect): soft assertions continue after .resolves/.rejects promise errors#9843

Open
mixelburg wants to merge 2 commits intovitest-dev:mainfrom
mixelburg:fix/soft-expect-resolves-rejects
Open

fix(expect): soft assertions continue after .resolves/.rejects promise errors#9843
mixelburg wants to merge 2 commits intovitest-dev:mainfrom
mixelburg:fix/soft-expect-resolves-rejects

Conversation

@mixelburg
Copy link

Bug

When expect.soft() is combined with .resolves and the awaited promise rejects, the test aborts instead of recording the failure and continuing (which is what expect.soft is designed to do).

The symmetric case also fails: expect.soft(resolvedPromise).rejects aborts when the promise resolves.

Fixes #9838

Root cause

Inside both the .resolves and .rejects proxy accessors, the method interceptor creates a new promise (the Promise.resolve(obj).then(...).catch(...) chain), then hands it to recordAsyncExpect:

return recordAsyncExpect(
  test,
  promise,
  createAssertionMessage(utils, this, !!args.length),
  error,
  // <-- isSoft was never passed!
)

recordAsyncExpect has a special isSoft branch:

if (isSoft) {
  promise = promise.then(noop, (err) => {
    handleTestError(test, err)  // record without throwing
  })
}

Without isSoft, the rejection propagates through the returned promise wrapper; when the caller awaits it, the test runner sees an unhandled rejection and aborts the test.

Fix

Pass utils.flag(this, 'soft') (which is already set by expect.soft()) as the fifth argument in both recordAsyncExpect calls.

Tests added

Added two new fixture test cases to test/cli/fixtures/expect-soft/expects/soft.test.ts:

  • promise rejection — two consecutive expect.soft(rejectedPromise).resolves.toBe(...) assertions; both should execute and both failures should be reported (regression for issue)
  • promise resolved instead of rejecting — two consecutive expect.soft(resolvedPromise).rejects.toBe(...) assertions; same expectation for symmetric case

And corresponding expectations in test/cli/test/expect-soft.test.ts that verify both failure messages appear in stderr.

…s proxies

When expect.soft() is used with .resolves, a rejection in the awaited
promise was not handled softly. Instead of recording the failure and
continuing (as expect.soft() should), the rejection propagated to the
test, aborting execution of subsequent assertions.

Root cause: the .resolves and .rejects proxy callbacks call
recordAsyncExpect() without passing the isSoft flag. When isSoft is
false/undefined, recordAsyncExpect does not attach a rejection handler
to swallow the error and record it in test.result.errors; the returned
promise still rejects, so awaiting it terminates the test.

Fix: pass utils.flag(this, 'soft') as the isSoft argument to both
recordAsyncExpect calls (in the resolves proxy and in the rejects
proxy).

The same fix also corrects the symmetric case for .rejects: if
expect.soft(resolvedPromise).rejects is used, the 'resolved instead of
rejecting' error is now recorded softly and execution continues.

Fixes vitest-dev#9838
@hi-ogawa
Copy link
Contributor

CI failing. fix please.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@netlify
Copy link

netlify bot commented Mar 12, 2026

Deploy Preview for vitest-dev ready!

Built without sensitive environment variables

Name Link
🔨 Latest commit 8fc9cff
🔍 Latest deploy log https://app.netlify.com/projects/vitest-dev/deploys/69b287980bb62700087505bd
😎 Deploy Preview https://deploy-preview-9843--vitest-dev.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

soft doesn't work for expect.soft(rejection).resolves

2 participants