fix(jest-mock): restore withImplementation after errors - #16219
fix(jest-mock): restore withImplementation after errors#16219fallintoplace wants to merge 3 commits into
Conversation
✅ Deploy Preview for jestjs ready!Built without sensitive environment variables
To edit notification comments on pull requests, go to your Netlify project configuration. |
babel-jest
babel-plugin-jest-hoist
babel-preset-jest
create-jest
@jest/diff-sequences
expect
@jest/expect-utils
jest
jest-changed-files
jest-circus
jest-cli
jest-config
@jest/console
@jest/core
@jest/create-cache-key-function
jest-diff
jest-docblock
jest-each
@jest/environment
jest-environment-jsdom
@jest/environment-jsdom-abstract
jest-environment-node
@jest/expect
@jest/fake-timers
@jest/get-type
@jest/globals
jest-haste-map
jest-jasmine2
jest-leak-detector
jest-matcher-utils
jest-message-util
jest-mock
@jest/pattern
jest-phabricator
jest-regex-util
@jest/reporters
jest-resolve
jest-resolve-dependencies
jest-runner
jest-runtime
@jest/schemas
jest-snapshot
@jest/snapshot-utils
@jest/source-map
@jest/test-result
@jest/test-sequencer
@jest/transform
@jest/types
jest-util
jest-validate
jest-watcher
jest-worker
pretty-format
commit: |
|
This PR is stale because it has been open 90 days with no activity. Remove stale label or comment or this will be closed in 30 days. |
| throw error; | ||
| } | ||
|
|
||
| if (isPromise(returnedValue)) { |
There was a problem hiding this comment.
isPromise(returnedValue) still runs outside the restoration try block. isPromise reads .then. If that property access throws, the error escapes before restore() and leaves the temporary implementation installed. The documentation explicitly supports callbacks that return thenables, so reading .then is part of this documented code path.
const mock = jest.fn(() => 'outside callback');
const thenable = Object.defineProperty({}, 'then', {
get() {
throw new Error('boom');
},
}) as Promise<unknown>;
expect(() =>
mock.withImplementation(() => 'inside callback', () => thenable),
).toThrow('boom');
expect(mock()).toBe('outside callback'); // Receives "inside callback"Could the try block also cover isPromise(returnedValue) and the Promise.resolve(...).then(...) setup? I reproduced this on the PR head 55ce3234b303562ceb63b41ad44a8832d9b0ba61 and after applying the PR to current main be425a0b0e3bd60a74e4a7e350aa38c63a2d25ef. Moving those operations inside the try restores the original implementation. Locally, all 261 jest-mock tests across 6 suites pass.
55ce323 to
6bbc503
Compare
soltonigiri
left a comment
There was a problem hiding this comment.
Thanks for fixing the restoration issue. I rechecked 6bbc5036, and an exception thrown while reading .then no longer skips restore(). I left two inline comments: the withImplementation tests don't exercise the changed source, and the changelog entry is under the released 30.5.0 section.
| - `[jest-runtime]` Throw `ERR_REQUIRE_CYCLE_MODULE` like Node when a CommonJS module `require()`s an ES module that is still being loaded, instead of evaluating the module a second time ([#16366](https://github.com/jestjs/jest/pull/16366)) | ||
| - `[jest-runtime]` Key builtin modules in the ESM registry by one canonical specifier ([#16341](https://github.com/jestjs/jest/pull/16341)) | ||
| - `[jest-runtime]` `import.meta.resolve()` for a builtin uses its `node:` specifier ([#16341](https://github.com/jestjs/jest/pull/16341)) | ||
| - `[jest-mock]` Restore `withImplementation(...)` after sync throws and async rejections ([#16219](https://github.com/jestjs/jest/pull/16219)) |
There was a problem hiding this comment.
30.5.0 is already out. Could you move this entry to ## main under Fixes? yarn check-changelog 16219 exits 1 because it expects the PR link there.
There was a problem hiding this comment.
Thanks a lot for the comments.
Are you a maintainer though? I feel like this PR is not getting any attention.
Once again, I appreciate the reviews.
There was a problem hiding this comment.
No, I'm not a maintainer, just a fellow contributor 😅 I don't have merge permissions.
This PR has been open for a while, so I just wanted to help get it into tip-top shape for when a maintainer gets a chance to review it.
Thanks for being so receptive to the feedback!
| }); | ||
|
|
||
| it('restores the previous implementation after the callback throws', () => { | ||
| const mock = jest |
There was a problem hiding this comment.
These tests create the target mocks with the runner's global jest.fn, so those mocks execute packages/jest-mock/build/index.js instead of the package source imported by this unit test. A focused coverage run of the full file left the changed index.ts:947-978 block uncovered. The three new error cases exercise those lines when switched to moduleMocker.fn, but the synchronous-success and fulfilled-promise restore cases need the same change. Could all withImplementation target mocks use moduleMocker.fn?
Summary
Fixes
mockFn.withImplementation(...)so it restores the previous mock state when the callback throws synchronously or rejects asynchronously.Closes #16218.
Motivation
withImplementationis documented as a temporary override, but the current implementation only restores state on the normal synchronous path and on fulfilled promises. That leaves the temporary implementation installed after exceptional exits.How this works
Promiseand restore on both fulfillment and rejectionmockImplementationOncestate andwhenCalledWithfallback behaviorValidation
yarn jest packages/jest-mock/src/__tests__/index.test.ts --runInBandyarn jest packages/jest-mock/src/__tests__ --runInBandyarn build:tsyarn typecheck:tests