diff --git a/src/bun.js/bindings/JSMockFunction.cpp b/src/bun.js/bindings/JSMockFunction.cpp index 1c1c7fe70f6..865cc86844c 100644 --- a/src/bun.js/bindings/JSMockFunction.cpp +++ b/src/bun.js/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(); } @@ -981,6 +982,26 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionCall, (JSGlobalObject * lexicalGlobalObje return JSValue::encode(jsUndefined()); } +JSC_DEFINE_HOST_FUNCTION(jsMockFunctionConstruct, (JSGlobalObject * lexicalGlobalObject, CallFrame* callframe)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + EncodedJSValue encodedResult = jsMockFunctionCall(lexicalGlobalObject, callframe); + RETURN_IF_EXCEPTION(scope, {}); + + JSValue result = JSValue::decode(encodedResult); + if (result.isObject()) [[likely]] + return encodedResult; + + JSObject* newTarget = asObject(callframe->newTarget()); + JSGlobalObject* functionGlobalObject = getFunctionRealm(lexicalGlobalObject, newTarget); + RETURN_IF_EXCEPTION(scope, {}); + Structure* structure = InternalFunction::createSubclassStructure(lexicalGlobalObject, newTarget, functionGlobalObject->objectStructureForObjectConstructor()); + RETURN_IF_EXCEPTION(scope, {}); + RELEASE_AND_RETURN(scope, JSValue::encode(JSC::constructEmptyObject(vm, structure))); +} + 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 7f6a244d980..1e16eb8db42 100644 --- a/test/js/bun/test/mock-fn.test.js +++ b/test/js/bun/test/mock-fn.test.js @@ -794,6 +794,27 @@ describe("mock()", () => { expect(bar()()).toBe(true); }); + + it("Reflect.construct on a mock returns an object", () => { + const empty = jest.fn(); + expect(typeof Reflect.construct(empty, [])).toBe("object"); + expect(empty).toHaveBeenCalledTimes(1); + + const returnsPrimitive = jest.fn().mockReturnValue(42); + expect(typeof Reflect.construct(returnsPrimitive, [])).toBe("object"); + + const returnsObject = jest.fn(() => ({ x: 1 })); + expect(Reflect.construct(returnsObject, [])).toEqual({ x: 1 }); + + class Base {} + expect(Reflect.construct(empty, [], Base)).toBeInstanceOf(Base); + + if (isBun) { + // Jest doesn't allow spying on non-function properties + const spied = spyOn({ foo: 42 }, "foo"); + expect(typeof Reflect.construct(spied, [])).toBe("object"); + } + }); }); describe("spyOn", () => {