diff --git a/src/jsc/bindings/JSMockFunction.cpp b/src/jsc/bindings/JSMockFunction.cpp index 1c1c7fe70f6..2aa6f6b9bd1 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,38 @@ 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); + + // For constructors implemented in C++, the this value slot holds new.target. + JSValue newTarget = callframe->newTarget(); + JSObject* prototype = nullptr; + if (newTarget && newTarget.isObject()) { + JSValue prototypeValue = asObject(newTarget)->get(lexicalGlobalObject, vm.propertyNames->prototype); + RETURN_IF_EXCEPTION(scope, {}); + if (prototypeValue.isObject()) + prototype = asObject(prototypeValue); + } + + JSObject* thisObject = prototype ? JSC::constructEmptyObject(lexicalGlobalObject, prototype) : JSC::constructEmptyObject(lexicalGlobalObject); + + JSValue result = JSValue::decode(jsMockFunctionCallImpl(lexicalGlobalObject, callframe, thisObject)); + RETURN_IF_EXCEPTION(scope, {}); + + // [[Construct]] must return an object, so fall back to the newly created + // instance when the mock implementation produced a primitive. + if (result && result.isObject()) + return JSValue::encode(result); + 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 7f6a244d980..f180b299dbb 100644 --- a/test/js/bun/test/mock-fn.test.js +++ b/test/js/bun/test/mock-fn.test.js @@ -1015,3 +1015,39 @@ describe("spyOn", () => { // spyOn does not work with getters/setters yet. }); + +describe("mock constructor calls", () => { + test("constructing a mock with no implementation returns an object", () => { + const fn = jest.fn(); + const instance = new fn(); + expect(typeof instance).toBe("object"); + expect(typeof Reflect.construct(fn, [])).toBe("object"); + expect(fn.mock.contexts[0]).toBe(instance); + }); + + test("constructing a mock whose implementation returns a primitive returns an object", () => { + const fn = jest.fn(() => 42); + expect(typeof new fn()).toBe("object"); + expect(typeof Reflect.construct(fn, [])).toBe("object"); + expect(fn.mock.results).toEqual([ + { type: "return", value: 42 }, + { type: "return", value: 42 }, + ]); + }); + + test("constructing a mock whose implementation returns an object returns that object", () => { + const obj = { a: 1 }; + const fn = jest.fn(() => obj); + expect(new fn()).toBe(obj); + expect(Reflect.construct(fn, [])).toBe(obj); + }); + + if (isBun) { + 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"); + spy.mockRestore(); + }); + } +});