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
50 changes: 47 additions & 3 deletions 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 @@ -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 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_DEFINE_HOST_FUNCTION(jsMockFunctionCall, (JSGlobalObject * lexicalGlobalObje
}

JSC::ArgList args = JSC::ArgList(callframe);
JSValue thisValue = callframe->thisValue();
JSC::JSArray* argumentsArray = nullptr;
{
JSC::ObjectInitializationScope object(vm);
Expand Down Expand Up @@ -879,6 +879,20 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionCall, (JSGlobalObject * lexicalGlobalObje
fn->contexts.set(vm, fn, contexts);
}

JSC::JSArray* instances = fn->instances.get();
if (instances) {
instances->push(globalObject, thisValue);
RETURN_IF_EXCEPTION(scope, {});
} else {
JSC::ObjectInitializationScope object(vm);
instances = JSC::JSArray::tryCreateUninitializedRestricted(
object,
globalObject->arrayStructureForIndexingTypeDuringAllocation(JSC::ArrayWithContiguous),
1);
instances->initializeIndex(object, 0, thisValue);
fn->instances.set(vm, fn, instances);
}

auto invocationId = JSMockModule::nextInvocationId();
JSC::JSArray* invocationCallOrder = fn->invocationCallOrder.get();
if (invocationCallOrder) {
Expand Down Expand Up @@ -981,6 +995,36 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionCall, (JSGlobalObject * lexicalGlobalObje
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);
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
54 changes: 54 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,60 @@ 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);
// `new` calls are recorded in mock.instances
expect(fn.mock.instances[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");
expect(fn.mock.instances[1]).toBe(reflected);

// 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);
expect(withImpl.mock.instances[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