diff --git a/src/jsc/bindings/JSMockFunction.cpp b/src/jsc/bindings/JSMockFunction.cpp index 14e82d405209..d2de6ec2b3c0 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 invokeMockFunction(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,27 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionCall, (JSGlobalObject * lexicalGlobalObje return JSValue::encode(jsUndefined()); } +JSC_DEFINE_HOST_FUNCTION(jsMockFunctionCall, (JSGlobalObject * lexicalGlobalObject, CallFrame* callframe)) +{ + return invokeMockFunction(lexicalGlobalObject, callframe, callframe->thisValue()); +} + +JSC_DEFINE_HOST_FUNCTION(jsMockFunctionConstruct, (JSGlobalObject * lexicalGlobalObject, CallFrame* callframe)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + // A [[Construct]] implemented in C++ must create `this` itself and must always return an object. + JSC::Structure* structure = JSC::InternalFunction::createSubclassStructure(lexicalGlobalObject, asObject(callframe->newTarget()), lexicalGlobalObject->objectStructureForObjectConstructor()); + RETURN_IF_EXCEPTION(scope, {}); + JSC::JSObject* thisObject = JSC::constructEmptyObject(vm, structure); + + JSValue result = JSValue::decode(invokeMockFunction(lexicalGlobalObject, callframe, thisObject)); + RETURN_IF_EXCEPTION(scope, {}); + + return JSValue::encode(result.isObject() ? result : 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 7f6a244d9806..6bc2e352223d 100644 --- a/test/js/bun/test/mock-fn.test.js +++ b/test/js/bun/test/mock-fn.test.js @@ -794,6 +794,59 @@ describe("mock()", () => { expect(bar()()).toBe(true); }); + + describe("used as a constructor", () => { + test("returns a new object when there is no implementation", () => { + const fn = jest.fn(); + expect(typeof new fn()).toBe("object"); + expect(typeof Reflect.construct(fn, [])).toBe("object"); + expect(fn).toHaveBeenCalledTimes(2); + }); + + test.each([ + ["undefined", undefined], + ["a string", "a string"], + ["a number", 1234], + ["a boolean", true], + ["a symbol", Symbol.iterator], + ["a bigint", 1234n], + ["null", null], + ])("ignores an implementation returning %s", (_label, returnValue) => { + const fn = jest.fn(() => returnValue); + expect(typeof new fn()).toBe("object"); + expect(typeof Reflect.construct(fn, [])).toBe("object"); + expect(fn()).toBe(returnValue); + }); + + test("returns the object an implementation returns", () => { + const returnValue = { a: 1 }; + const fn = jest.fn(() => returnValue); + expect(new fn()).toBe(returnValue); + expect(Reflect.construct(fn, [])).toBe(returnValue); + }); + + test("passes the new object as `this` to the implementation", () => { + const fn = jest.fn(function (value) { + this.value = value; + }); + const instance = new fn(42); + expect(instance).toEqual({ value: 42 }); + expect(fn.mock.contexts[0]).toBe(instance); + }); + + test("mockReturnValue with a primitive still returns an object", () => { + const fn = jest.fn(); + fn.mockReturnValue("nope"); + expect(typeof new fn()).toBe("object"); + expect(fn()).toBe("nope"); + }); + + test("Reflect.construct honors an explicit newTarget", () => { + class Target {} + const fn = jest.fn(); + expect(Reflect.construct(fn, [], Target)).toBeInstanceOf(Target); + }); + }); }); describe("spyOn", () => {