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
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 @@ 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 @@ -981,6 +981,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);

// [[Construct]] must return an object. Mimic ordinary constructor semantics:
// create `this` from newTarget.prototype, run the mock body with it, and
// return the body'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);

JSValue result = JSValue::decode(jsMockFunctionCallImpl(lexicalGlobalObject, callframe, thisObject));
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
44 changes: 44 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,34 @@ describe("mock()", () => {
expect(fn).toHaveBeenCalledWith();
});

test("can be constructed with new", () => {
// with no implementation, `new` returns a newly created object
const fn = jest.fn();
const instance = new fn();
expect(typeof instance).toBe("object");
expect(instance).not.toBeNull();
const reflected = Reflect.construct(fn, []);
expect(typeof reflected).toBe("object");
expect(reflected).not.toBeNull();
expect(fn).toHaveBeenCalledTimes(2);

// a primitive return value is ignored, like ordinary constructors
const returnsPrimitive = jest.fn(() => 42);
expect(typeof new returnsPrimitive()).toBe("object");
expect(typeof Reflect.construct(returnsPrimitive, [])).toBe("object");

// an object return value is passed through, like ordinary constructors
const result = { value: 42 };
const returnsObject = jest.fn(() => result);
expect(new returnsObject()).toBe(result);
expect(Reflect.construct(returnsObject, [])).toBe(result);

// mockReturnValue with a primitive still constructs an object
const mockedReturnValue = jest.fn().mockReturnValue(123);
expect(typeof new mockedReturnValue()).toBe("object");
expect(typeof Reflect.construct(mockedReturnValue, [])).toBe("object");
});

test("mockName returns this", () => {
const fn = jest.fn();
expect(fn.mockName()).toBe(fn);
Expand Down Expand Up @@ -828,6 +856,22 @@ describe("spyOn", () => {
expect(fn).not.toHaveBeenCalled();
});

if (isBun) {
test("spy on a missing property can be constructed with new", () => {
// the spy's implementation returns the original value (undefined), which
// must not escape as the result of a [[Construct]]
const target = {};
const fn = spyOn(target, "doesNotExist");
const instance = new fn();
expect(typeof instance).toBe("object");
expect(instance).not.toBeNull();
const reflected = Reflect.construct(fn, []);
expect(typeof reflected).toBe("object");
expect(reflected).not.toBeNull();
fn.mockRestore();
});
}

test("override impl after doesnt break restore", () => {
var obj = {
original() {
Expand Down
Loading