diff --git a/src/jsc/bindings/JSMockFunction.cpp b/src/jsc/bindings/JSMockFunction.cpp index bbe197002249..8c0889053a22 100644 --- a/src/jsc/bindings/JSMockFunction.cpp +++ b/src/jsc/bindings/JSMockFunction.cpp @@ -87,6 +87,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); @@ -463,7 +464,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(); } @@ -976,6 +977,48 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionCall, (JSGlobalObject * lexicalGlobalObje return JSValue::encode(jsUndefined()); } +JSC_DEFINE_HOST_FUNCTION(jsMockFunctionConstruct, (JSGlobalObject * lexicalGlobalObject, CallFrame* callframe)) +{ + Zig::GlobalObject* globalObject = uncheckedDowncast(lexicalGlobalObject); + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSMockFunction* fn = dynamicDowncast(callframe->jsCallee()); + if (!fn) [[unlikely]] { + throwTypeError(globalObject, scope, "Expected callee to be mock function"_s); + return {}; + } + + JSValue newTarget = callframe->newTarget(); + JSObject* newTargetObject = newTarget.isObject() ? asObject(newTarget) : fn; + JSValue prototype = newTargetObject->get(globalObject, vm.propertyNames->prototype); + RETURN_IF_EXCEPTION(scope, {}); + JSObject* instance = prototype.isObject() + ? JSC::constructEmptyObject(globalObject, asObject(prototype)) + : JSC::constructEmptyObject(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + + if (JSC::JSArray* instances = fn->instances.get()) { + instances->push(globalObject, instance); + RETURN_IF_EXCEPTION(scope, {}); + } else { + JSC::ObjectInitializationScope object(vm); + instances = JSC::JSArray::tryCreateUninitializedRestricted( + object, + globalObject->arrayStructureForIndexingTypeDuringAllocation(JSC::ArrayWithContiguous), + 1); + instances->initializeIndex(object, 0, instance); + fn->instances.set(vm, fn, instances); + } + + callframe->setThisValue(instance); + EncodedJSValue encodedResult = jsMockFunctionCall(lexicalGlobalObject, callframe); + RETURN_IF_EXCEPTION(scope, {}); + JSValue result = JSValue::decode(encodedResult); + if (result.isObject()) + return encodedResult; + return JSValue::encode(instance); +} + 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 64cc036122d2..c62fddb01458 100644 --- a/test/js/bun/test/mock-fn.test.js +++ b/test/js/bun/test/mock-fn.test.js @@ -803,6 +803,40 @@ describe("mock()", () => { expect(bar()()).toBe(true); }); + + test("Reflect.construct on a mock returns an object and records the instance", () => { + const fn = jest.fn(); + const instance = Reflect.construct(fn, [1, 2]); + expect(typeof instance).toBe("object"); + expect(instance).not.toBeNull(); + expect(fn.mock.instances).toEqual([instance]); + expect(fn.mock.contexts).toEqual([instance]); + expect(fn.mock.calls).toEqual([[1, 2]]); + + const fn2 = jest.fn(() => ({ tag: "impl" })); + const result2 = Reflect.construct(fn2, []); + expect(result2).toEqual({ tag: "impl" }); + expect(fn2.mock.instances).toHaveLength(1); + expect(fn2.mock.instances[0]).not.toBe(result2); + + const fn3 = jest.fn(() => 42); + const result3 = new fn3(); + expect(typeof result3).toBe("object"); + expect(fn3.mock.instances).toEqual([result3]); + + class NewTarget {} + const fn4 = jest.fn(); + const result4 = Reflect.construct(fn4, [], NewTarget); + expect(Object.getPrototypeOf(result4)).toBe(NewTarget.prototype); + expect(result4).toBeInstanceOf(NewTarget); + expect(fn4.mock.instances).toEqual([result4]); + + const fn5 = jest.fn(() => { + throw new Error("boom"); + }); + expect(() => new fn5()).toThrow("boom"); + expect(() => Reflect.construct(fn5, [])).toThrow("boom"); + }); }); describe("resetAllMocks", () => {