diff --git a/src/jsc/bindings/JSMockFunction.cpp b/src/jsc/bindings/JSMockFunction.cpp index 1c1c7fe70f6..9bd7278dcd7 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(); } @@ -981,6 +982,35 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionCall, (JSGlobalObject * lexicalGlobalObje return JSValue::encode(jsUndefined()); } +JSC_DEFINE_HOST_FUNCTION(jsMockFunctionConstruct, (JSGlobalObject * globalObject, CallFrame* callframe)) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + // [[Construct]] must return an object, but the mock implementation can return any value. + // Mirror ordinary JS constructor semantics: create `this` from new.target's prototype, + // invoke the mock with it, and fall back to it when the implementation returns a primitive. + JSValue newTarget = callframe->newTarget(); + JSObject* thisObject = nullptr; + if (newTarget.isObject()) [[likely]] { + JSValue prototype = asObject(newTarget)->get(globalObject, vm.propertyNames->prototype); + RETURN_IF_EXCEPTION(scope, {}); + if (prototype.isObject()) + thisObject = JSC::constructEmptyObject(globalObject, asObject(prototype)); + } + if (!thisObject) + thisObject = JSC::constructEmptyObject(globalObject); + + callframe->setThisValue(thisObject); + JSValue result = JSValue::decode(jsMockFunctionCall(globalObject, callframe)); + RETURN_IF_EXCEPTION(scope, {}); + + if (result && result.isObject()) + return JSValue::encode(result); + + 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..d63db49810e 100644 --- a/test/js/bun/test/mock-fn.test.js +++ b/test/js/bun/test/mock-fn.test.js @@ -309,6 +309,39 @@ describe("mock()", () => { expect(fn).toHaveBeenLastCalledWith(); expect(fn).toHaveBeenCalledWith(); }); + test("new works", () => { + const fn = jest.fn(function () { + this.a = 1; + }); + const instance = new fn(); + expect(typeof instance).toBe("object"); + expect(instance.a).toBe(1); + expect(fn).toHaveBeenCalledTimes(1); + expect(fn.mock.contexts[0]).toBe(instance); + + // an object returned from the implementation becomes the result of `new` + const returned = { b: 2 }; + const fn2 = jest.fn(() => returned); + expect(new fn2()).toBe(returned); + + // a primitive return value is ignored, like ordinary constructors + const fn3 = jest.fn(() => 42); + expect(typeof new fn3()).toBe("object"); + + // same for mockReturnValue + const fn4 = jest.fn().mockReturnValue(5); + expect(typeof new fn4()).toBe("object"); + + // Reflect.construct on a mock with no implementation returns an object + const fn5 = jest.fn(); + expect(typeof Reflect.construct(fn5, [])).toBe("object"); + expect(fn5).toHaveBeenCalledTimes(1); + + // the prototype of the instance comes from the mock's .prototype + const fn6 = jest.fn(); + fn6.prototype = { marker: 3 }; + expect(new fn6().marker).toBe(3); + }); test(".name works", () => { const fn = jest.fn(function hey() { return this; @@ -828,6 +861,19 @@ 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); + }); + test("override impl after doesnt break restore", () => { var obj = { original() {