diff --git a/src/jsc/bindings/JSMockFunction.cpp b/src/jsc/bindings/JSMockFunction.cpp index 1c1c7fe70f6e..0435c58b15d2 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 EncodedJSValue jsMockFunctionCallOrConstruct(JSGlobalObject* lexicalGlobalObject, CallFrame* callframe, bool isConstructCall) { Zig::GlobalObject* globalObject = uncheckedDowncast(lexicalGlobalObject); auto& vm = JSC::getVM(globalObject); @@ -839,6 +840,39 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionCall, (JSGlobalObject * lexicalGlobalObje JSC::ArgList args = JSC::ArgList(callframe); JSValue thisValue = callframe->thisValue(); + + if (isConstructCall) { + JSValue newTarget = callframe->newTarget(); + JSObject* prototype = globalObject->objectPrototype(); + if (newTarget && newTarget.isObject()) { + JSValue prototypeValue = asObject(newTarget)->get(globalObject, vm.propertyNames->prototype); + RETURN_IF_EXCEPTION(scope, {}); + if (prototypeValue.isObject()) + prototype = asObject(prototypeValue); + } + thisValue = JSC::constructEmptyObject(globalObject, prototype); + RETURN_IF_EXCEPTION(scope, {}); + + if (auto* instances = fn->instances.get()) { + 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 encodeReturn = [&](JSValue value) -> EncodedJSValue { + if (isConstructCall && !value.isObject()) + return JSValue::encode(thisValue); + return JSValue::encode(value); + }; + JSC::JSArray* argumentsArray = nullptr; { JSC::ObjectInitializationScope object(vm); @@ -954,22 +988,22 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionCall, (JSGlobalObject * lexicalGlobalObje fn->returnValues.set(vm, fn, returnValuesArray); } - return JSValue::encode(returnValue); + return encodeReturn(returnValue); } case JSMockImplementation::Kind::ReturnValue: { JSValue returnValue = impl->underlyingValue.get(); setReturnValue(createMockResult(vm, globalObject, "return"_s, returnValue)); - return JSValue::encode(returnValue); + return encodeReturn(returnValue); } case JSMockImplementation::Kind::ReturnThis: { setReturnValue(createMockResult(vm, globalObject, "return"_s, thisValue)); - return JSValue::encode(thisValue); + return encodeReturn(thisValue); } case JSMockImplementation::Kind::RejectedValue: { JSValue rejectedPromise = JSC::JSPromise::rejectedPromise(globalObject, impl->underlyingValue.get()); RETURN_IF_EXCEPTION(scope, {}); setReturnValue(createMockResult(vm, globalObject, "return"_s, rejectedPromise)); - return JSValue::encode(rejectedPromise); + return encodeReturn(rejectedPromise); } default: { RELEASE_ASSERT_NOT_REACHED(); @@ -978,7 +1012,17 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionCall, (JSGlobalObject * lexicalGlobalObje } setReturnValue(createMockResult(vm, globalObject, "return"_s, jsUndefined())); - return JSValue::encode(jsUndefined()); + return encodeReturn(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsMockFunctionCall, (JSGlobalObject * lexicalGlobalObject, CallFrame* callframe)) +{ + return jsMockFunctionCallOrConstruct(lexicalGlobalObject, callframe, false); +} + +JSC_DEFINE_HOST_FUNCTION(jsMockFunctionConstruct, (JSGlobalObject * lexicalGlobalObject, CallFrame* callframe)) +{ + return jsMockFunctionCallOrConstruct(lexicalGlobalObject, callframe, true); } 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 7f6a244d9806..85fdca387fb5 100644 --- a/test/js/bun/test/mock-fn.test.js +++ b/test/js/bun/test/mock-fn.test.js @@ -794,6 +794,69 @@ describe("mock()", () => { expect(bar()()).toBe(true); }); + + describe("as constructor", () => { + test("returns an object when the implementation returns a primitive", () => { + const fn = jest.fn(function () { + this.x = 1; + return "primitive"; + }); + expect(fn.call({})).toBe("primitive"); + const instance = new fn(); + expect(typeof instance).toBe("object"); + expect(instance.x).toBe(1); + expect(typeof Reflect.construct(fn, [])).toBe("object"); + }); + + test("records instances and results", () => { + const fn = jest.fn(function () { + this.x = 1; + return "primitive"; + }); + const instance = new fn(); + expect(fn.mock.instances).toHaveLength(1); + expect(fn.mock.instances[0]).toBe(instance); + expect(fn.mock.contexts[0]).toBe(instance); + expect(fn.mock.results[0]).toEqual({ type: "return", value: "primitive" }); + }); + + test("returns the implementation's return value when it is an object", () => { + const obj = { custom: true }; + const fn = jest.fn(() => obj); + expect(new fn()).toBe(obj); + expect(Reflect.construct(fn, [])).toBe(obj); + }); + + test("works with no implementation", () => { + const fn = jest.fn(); + expect(typeof new fn()).toBe("object"); + expect(typeof Reflect.construct(fn, [])).toBe("object"); + }); + + test("works with mockReturnValue", () => { + const fn = jest.fn().mockReturnValue(42); + expect(fn()).toBe(42); + expect(typeof new fn()).toBe("object"); + expect(typeof Reflect.construct(fn, [])).toBe("object"); + }); + + test("sets the prototype from new.target", () => { + class Base {} + const fn = jest.fn(() => undefined); + expect(Reflect.construct(fn, [], Base)).toBeInstanceOf(Base); + }); + + if (isBun) { + test("no crash when implementation returns a non-object cell", () => { + for (const impl of [Symbol, BigInt, () => "str", () => 1n]) { + const fn = jest.fn(impl); + expect(typeof Reflect.construct(fn, [1])).toBe("object"); + expect(typeof new fn(1)).toBe("object"); + } + Bun.gc(true); + }); + } + }); }); describe("spyOn", () => {