Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion src/jsc/bindings/JSMockFunction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ inline To tryJSDynamicCast(JSC::WriteBarrier<WriteBarrierT>& 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);
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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);
Expand Down
46 changes: 46 additions & 0 deletions test/js/bun/test/mock-fn.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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() {
Expand Down
Loading