Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
36 changes: 33 additions & 3 deletions src/jsc/bindings/JSMockFunction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
}

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 @@
}

JSMockFunction(JSC::VM& vm, JSC::Structure* structure, CallbackKind wrapKind)
: Base(vm, structure, jsMockFunctionCall, jsMockFunctionCall)
: Base(vm, structure, jsMockFunctionCall, jsMockFunctionConstruct)
{
initMock();
}
Expand Down Expand Up @@ -826,7 +827,7 @@
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<Zig::GlobalObject>(lexicalGlobalObject);
auto& vm = JSC::getVM(globalObject);
Expand All @@ -838,7 +839,6 @@
}

JSC::ArgList args = JSC::ArgList(callframe);
JSValue thisValue = callframe->thisValue();
JSC::JSArray* argumentsArray = nullptr;
{
JSC::ObjectInitializationScope object(vm);
Expand Down Expand Up @@ -981,6 +981,36 @@
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);

// A native construct callback must return an object, so build `this` from
// newTarget's prototype and fall back to it when the mock returns a non-object.
JSObject* newTarget = asObject(callframe->newTarget());
JSValue prototype = newTarget->get(lexicalGlobalObject, vm.propertyNames->prototype);
RETURN_IF_EXCEPTION(scope, {});

JSObject* thisObject = prototype.isObject()
? JSC::constructEmptyObject(lexicalGlobalObject, asObject(prototype))
: JSC::constructEmptyObject(lexicalGlobalObject);

JSValue returnValue = JSValue::decode(jsMockFunctionCallImpl(lexicalGlobalObject, callframe, thisObject));
RETURN_IF_EXCEPTION(scope, {});

if (returnValue && returnValue.isObject()) {
return JSValue::encode(returnValue);
}

return JSValue::encode(thisObject);

Check notice on line 1011 in src/jsc/bindings/JSMockFunction.cpp

View check run for this annotation

Claude / Claude Code Review

mock.instances not populated on construct

Note: `mock.instances` is still never populated — Jest pushes the constructed `this` onto `mock.instances` on every `new` call, but Bun's `fn->instances` array is exposed/cleared/GC-visited yet nothing ever pushes to it, so `fn.mock.instances` stays `[]`. This is a pre-existing gap (it was empty before this PR too), but since `jsMockFunctionConstruct` now explicitly creates `thisObject`, it's the natural place to also push it onto `fn->instances` to close the Jest-compat gap.
Comment thread
claude[bot] marked this conversation as resolved.
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

void JSMockFunctionPrototype::finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject)
{
Base::finishCreation(vm);
Expand Down
50 changes: 50 additions & 0 deletions test/js/bun/test/mock-fn.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,56 @@ describe("mock()", () => {
expect(fn).toHaveBeenCalledWith();
});

test("are constructable with new", () => {
// no implementation: `new` should produce a fresh object, like `new` on an ordinary function
const fn = jest.fn();
const instance = new fn(1, 2);
expect(typeof instance).toBe("object");
expect(instance).not.toBe(null);
expect(fn.mock.calls).toEqual([[1, 2]]);
expect(fn.mock.contexts[0]).toBe(instance);

// Reflect.construct used to crash when the mock returned a non-object
const reflected = Reflect.construct(fn, []);
expect(typeof reflected).toBe("object");

// implementation operating on `this`
const withImpl = jest.fn(function (value) {
this.value = value;
});
const constructed = new withImpl(42);
expect(constructed.value).toBe(42);
expect(withImpl.mock.contexts[0]).toBe(constructed);

// implementation returning an object wins over the created `this`
const returnsObject = jest.fn(() => ({ a: 1 }));
expect(new returnsObject()).toEqual({ a: 1 });

// primitive return values are ignored by `new`, like ordinary functions
const returnsPrimitive = jest.fn().mockReturnValue(42);
expect(typeof new returnsPrimitive()).toBe("object");

// newTarget.prototype is respected
const classLike = jest.fn();
classLike.prototype = {
greet() {
return "hello";
},
};
const classInstance = new classLike();
expect(classInstance.greet()).toBe("hello");
expect(classInstance instanceof classLike).toBe(true);

// Reflect.construct with an explicit newTarget honors its prototype
class Base {}
expect(Reflect.construct(jest.fn(), [], Base)).toBeInstanceOf(Base);

// a normal call with a non-undefined `this` must still return the mock's value
const method = jest.fn().mockReturnValue(42);
const host = { method };
expect(host.method()).toBe(42);
});

test("mockName returns this", () => {
const fn = jest.fn();
expect(fn.mockName()).toBe(fn);
Expand Down
Loading