diff --git a/src/jsc/bindings/JSMockFunction.cpp b/src/jsc/bindings/JSMockFunction.cpp index 1c1c7fe70f6..c2239b49da1 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(); } @@ -981,6 +982,34 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionCall, (JSGlobalObject * lexicalGlobalObje return JSValue::encode(jsUndefined()); } +// A native constructor must return an object, but a mock implementation can produce any value. +// Follow ordinary [[Construct]] semantics: if the implementation does not return an object, +// return a fresh object created from newTarget's prototype instead. +JSC_DEFINE_HOST_FUNCTION(jsMockFunctionConstruct, (JSGlobalObject * globalObject, CallFrame* callframe)) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSValue returnValue = JSValue::decode(jsMockFunctionCall(globalObject, callframe)); + RETURN_IF_EXCEPTION(scope, {}); + + if (returnValue && returnValue.isObject()) { + return JSValue::encode(returnValue); + } + + JSValue newTarget = callframe->newTarget(); + JSValue prototype = jsUndefined(); + if (newTarget && newTarget.isObject()) { + prototype = asObject(newTarget)->get(globalObject, vm.propertyNames->prototype); + RETURN_IF_EXCEPTION(scope, {}); + } + + JSObject* thisObject = prototype.isObject() + ? JSC::constructEmptyObject(globalObject, asObject(prototype)) + : JSC::constructEmptyObject(globalObject); + RELEASE_AND_RETURN(scope, 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 7f6a244d980..6715d02d918 100644 --- a/test/js/bun/test/mock-fn.test.js +++ b/test/js/bun/test/mock-fn.test.js @@ -794,6 +794,22 @@ describe("mock()", () => { expect(bar()()).toBe(true); }); + + test("constructing a mock returns an object even when the implementation returns a primitive", () => { + const fn = jest.fn(); + expect(typeof new fn()).toBe("object"); + expect(typeof Reflect.construct(fn, [])).toBe("object"); + + const returnsPrimitive = jest.fn().mockReturnValue(42); + expect(typeof new returnsPrimitive()).toBe("object"); + expect(typeof Reflect.construct(returnsPrimitive, [])).toBe("object"); + expect(returnsPrimitive).toHaveBeenCalledTimes(2); + + const instance = { a: 1 }; + const returnsObject = jest.fn(() => instance); + expect(new returnsObject()).toBe(instance); + expect(Reflect.construct(returnsObject, [])).toBe(instance); + }); }); describe("spyOn", () => { @@ -1011,6 +1027,14 @@ describe("spyOn", () => { expect(arr[14]()).toBe(456); expect(fn).not.toHaveBeenCalled(); }); + + test("constructing a spy on a missing property does not crash", () => { + const target = {}; + const spy = spyOn(target, "doesNotExist"); + expect(typeof Reflect.construct(spy, [])).toBe("object"); + expect(spy).toHaveBeenCalledTimes(1); + spy.mockRestore(); + }); } // spyOn does not work with getters/setters yet.