-
Notifications
You must be signed in to change notification settings - Fork 4.7k
fix(bun:test): constructing a mock whose implementation returns a primitive now yields an object #31362
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
fix(bun:test): constructing a mock whose implementation returns a primitive now yields an object #31362
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 This fixes the crash, but the comment claiming "ordinary
[[Construct]]semantics" overstates what the code does: ordinary[[Construct]]allocates the receiver fromnewTarget.prototypebefore the body runs and binds it asthis, whereas here the implementation runs first (viajsMockFunctionCall, withcallframe->thisValue()) and the fresh object is created only afterward. Soconst fn = jest.fn(function () { this.value = 42 }); new fn()returns an empty object in Bun but{value: 42}in Jest, andmock.contextsrecords the wrong receiver. Not a regression — strictly better than the prior crash/undefined — so fine as a follow-up; relatedly (and pre-existing), nothing ever pushes ontofn->instances, somock.instancesstays[]afternew fn()even though there is now an instance to record.Extended reasoning...
What the gap is
jsMockFunctionConstructdoes:jsMockFunctionCall(globalObject, callframe)— which readsthisValue = callframe->thisValue()and invokes the implementation viaBun::call(..., thisValue, args)as a regular callnewTarget.prototypeand returns it when the implementation result was not an objectOrdinary ECMA-262
[[Construct]]does the opposite: it creates the receiver fromnewTarget.prototypebefore evaluating the function body, binds it asthis, runs the body, and returns that receiver if the body returned a non-object. Jest's mocks are plain JS functions (the wrapper doesimpl.apply(this, args)), so they get this for free.Concrete walkthrough
new fn()creates a freshthisfromfn.prototype, the wrapper doesimpl.apply(this, args),this.value = 42lands on the fresh object, the wrapper returnsundefined,[[Construct]]returns the mutatedthis.inst.value === 42, andfn.mock.contexts[0] === inst.jsMockFunctionCallruns the implementation withcallframe->thisValue()(the native construct frame's this, not a fresh instance), sothis.value = 42lands on whatever that is. The implementation returnsundefined, sojsMockFunctionConstructthen allocates a brand-new empty object at lines 1007-1009 and returns it.inst.value === undefined, andmock.contexts[0]is notinst.The new tests don't catch this because they only assert
typeof result === "object"or check an implementation that explicitly returns an object — none exercise an implementation that mutatesthiswithout returning.Why this is not a regression
Before this PR the same input either returned
undefined(new fn()bytecode path) or crashed with theisCell()assertion (Reflect.constructpath). The implementation has always been invoked viaBun::callwithcallframe->thisValue(), so this-mutation was never preserved andmock.contextsalready recorded the same value it does now. Returning an empty object is strictly better than crashing or returning a primitive; this is an incomplete fix, not a new bug.Adjacent pre-existing gap:
mock.instancesGrepping the file,
fn->instancesis declared, cleared, lazily created, GC-visited and exposed on the mock object — but nothing ever pushes onto it, in eitherjsMockFunctionCallor the newjsMockFunctionConstruct. Sofn.mock.instancesis always[]in Bun, whereas in Jestnew fn()records the instance. This was equally true before the PR (it's a long-standing stub), so it's out of scope for a crash fix; mentioning it only because the construct path now actually produces an instance object that could be recorded, and the fix below naturally gives you the value to push.On the refutation
One verifier argued the
mock.instancespart should be dropped entirely as a pre-existing missing feature with nothing in the diff to comment on. That's fair — it is folded in here as a one-line aside on the same construct callback rather than a standalone finding, and is explicitly flagged as pre-existing / follow-up rather than something to change in this PR.Suggested follow-up fix
To fully match Jest,
jsMockFunctionConstructshould allocate the fresh object fromnewTarget.prototypefirst, then invoke the mock with that object asthisValue(this requires either threading athisValueintojsMockFunctionCallor extracting a helper that takes one). That same object can then be pushed ontomock.contextsandmock.instances. None of that needs to block this PR — the crash fix is correct as-is — but the "ordinary[[Construct]]semantics" comment should probably be softened in the meantime.