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", () => {