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
27 changes: 24 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 EncodedJSValue invokeMockFunction(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 @@ -981,6 +981,27 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionCall, (JSGlobalObject * lexicalGlobalObje
return JSValue::encode(jsUndefined());
}

JSC_DEFINE_HOST_FUNCTION(jsMockFunctionCall, (JSGlobalObject * lexicalGlobalObject, CallFrame* callframe))
{
return invokeMockFunction(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 [[Construct]] implemented in C++ must create `this` itself and must always return an object.
JSC::Structure* structure = JSC::InternalFunction::createSubclassStructure(lexicalGlobalObject, asObject(callframe->newTarget()), lexicalGlobalObject->objectStructureForObjectConstructor());
RETURN_IF_EXCEPTION(scope, {});
JSC::JSObject* thisObject = JSC::constructEmptyObject(vm, structure);

JSValue result = JSValue::decode(invokeMockFunction(lexicalGlobalObject, callframe, thisObject));
RETURN_IF_EXCEPTION(scope, {});

return JSValue::encode(result.isObject() ? result : thisObject);
}

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

expect(bar()()).toBe(true);
});

describe("used as a constructor", () => {
test("returns a new object when there is no implementation", () => {
const fn = jest.fn();
expect(typeof new fn()).toBe("object");
expect(typeof Reflect.construct(fn, [])).toBe("object");
expect(fn).toHaveBeenCalledTimes(2);
});

test.each([
["undefined", undefined],
["a string", "a string"],
["a number", 1234],
["a boolean", true],
["a symbol", Symbol.iterator],
["a bigint", 1234n],
["null", null],
])("ignores an implementation returning %s", (_label, returnValue) => {
const fn = jest.fn(() => returnValue);
expect(typeof new fn()).toBe("object");
expect(typeof Reflect.construct(fn, [])).toBe("object");
expect(fn()).toBe(returnValue);
});

test("returns the object an implementation returns", () => {
const returnValue = { a: 1 };
const fn = jest.fn(() => returnValue);
expect(new fn()).toBe(returnValue);
expect(Reflect.construct(fn, [])).toBe(returnValue);
});

test("passes the new object as `this` to the implementation", () => {
const fn = jest.fn(function (value) {
this.value = value;
});
const instance = new fn(42);
expect(instance).toEqual({ value: 42 });
expect(fn.mock.contexts[0]).toBe(instance);
});

test("mockReturnValue with a primitive still returns an object", () => {
const fn = jest.fn();
fn.mockReturnValue("nope");
expect(typeof new fn()).toBe("object");
expect(fn()).toBe("nope");
});

test("Reflect.construct honors an explicit newTarget", () => {
class Target {}
const fn = jest.fn();
expect(Reflect.construct(fn, [], Target)).toBeInstanceOf(Target);
});
});
});

describe("spyOn", () => {
Expand Down
Loading