Skip to content
Open
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
37 changes: 34 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,37 @@
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. Behave like `new` on an
// ordinary JS function: create `this` from newTarget's prototype, run the mock
// with it, and return the mock's result only if it is an 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);
Comment thread
claude[bot] marked this conversation as resolved.

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 warning on line 1013 in src/jsc/bindings/JSMockFunction.cpp

View check run for this annotation

Claude / Claude Code Review

mock.instances not populated on construct

Now that mocks have a working `[[Construct]]` path, it might be worth pushing the constructed instance onto `fn->instances` here so `mock.instances` is populated like Jest does — currently the field is declared, cleared, GC-visited, and exposed on `mock` but never written to anywhere, so it's always `[]`. This is a pre-existing gap (not a regression from this PR), so feel free to defer to a follow-up; just flagging since the new test only checks `fn.mock.contexts[0]` and wouldn't catch it.
Comment thread
claude[bot] marked this conversation as resolved.

void JSMockFunctionPrototype::finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject)
{
Base::finishCreation(vm);
Expand Down
41 changes: 41 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,47 @@ 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);
});

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