From 8a825f5d4d52c048d50a84059ca35004a17c5c30 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 25 May 2026 10:27:04 +0000 Subject: [PATCH 1/9] fix(bun:test): return an object when a mock function is constructed with new Mock functions registered jsMockFunctionCall as both their call and construct callback, so `new mock()` could return a non-object (undefined when the mock has no implementation, or a primitive return value). JSC requires native construct callbacks to return an object; Reflect.construct() on such a mock hit an isCell() assertion in debug builds and `new mock()` evaluated to undefined. Add a dedicated construct callback that mirrors `new` on an ordinary JS function: create `this` from newTarget.prototype, invoke the mock with it, and return the mock's result only when it is an object, otherwise the created `this`. --- src/jsc/bindings/JSMockFunction.cpp | 37 +++++++++++++++++++++++--- test/js/bun/test/mock-fn.test.js | 41 +++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 3 deletions(-) diff --git a/src/jsc/bindings/JSMockFunction.cpp b/src/jsc/bindings/JSMockFunction.cpp index 1c1c7fe70f6e..02f7df8eb30d 100644 --- a/src/jsc/bindings/JSMockFunction.cpp +++ b/src/jsc/bindings/JSMockFunction.cpp @@ -86,6 +86,7 @@ inline To tryJSDynamicCast(JSC::WriteBarrier& from) } JSC_DECLARE_HOST_FUNCTION(jsMockFunctionCall); +JSC_DECLARE_HOST_FUNCTION(jsMockFunctionConstruct); JSC_DECLARE_CUSTOM_GETTER(jsMockFunctionGetter_protoImpl); JSC_DECLARE_CUSTOM_GETTER(jsMockFunctionGetter_mock); JSC_DECLARE_HOST_FUNCTION(jsMockFunctionGetter_mockGetLastCall); @@ -462,7 +463,7 @@ class JSMockFunction : public JSC::InternalFunction { } JSMockFunction(JSC::VM& vm, JSC::Structure* structure, CallbackKind wrapKind) - : Base(vm, structure, jsMockFunctionCall, jsMockFunctionCall) + : Base(vm, structure, jsMockFunctionCall, jsMockFunctionConstruct) { initMock(); } @@ -826,7 +827,7 @@ static JSValue createMockResult(JSC::VM& vm, Zig::GlobalObject* globalObject, co return result; } -JSC_DEFINE_HOST_FUNCTION(jsMockFunctionCall, (JSGlobalObject * lexicalGlobalObject, CallFrame* callframe)) +static JSC::EncodedJSValue jsMockFunctionCallImpl(JSGlobalObject* lexicalGlobalObject, CallFrame* callframe, JSValue thisValue) { Zig::GlobalObject* globalObject = uncheckedDowncast(lexicalGlobalObject); auto& vm = JSC::getVM(globalObject); @@ -838,7 +839,6 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionCall, (JSGlobalObject * lexicalGlobalObje } JSC::ArgList args = JSC::ArgList(callframe); - JSValue thisValue = callframe->thisValue(); JSC::JSArray* argumentsArray = nullptr; { JSC::ObjectInitializationScope object(vm); @@ -981,6 +981,37 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionCall, (JSGlobalObject * lexicalGlobalObje return JSValue::encode(jsUndefined()); } +JSC_DEFINE_HOST_FUNCTION(jsMockFunctionCall, (JSGlobalObject * lexicalGlobalObject, CallFrame* callframe)) +{ + return jsMockFunctionCallImpl(lexicalGlobalObject, callframe, callframe->thisValue()); +} + +JSC_DEFINE_HOST_FUNCTION(jsMockFunctionConstruct, (JSGlobalObject * lexicalGlobalObject, CallFrame* callframe)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + // A native construct callback must return an object. Behave like `new` on an + // ordinary JS function: create `this` from newTarget's prototype, run the mock + // with it, and return the mock's result only if it is an object. + JSObject* newTarget = asObject(callframe->newTarget()); + JSValue prototype = newTarget->get(lexicalGlobalObject, vm.propertyNames->prototype); + RETURN_IF_EXCEPTION(scope, {}); + + JSObject* thisObject = prototype.isObject() + ? JSC::constructEmptyObject(lexicalGlobalObject, asObject(prototype)) + : JSC::constructEmptyObject(lexicalGlobalObject); + + JSValue returnValue = JSValue::decode(jsMockFunctionCallImpl(lexicalGlobalObject, callframe, thisObject)); + RETURN_IF_EXCEPTION(scope, {}); + + if (returnValue && returnValue.isObject()) { + return JSValue::encode(returnValue); + } + + return JSValue::encode(thisObject); +} + void JSMockFunctionPrototype::finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject) { Base::finishCreation(vm); diff --git a/test/js/bun/test/mock-fn.test.js b/test/js/bun/test/mock-fn.test.js index 7f6a244d9806..81301787c350 100644 --- a/test/js/bun/test/mock-fn.test.js +++ b/test/js/bun/test/mock-fn.test.js @@ -117,6 +117,47 @@ describe("mock()", () => { expect(fn).toHaveBeenCalledWith(); }); + test("are constructable with new", () => { + // no implementation: `new` should produce a fresh object, like `new` on an ordinary function + const fn = jest.fn(); + const instance = new fn(1, 2); + expect(typeof instance).toBe("object"); + expect(instance).not.toBe(null); + expect(fn.mock.calls).toEqual([[1, 2]]); + expect(fn.mock.contexts[0]).toBe(instance); + + // Reflect.construct used to crash when the mock returned a non-object + const reflected = Reflect.construct(fn, []); + expect(typeof reflected).toBe("object"); + + // implementation operating on `this` + const withImpl = jest.fn(function (value) { + this.value = value; + }); + const constructed = new withImpl(42); + expect(constructed.value).toBe(42); + expect(withImpl.mock.contexts[0]).toBe(constructed); + + // implementation returning an object wins over the created `this` + const returnsObject = jest.fn(() => ({ a: 1 })); + expect(new returnsObject()).toEqual({ a: 1 }); + + // primitive return values are ignored by `new`, like ordinary functions + const returnsPrimitive = jest.fn().mockReturnValue(42); + expect(typeof new returnsPrimitive()).toBe("object"); + + // newTarget.prototype is respected + const classLike = jest.fn(); + classLike.prototype = { + greet() { + return "hello"; + }, + }; + const classInstance = new classLike(); + expect(classInstance.greet()).toBe("hello"); + expect(classInstance instanceof classLike).toBe(true); + }); + test("mockName returns this", () => { const fn = jest.fn(); expect(fn.mockName()).toBe(fn); From f72813b35d6c7ee396b18cf4ac8fa15a50df4021 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 25 May 2026 10:42:05 +0000 Subject: [PATCH 2/9] test: cover spy construction and explicit newTarget prototype --- test/js/bun/test/mock-fn.test.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/test/js/bun/test/mock-fn.test.js b/test/js/bun/test/mock-fn.test.js index 81301787c350..04e9a49c280f 100644 --- a/test/js/bun/test/mock-fn.test.js +++ b/test/js/bun/test/mock-fn.test.js @@ -156,6 +156,12 @@ describe("mock()", () => { const classInstance = new classLike(); expect(classInstance.greet()).toBe("hello"); expect(classInstance instanceof classLike).toBe(true); + + // Reflect.construct with an explicit newTarget uses its prototype + function NewTarget() {} + NewTarget.prototype = { marker: true }; + const withNewTarget = Reflect.construct(jest.fn(), [], NewTarget); + expect(Object.getPrototypeOf(withNewTarget)).toBe(NewTarget.prototype); }); test("mockName returns this", () => { @@ -869,6 +875,30 @@ describe("spyOn", () => { expect(fn).not.toHaveBeenCalled(); }); + test("constructing a spy calls the original with the new instance", () => { + var obj = { + Original: function () { + this.ok = true; + }, + }; + const fn = spyOn(obj, "Original"); + const instance = Reflect.construct(obj.Original, []); + expect(typeof instance).toBe("object"); + expect(instance.ok).toBe(true); + expect(fn).toHaveBeenCalledTimes(1); + fn.mockRestore(); + }); + + if (isBun) { + test("constructing a spy on a missing property returns an object", () => { + const target = {}; + const fn = spyOn(target, "doesNotExist"); + expect(typeof Reflect.construct(fn, [])).toBe("object"); + expect(typeof new fn()).toBe("object"); + fn.mockRestore(); + }); + } + test("override impl after doesnt break restore", () => { var obj = { original() { From dfc04efe6c286765af9480914254115c4a6128e2 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 25 May 2026 12:09:11 +0000 Subject: [PATCH 3/9] ci: retrigger From 3b292844604d7fafeda8ae343c3072663fab3f27 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 31 May 2026 00:12:20 +0000 Subject: [PATCH 4/9] fix(bun:test): record mock.instances on every call (matches jest) Populate fn->instances alongside fn->contexts in the shared call helper so `new`/Reflect.construct invocations appear in mock.instances, mirroring jest (which records each call's `this`). Verified identical output against jest for plain construct, object-returning impl, this-mutating impl, and regular non-construct calls. --- src/jsc/bindings/JSMockFunction.cpp | 14 ++++++++++++++ test/js/bun/test/mock-fn.test.js | 4 ++++ 2 files changed, 18 insertions(+) diff --git a/src/jsc/bindings/JSMockFunction.cpp b/src/jsc/bindings/JSMockFunction.cpp index 02f7df8eb30d..f744efa7694f 100644 --- a/src/jsc/bindings/JSMockFunction.cpp +++ b/src/jsc/bindings/JSMockFunction.cpp @@ -879,6 +879,20 @@ static JSC::EncodedJSValue jsMockFunctionCallImpl(JSGlobalObject* lexicalGlobalO fn->contexts.set(vm, fn, contexts); } + JSC::JSArray* instances = fn->instances.get(); + if (instances) { + instances->push(globalObject, thisValue); + RETURN_IF_EXCEPTION(scope, {}); + } else { + JSC::ObjectInitializationScope object(vm); + instances = JSC::JSArray::tryCreateUninitializedRestricted( + object, + globalObject->arrayStructureForIndexingTypeDuringAllocation(JSC::ArrayWithContiguous), + 1); + instances->initializeIndex(object, 0, thisValue); + fn->instances.set(vm, fn, instances); + } + auto invocationId = JSMockModule::nextInvocationId(); JSC::JSArray* invocationCallOrder = fn->invocationCallOrder.get(); if (invocationCallOrder) { diff --git a/test/js/bun/test/mock-fn.test.js b/test/js/bun/test/mock-fn.test.js index 04e9a49c280f..97e1608e5517 100644 --- a/test/js/bun/test/mock-fn.test.js +++ b/test/js/bun/test/mock-fn.test.js @@ -125,10 +125,13 @@ describe("mock()", () => { expect(instance).not.toBe(null); expect(fn.mock.calls).toEqual([[1, 2]]); expect(fn.mock.contexts[0]).toBe(instance); + // `new` calls are recorded in mock.instances + expect(fn.mock.instances[0]).toBe(instance); // Reflect.construct used to crash when the mock returned a non-object const reflected = Reflect.construct(fn, []); expect(typeof reflected).toBe("object"); + expect(fn.mock.instances[1]).toBe(reflected); // implementation operating on `this` const withImpl = jest.fn(function (value) { @@ -137,6 +140,7 @@ describe("mock()", () => { const constructed = new withImpl(42); expect(constructed.value).toBe(42); expect(withImpl.mock.contexts[0]).toBe(constructed); + expect(withImpl.mock.instances[0]).toBe(constructed); // implementation returning an object wins over the created `this` const returnsObject = jest.fn(() => ({ a: 1 })); From 8149cc30342100eb89577563050cb8684b5e6df6 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 31 May 2026 18:35:23 +0000 Subject: [PATCH 5/9] fix(bun:test): only record mock.instances for new/construct calls mock.instances contains only instances created with new, matching jest; regular calls record this in mock.contexts instead. Thread an isConstruct flag through the shared call helper so the instances array is populated only on the construct path. --- src/jsc/bindings/JSMockFunction.cpp | 32 +++++++++++++++-------------- test/js/bun/test/mock-fn.test.js | 15 ++++++++++++++ 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/src/jsc/bindings/JSMockFunction.cpp b/src/jsc/bindings/JSMockFunction.cpp index f744efa7694f..ccf97b8db0ec 100644 --- a/src/jsc/bindings/JSMockFunction.cpp +++ b/src/jsc/bindings/JSMockFunction.cpp @@ -827,7 +827,7 @@ static JSValue createMockResult(JSC::VM& vm, Zig::GlobalObject* globalObject, co return result; } -static JSC::EncodedJSValue jsMockFunctionCallImpl(JSGlobalObject* lexicalGlobalObject, CallFrame* callframe, JSValue thisValue) +static JSC::EncodedJSValue jsMockFunctionCallImpl(JSGlobalObject* lexicalGlobalObject, CallFrame* callframe, JSValue thisValue, bool isConstruct) { Zig::GlobalObject* globalObject = uncheckedDowncast(lexicalGlobalObject); auto& vm = JSC::getVM(globalObject); @@ -879,18 +879,20 @@ static JSC::EncodedJSValue jsMockFunctionCallImpl(JSGlobalObject* lexicalGlobalO fn->contexts.set(vm, fn, contexts); } - JSC::JSArray* instances = fn->instances.get(); - if (instances) { - instances->push(globalObject, thisValue); - RETURN_IF_EXCEPTION(scope, {}); - } else { - JSC::ObjectInitializationScope object(vm); - instances = JSC::JSArray::tryCreateUninitializedRestricted( - object, - globalObject->arrayStructureForIndexingTypeDuringAllocation(JSC::ArrayWithContiguous), - 1); - instances->initializeIndex(object, 0, thisValue); - fn->instances.set(vm, fn, instances); + if (isConstruct) { + JSC::JSArray* instances = fn->instances.get(); + if (instances) { + instances->push(globalObject, thisValue); + RETURN_IF_EXCEPTION(scope, {}); + } else { + JSC::ObjectInitializationScope object(vm); + instances = JSC::JSArray::tryCreateUninitializedRestricted( + object, + globalObject->arrayStructureForIndexingTypeDuringAllocation(JSC::ArrayWithContiguous), + 1); + instances->initializeIndex(object, 0, thisValue); + fn->instances.set(vm, fn, instances); + } } auto invocationId = JSMockModule::nextInvocationId(); @@ -997,7 +999,7 @@ static JSC::EncodedJSValue jsMockFunctionCallImpl(JSGlobalObject* lexicalGlobalO JSC_DEFINE_HOST_FUNCTION(jsMockFunctionCall, (JSGlobalObject * lexicalGlobalObject, CallFrame* callframe)) { - return jsMockFunctionCallImpl(lexicalGlobalObject, callframe, callframe->thisValue()); + return jsMockFunctionCallImpl(lexicalGlobalObject, callframe, callframe->thisValue(), false); } JSC_DEFINE_HOST_FUNCTION(jsMockFunctionConstruct, (JSGlobalObject * lexicalGlobalObject, CallFrame* callframe)) @@ -1016,7 +1018,7 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionConstruct, (JSGlobalObject * lexicalGloba ? JSC::constructEmptyObject(lexicalGlobalObject, asObject(prototype)) : JSC::constructEmptyObject(lexicalGlobalObject); - JSValue returnValue = JSValue::decode(jsMockFunctionCallImpl(lexicalGlobalObject, callframe, thisObject)); + JSValue returnValue = JSValue::decode(jsMockFunctionCallImpl(lexicalGlobalObject, callframe, thisObject, true)); RETURN_IF_EXCEPTION(scope, {}); if (returnValue && returnValue.isObject()) { diff --git a/test/js/bun/test/mock-fn.test.js b/test/js/bun/test/mock-fn.test.js index 97e1608e5517..84d789aa61cd 100644 --- a/test/js/bun/test/mock-fn.test.js +++ b/test/js/bun/test/mock-fn.test.js @@ -168,6 +168,21 @@ describe("mock()", () => { expect(Object.getPrototypeOf(withNewTarget)).toBe(NewTarget.prototype); }); + test("mock.instances only records `new` calls, not regular calls", () => { + // jest: mock.instances contains only instances created with `new`; + // every call's `this` is recorded in mock.contexts instead. + const fn = jest.fn(); + const ctx = {}; + fn.call(ctx); + fn(); + expect(fn.mock.contexts).toEqual([ctx, undefined]); + expect(fn.mock.instances).toBeEmpty(); + + const instance = new fn(); + expect(fn.mock.contexts).toEqual([ctx, undefined, instance]); + expect(fn.mock.instances).toEqual([instance]); + }); + test("mockName returns this", () => { const fn = jest.fn(); expect(fn.mockName()).toBe(fn); From b32623a806daa7d532d7863d2821bb29a8df140e Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 31 May 2026 18:50:02 +0000 Subject: [PATCH 6/9] fix(bun:test): give mock functions a writable .prototype InternalFunction doesn't auto-create a .prototype like JSFunction, so jest.fn().prototype was undefined: new fn() instances inherited from Object.prototype and (new fn()) instanceof fn threw a TypeError. Install a fresh writable prototype with a constructor back-reference at creation, so mocks behave like ordinary JS functions under new (matches jest). --- src/jsc/bindings/JSMockFunction.cpp | 7 +++++++ test/js/bun/test/mock-fn.test.js | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/src/jsc/bindings/JSMockFunction.cpp b/src/jsc/bindings/JSMockFunction.cpp index ccf97b8db0ec..3549dd4b6499 100644 --- a/src/jsc/bindings/JSMockFunction.cpp +++ b/src/jsc/bindings/JSMockFunction.cpp @@ -239,6 +239,13 @@ class JSMockFunction : public JSC::InternalFunction { JSMockFunction* function = new (NotNull, JSC::allocateCell(vm)) JSMockFunction(vm, structure, kind); function->finishCreation(vm); + // InternalFunction does not auto-create a `.prototype` like JSFunction does. + // Install a writable one with a `constructor` back-reference so mocks behave + // like ordinary JS functions under `new` (matches jest). + JSObject* prototype = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype()); + prototype->putDirect(vm, vm.propertyNames->constructor, function, static_cast(JSC::PropertyAttribute::DontEnum)); + function->putDirect(vm, vm.propertyNames->prototype, prototype, static_cast(JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete)); + // Do not forget to set the original name: https://github.com/oven-sh/bun/issues/8794 function->m_originalName.set(vm, function, globalObject->commonStrings().mockedFunctionString(globalObject)); diff --git a/test/js/bun/test/mock-fn.test.js b/test/js/bun/test/mock-fn.test.js index 84d789aa61cd..c987df699b37 100644 --- a/test/js/bun/test/mock-fn.test.js +++ b/test/js/bun/test/mock-fn.test.js @@ -120,9 +120,15 @@ describe("mock()", () => { test("are constructable with new", () => { // no implementation: `new` should produce a fresh object, like `new` on an ordinary function const fn = jest.fn(); + // like an ordinary function, a mock has a writable `.prototype` with a `constructor` back-reference + expect(typeof fn.prototype).toBe("object"); + expect(fn.prototype.constructor).toBe(fn); const instance = new fn(1, 2); expect(typeof instance).toBe("object"); expect(instance).not.toBe(null); + // the instance inherits from the mock's prototype, so `instanceof` works without assigning one + expect(Object.getPrototypeOf(instance)).toBe(fn.prototype); + expect(instance instanceof fn).toBe(true); expect(fn.mock.calls).toEqual([[1, 2]]); expect(fn.mock.contexts[0]).toBe(instance); // `new` calls are recorded in mock.instances From 6e0babee5879c7c7373bda0cf0b8d1f0adf07dcf Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 31 May 2026 18:57:46 +0000 Subject: [PATCH 7/9] fix(bun:test): record mock.instances on every call to match jest/vitest Both jest-mock and @vitest/spy push `this` onto mock.instances and mock.contexts unconditionally on every call (no new.target check), so the two arrays stay in lock-step. An earlier commit gated the instances push on construct based on jest's docs, but the runtime implementation records every call; verified against jest 29.7.0 and @vitest/spy. Revert the gating so mock.instances is drop-in compatible, and update the test to assert the real every-call behavior. --- src/jsc/bindings/JSMockFunction.cpp | 32 ++++++++++++++--------------- test/js/bun/test/mock-fn.test.js | 10 ++++----- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/jsc/bindings/JSMockFunction.cpp b/src/jsc/bindings/JSMockFunction.cpp index 3549dd4b6499..aee6e0e71bb3 100644 --- a/src/jsc/bindings/JSMockFunction.cpp +++ b/src/jsc/bindings/JSMockFunction.cpp @@ -834,7 +834,7 @@ static JSValue createMockResult(JSC::VM& vm, Zig::GlobalObject* globalObject, co return result; } -static JSC::EncodedJSValue jsMockFunctionCallImpl(JSGlobalObject* lexicalGlobalObject, CallFrame* callframe, JSValue thisValue, bool isConstruct) +static JSC::EncodedJSValue jsMockFunctionCallImpl(JSGlobalObject* lexicalGlobalObject, CallFrame* callframe, JSValue thisValue) { Zig::GlobalObject* globalObject = uncheckedDowncast(lexicalGlobalObject); auto& vm = JSC::getVM(globalObject); @@ -886,20 +886,18 @@ static JSC::EncodedJSValue jsMockFunctionCallImpl(JSGlobalObject* lexicalGlobalO fn->contexts.set(vm, fn, contexts); } - if (isConstruct) { - JSC::JSArray* instances = fn->instances.get(); - if (instances) { - instances->push(globalObject, thisValue); - RETURN_IF_EXCEPTION(scope, {}); - } else { - JSC::ObjectInitializationScope object(vm); - instances = JSC::JSArray::tryCreateUninitializedRestricted( - object, - globalObject->arrayStructureForIndexingTypeDuringAllocation(JSC::ArrayWithContiguous), - 1); - instances->initializeIndex(object, 0, thisValue); - fn->instances.set(vm, fn, instances); - } + JSC::JSArray* instances = fn->instances.get(); + if (instances) { + instances->push(globalObject, thisValue); + RETURN_IF_EXCEPTION(scope, {}); + } else { + JSC::ObjectInitializationScope object(vm); + instances = JSC::JSArray::tryCreateUninitializedRestricted( + object, + globalObject->arrayStructureForIndexingTypeDuringAllocation(JSC::ArrayWithContiguous), + 1); + instances->initializeIndex(object, 0, thisValue); + fn->instances.set(vm, fn, instances); } auto invocationId = JSMockModule::nextInvocationId(); @@ -1006,7 +1004,7 @@ static JSC::EncodedJSValue jsMockFunctionCallImpl(JSGlobalObject* lexicalGlobalO JSC_DEFINE_HOST_FUNCTION(jsMockFunctionCall, (JSGlobalObject * lexicalGlobalObject, CallFrame* callframe)) { - return jsMockFunctionCallImpl(lexicalGlobalObject, callframe, callframe->thisValue(), false); + return jsMockFunctionCallImpl(lexicalGlobalObject, callframe, callframe->thisValue()); } JSC_DEFINE_HOST_FUNCTION(jsMockFunctionConstruct, (JSGlobalObject * lexicalGlobalObject, CallFrame* callframe)) @@ -1025,7 +1023,7 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionConstruct, (JSGlobalObject * lexicalGloba ? JSC::constructEmptyObject(lexicalGlobalObject, asObject(prototype)) : JSC::constructEmptyObject(lexicalGlobalObject); - JSValue returnValue = JSValue::decode(jsMockFunctionCallImpl(lexicalGlobalObject, callframe, thisObject, true)); + JSValue returnValue = JSValue::decode(jsMockFunctionCallImpl(lexicalGlobalObject, callframe, thisObject)); RETURN_IF_EXCEPTION(scope, {}); if (returnValue && returnValue.isObject()) { diff --git a/test/js/bun/test/mock-fn.test.js b/test/js/bun/test/mock-fn.test.js index c987df699b37..893473b7b292 100644 --- a/test/js/bun/test/mock-fn.test.js +++ b/test/js/bun/test/mock-fn.test.js @@ -174,19 +174,19 @@ describe("mock()", () => { expect(Object.getPrototypeOf(withNewTarget)).toBe(NewTarget.prototype); }); - test("mock.instances only records `new` calls, not regular calls", () => { - // jest: mock.instances contains only instances created with `new`; - // every call's `this` is recorded in mock.contexts instead. + test("mock.instances records `this` on every call, like mock.contexts", () => { + // jest-mock/@vitest/spy push `this` onto both instances and contexts on + // every call (no new.target check), so the two arrays stay in lock-step. const fn = jest.fn(); const ctx = {}; fn.call(ctx); fn(); expect(fn.mock.contexts).toEqual([ctx, undefined]); - expect(fn.mock.instances).toBeEmpty(); + expect(fn.mock.instances).toEqual([ctx, undefined]); const instance = new fn(); expect(fn.mock.contexts).toEqual([ctx, undefined, instance]); - expect(fn.mock.instances).toEqual([instance]); + expect(fn.mock.instances).toEqual([ctx, undefined, instance]); }); test("mockName returns this", () => { From 69db3db529a6acea744e2fb122ecb1d23f5e42f7 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 31 May 2026 19:48:39 +0000 Subject: [PATCH 8/9] test(bun:test): cover constructing a bound mock function A bound mock (mock.bind(...)) forwards [[Construct]] to the mock's construct callback; fuzzing rediscovered the isCell() assertion through this path on unfixed builds. Assert the bound construct returns an object and runs the mock's this-mutating implementation. --- test/js/bun/test/mock-fn.test.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/js/bun/test/mock-fn.test.js b/test/js/bun/test/mock-fn.test.js index 893473b7b292..ebc047de0fa1 100644 --- a/test/js/bun/test/mock-fn.test.js +++ b/test/js/bun/test/mock-fn.test.js @@ -172,6 +172,14 @@ describe("mock()", () => { NewTarget.prototype = { marker: true }; const withNewTarget = Reflect.construct(jest.fn(), [], NewTarget); expect(Object.getPrototypeOf(withNewTarget)).toBe(NewTarget.prototype); + + // constructing a bound mock forwards [[Construct]] to the mock and returns an object + const bound = jest.fn(function (value) { + this.value = value; + }).bind(null, 7); + const boundInstance = new bound(); + expect(typeof boundInstance).toBe("object"); + expect(boundInstance.value).toBe(7); }); test("mock.instances records `this` on every call, like mock.contexts", () => { From 93736e3928db09fb32495f67d4e85f748bfb89a2 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 31 May 2026 19:50:33 +0000 Subject: [PATCH 9/9] [autofix.ci] apply automated fixes --- test/js/bun/test/mock-fn.test.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/js/bun/test/mock-fn.test.js b/test/js/bun/test/mock-fn.test.js index ebc047de0fa1..4d26e50dcd27 100644 --- a/test/js/bun/test/mock-fn.test.js +++ b/test/js/bun/test/mock-fn.test.js @@ -174,9 +174,11 @@ describe("mock()", () => { expect(Object.getPrototypeOf(withNewTarget)).toBe(NewTarget.prototype); // constructing a bound mock forwards [[Construct]] to the mock and returns an object - const bound = jest.fn(function (value) { - this.value = value; - }).bind(null, 7); + const bound = jest + .fn(function (value) { + this.value = value; + }) + .bind(null, 7); const boundInstance = new bound(); expect(typeof boundInstance).toBe("object"); expect(boundInstance.value).toBe(7);