diff --git a/src/jsc/bindings/JSMockFunction.cpp b/src/jsc/bindings/JSMockFunction.cpp index 1c1c7fe70f6..03b0b457ac2 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,26 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionCall, (JSGlobalObject * lexicalGlobalObje JSC::ArgList args = JSC::ArgList(callframe); JSValue thisValue = callframe->thisValue(); + + if (isConstructCall) { + JSValue newTarget = callframe->newTarget(); + if (JSObject* newTargetObject = newTarget.getObject()) { + JSGlobalObject* functionGlobalObject = getFunctionRealm(globalObject, newTargetObject); + RETURN_IF_EXCEPTION(scope, {}); + Structure* structure = InternalFunction::createSubclassStructure(globalObject, newTargetObject, functionGlobalObject->objectStructureForObjectConstructor()); + RETURN_IF_EXCEPTION(scope, {}); + thisValue = constructEmptyObject(vm, structure); + } else { + thisValue = constructEmptyObject(globalObject); + } + } + + auto encodedReturnValue = [&](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 +975,22 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionCall, (JSGlobalObject * lexicalGlobalObje fn->returnValues.set(vm, fn, returnValuesArray); } - return JSValue::encode(returnValue); + return encodedReturnValue(returnValue); } case JSMockImplementation::Kind::ReturnValue: { JSValue returnValue = impl->underlyingValue.get(); setReturnValue(createMockResult(vm, globalObject, "return"_s, returnValue)); - return JSValue::encode(returnValue); + return encodedReturnValue(returnValue); } case JSMockImplementation::Kind::ReturnThis: { setReturnValue(createMockResult(vm, globalObject, "return"_s, thisValue)); - return JSValue::encode(thisValue); + return encodedReturnValue(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 encodedReturnValue(rejectedPromise); } default: { RELEASE_ASSERT_NOT_REACHED(); @@ -978,7 +999,17 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionCall, (JSGlobalObject * lexicalGlobalObje } setReturnValue(createMockResult(vm, globalObject, "return"_s, jsUndefined())); - return JSValue::encode(jsUndefined()); + return encodedReturnValue(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 7f6a244d980..ea696ff8fd5 100644 --- a/test/js/bun/test/mock-fn.test.js +++ b/test/js/bun/test/mock-fn.test.js @@ -238,6 +238,51 @@ describe("mock()", () => { const obj = { fn }; expect(obj.fn()).toBe(obj); }); + test("Reflect.construct on mock with no implementation returns an object", () => { + const fn = jest.fn(); + const result = Reflect.construct(fn, []); + expect(typeof result).toBe("object"); + expect(result).not.toBeNull(); + }); + test("Reflect.construct on mock returning a primitive returns an object", () => { + const fn = jest.fn(() => 42); + const result = Reflect.construct(fn, []); + expect(typeof result).toBe("object"); + expect(result).not.toBeNull(); + + const fn2 = jest.fn().mockReturnValue("hello"); + const result2 = Reflect.construct(fn2, []); + expect(typeof result2).toBe("object"); + expect(result2).not.toBeNull(); + }); + test("Reflect.construct on mock returning an object returns that object", () => { + const obj = { foo: "bar" }; + const fn = jest.fn(() => obj); + expect(Reflect.construct(fn, [])).toBe(obj); + expect(new fn()).toBe(obj); + }); + test("Reflect.construct on mock uses newTarget prototype", () => { + function NewTarget() {} + NewTarget.prototype = { marker: true }; + const fn = jest.fn(); + const result = Reflect.construct(fn, [], NewTarget); + expect(Object.getPrototypeOf(result)).toBe(NewTarget.prototype); + }); + test("new on mock uses assigned prototype", () => { + const fn = jest.fn(); + const proto = { marker: true }; + fn.prototype = proto; + const result = new fn(); + expect(Object.getPrototypeOf(result)).toBe(proto); + }); + test("new on mock passes this to the implementation", () => { + const fn = jest.fn(function () { + this.x = 1; + }); + const result = new fn(); + expect(result).toEqual({ x: 1 }); + expect(fn.mock.contexts[0]).toBe(result); + }); if (isBun) { test("jest.fn(10) return value shorthand", () => { expect(jest.fn(10)()).toBe(10); @@ -828,6 +873,29 @@ describe("spyOn", () => { expect(fn).not.toHaveBeenCalled(); }); + test("constructing a spy works", () => { + var obj = { + Original: function () { + this.ok = true; + }, + }; + const fn = spyOn(obj, "Original"); + const instance = Reflect.construct(obj.Original, []); + expect(typeof instance).toBe("object"); + expect(instance.ok).toBe(true); + expect(fn).toHaveBeenCalledTimes(1); + }); + + if (isBun) { + test("constructing a spy on a missing property returns an object", () => { + const target = {}; + const fn = spyOn(target, "doesNotExist"); + expect(typeof Reflect.construct(fn, [])).toBe("object"); + expect(typeof new fn()).toBe("object"); + fn.mockRestore(); + }); + } + test("override impl after doesnt break restore", () => { var obj = { original() {