diff --git a/src/jsc/bindings/JSMockFunction.cpp b/src/jsc/bindings/JSMockFunction.cpp index 1c1c7fe70f6..36f115dc478 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,34 @@ 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); + + // [[Construct]] must return an object: create `this` from new.target's prototype, + // run the mock, and return the mock's result only if it is an object. + JSObject* newTarget = asObject(callframe->newTarget()); + JSGlobalObject* functionGlobalObject = getFunctionRealm(lexicalGlobalObject, newTarget); + RETURN_IF_EXCEPTION(scope, {}); + Structure* structure = InternalFunction::createSubclassStructure(lexicalGlobalObject, newTarget, functionGlobalObject->objectStructureForObjectConstructor()); + RETURN_IF_EXCEPTION(scope, {}); + JSObject* thisObject = JSC::constructEmptyObject(vm, structure); + + JSValue returnValue = JSValue::decode(jsMockFunctionCallImpl(lexicalGlobalObject, callframe, thisObject)); + RETURN_IF_EXCEPTION(scope, {}); + if (returnValue && returnValue.isObject()) { + return JSValue::encode(returnValue); + } + + 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..a45e0a5e449 100644 --- a/test/js/bun/test/mock-fn.test.js +++ b/test/js/bun/test/mock-fn.test.js @@ -238,6 +238,39 @@ describe("mock()", () => { const obj = { fn }; expect(obj.fn()).toBe(obj); }); + test("can be constructed with new", () => { + const fn = jest.fn(); + const instance = new fn(); + expect(typeof instance).toBe("object"); + expect(instance).not.toBe(null); + expect(fn).toHaveBeenCalledTimes(1); + expect(fn.mock.results[0]).toEqual({ + type: "return", + value: undefined, + }); + + // the implementation is called with `this` set to the newly created object + const fn2 = jest.fn(function () { + this.x = 42; + }); + const instance2 = new fn2(); + expect(instance2.x).toBe(42); + expect(fn2.mock.contexts[0]).toBe(instance2); + + // a primitive return value from the implementation is ignored, like a plain constructor + const fn3 = jest.fn(() => 123); + expect(typeof new fn3()).toBe("object"); + + // an object return value becomes the result of `new` + const result = { a: 1 }; + const fn4 = jest.fn(() => result); + expect(new fn4()).toBe(result); + + // Reflect.construct with a different new.target respects its prototype + class Base {} + const fn5 = jest.fn(); + expect(Reflect.construct(fn5, [], Base)).toBeInstanceOf(Base); + }); if (isBun) { test("jest.fn(10) return value shorthand", () => { expect(jest.fn(10)()).toBe(10);