Skip to content
Open
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
68 changes: 59 additions & 9 deletions src/jsc/bindings/JSMockFunction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,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 @@ -463,7 +464,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 @@ -798,19 +799,12 @@ 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(Zig::GlobalObject* globalObject, JSMockFunction* fn, CallFrame* callframe, JSValue thisValue)
{
Zig::GlobalObject* globalObject = uncheckedDowncast<Zig::GlobalObject>(lexicalGlobalObject);
auto& vm = JSC::getVM(globalObject);
JSMockFunction* fn = dynamicDowncast<JSMockFunction>(callframe->jsCallee());
auto scope = DECLARE_THROW_SCOPE(vm);
if (!fn) [[unlikely]] {
throwTypeError(globalObject, scope, "Expected callee to be mock function"_s);
return {};
}

JSC::ArgList args = JSC::ArgList(callframe);
JSValue thisValue = callframe->thisValue().toThis(globalObject, ECMAMode::strict());
JSC::JSArray* argumentsArray = nullptr;
{
JSC::ObjectInitializationScope object(vm);
Expand Down Expand Up @@ -851,6 +845,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 @@ -958,6 +966,48 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionCall, (JSGlobalObject * lexicalGlobalObje
return JSValue::encode(jsUndefined());
}

JSC_DEFINE_HOST_FUNCTION(jsMockFunctionCall, (JSGlobalObject * lexicalGlobalObject, CallFrame* callframe))
{
Zig::GlobalObject* globalObject = uncheckedDowncast<Zig::GlobalObject>(lexicalGlobalObject);
auto& vm = JSC::getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
JSMockFunction* fn = dynamicDowncast<JSMockFunction>(callframe->jsCallee());
if (!fn) [[unlikely]] {
throwTypeError(globalObject, scope, "Expected callee to be mock function"_s);
return {};
}

JSValue thisValue = callframe->thisValue().toThis(globalObject, ECMAMode::strict());
RELEASE_AND_RETURN(scope, jsMockFunctionCallImpl(globalObject, fn, callframe, thisValue));
}

// A native [[Construct]] must return an object (Interpreter::executeConstruct calls asObject on the result).
JSC_DEFINE_HOST_FUNCTION(jsMockFunctionConstruct, (JSGlobalObject * lexicalGlobalObject, CallFrame* callframe))
{
Zig::GlobalObject* globalObject = uncheckedDowncast<Zig::GlobalObject>(lexicalGlobalObject);
auto& vm = JSC::getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
JSMockFunction* fn = dynamicDowncast<JSMockFunction>(callframe->jsCallee());
if (!fn) [[unlikely]] {
throwTypeError(globalObject, scope, "Expected callee to be mock function"_s);
return {};
}

JSObject* newTarget = asObject(callframe->newTarget());
JSGlobalObject* functionGlobalObject = getFunctionRealm(globalObject, newTarget);
RETURN_IF_EXCEPTION(scope, {});
Structure* structure = InternalFunction::createSubclassStructure(globalObject, newTarget, functionGlobalObject->objectStructureForObjectConstructor());
RETURN_IF_EXCEPTION(scope, {});
JSObject* thisObject = JSC::constructEmptyObject(vm, structure);

JSValue result = JSValue::decode(jsMockFunctionCallImpl(globalObject, fn, callframe, thisObject));
RETURN_IF_EXCEPTION(scope, {});

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

void JSMockFunctionPrototype::finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject)
{
Base::finishCreation(vm);
Expand Down
94 changes: 94 additions & 0 deletions test/js/bun/test/mock-fn.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* `bunx vitest test/js/bun/test/mock-fn.test.js`
* `NODE_OPTIONS=--experimental-vm-modules npx jest test/js/bun/test/mock-fn.test.js`
*/
import { runInNewContext } from "node:vm";
import test_interop from "./test-interop.js";
var { isBun, describe, test, it, expect, jest, vi, mock, spyOn } = await test_interop();

Expand Down Expand Up @@ -849,6 +850,99 @@ describe("mock()", () => {

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

describe("construct", () => {
test("new on a mock with no implementation returns a new object", () => {
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);
expect(fn.mock.instances[0]).toBe(instance);
expect(fn.mock.results).toEqual([{ type: "return", value: undefined }]);
});

test("implementation runs with `this` set to the new instance", () => {
const fn = jest.fn(function (value) {
this.value = value;
});
const instance = new fn(42);
expect(instance.value).toBe(42);
});

test("object returned by the implementation replaces the instance", () => {
const obj = { a: 1 };
const fn = jest.fn(() => obj);
expect(new fn()).toBe(obj);
});

test("non-object return values are ignored under new", () => {
const fn = jest.fn();
fn.mockReturnValue(42);
const instance = new fn();
expect(typeof instance).toBe("object");
expect(fn()).toBe(42);
});

test("uses the prototype property of the constructor", () => {
const fn = jest.fn();
fn.prototype = { marker: true };
const instance = new fn();
expect(Object.getPrototypeOf(instance)).toBe(fn.prototype);
expect(instance.marker).toBe(true);
});

test("Reflect.construct uses newTarget's prototype", () => {
const fn = jest.fn();
function NewTarget() {}
NewTarget.prototype = { fromNewTarget: true };
const instance = Reflect.construct(fn, [], NewTarget);
expect(Object.getPrototypeOf(instance)).toBe(NewTarget.prototype);
});

test("throwing implementation propagates and records the result", () => {
const error = new Error("construct error");
const fn = jest.fn(() => {
throw error;
});
expect(() => new fn()).toThrow("construct error");
expect(fn.mock.results).toEqual([{ type: "throw", value: error }]);
});

if (isBun) {
test("constructing a spy on a missing property does not crash", () => {
const target = {};
const spy = spyOn(target, 9);
const instance = Reflect.construct(spy, []);
expect(typeof instance).toBe("object");
spy.mockRestore();
});

test("instances records `this` for every invocation, in call order", () => {
const fn = jest.fn();
const obj = { fn };
fn();
obj.fn();
const instance = new fn();
expect(fn.mock.instances).toHaveLength(fn.mock.calls.length);
expect(fn.mock.instances[0]).toBeUndefined();
expect(fn.mock.instances[1]).toBe(obj);
expect(fn.mock.instances[2]).toBe(instance);
});

test("falls back to Object.prototype of newTarget's realm when its prototype is not an object", () => {
const fn = jest.fn();
const { NewTarget, otherObjectPrototype } = runInNewContext(
"({ NewTarget: function NewTarget() {}, otherObjectPrototype: Object.prototype })",
);
expect(otherObjectPrototype).not.toBe(Object.prototype);
NewTarget.prototype = null;
const instance = Reflect.construct(fn, [], NewTarget);
expect(Object.getPrototypeOf(instance)).toBe(otherObjectPrototype);
});
}
});
});

describe("resetAllMocks", () => {
Expand Down