From 281711e19dbc48be93b42b1a4f772e91151c5e06 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 13 May 2026 22:14:45 +0000 Subject: [PATCH] Fix crash when constructing a jest.fn() mock JSMockFunction used the same native entry point for [[Call]] and [[Construct]], and that entry point could return primitives (undefined, the mockReturnValue, or the implementation's return value). When invoked via Reflect.construct, JSC's Interpreter::executeConstruct unconditionally does asObject() on the result of a native construct, triggering an assertion when the result was not an object. Split the entry point into separate call and construct variants. On construct, allocate a fresh this object derived from new.target, record it in mock.instances, pass it as this to the implementation, and return it whenever the implementation result is not an object. This matches normal [[Construct]] semantics and Jest's behaviour for mock.instances. --- src/jsc/bindings/JSMockFunction.cpp | 55 +++++++++++++++++++++++++--- test/js/bun/test/mock-fn.test.js | 57 +++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 5 deletions(-) diff --git a/src/jsc/bindings/JSMockFunction.cpp b/src/jsc/bindings/JSMockFunction.cpp index 1c1c7fe70f6..aeed7be1e06 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,8 @@ static JSValue createMockResult(JSC::VM& vm, Zig::GlobalObject* globalObject, co return result; } -JSC_DEFINE_HOST_FUNCTION(jsMockFunctionCall, (JSGlobalObject * lexicalGlobalObject, CallFrame* callframe)) +template +static JSC::EncodedJSValue jsMockFunctionCallImpl(JSGlobalObject* lexicalGlobalObject, CallFrame* callframe) { Zig::GlobalObject* globalObject = uncheckedDowncast(lexicalGlobalObject); auto& vm = JSC::getVM(globalObject); @@ -839,6 +841,39 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionCall, (JSGlobalObject * lexicalGlobalObje JSC::ArgList args = JSC::ArgList(callframe); JSValue thisValue = callframe->thisValue(); + + if constexpr (isConstructCall) { + JSObject* newTargetObject = asObject(callframe->newTarget()); + JSGlobalObject* functionGlobalObject = getFunctionRealm(globalObject, newTargetObject); + RETURN_IF_EXCEPTION(scope, {}); + Structure* baseStructure = functionGlobalObject->objectStructureForObjectConstructor(); + Structure* structure = newTargetObject == fn ? baseStructure : InternalFunction::createSubclassStructure(globalObject, newTargetObject, baseStructure); + RETURN_IF_EXCEPTION(scope, {}); + thisValue = constructEmptyObject(vm, structure); + + JSC::JSArray* instancesArray = fn->instances.get(); + if (instancesArray) { + instancesArray->push(globalObject, thisValue); + RETURN_IF_EXCEPTION(scope, {}); + } else { + JSC::ObjectInitializationScope object(vm); + instancesArray = JSC::JSArray::tryCreateUninitializedRestricted( + object, + globalObject->arrayStructureForIndexingTypeDuringAllocation(JSC::ArrayWithContiguous), + 1); + instancesArray->initializeIndex(object, 0, thisValue); + fn->instances.set(vm, fn, instancesArray); + } + } + + auto constructorResult = [&](JSValue returnValue) -> JSValue { + if constexpr (isConstructCall) { + if (!returnValue.isObject()) + return thisValue; + } + return returnValue; + }; + JSC::JSArray* argumentsArray = nullptr; { JSC::ObjectInitializationScope object(vm); @@ -954,12 +989,12 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionCall, (JSGlobalObject * lexicalGlobalObje fn->returnValues.set(vm, fn, returnValuesArray); } - return JSValue::encode(returnValue); + return JSValue::encode(constructorResult(returnValue)); } case JSMockImplementation::Kind::ReturnValue: { JSValue returnValue = impl->underlyingValue.get(); setReturnValue(createMockResult(vm, globalObject, "return"_s, returnValue)); - return JSValue::encode(returnValue); + return JSValue::encode(constructorResult(returnValue)); } case JSMockImplementation::Kind::ReturnThis: { setReturnValue(createMockResult(vm, globalObject, "return"_s, thisValue)); @@ -978,7 +1013,17 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionCall, (JSGlobalObject * lexicalGlobalObje } setReturnValue(createMockResult(vm, globalObject, "return"_s, jsUndefined())); - return JSValue::encode(jsUndefined()); + return JSValue::encode(constructorResult(jsUndefined())); +} + +JSC_DEFINE_HOST_FUNCTION(jsMockFunctionCall, (JSGlobalObject * lexicalGlobalObject, CallFrame* callframe)) +{ + return jsMockFunctionCallImpl(lexicalGlobalObject, callframe); +} + +JSC_DEFINE_HOST_FUNCTION(jsMockFunctionConstruct, (JSGlobalObject * lexicalGlobalObject, CallFrame* callframe)) +{ + return jsMockFunctionCallImpl(lexicalGlobalObject, callframe); } void JSMockFunctionPrototype::finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject) diff --git a/test/js/bun/test/mock-fn.test.js b/test/js/bun/test/mock-fn.test.js index 7f6a244d980..0e6ead47456 100644 --- a/test/js/bun/test/mock-fn.test.js +++ b/test/js/bun/test/mock-fn.test.js @@ -794,6 +794,63 @@ describe("mock()", () => { expect(bar()()).toBe(true); }); + + describe("as a constructor", () => { + test("Reflect.construct on jest.fn() with no implementation", () => { + const fn = jest.fn(); + const inst = Reflect.construct(fn, []); + expect(typeof inst).toBe("object"); + expect(inst).not.toBeNull(); + expect(fn.mock.instances).toHaveLength(1); + expect(fn.mock.instances[0]).toBe(inst); + }); + + test("Reflect.construct on jest.fn() with mockReturnValue(primitive)", () => { + const fn = jest.fn().mockReturnValue("hello"); + const inst = Reflect.construct(fn, []); + expect(typeof inst).toBe("object"); + expect(fn()).toBe("hello"); + }); + + test("Reflect.construct on jest.fn() with mockReturnValue(object)", () => { + const obj = { a: 1 }; + const fn = jest.fn().mockReturnValue(obj); + const inst = Reflect.construct(fn, []); + expect(inst).toBe(obj); + }); + + test("Reflect.construct on jest.fn() with implementation returning a primitive", () => { + const fn = jest.fn(() => 42); + const inst = Reflect.construct(fn, []); + expect(typeof inst).toBe("object"); + expect(fn()).toBe(42); + }); + + test("new on jest.fn() returns an object and records the instance", () => { + const fn = jest.fn(function () { + this.x = 123; + }); + const inst = new fn(); + expect(typeof inst).toBe("object"); + expect(inst.x).toBe(123); + expect(fn.mock.instances).toHaveLength(1); + expect(fn.mock.instances[0]).toBe(inst); + expect(fn.mock.contexts[0]).toBe(inst); + }); + + test("Reflect.construct respects new.target prototype", () => { + class MyClass {} + const fn = jest.fn(); + const inst = Reflect.construct(fn, [], MyClass); + expect(inst instanceof MyClass).toBe(true); + }); + + test("calling as a function does not populate instances", () => { + const fn = jest.fn().mockReturnValue(1); + expect(fn()).toBe(1); + expect(fn.mock.instances).toHaveLength(0); + }); + }); }); describe("spyOn", () => {