aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Jarred Sumner <jarred@jarredsumner.com> 2023-06-09 16:38:06 -0700
committerGravatar GitHub <noreply@github.com> 2023-06-09 16:38:06 -0700
commitbf518222d456c913fc5e6b6e0d14952d76c0ce91 (patch)
tree99431a14fefb1b52b75ea0528b68232e8af2cb7c
parent6565bd89d533b17d0d975f2790a4b4d37d8aecc1 (diff)
downloadbun-bf518222d456c913fc5e6b6e0d14952d76c0ce91.tar.gz
bun-bf518222d456c913fc5e6b6e0d14952d76c0ce91.tar.zst
bun-bf518222d456c913fc5e6b6e0d14952d76c0ce91.zip
Implement mocks in bun:test (#3252)
* wip * wip * most of the code for mocks in bun:test * finishing up * Implement `toHaveBeenCalled` and `toHaveBeenCalledTimes(1)` * Test * visit * results, not returnValues * exact * Update jest.zig * A couple more tests * Add jest.fn * support resetting mocks * Implement spyOn --------- Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com>
-rw-r--r--packages/bun-types/bun-test.d.ts43
-rw-r--r--src/bun.js/bindings/JSMockFunction.cpp1154
-rw-r--r--src/bun.js/bindings/JSMockFunction.h30
-rw-r--r--src/bun.js/bindings/ZigGeneratedClasses.cpp31
-rw-r--r--src/bun.js/bindings/ZigGlobalObject.cpp8
-rw-r--r--src/bun.js/bindings/ZigGlobalObject.h3
-rw-r--r--src/bun.js/bindings/generated_classes.zig3
-rw-r--r--src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h3
-rw-r--r--src/bun.js/bindings/webcore/DOMIsoSubspaces.h2
-rw-r--r--src/bun.js/test/jest.classes.ts4
-rw-r--r--src/bun.js/test/jest.zig143
-rw-r--r--src/js_parser.zig2
-rw-r--r--test/js/bun/test/mock-test.test.ts131
13 files changed, 1550 insertions, 7 deletions
diff --git a/packages/bun-types/bun-test.d.ts b/packages/bun-types/bun-test.d.ts
index a5cee34c3..ed5298f38 100644
--- a/packages/bun-types/bun-test.d.ts
+++ b/packages/bun-types/bun-test.d.ts
@@ -16,6 +16,49 @@
declare module "bun:test" {
/**
+ * -- Mocks --
+ */
+ export type Mock<T extends (...args: any[]) => any> = T & {
+ mockImplementation(fn: T): Mock<T>;
+ mockImplementationOnce(fn: T): Mock<T>;
+ mockReset(): void;
+ mockRestore(): void;
+ mockReturnValue(value: ReturnType<T>): Mock<T>;
+ mockReturnValueOnce(value: ReturnType<T>): Mock<T>;
+ mockResolvedValue(value: ReturnType<T>): Mock<T>;
+ mockResolvedValueOnce(value: ReturnType<T>): Mock<T>;
+ mockRejectedValue(value: ReturnType<T>): Mock<T>;
+ mockRejectedValueOnce(value: ReturnType<T>): Mock<T>;
+ mockName(name: string): Mock<T>;
+ mockClear(): void;
+ mock: {
+ calls: any[][];
+ instances: any[];
+ results: Array<{ type: "return" | "throw"; value: any }>;
+ contexts: any[];
+ };
+
+ (...args: Parameters<T>): ReturnType<T>;
+ };
+
+ export const mock: {
+ mockImplementation<T>(fn: T): Mock<T>;
+ mockImplementationOnce<T>(fn: T): Mock<T>;
+ mockReturnValue<T>(value: ReturnType<T>): Mock<T>;
+ mockReturnValueOnce<T>(value: ReturnType<T>): Mock<T>;
+ mockResolvedValue<T>(value: ReturnType<T>): Mock<T>;
+ mockResolvedValueOnce<T>(value: ReturnType<T>): Mock<T>;
+ mockRejectedValue<T>(value: ReturnType<T>): Mock<T>;
+ mockRejectedValueOnce<T>(value: ReturnType<T>): Mock<T>;
+ <T extends CallableFunction>(Function: T): Mock<T>;
+ };
+
+ export function spyOn<T extends object, K extends keyof T>(
+ obj: T,
+ methodOrPropertyValue: K,
+ ): Mock<T[K]>;
+
+ /**
* Describes a group of related tests.
*
* @example
diff --git a/src/bun.js/bindings/JSMockFunction.cpp b/src/bun.js/bindings/JSMockFunction.cpp
new file mode 100644
index 000000000..6aeb44ded
--- /dev/null
+++ b/src/bun.js/bindings/JSMockFunction.cpp
@@ -0,0 +1,1154 @@
+#include "root.h"
+
+#include "JSMockFunction.h"
+#include <JavaScriptCore/JSPromise.h>
+#include "ZigGlobalObject.h"
+#include <JavaScriptCore/InternalFunction.h>
+#include "JavaScriptCore/Completion.h"
+#include "JavaScriptCore/ObjectConstructor.h"
+#include "ExtendedDOMClientIsoSubspaces.h"
+#include "ExtendedDOMIsoSubspaces.h"
+#include "BunClientData.h"
+#include "JavaScriptCore/LazyProperty.h"
+#include "JavaScriptCore/JSCJSValueInlines.h"
+#include "JavaScriptCore/JSInternalPromise.h"
+#include "JavaScriptCore/LazyPropertyInlines.h"
+#include "JavaScriptCore/VMTrapsInlines.h"
+#include <JavaScriptCore/Weak.h>
+#include <JavaScriptCore/GetterSetter.h>
+#include <JavaScriptCore/WeakMapImpl.h>
+
+namespace Bun {
+
+JSC_DECLARE_HOST_FUNCTION(jsMockFunctionCall);
+JSC_DECLARE_CUSTOM_GETTER(jsMockFunctionGetter_protoImpl);
+JSC_DECLARE_CUSTOM_GETTER(jsMockFunctionGetter_mock);
+JSC_DECLARE_HOST_FUNCTION(jsMockFunctionGetMockImplementation);
+JSC_DECLARE_HOST_FUNCTION(jsMockFunctionGetMockName);
+JSC_DECLARE_HOST_FUNCTION(jsMockFunctionMockClear);
+JSC_DECLARE_HOST_FUNCTION(jsMockFunctionMockReset);
+JSC_DECLARE_HOST_FUNCTION(jsMockFunctionMockRestore);
+JSC_DECLARE_HOST_FUNCTION(jsMockFunctionMockImplementation);
+JSC_DECLARE_HOST_FUNCTION(jsMockFunctionMockImplementationOnce);
+JSC_DECLARE_HOST_FUNCTION(jsMockFunctionMockName);
+JSC_DECLARE_HOST_FUNCTION(jsMockFunctionMockReturnThis);
+JSC_DECLARE_HOST_FUNCTION(jsMockFunctionMockReturnValue);
+JSC_DECLARE_HOST_FUNCTION(jsMockFunctionMockReturnValueOnce);
+JSC_DECLARE_HOST_FUNCTION(jsMockFunctionMockResolvedValue);
+JSC_DECLARE_HOST_FUNCTION(jsMockFunctionMockResolvedValueOnce);
+JSC_DECLARE_HOST_FUNCTION(jsMockFunctionMockRejectedValue);
+JSC_DECLARE_HOST_FUNCTION(jsMockFunctionMockRejectedValueOnce);
+JSC_DECLARE_HOST_FUNCTION(jsMockFunctionWithImplementation);
+JSC_DECLARE_HOST_FUNCTION(jsMockFunctionMockImplementationOnce);
+
+// This is taken from JSWeakSet
+// We only want to hold onto the list of active spies which haven't already been collected
+// So we use a WeakSet
+// Unlike using WeakSet from JS, we are able to iterate through the WeakSet.
+class ActiveSpySet final : public WeakMapImpl<WeakMapBucket<WeakMapBucketDataKey>> {
+public:
+ using Base = WeakMapImpl<WeakMapBucket<WeakMapBucketDataKey>>;
+
+ DECLARE_EXPORT_INFO;
+
+ static Structure* createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype)
+ {
+ return Structure::create(vm, globalObject, prototype, TypeInfo(JSWeakSetType, StructureFlags), info());
+ }
+
+ static ActiveSpySet* create(VM& vm, Structure* structure)
+ {
+ ActiveSpySet* instance = new (NotNull, allocateCell<ActiveSpySet>(vm)) ActiveSpySet(vm, structure);
+ instance->finishCreation(vm);
+ return instance;
+ }
+
+private:
+ ActiveSpySet(VM& vm, Structure* structure)
+ : Base(vm, structure)
+ {
+ }
+};
+
+static_assert(std::is_final<ActiveSpySet>::value, "Required for JSType based casting");
+const ClassInfo ActiveSpySet::s_info = { "ActiveSpySet"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(ActiveSpySet) };
+
+class JSMockImplementation final : public JSNonFinalObject {
+public:
+ enum class Kind : uint8_t {
+ Call,
+ Promise,
+ ReturnValue,
+ ThrowValue,
+ ReturnThis,
+ };
+
+ static JSMockImplementation* create(JSC::JSGlobalObject* globalObject, JSC::Structure* structure, Kind kind, JSC::JSValue heldValue, bool isOnce)
+ {
+ auto& vm = globalObject->vm();
+ JSMockImplementation* impl = new (NotNull, allocateCell<JSMockImplementation>(vm)) JSMockImplementation(vm, structure, kind);
+ impl->finishCreation(vm, heldValue, isOnce ? jsNumber(1) : jsUndefined());
+ return impl;
+ }
+
+ using Base = JSC::JSNonFinalObject;
+ static Structure* createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype)
+ {
+ return Structure::create(vm, globalObject, prototype, TypeInfo(JSC::ObjectType, StructureFlags), info());
+ }
+ template<typename, JSC::SubspaceAccess mode> static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm)
+ {
+ if constexpr (mode == JSC::SubspaceAccess::Concurrently)
+ return nullptr;
+ return WebCore::subspaceForImpl<JSMockImplementation, UseCustomHeapCellType::No>(
+ vm,
+ [](auto& spaces) { return spaces.m_clientSubspaceForJSMockImplementation.get(); },
+ [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForJSMockImplementation = std::forward<decltype(space)>(space); },
+ [](auto& spaces) { return spaces.m_subspaceForJSMockImplementation.get(); },
+ [](auto& spaces, auto&& space) { spaces.m_subspaceForJSMockImplementation = std::forward<decltype(space)>(space); });
+ }
+
+ static constexpr unsigned numberOfInternalFields = 2;
+
+ mutable JSC::WriteBarrier<Unknown> internalFields[2];
+
+ DECLARE_EXPORT_INFO;
+ DECLARE_VISIT_CHILDREN;
+
+ Kind kind { Kind::ReturnValue };
+
+ bool isOnce()
+ {
+ auto secondField = internalFields[1].get();
+ if (secondField.isNumber() && secondField.asInt32() == 1) {
+ return true;
+ }
+ return jsDynamicCast<JSMockImplementation*>(secondField.asCell());
+ }
+
+ JSMockImplementation(JSC::VM& vm, JSC::Structure* structure, Kind kind)
+ : Base(vm, structure)
+ , kind(kind)
+ {
+ }
+
+ void finishCreation(JSC::VM& vm, JSC::JSValue first, JSC::JSValue second)
+ {
+ Base::finishCreation(vm);
+ this->internalFields[0].set(vm, this, first);
+ this->internalFields[1].set(vm, this, second);
+ }
+};
+
+template<typename Visitor>
+void JSMockImplementation::visitChildrenImpl(JSCell* cell, Visitor& visitor)
+{
+ JSMockImplementation* fn = jsCast<JSMockImplementation*>(cell);
+ ASSERT_GC_OBJECT_INHERITS(fn, info());
+ Base::visitChildren(fn, visitor);
+
+ visitor.append(fn->internalFields[0]);
+ visitor.append(fn->internalFields[1]);
+}
+
+DEFINE_VISIT_CHILDREN(JSMockImplementation);
+
+enum class CallbackKind : uint8_t {
+ Wrapper,
+ Call,
+ GetterSetter,
+};
+
+static NativeFunction jsMockFunctionForCallbackKind(CallbackKind kind)
+{
+ switch (kind) {
+ // return jsMockFunctionGetterSetter;
+ case CallbackKind::Wrapper:
+ return jsMockFunctionMockImplementation;
+ case CallbackKind::GetterSetter:
+ case CallbackKind::Call:
+ return jsMockFunctionCall;
+ default:
+ RELEASE_ASSERT_NOT_REACHED();
+ }
+}
+
+const ClassInfo JSMockImplementation::s_info = { "MockImpl"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSMockImplementation) };
+
+class JSMockFunction : public JSC::InternalFunction {
+public:
+ using Base = JSC::InternalFunction;
+ static constexpr unsigned StructureFlags = Base::StructureFlags;
+
+ static JSMockFunction* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, CallbackKind kind = CallbackKind::Call)
+ {
+ JSMockFunction* function = new (NotNull, JSC::allocateCell<JSMockFunction>(vm)) JSMockFunction(vm, structure, kind);
+ function->finishCreation(vm);
+ return function;
+ }
+ static Structure* createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype)
+ {
+ return Structure::create(vm, globalObject, prototype, TypeInfo(InternalFunctionType, StructureFlags), info());
+ }
+
+ DECLARE_INFO;
+ DECLARE_VISIT_CHILDREN;
+
+ JSC::LazyProperty<JSMockFunction, JSObject> mock;
+ mutable JSC::WriteBarrier<JSC::Unknown> implementation;
+ mutable JSC::WriteBarrier<JSC::JSArray> calls;
+ mutable JSC::WriteBarrier<JSC::JSArray> contexts;
+ mutable JSC::WriteBarrier<JSC::JSArray> instances;
+ mutable JSC::WriteBarrier<JSC::JSArray> returnValues;
+ mutable JSC::WriteBarrier<JSC::Unknown> tail;
+
+ JSC::Weak<JSObject> spyTarget;
+ JSC::Identifier spyIdentifier;
+ unsigned spyAttributes = 0;
+
+ void initMock()
+ {
+ mock.initLater(
+ [](const JSC::LazyProperty<JSMockFunction, JSObject>::Initializer& init) {
+ JSMockFunction* mock = init.owner;
+ Zig::GlobalObject* globalObject = jsCast<Zig::GlobalObject*>(mock->globalObject());
+ JSC::Structure* structure = globalObject->mockModule.mockObjectStructure.getInitializedOnMainThread(globalObject);
+ JSObject* object = JSC::constructEmptyObject(init.vm, structure);
+ object->putDirectOffset(init.vm, 0, mock->getCalls());
+ object->putDirectOffset(init.vm, 1, mock->getContexts());
+ object->putDirectOffset(init.vm, 2, mock->getInstances());
+ object->putDirectOffset(init.vm, 3, mock->getReturnValues());
+ init.set(object);
+ });
+ }
+
+ void reset()
+ {
+ this->calls.clear();
+ this->instances.clear();
+ this->returnValues.clear();
+ this->contexts.clear();
+
+ if (this->mock.isInitialized()) {
+ this->initMock();
+ }
+ }
+
+ void clearSpy()
+ {
+ if (auto* target = this->spyTarget.get()) {
+ JSValue implValue = jsUndefined();
+ if (auto* impl = jsDynamicCast<JSMockImplementation*>(this->implementation.get())) {
+ implValue = impl->internalFields[0].get();
+ }
+
+ // Reset the spy back to the original value.
+ target->putDirect(this->vm(), this->spyIdentifier, implValue, this->spyAttributes);
+ }
+
+ this->spyTarget.clear();
+ this->spyIdentifier = JSC::Identifier();
+ this->spyAttributes = 0;
+ }
+
+ JSArray* getCalls() const
+ {
+ JSArray* val = calls.get();
+ if (!val) {
+ val = JSC::constructEmptyArray(globalObject(), nullptr, 0);
+ this->calls.set(vm(), this, val);
+ }
+ return val;
+ }
+ JSArray* getContexts() const
+ {
+ JSArray* val = contexts.get();
+ if (!val) {
+ val = JSC::constructEmptyArray(globalObject(), nullptr, 0);
+ this->contexts.set(vm(), this, val);
+ }
+ return val;
+ }
+ JSArray* getInstances() const
+ {
+ JSArray* val = instances.get();
+ if (!val) {
+ val = JSC::constructEmptyArray(globalObject(), nullptr, 0);
+ this->instances.set(vm(), this, val);
+ }
+ return val;
+ }
+ JSArray* getReturnValues() const
+ {
+ JSArray* val = returnValues.get();
+ if (!val) {
+ val = JSC::constructEmptyArray(globalObject(), nullptr, 0);
+ this->returnValues.set(vm(), this, val);
+ }
+ return val;
+ }
+
+ template<typename, JSC::SubspaceAccess mode>
+ static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm)
+ {
+ if constexpr (mode == JSC::SubspaceAccess::Concurrently)
+ return nullptr;
+ return WebCore::subspaceForImpl<JSMockFunction, UseCustomHeapCellType::No>(
+ vm,
+ [](auto& spaces) { return spaces.m_clientSubspaceForJSMockFunction.get(); },
+ [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForJSMockFunction = std::forward<decltype(space)>(space); },
+ [](auto& spaces) { return spaces.m_subspaceForJSMockFunction.get(); },
+ [](auto& spaces, auto&& space) { spaces.m_subspaceForJSMockFunction = std::forward<decltype(space)>(space); });
+ }
+
+ JSMockFunction(JSC::VM& vm, JSC::Structure* structure, CallbackKind wrapKind)
+ : Base(vm, structure, jsMockFunctionForCallbackKind(wrapKind), jsMockFunctionForCallbackKind(wrapKind))
+ {
+ initMock();
+ }
+};
+
+template<typename Visitor>
+void JSMockFunction::visitChildrenImpl(JSCell* cell, Visitor& visitor)
+{
+ JSMockFunction* fn = jsCast<JSMockFunction*>(cell);
+ ASSERT_GC_OBJECT_INHERITS(fn, info());
+ Base::visitChildren(fn, visitor);
+
+ visitor.append(fn->implementation);
+ visitor.append(fn->calls);
+ visitor.append(fn->contexts);
+ visitor.append(fn->instances);
+ visitor.append(fn->returnValues);
+ visitor.append(fn->tail);
+ fn->mock.visit(visitor);
+}
+DEFINE_VISIT_CHILDREN(JSMockFunction);
+
+static void pushImplInternal(JSMockFunction* fn, JSGlobalObject* jsGlobalObject, JSMockImplementation::Kind kind, JSValue value, bool isOnce)
+{
+ Zig::GlobalObject* globalObject = jsCast<Zig::GlobalObject*>(jsGlobalObject);
+ auto& vm = globalObject->vm();
+ JSValue currentTail = fn->tail.get();
+ JSMockImplementation* impl = JSMockImplementation::create(globalObject, globalObject->mockModule.mockImplementationStructure.getInitializedOnMainThread(globalObject), kind, value, isOnce);
+ JSValue currentImpl = fn->implementation.get();
+ if (currentTail) {
+ if (auto* current = jsDynamicCast<JSMockImplementation*>(currentTail)) {
+ current->internalFields[1].set(vm, current, impl);
+ }
+ }
+ fn->tail.set(vm, fn, impl);
+ if (!currentImpl || !currentImpl.inherits<JSMockImplementation>()) {
+ fn->implementation.set(vm, fn, impl);
+ }
+}
+
+static void pushImpl(JSMockFunction* fn, JSGlobalObject* globalObject, JSMockImplementation::Kind kind, JSValue value)
+{
+ pushImplInternal(fn, globalObject, kind, value, false);
+}
+
+static void pushImplOnce(JSMockFunction* fn, JSGlobalObject* globalObject, JSMockImplementation::Kind kind, JSValue value)
+{
+ pushImplInternal(fn, globalObject, kind, value, true);
+}
+
+class JSMockFunctionPrototype final : public JSC::JSNonFinalObject {
+public:
+ using Base = JSC::JSNonFinalObject;
+
+ static JSMockFunctionPrototype* create(JSC::VM& vm, JSGlobalObject* globalObject, JSC::Structure* structure)
+ {
+ JSMockFunctionPrototype* ptr = new (NotNull, JSC::allocateCell<JSMockFunctionPrototype>(vm)) JSMockFunctionPrototype(vm, globalObject, structure);
+ ptr->finishCreation(vm, globalObject);
+ return ptr;
+ }
+
+ DECLARE_INFO;
+ template<typename CellType, JSC::SubspaceAccess>
+ static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm)
+ {
+ return &vm.plainObjectSpace();
+ }
+ static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype)
+ {
+ return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info());
+ }
+
+private:
+ JSMockFunctionPrototype(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure)
+ : Base(vm, structure)
+ {
+ }
+
+ void finishCreation(JSC::VM&, JSC::JSGlobalObject*);
+};
+
+static const HashTableValue JSMockFunctionPrototypeTableValues[] = {
+ { "mock"_s, static_cast<unsigned>(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, jsMockFunctionGetter_mock, 0 } },
+ { "_protoImpl"_s, static_cast<unsigned>(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, jsMockFunctionGetter_protoImpl, 0 } },
+ { "getMockImplementation"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, jsMockFunctionGetMockImplementation, 0 } },
+ { "getMockName"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, jsMockFunctionGetMockName, 0 } },
+ { "mockClear"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, jsMockFunctionMockClear, 0 } },
+ { "mockReset"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, jsMockFunctionMockReset, 0 } },
+ { "mockRestore"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, jsMockFunctionMockRestore, 0 } },
+ { "mockImplementation"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, jsMockFunctionMockImplementation, 1 } },
+ { "mockImplementationOnce"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, jsMockFunctionMockImplementationOnce, 1 } },
+ { "withImplementation"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, jsMockFunctionWithImplementation, 1 } },
+ { "mockName"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, jsMockFunctionMockName, 1 } },
+ { "mockReturnThis"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, jsMockFunctionMockReturnThis, 1 } },
+ { "mockReturnValue"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, jsMockFunctionMockReturnValue, 1 } },
+ { "mockReturnValueOnce"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, jsMockFunctionMockReturnValueOnce, 1 } },
+ { "mockResolvedValue"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, jsMockFunctionMockResolvedValue, 1 } },
+ { "mockResolvedValueOnce"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, jsMockFunctionMockResolvedValueOnce, 1 } },
+ { "mockRejectedValueOnce"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, jsMockFunctionMockRejectedValue, 1 } },
+ { "mockRejectedValue"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, jsMockFunctionMockRejectedValueOnce, 1 } },
+};
+
+const ClassInfo JSMockFunction::s_info = { "Mock"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSMockFunction) };
+
+class SpyWeakHandleOwner final : public JSC::WeakHandleOwner {
+public:
+ void finalize(JSC::Handle<JSC::Unknown>, void* context) final {}
+};
+
+static SpyWeakHandleOwner& weakValueHandleOwner()
+{
+ static NeverDestroyed<SpyWeakHandleOwner> jscWeakValueHandleOwner;
+ return jscWeakValueHandleOwner;
+}
+
+const ClassInfo JSMockFunctionPrototype::s_info = { "Mock"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSMockFunctionPrototype) };
+
+extern "C" void JSMock__resetSpies(Zig::GlobalObject* globalObject)
+{
+ if (!globalObject->mockModule.activeSpies) {
+ return;
+ }
+ auto spiesValue = globalObject->mockModule.activeSpies.get();
+
+ ActiveSpySet* activeSpies = jsCast<ActiveSpySet*>(spiesValue);
+ MarkedArgumentBuffer active;
+ activeSpies->takeSnapshot(active);
+ size_t size = active.size();
+
+ for (size_t i = 0; i < size; ++i) {
+ JSValue spy = active.at(i);
+ if (!spy.isObject())
+ continue;
+
+ auto* spyObject = jsCast<JSMockFunction*>(spy);
+ spyObject->reset();
+ spyObject->clearSpy();
+ }
+ globalObject->mockModule.activeSpies.clear();
+}
+
+extern "C" EncodedJSValue jsFunctionResetSpies(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callframe)
+{
+ JSMock__resetSpies(jsCast<Zig::GlobalObject*>(globalObject));
+ return JSValue::encode(jsUndefined());
+}
+
+extern "C" EncodedJSValue JSMock__spyOn(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callframe)
+{
+ auto& vm = lexicalGlobalObject->vm();
+ auto scope = DECLARE_THROW_SCOPE(vm);
+
+ auto* globalObject = jsDynamicCast<Zig::GlobalObject*>(lexicalGlobalObject);
+ if (UNLIKELY(!globalObject)) {
+ throwVMError(globalObject, scope, "Cannot run spyOn from a different global context"_s);
+ return {};
+ }
+
+ JSValue objectValue = callframe->argument(0);
+ JSValue propertyKeyValue = callframe->argument(1);
+
+ if (callframe->argumentCount() < 2 || !objectValue.isObject()) {
+ throwVMError(globalObject, scope, "spyOn(target, prop) expects a target object and a property key"_s);
+ return {};
+ }
+
+ PropertyName propertyKey = propertyKeyValue.toPropertyKey(globalObject);
+ RETURN_IF_EXCEPTION(scope, {});
+
+ if (propertyKey.isNull()) {
+ throwVMError(globalObject, scope, "spyOn(target, prop) expects a property key"_s);
+ return {};
+ }
+
+ JSC::JSObject* object = objectValue.getObject();
+ if (object->type() == JSC::JSType::GlobalProxyType)
+ object = jsCast<JSC::JSGlobalProxy*>(object)->target();
+
+ JSC::PropertySlot slot(object, JSC::PropertySlot::InternalMethodType::HasProperty);
+ bool hasValue = object->getPropertySlot(globalObject, propertyKey, slot);
+
+ // easymode: regular property or missing property
+ if (!hasValue || slot.isValue()) {
+ auto* mock = JSMockFunction::create(vm, globalObject, globalObject->mockModule.mockFunctionStructure.getInitializedOnMainThread(globalObject), CallbackKind::GetterSetter);
+ mock->spyTarget = JSC::Weak<JSObject>(object, &weakValueHandleOwner(), nullptr);
+ mock->spyIdentifier = propertyKey.isSymbol() ? Identifier::fromUid(vm, propertyKey.uid()) : Identifier::fromString(vm, propertyKey.publicName());
+ mock->spyAttributes = hasValue ? slot.attributes() : 0;
+ unsigned attributes = 0;
+ JSValue value = jsUndefined();
+
+ if (hasValue)
+ value = slot.getValue(globalObject, propertyKey);
+
+ if (hasValue && ((slot.attributes() & PropertyAttribute::Function) != 0 || (value.isCell() && value.isCallable()))) {
+ if (hasValue)
+ attributes = slot.attributes();
+
+ attributes |= PropertyAttribute::Function;
+ object->putDirect(vm, propertyKey, mock, attributes);
+ RETURN_IF_EXCEPTION(scope, {});
+
+ pushImpl(mock, globalObject, JSMockImplementation::Kind::Call, value);
+ } else {
+ if (hasValue)
+ attributes = slot.attributes();
+
+ attributes |= PropertyAttribute::Accessor;
+ object->putDirect(vm, propertyKey, JSC::GetterSetter::create(vm, globalObject, mock, mock), attributes);
+ RETURN_IF_EXCEPTION(scope, {});
+
+ pushImpl(mock, globalObject, JSMockImplementation::Kind::ReturnValue, value);
+ }
+
+ if (!globalObject->mockModule.activeSpies) {
+ ActiveSpySet* activeSpies = ActiveSpySet::create(vm, globalObject->mockModule.activeSpySetStructure.getInitializedOnMainThread(globalObject));
+ globalObject->mockModule.activeSpies.set(vm, activeSpies);
+ }
+
+ ActiveSpySet* activeSpies = jsCast<ActiveSpySet*>(globalObject->mockModule.activeSpies.get());
+ activeSpies->add(vm, mock, mock);
+
+ return JSValue::encode(mock);
+ }
+
+ // hardmode: accessor property
+ throwVMError(globalObject, scope, "spyOn(target, prop) does not support accessor properties yet"_s);
+ return {};
+}
+
+JSMockModule JSMockModule::create(JSC::JSGlobalObject* globalObject)
+{
+ JSMockModule mock;
+ mock.mockFunctionStructure.initLater(
+ [](const JSC::LazyProperty<JSC::JSGlobalObject, JSC::Structure>::Initializer& init) {
+ auto* prototype = JSMockFunctionPrototype::create(init.vm, init.owner, JSMockFunctionPrototype::createStructure(init.vm, init.owner, jsNull()));
+
+ init.set(JSMockFunction::createStructure(init.vm, init.owner, prototype));
+ });
+ mock.mockResultStructure.initLater(
+ [](const JSC::LazyProperty<JSC::JSGlobalObject, JSC::Structure>::Initializer& init) {
+ Zig::GlobalObject* globalObject = jsCast<Zig::GlobalObject*>(init.owner);
+ JSC::Structure* structure = globalObject->structureCache().emptyObjectStructureForPrototype(
+ globalObject,
+ globalObject->objectPrototype(),
+ 2);
+ JSC::PropertyOffset offset;
+
+ structure = structure->addPropertyTransition(
+ init.vm,
+ structure,
+ JSC::Identifier::fromString(init.vm, "type"_s),
+ 0,
+ offset);
+
+ structure = structure->addPropertyTransition(
+ init.vm,
+ structure,
+ JSC::Identifier::fromString(init.vm, "value"_s),
+
+ 0,
+ offset);
+
+ init.set(structure);
+ });
+ mock.activeSpySetStructure.initLater([](const JSC::LazyProperty<JSC::JSGlobalObject, JSC::Structure>::Initializer& init) {
+ Structure* implementation = ActiveSpySet::createStructure(init.vm, init.owner, jsNull());
+ init.set(implementation);
+ });
+ mock.mockImplementationStructure.initLater(
+ [](const JSC::LazyProperty<JSC::JSGlobalObject, JSC::Structure>::Initializer& init) {
+ Structure* implementation = JSMockImplementation::createStructure(init.vm, init.owner, jsNull());
+ init.set(implementation);
+ });
+ mock.mockObjectStructure.initLater(
+ [](const JSC::LazyProperty<JSC::JSGlobalObject, JSC::Structure>::Initializer& init) {
+ Zig::GlobalObject* globalObject = jsCast<Zig::GlobalObject*>(init.owner);
+ JSC::Structure* structure = globalObject->structureCache().emptyObjectStructureForPrototype(
+ globalObject,
+ globalObject->objectPrototype(),
+ 4);
+ JSC::PropertyOffset offset;
+ structure = structure->addPropertyTransition(
+ init.vm,
+ structure,
+ JSC::Identifier::fromString(init.vm, "calls"_s),
+ JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly,
+ offset);
+ structure = structure->addPropertyTransition(
+ init.vm,
+ structure,
+ JSC::Identifier::fromString(init.vm, "contexts"_s),
+ JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly,
+ offset);
+ structure = structure->addPropertyTransition(
+ init.vm,
+ structure,
+ JSC::Identifier::fromString(init.vm, "instances"_s),
+ JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly,
+ offset);
+ structure = structure->addPropertyTransition(
+ init.vm,
+ structure,
+ JSC::Identifier::fromString(init.vm, "results"_s),
+ JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly,
+ offset);
+
+ init.set(structure);
+ });
+ return mock;
+}
+
+extern Structure* createMockResultStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject)
+{
+ JSC::Structure* structure = globalObject->structureCache().emptyObjectStructureForPrototype(
+ globalObject,
+ globalObject->objectPrototype(),
+ 2);
+ JSC::PropertyOffset offset;
+
+ structure = structure->addPropertyTransition(
+ vm,
+ structure,
+ JSC::Identifier::fromString(vm, "type"_s),
+ 0,
+ offset);
+
+ structure = structure->addPropertyTransition(
+ vm,
+ structure,
+ JSC::Identifier::fromString(vm, "value"_s),
+ 0, offset);
+ return structure;
+}
+
+static JSValue createMockResult(JSC::VM& vm, Zig::GlobalObject* globalObject, const WTF::String& type, JSC::JSValue value)
+{
+ JSC::Structure* structure = globalObject->mockModule.mockResultStructure.getInitializedOnMainThread(globalObject);
+
+ JSC::JSObject* result = JSC::constructEmptyObject(vm, structure);
+ result->putDirectOffset(vm, 0, jsString(vm, type));
+ result->putDirectOffset(vm, 1, value);
+ return result;
+}
+
+JSC_DEFINE_HOST_FUNCTION(jsMockFunctionCall, (JSGlobalObject * lexicalGlobalObject, CallFrame* callframe))
+{
+ Zig::GlobalObject* globalObject = jsCast<Zig::GlobalObject*>(lexicalGlobalObject);
+ auto& vm = globalObject->vm();
+ JSMockFunction* fn = jsDynamicCast<JSMockFunction*>(callframe->jsCallee());
+ auto scope = DECLARE_THROW_SCOPE(vm);
+ if (UNLIKELY(!fn)) {
+ throwTypeError(globalObject, scope, "Expected callee to be mock function"_s);
+ return {};
+ }
+
+ JSC::ArgList args = JSC::ArgList(callframe);
+ JSValue thisValue = callframe->thisValue();
+ JSC::JSArray* argumentsArray = nullptr;
+ {
+ JSC::ObjectInitializationScope object(vm);
+ argumentsArray = JSC::JSArray::tryCreateUninitializedRestricted(
+ object,
+ globalObject->arrayStructureForIndexingTypeDuringAllocation(JSC::ArrayWithContiguous),
+ callframe->argumentCount());
+ for (size_t i = 0; i < args.size(); i++) {
+ argumentsArray->initializeIndex(object, i, args.at(i));
+ }
+ }
+
+ JSC::JSArray* calls = fn->calls.get();
+ if (calls) {
+ calls->push(globalObject, argumentsArray);
+ } else {
+ JSC::ObjectInitializationScope object(vm);
+ calls = JSC::JSArray::tryCreateUninitializedRestricted(
+ object,
+ globalObject->arrayStructureForIndexingTypeDuringAllocation(JSC::ArrayWithContiguous),
+ 1);
+ calls->initializeIndex(object, 0, argumentsArray);
+ }
+ fn->calls.set(vm, fn, calls);
+
+ JSC::JSArray* contexts = fn->contexts.get();
+ if (contexts) {
+ contexts->push(globalObject, thisValue);
+ } else {
+ JSC::ObjectInitializationScope object(vm);
+ contexts = JSC::JSArray::tryCreateUninitializedRestricted(
+ object,
+ globalObject->arrayStructureForIndexingTypeDuringAllocation(JSC::ArrayWithContiguous),
+ 1);
+ contexts->initializeIndex(object, 0, thisValue);
+ }
+ fn->contexts.set(vm, fn, contexts);
+
+ JSValue implementationValue = fn->implementation.get();
+ if (!implementationValue)
+ implementationValue = jsUndefined();
+
+ if (auto* impl = jsDynamicCast<JSMockImplementation*>(implementationValue)) {
+ if (JSValue nextValue = impl->internalFields[1].get()) {
+ if (nextValue.inherits<JSMockImplementation>() || (nextValue.isInt32() && nextValue.asInt32() == 1)) {
+ fn->implementation.set(vm, fn, nextValue);
+ }
+ }
+
+ unsigned int returnValueIndex = 0;
+ auto setReturnValue = [&](JSC::JSValue value) -> void {
+ if (auto* returnValuesArray = fn->returnValues.get()) {
+ returnValuesArray->push(globalObject, value);
+ returnValueIndex = returnValuesArray->length() - 1;
+ } else {
+ JSC::ObjectInitializationScope object(vm);
+ returnValuesArray = JSC::JSArray::tryCreateUninitializedRestricted(
+ object,
+ globalObject->arrayStructureForIndexingTypeDuringAllocation(JSC::ArrayWithContiguous),
+ 1);
+ returnValuesArray->initializeIndex(object, 0, value);
+ fn->returnValues.set(vm, fn, returnValuesArray);
+ }
+ };
+
+ switch (impl->kind) {
+ case JSMockImplementation::Kind::Call: {
+ JSValue result = impl->internalFields[0].get();
+ JSC::CallData callData = JSC::getCallData(result);
+ if (UNLIKELY(callData.type == JSC::CallData::Type::None)) {
+ throwTypeError(globalObject, scope, "Expected mock implementation to be callable"_s);
+ return {};
+ }
+
+ setReturnValue(createMockResult(vm, globalObject, "incomplete"_s, jsUndefined()));
+
+ WTF::NakedPtr<JSC::Exception> exception;
+
+ JSValue returnValue = call(globalObject, result, callData, thisValue, args, exception);
+
+ if (auto* exc = exception.get()) {
+ if (auto* returnValuesArray = fn->returnValues.get()) {
+ returnValuesArray->putDirectIndex(globalObject, returnValueIndex, createMockResult(vm, globalObject, "throw"_s, exc->value()));
+ fn->returnValues.set(vm, fn, returnValuesArray);
+ JSC::throwException(globalObject, scope, exc);
+ return {};
+ }
+ }
+
+ if (UNLIKELY(!returnValue)) {
+ returnValue = jsUndefined();
+ }
+
+ if (auto* returnValuesArray = fn->returnValues.get()) {
+ returnValuesArray->putDirectIndex(globalObject, returnValueIndex, createMockResult(vm, globalObject, "return"_s, returnValue));
+ fn->returnValues.set(vm, fn, returnValuesArray);
+ }
+
+ return JSValue::encode(returnValue);
+ }
+ case JSMockImplementation::Kind::ReturnValue:
+ case JSMockImplementation::Kind::Promise: {
+ JSValue returnValue = impl->internalFields[0].get();
+ setReturnValue(createMockResult(vm, globalObject, "return"_s, returnValue));
+ return JSValue::encode(returnValue);
+ }
+ case JSMockImplementation::Kind::ReturnThis: {
+ setReturnValue(createMockResult(vm, globalObject, "return"_s, thisValue));
+ return JSValue::encode(thisValue);
+ }
+ default: {
+ RELEASE_ASSERT_NOT_REACHED();
+ }
+ }
+ }
+
+ return JSValue::encode(jsUndefined());
+}
+
+void JSMockFunctionPrototype::finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject)
+{
+ Base::finishCreation(vm);
+ reifyStaticProperties(vm, JSMockFunction::info(), JSMockFunctionPrototypeTableValues, *this);
+ JSC_TO_STRING_TAG_WITHOUT_TRANSITION();
+
+ this->putDirect(vm, Identifier::fromString(vm, "_isMockFunction"_s), jsBoolean(true), 0);
+}
+
+JSC_DEFINE_HOST_FUNCTION(jsMockFunctionGetMockImplementation, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe))
+{
+ JSMockFunction* thisObject = jsDynamicCast<JSMockFunction*>(callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict()));
+ auto& vm = globalObject->vm();
+ auto scope = DECLARE_THROW_SCOPE(vm);
+ if (UNLIKELY(!thisObject)) {
+ throwTypeError(globalObject, scope, "Expected Mock"_s);
+ }
+
+ JSValue impl = thisObject->implementation.get();
+ if (auto* implementation = jsDynamicCast<JSMockImplementation*>(impl)) {
+ if (implementation->kind == JSMockImplementation::Kind::Call) {
+ RELEASE_AND_RETURN(scope, JSValue::encode(implementation->internalFields[0].get()));
+ }
+ }
+
+ RELEASE_AND_RETURN(scope, JSValue::encode(jsUndefined()));
+}
+
+JSC_DEFINE_CUSTOM_GETTER(jsMockFunctionGetter_mock, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName))
+{
+ Bun::JSMockFunction* thisObject = jsDynamicCast<Bun::JSMockFunction*>(JSValue::decode(thisValue));
+ auto scope = DECLARE_THROW_SCOPE(globalObject->vm());
+ if (UNLIKELY(!thisObject)) {
+ throwTypeError(globalObject, scope, "Expected Mock"_s);
+ return {};
+ }
+
+ return JSValue::encode(thisObject->mock.getInitializedOnMainThread(thisObject));
+}
+
+JSC_DEFINE_CUSTOM_GETTER(jsMockFunctionGetter_protoImpl, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName))
+{
+ Bun::JSMockFunction* thisObject = jsDynamicCast<Bun::JSMockFunction*>(JSValue::decode(thisValue));
+ auto scope = DECLARE_THROW_SCOPE(globalObject->vm());
+ if (UNLIKELY(!thisObject)) {
+ throwTypeError(globalObject, scope, "Expected Mock"_s);
+ return {};
+ }
+
+ if (auto implValue = thisObject->implementation.get()) {
+ if (auto* impl = jsDynamicCast<JSMockImplementation*>(implValue)) {
+ if (impl->kind == JSMockImplementation::Kind::Call) {
+ return JSValue::encode(impl->internalFields[0].get());
+ }
+
+ return JSValue::encode(jsUndefined());
+ }
+ }
+
+ return JSValue::encode(jsUndefined());
+}
+
+extern "C" EncodedJSValue JSMockFunction__createObject(Zig::GlobalObject* globalObject)
+{
+ return JSValue::encode(
+ JSMockFunction::create(globalObject->vm(), globalObject, globalObject->mockModule.mockFunctionStructure.getInitializedOnMainThread(globalObject), CallbackKind::Wrapper));
+}
+
+extern "C" EncodedJSValue JSMockFunction__getCalls(EncodedJSValue encodedValue)
+{
+ JSValue value = JSValue::decode(encodedValue);
+ if (value) {
+ if (auto* mock = jsDynamicCast<JSMockFunction*>(value)) {
+ return JSValue::encode(mock->getCalls());
+ }
+ }
+
+ return JSValue::encode({});
+}
+extern "C" EncodedJSValue JSMockFunction__getReturns(EncodedJSValue encodedValue)
+{
+ JSValue value = JSValue::decode(encodedValue);
+ if (value) {
+ if (auto* mock = jsDynamicCast<JSMockFunction*>(value)) {
+ return JSValue::encode(mock->getReturnValues());
+ }
+ }
+
+ return JSValue::encode({});
+}
+
+JSC_DEFINE_HOST_FUNCTION(jsMockFunctionGetMockName, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe))
+{
+ JSMockFunction* thisObject = jsDynamicCast<JSMockFunction*>(callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict()));
+ auto& vm = globalObject->vm();
+ auto scope = DECLARE_THROW_SCOPE(vm);
+ if (UNLIKELY(!thisObject)) {
+ throwTypeError(globalObject, scope, "Expected Mock"_s);
+ }
+
+ JSValue implValue = thisObject->implementation.get();
+ if (!implValue) {
+ implValue = jsUndefined();
+ }
+
+ if (auto* impl = jsDynamicCast<JSMockImplementation*>(implValue)) {
+ if (impl->kind == JSMockImplementation::Kind::Call) {
+ JSObject* object = impl->internalFields[0].get().asCell()->getObject();
+ if (auto nameValue = object->getIfPropertyExists(globalObject, PropertyName(vm.propertyNames->name))) {
+ RELEASE_AND_RETURN(scope, JSValue::encode(nameValue));
+ }
+
+ RETURN_IF_EXCEPTION(scope, {});
+ }
+ }
+
+ RELEASE_AND_RETURN(scope, JSValue::encode(jsEmptyString(vm)));
+}
+JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockClear, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe))
+{
+ JSMockFunction* thisObject = jsDynamicCast<JSMockFunction*>(callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict()));
+ auto& vm = globalObject->vm();
+ auto scope = DECLARE_THROW_SCOPE(vm);
+ if (UNLIKELY(!thisObject)) {
+ throwTypeError(globalObject, scope, "Expected Mock"_s);
+ }
+
+ thisObject->reset();
+
+ RELEASE_AND_RETURN(scope, JSValue::encode(thisObject));
+}
+JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockReset, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe))
+{
+ JSMockFunction* thisObject = jsDynamicCast<JSMockFunction*>(callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict()));
+ auto& vm = globalObject->vm();
+ auto scope = DECLARE_THROW_SCOPE(vm);
+ if (UNLIKELY(!thisObject)) {
+ throwTypeError(globalObject, scope, "Expected Mock"_s);
+ }
+
+ thisObject->reset();
+
+ RELEASE_AND_RETURN(scope, JSValue::encode(thisObject));
+}
+JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockRestore, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe))
+{
+ JSMockFunction* thisObject = jsDynamicCast<JSMockFunction*>(callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict()));
+ auto& vm = globalObject->vm();
+ auto scope = DECLARE_THROW_SCOPE(vm);
+ if (UNLIKELY(!thisObject)) {
+ throwTypeError(globalObject, scope, "Expected Mock"_s);
+ }
+
+ RELEASE_AND_RETURN(scope, JSValue::encode(thisObject));
+}
+JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockImplementation, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callframe))
+{
+ auto& vm = lexicalGlobalObject->vm();
+ auto* globalObject = jsCast<Zig::GlobalObject*>(lexicalGlobalObject);
+ auto scope = DECLARE_THROW_SCOPE(vm);
+ JSMockFunction* thisObject = JSMockFunction::create(
+ vm,
+ globalObject,
+ globalObject->mockModule.mockFunctionStructure.getInitializedOnMainThread(globalObject));
+
+ if (UNLIKELY(!thisObject)) {
+ throwOutOfMemoryError(globalObject, scope);
+ return {};
+ }
+
+ if (callframe->argumentCount() > 0) {
+ JSValue arg = callframe->argument(0);
+ if (arg.isCallable()) {
+ pushImpl(thisObject, globalObject, JSMockImplementation::Kind::Call, arg);
+ } else {
+ pushImpl(thisObject, globalObject, JSMockImplementation::Kind::ReturnValue, arg);
+ }
+ }
+
+ return JSValue::encode(thisObject);
+}
+JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockImplementationOnce, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callframe))
+{
+ auto& vm = lexicalGlobalObject->vm();
+ auto* globalObject = jsCast<Zig::GlobalObject*>(lexicalGlobalObject);
+ auto scope = DECLARE_THROW_SCOPE(vm);
+ JSMockFunction* thisObject = jsDynamicCast<JSMockFunction*>(callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict()));
+
+ if (UNLIKELY(!thisObject)) {
+ thisObject = JSMockFunction::create(
+ vm,
+ globalObject,
+ globalObject->mockModule.mockFunctionStructure.getInitializedOnMainThread(globalObject),
+ CallbackKind::Wrapper);
+ }
+
+ if (UNLIKELY(!thisObject)) {
+ throwOutOfMemoryError(globalObject, scope);
+ return {};
+ }
+
+ if (callframe->argumentCount() > 0) {
+ JSValue arg = callframe->argument(0);
+ if (arg.isCallable()) {
+ pushImpl(thisObject, globalObject, JSMockImplementation::Kind::Call, arg);
+ } else {
+ pushImpl(thisObject, globalObject, JSMockImplementation::Kind::ReturnValue, arg);
+ }
+ }
+
+ return JSValue::encode(thisObject);
+}
+
+JSC_DEFINE_HOST_FUNCTION(jsMockFunctionWithImplementation, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe))
+{
+ JSMockFunction* thisObject = jsDynamicCast<JSMockFunction*>(callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict()));
+ auto& vm = globalObject->vm();
+ auto scope = DECLARE_THROW_SCOPE(vm);
+ if (UNLIKELY(!thisObject)) {
+ throwTypeError(globalObject, scope, "Expected Mock"_s);
+ RELEASE_AND_RETURN(scope, JSValue::encode(jsUndefined()));
+ }
+
+ JSValue arg = callframe->argument(0);
+
+ if (callframe->argumentCount() < 1 || arg.isEmpty() || arg.isUndefined()) {
+ pushImpl(thisObject, globalObject, JSMockImplementation::Kind::ReturnValue, jsUndefined());
+ } else if (arg.isCallable()) {
+ pushImpl(thisObject, globalObject, JSMockImplementation::Kind::Call, arg);
+ } else {
+ throwTypeError(globalObject, scope, "Expected a function or undefined"_s);
+ RELEASE_AND_RETURN(scope, JSValue::encode(jsUndefined()));
+ }
+
+ RELEASE_AND_RETURN(scope, JSValue::encode(thisObject));
+}
+JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockName, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe))
+{
+ JSMockFunction* thisObject = jsDynamicCast<JSMockFunction*>(callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict()));
+ auto& vm = globalObject->vm();
+ auto scope = DECLARE_THROW_SCOPE(vm);
+ if (UNLIKELY(!thisObject)) {
+ throwTypeError(globalObject, scope, "Expected Mock"_s);
+ return {};
+ }
+ if (callframe->argumentCount() > 0) {
+ auto* newName = callframe->argument(0).toStringOrNull(globalObject);
+ if (UNLIKELY(!newName)) {
+ return {};
+ }
+
+ thisObject->putDirect(vm, vm.propertyNames->name, newName, 0);
+ RELEASE_AND_RETURN(scope, JSValue::encode(newName));
+ }
+
+ RELEASE_AND_RETURN(scope, JSValue::encode(jsString(vm, thisObject->calculatedDisplayName(vm))));
+}
+JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockReturnThis, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe))
+{
+ JSMockFunction* thisObject = jsDynamicCast<JSMockFunction*>(callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict()));
+ auto& vm = globalObject->vm();
+ auto scope = DECLARE_THROW_SCOPE(vm);
+ if (UNLIKELY(!thisObject)) {
+ throwTypeError(globalObject, scope, "Expected Mock"_s);
+ }
+
+ pushImpl(thisObject, globalObject, JSMockImplementation::Kind::ReturnThis, jsUndefined());
+
+ RELEASE_AND_RETURN(scope, JSValue::encode(thisObject));
+}
+JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockReturnValue, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe))
+{
+ JSMockFunction* thisObject = jsDynamicCast<JSMockFunction*>(callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict()));
+ auto& vm = globalObject->vm();
+ auto scope = DECLARE_THROW_SCOPE(vm);
+ if (UNLIKELY(!thisObject)) {
+ throwTypeError(globalObject, scope, "Expected Mock"_s);
+ }
+
+ if (callframe->argumentCount() < 1) {
+ pushImpl(thisObject, globalObject, JSMockImplementation::Kind::ReturnValue, jsUndefined());
+ } else {
+ pushImpl(thisObject, globalObject, JSMockImplementation::Kind::ReturnValue, callframe->argument(0));
+ }
+
+ RELEASE_AND_RETURN(scope, JSValue::encode(thisObject));
+}
+JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockReturnValueOnce, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe))
+{
+ JSMockFunction* thisObject = jsDynamicCast<JSMockFunction*>(callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict()));
+ auto& vm = globalObject->vm();
+ auto scope = DECLARE_THROW_SCOPE(vm);
+ if (UNLIKELY(!thisObject)) {
+ throwTypeError(globalObject, scope, "Expected Mock"_s);
+ }
+
+ if (callframe->argumentCount() < 1) {
+ pushImplOnce(thisObject, globalObject, JSMockImplementation::Kind::ReturnValue, jsUndefined());
+ } else {
+ pushImplOnce(thisObject, globalObject, JSMockImplementation::Kind::ReturnValue, callframe->argument(0));
+ }
+
+ RELEASE_AND_RETURN(scope, JSValue::encode(thisObject));
+}
+JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockResolvedValue, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe))
+{
+ JSMockFunction* thisObject = jsDynamicCast<JSMockFunction*>(callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict()));
+ auto& vm = globalObject->vm();
+ auto scope = DECLARE_THROW_SCOPE(vm);
+ if (UNLIKELY(!thisObject)) {
+ throwTypeError(globalObject, scope, "Expected Mock"_s);
+ }
+
+ if (callframe->argumentCount() < 1) {
+ pushImpl(thisObject, globalObject, JSMockImplementation::Kind::Promise, JSC::JSPromise::resolvedPromise(globalObject, jsUndefined()));
+ } else {
+ pushImpl(thisObject, globalObject, JSMockImplementation::Kind::Promise, JSC::JSPromise::resolvedPromise(globalObject, callframe->argument(0)));
+ }
+
+ RELEASE_AND_RETURN(scope, JSValue::encode(thisObject));
+}
+JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockResolvedValueOnce, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe))
+{
+ JSMockFunction* thisObject = jsDynamicCast<JSMockFunction*>(callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict()));
+ auto& vm = globalObject->vm();
+ auto scope = DECLARE_THROW_SCOPE(vm);
+ if (UNLIKELY(!thisObject)) {
+ throwTypeError(globalObject, scope, "Expected Mock"_s);
+ }
+
+ if (callframe->argumentCount() < 1) {
+ pushImplOnce(thisObject, globalObject, JSMockImplementation::Kind::Promise, JSC::JSPromise::resolvedPromise(globalObject, jsUndefined()));
+ } else {
+ pushImplOnce(thisObject, globalObject, JSMockImplementation::Kind::Promise, JSC::JSPromise::resolvedPromise(globalObject, callframe->argument(0)));
+ }
+
+ RELEASE_AND_RETURN(scope, JSValue::encode(thisObject));
+}
+JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockRejectedValue, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe))
+{
+ JSMockFunction* thisObject = jsDynamicCast<JSMockFunction*>(callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict()));
+ auto& vm = globalObject->vm();
+ auto scope = DECLARE_THROW_SCOPE(vm);
+ if (UNLIKELY(!thisObject)) {
+ throwTypeError(globalObject, scope, "Expected Mock"_s);
+ }
+
+ if (callframe->argumentCount() < 1) {
+ pushImpl(thisObject, globalObject, JSMockImplementation::Kind::Promise, JSC::JSPromise::rejectedPromise(globalObject, jsUndefined()));
+ } else {
+ pushImpl(thisObject, globalObject, JSMockImplementation::Kind::Promise, JSC::JSPromise::rejectedPromise(globalObject, callframe->argument(0)));
+ }
+
+ RELEASE_AND_RETURN(scope, JSValue::encode(thisObject));
+}
+JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockRejectedValueOnce, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe))
+{
+ JSMockFunction* thisObject = jsDynamicCast<JSMockFunction*>(callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict()));
+ auto& vm = globalObject->vm();
+ auto scope = DECLARE_THROW_SCOPE(vm);
+ if (UNLIKELY(!thisObject)) {
+ throwTypeError(globalObject, scope, "Expected Mock"_s);
+ }
+
+ if (callframe->argumentCount() < 1) {
+ pushImplOnce(thisObject, globalObject, JSMockImplementation::Kind::Promise, JSC::JSPromise::rejectedPromise(globalObject, jsUndefined()));
+ } else {
+ pushImplOnce(thisObject, globalObject, JSMockImplementation::Kind::Promise, JSC::JSPromise::resolvedPromise(globalObject, callframe->argument(0)));
+ }
+
+ RELEASE_AND_RETURN(scope, JSValue::encode(thisObject));
+}
+} \ No newline at end of file
diff --git a/src/bun.js/bindings/JSMockFunction.h b/src/bun.js/bindings/JSMockFunction.h
new file mode 100644
index 000000000..288ca2e89
--- /dev/null
+++ b/src/bun.js/bindings/JSMockFunction.h
@@ -0,0 +1,30 @@
+#pragma once
+
+#include "root.h"
+#include "JavaScriptCore/LazyProperty.h"
+#include "JavaScriptCore/Strong.h"
+
+namespace WebCore {
+}
+
+namespace Bun {
+
+using namespace JSC;
+using namespace WebCore;
+
+class JSMockFunction;
+
+class JSMockModule final {
+public:
+ LazyProperty<JSC::JSGlobalObject, Structure> mockFunctionStructure;
+ LazyProperty<JSC::JSGlobalObject, Structure> mockResultStructure;
+ LazyProperty<JSC::JSGlobalObject, Structure> mockImplementationStructure;
+ LazyProperty<JSC::JSGlobalObject, Structure> mockObjectStructure;
+ LazyProperty<JSC::JSGlobalObject, Structure> activeSpySetStructure;
+
+ static JSMockModule create(JSC::JSGlobalObject*);
+
+ JSC::Strong<Unknown> activeSpies;
+};
+
+} \ No newline at end of file
diff --git a/src/bun.js/bindings/ZigGeneratedClasses.cpp b/src/bun.js/bindings/ZigGeneratedClasses.cpp
index 86f0ab29d..41d9cc888 100644
--- a/src/bun.js/bindings/ZigGeneratedClasses.cpp
+++ b/src/bun.js/bindings/ZigGeneratedClasses.cpp
@@ -2764,6 +2764,9 @@ JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toEndWithCallback);
extern "C" EncodedJSValue ExpectPrototype__toEqual(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame);
JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toEqualCallback);
+extern "C" EncodedJSValue ExpectPrototype__toHaveBeenCalled(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame);
+JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toHaveBeenCalledCallback);
+
extern "C" EncodedJSValue ExpectPrototype__toHaveBeenCalledTimes(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame);
JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toHaveBeenCalledTimesCallback);
@@ -2864,6 +2867,7 @@ static const HashTableValue JSExpectPrototypeTableValues[] = {
{ "toContainEqual"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toContainEqualCallback, 1 } },
{ "toEndWith"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toEndWithCallback, 1 } },
{ "toEqual"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toEqualCallback, 1 } },
+ { "toHaveBeenCalled"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toHaveBeenCalledCallback, 0 } },
{ "toHaveBeenCalledTimes"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toHaveBeenCalledTimesCallback, 1 } },
{ "toHaveBeenCalledWith"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toHaveBeenCalledWithCallback, 1 } },
{ "toHaveBeenLastCalledWith"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toHaveBeenLastCalledWithCallback, 1 } },
@@ -3854,6 +3858,33 @@ JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toEqualCallback, (JSGlobalObject * lex
return ExpectPrototype__toEqual(thisObject->wrapped(), lexicalGlobalObject, callFrame);
}
+JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toHaveBeenCalledCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame))
+{
+ auto& vm = lexicalGlobalObject->vm();
+
+ JSExpect* thisObject = jsDynamicCast<JSExpect*>(callFrame->thisValue());
+
+ if (UNLIKELY(!thisObject)) {
+ auto throwScope = DECLARE_THROW_SCOPE(vm);
+ return throwVMTypeError(lexicalGlobalObject, throwScope);
+ }
+
+ JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject);
+
+#ifdef BUN_DEBUG
+ /** View the file name of the JS file that called this function
+ * from a debugger */
+ SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm);
+ const char* fileName = sourceOrigin.string().utf8().data();
+ static const char* lastFileName = nullptr;
+ if (lastFileName != fileName) {
+ lastFileName = fileName;
+ }
+#endif
+
+ return ExpectPrototype__toHaveBeenCalled(thisObject->wrapped(), lexicalGlobalObject, callFrame);
+}
+
JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toHaveBeenCalledTimesCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame))
{
auto& vm = lexicalGlobalObject->vm();
diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp
index 3e01e2bc7..f2d0e248e 100644
--- a/src/bun.js/bindings/ZigGlobalObject.cpp
+++ b/src/bun.js/bindings/ZigGlobalObject.cpp
@@ -504,7 +504,7 @@ GlobalObject::GlobalObject(JSC::VM& vm, JSC::Structure* structure)
, m_builtinInternalFunctions(vm)
{
-
+ mockModule = Bun::JSMockModule::create(this);
m_scriptExecutionContext = new WebCore::ScriptExecutionContext(&vm, this);
}
@@ -3888,6 +3888,12 @@ void GlobalObject::visitChildrenImpl(JSCell* cell, Visitor& visitor)
thisObject->m_cachedGlobalObjectStructure.visit(visitor);
thisObject->m_cachedGlobalProxyStructure.visit(visitor);
+ thisObject->mockModule.mockFunctionStructure.visit(visitor);
+ thisObject->mockModule.mockResultStructure.visit(visitor);
+ thisObject->mockModule.mockImplementationStructure.visit(visitor);
+ thisObject->mockModule.mockObjectStructure.visit(visitor);
+ thisObject->mockModule.activeSpySetStructure.visit(visitor);
+
for (auto& barrier : thisObject->m_thenables) {
visitor.append(barrier);
}
diff --git a/src/bun.js/bindings/ZigGlobalObject.h b/src/bun.js/bindings/ZigGlobalObject.h
index dbafa1a93..3cf88bf29 100644
--- a/src/bun.js/bindings/ZigGlobalObject.h
+++ b/src/bun.js/bindings/ZigGlobalObject.h
@@ -41,6 +41,7 @@ class DOMWrapperWorld;
#include "DOMConstructors.h"
#include "BunPlugin.h"
+#include "JSMockFunction.h"
namespace WebCore {
class SubtleCrypto;
@@ -405,6 +406,8 @@ public:
void* napiInstanceDataFinalizer = nullptr;
void* napiInstanceDataFinalizerHint = nullptr;
+ Bun::JSMockModule mockModule;
+
#include "ZigGeneratedClasses+lazyStructureHeader.h"
private:
diff --git a/src/bun.js/bindings/generated_classes.zig b/src/bun.js/bindings/generated_classes.zig
index d24c8c81f..a4bbd2cab 100644
--- a/src/bun.js/bindings/generated_classes.zig
+++ b/src/bun.js/bindings/generated_classes.zig
@@ -944,6 +944,8 @@ pub const JSExpect = struct {
@compileLog("Expected Expect.toEndWith to be a callback but received " ++ @typeName(@TypeOf(Expect.toEndWith)));
if (@TypeOf(Expect.toEqual) != CallbackType)
@compileLog("Expected Expect.toEqual to be a callback but received " ++ @typeName(@TypeOf(Expect.toEqual)));
+ if (@TypeOf(Expect.toHaveBeenCalled) != CallbackType)
+ @compileLog("Expected Expect.toHaveBeenCalled to be a callback but received " ++ @typeName(@TypeOf(Expect.toHaveBeenCalled)));
if (@TypeOf(Expect.toHaveBeenCalledTimes) != CallbackType)
@compileLog("Expected Expect.toHaveBeenCalledTimes to be a callback but received " ++ @typeName(@TypeOf(Expect.toHaveBeenCalledTimes)));
if (@TypeOf(Expect.toHaveBeenCalledWith) != CallbackType)
@@ -1069,6 +1071,7 @@ pub const JSExpect = struct {
@export(Expect.toContainEqual, .{ .name = "ExpectPrototype__toContainEqual" });
@export(Expect.toEndWith, .{ .name = "ExpectPrototype__toEndWith" });
@export(Expect.toEqual, .{ .name = "ExpectPrototype__toEqual" });
+ @export(Expect.toHaveBeenCalled, .{ .name = "ExpectPrototype__toHaveBeenCalled" });
@export(Expect.toHaveBeenCalledTimes, .{ .name = "ExpectPrototype__toHaveBeenCalledTimes" });
@export(Expect.toHaveBeenCalledWith, .{ .name = "ExpectPrototype__toHaveBeenCalledWith" });
@export(Expect.toHaveBeenLastCalledWith, .{ .name = "ExpectPrototype__toHaveBeenLastCalledWith" });
diff --git a/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h b/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h
index 626b512be..d595dc866 100644
--- a/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h
+++ b/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h
@@ -34,6 +34,9 @@ public:
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForBundlerPlugin;
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForNodeVMScript;
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForCommonJSModuleRecord;
+ std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForJSMockImplementation;
+ std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForJSMockFunction;
+
#include "ZigGeneratedClasses+DOMClientIsoSubspaces.h"
/* --- bun --- */
diff --git a/src/bun.js/bindings/webcore/DOMIsoSubspaces.h b/src/bun.js/bindings/webcore/DOMIsoSubspaces.h
index c5767702a..b4a5e9d55 100644
--- a/src/bun.js/bindings/webcore/DOMIsoSubspaces.h
+++ b/src/bun.js/bindings/webcore/DOMIsoSubspaces.h
@@ -34,6 +34,8 @@ public:
std::unique_ptr<IsoSubspace> m_subspaceForBundlerPlugin;
std::unique_ptr<IsoSubspace> m_subspaceForNodeVMScript;
std::unique_ptr<IsoSubspace> m_subspaceForCommonJSModuleRecord;
+ std::unique_ptr<IsoSubspace> m_subspaceForJSMockImplementation;
+ std::unique_ptr<IsoSubspace> m_subspaceForJSMockFunction;
#include "ZigGeneratedClasses+DOMIsoSubspaces.h"
/*-- BUN --*/
diff --git a/src/bun.js/test/jest.classes.ts b/src/bun.js/test/jest.classes.ts
index 8ed291ef5..de9f260d2 100644
--- a/src/bun.js/test/jest.classes.ts
+++ b/src/bun.js/test/jest.classes.ts
@@ -77,6 +77,10 @@ export default [
fn: "toBe",
length: 1,
},
+ toHaveBeenCalled: {
+ fn: "toHaveBeenCalled",
+ length: 0,
+ },
toHaveBeenCalledTimes: {
fn: "toHaveBeenCalledTimes",
length: 1,
diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig
index a1260ecb1..f2e832ff9 100644
--- a/src/bun.js/test/jest.zig
+++ b/src/bun.js/test/jest.zig
@@ -599,7 +599,7 @@ pub const Jest = struct {
pub fn Bun__Jest__createTestModuleObject(globalObject: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue {
JSC.markBinding(@src());
- const module = JSC.JSValue.createEmptyObject(globalObject, 7);
+ const module = JSC.JSValue.createEmptyObject(globalObject, 11);
const test_fn = JSC.NewFunction(globalObject, ZigString.static("test"), 2, TestScope.call, false);
module.put(
@@ -697,11 +697,31 @@ pub const Jest = struct {
Expect.getConstructor(globalObject),
);
+ const mock_object = JSMockFunction__createObject(globalObject);
+ const spyOn = JSC.NewFunction(globalObject, ZigString.static("spyOn"), 2, JSMock__spyOn, false);
+ const restoreAllMocks = JSC.NewFunction(globalObject, ZigString.static("restoreAllMocks"), 2, jsFunctionResetSpies, false);
+ module.put(
+ globalObject,
+ ZigString.static("mock"),
+ mock_object,
+ );
+
+ const jest = JSValue.createEmptyObject(globalObject, 3);
+ jest.put(globalObject, ZigString.static("fn"), mock_object);
+ jest.put(globalObject, ZigString.static("spyOn"), spyOn);
+ jest.put(globalObject, ZigString.static("restoreAllMocks"), restoreAllMocks);
+ module.put(globalObject, ZigString.static("jest"), jest);
+ module.put(globalObject, ZigString.static("spyOn"), spyOn);
+
return module;
}
+ extern fn JSMockFunction__createObject(*JSC.JSGlobalObject) JSC.JSValue;
+
extern fn Bun__Jest__testPreloadObject(*JSC.JSGlobalObject) JSC.JSValue;
extern fn Bun__Jest__testModuleObject(*JSC.JSGlobalObject) JSC.JSValue;
+ extern fn jsFunctionResetSpies(*JSC.JSGlobalObject, *JSC.CallFrame) JSC.JSValue;
+ extern fn JSMock__spyOn(*JSC.JSGlobalObject, *JSC.CallFrame) JSC.JSValue;
pub fn call(
_: void,
@@ -3757,13 +3777,13 @@ pub const Expect = struct {
const _arguments = callFrame.arguments(1);
const arguments: []const JSValue = _arguments.ptr[0.._arguments.len];
- if (arguments.len < 1) {
- globalObject.throwInvalidArguments("toMatch() requires 1 argument", .{});
+ if (this.scope.tests.items.len <= this.test_id) {
+ globalObject.throw("toMatch() must be called in a test", .{});
return .zero;
}
- if (this.scope.tests.items.len <= this.test_id) {
- globalObject.throw("toMatch() must be called in a test", .{});
+ if (arguments.len < 1) {
+ globalObject.throwInvalidArguments("toMatch() requires 1 argument", .{});
return .zero;
}
@@ -3821,7 +3841,110 @@ pub const Expect = struct {
return .zero;
}
- pub const toHaveBeenCalledTimes = notImplementedJSCFn;
+ pub fn toHaveBeenCalled(this: *Expect, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSC.JSValue {
+ const thisValue = callframe.this();
+ defer this.postMatch(globalObject);
+
+ const value: JSValue = JSC.Jest.Expect.capturedValueGetCached(thisValue) orelse {
+ globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{});
+ return .zero;
+ };
+
+ const calls = JSMockFunction__getCalls(value);
+ active_test_expectation_counter.actual += 1;
+
+ if (calls == .zero or !calls.jsType().isArray()) {
+ globalObject.throw("Expected value must be a mock function: {}", .{value});
+ return .zero;
+ }
+
+ var pass = calls.getLength(globalObject) > 0;
+
+ const not = this.op.contains(.not);
+ if (not) pass = !pass;
+ if (pass) return thisValue;
+
+ // handle failure
+ var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true };
+ if (not) {
+ const signature = comptime getSignature("toHaveBeenCalled", "<green>expected<r>", true);
+ const fmt = signature ++ "\n\nExpected: not <green>{any}<r>\n";
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{calls.toFmt(globalObject, &formatter)});
+ return .zero;
+ }
+ globalObject.throw(Output.prettyFmt(fmt, false), .{calls.toFmt(globalObject, &formatter)});
+ return .zero;
+ } else {
+ const signature = comptime getSignature("toHaveBeenCalled", "<green>expected<r>", true);
+ const fmt = signature ++ "\n\nExpected <green>{any}<r>\n";
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{calls.toFmt(globalObject, &formatter)});
+ return .zero;
+ }
+ globalObject.throw(Output.prettyFmt(fmt, false), .{calls.toFmt(globalObject, &formatter)});
+ return .zero;
+ }
+
+ unreachable;
+ }
+ pub fn toHaveBeenCalledTimes(this: *Expect, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSC.JSValue {
+ const thisValue = callframe.this();
+ const arguments_ = callframe.arguments(1);
+ const arguments: []const JSValue = arguments_.ptr[0..arguments_.len];
+ defer this.postMatch(globalObject);
+ const value: JSValue = JSC.Jest.Expect.capturedValueGetCached(thisValue) orelse {
+ globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{});
+ return .zero;
+ };
+
+ active_test_expectation_counter.actual += 1;
+
+ const calls = JSMockFunction__getCalls(value);
+
+ if (calls == .zero or !calls.jsType().isArray()) {
+ globalObject.throw("Expected value must be a mock function: {}", .{value});
+ return .zero;
+ }
+
+ if (arguments.len < 1 or !arguments[0].isAnyInt()) {
+ globalObject.throwInvalidArguments("toHaveBeenCalledTimes() requires 1 integer argument", .{});
+ return .zero;
+ }
+
+ const times = arguments[0].coerce(i32, globalObject);
+
+ var pass = @intCast(i32, calls.getLength(globalObject)) == times;
+
+ const not = this.op.contains(.not);
+ if (not) pass = !pass;
+ if (pass) return thisValue;
+
+ // handle failure
+ var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true };
+ if (not) {
+ const signature = comptime getSignature("toHaveBeenCalled", "<green>expected<r>", true);
+ const fmt = signature ++ "\n\nExpected: not <green>{any}<r>\n";
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{calls.toFmt(globalObject, &formatter)});
+ return .zero;
+ }
+ globalObject.throw(Output.prettyFmt(fmt, false), .{calls.toFmt(globalObject, &formatter)});
+ return .zero;
+ } else {
+ const signature = comptime getSignature("toHaveBeenCalled", "<green>expected<r>", true);
+ const fmt = signature ++ "\n\nExpected <green>{any}<r>\n";
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{calls.toFmt(globalObject, &formatter)});
+ return .zero;
+ }
+ globalObject.throw(Output.prettyFmt(fmt, false), .{calls.toFmt(globalObject, &formatter)});
+ return .zero;
+ }
+
+ unreachable;
+ }
+
pub const toHaveBeenCalledWith = notImplementedJSCFn;
pub const toHaveBeenLastCalledWith = notImplementedJSCFn;
pub const toHaveBeenNthCalledWith = notImplementedJSCFn;
@@ -5007,3 +5130,11 @@ pub fn printGithubAnnotation(exception: *JSC.ZigException) void {
Output.printError("\n", .{});
Output.flush();
}
+
+/// JSValue.zero is used to indicate it was not a JSMockFunction
+/// If there were no calls, it returns an empty JSArray*
+extern fn JSMockFunction__getCalls(JSValue) JSValue;
+
+/// JSValue.zero is used to indicate it was not a JSMockFunction
+/// If there were no calls, it returns an empty JSArray*
+extern fn JSMockFunction__getReturns(JSValue) JSValue;
diff --git a/src/js_parser.zig b/src/js_parser.zig
index 5f46507d7..5dc69f469 100644
--- a/src/js_parser.zig
+++ b/src/js_parser.zig
@@ -4674,6 +4674,7 @@ const Jest = struct {
afterEach: Ref = Ref.None,
beforeAll: Ref = Ref.None,
afterAll: Ref = Ref.None,
+ jest: Ref = Ref.None,
};
// workaround for https://github.com/ziglang/zig/issues/10903
@@ -6474,6 +6475,7 @@ fn NewParser_(
if (p.options.features.inject_jest_globals) {
p.jest.describe = try p.declareCommonJSSymbol(.unbound, "describe");
p.jest.@"test" = try p.declareCommonJSSymbol(.unbound, "test");
+ p.jest.jest = try p.declareCommonJSSymbol(.unbound, "jest");
p.jest.it = try p.declareCommonJSSymbol(.unbound, "it");
p.jest.expect = try p.declareCommonJSSymbol(.unbound, "expect");
p.jest.beforeEach = try p.declareCommonJSSymbol(.unbound, "beforeEach");
diff --git a/test/js/bun/test/mock-test.test.ts b/test/js/bun/test/mock-test.test.ts
new file mode 100644
index 000000000..16e14784f
--- /dev/null
+++ b/test/js/bun/test/mock-test.test.ts
@@ -0,0 +1,131 @@
+import { test, mock, expect, spyOn, jest } from "bun:test";
+
+test("are callable", () => {
+ const fn = mock(() => 42);
+ expect(fn()).toBe(42);
+ expect(fn).toHaveBeenCalled();
+ expect(fn).toHaveBeenCalledTimes(1);
+ expect(fn.mock.calls).toHaveLength(1);
+ expect(fn.mock.calls[0]).toBeEmpty();
+
+ expect(fn()).toBe(42);
+ expect(fn).toHaveBeenCalledTimes(2);
+
+ expect(fn.mock.calls).toHaveLength(2);
+ expect(fn.mock.calls[1]).toBeEmpty();
+});
+
+test("include arguments", () => {
+ const fn = mock(f => f);
+ expect(fn(43)).toBe(43);
+ expect(fn.mock.results[0]).toEqual({
+ type: "return",
+ value: 43,
+ });
+ expect(fn.mock.calls[0]).toEqual([43]);
+});
+
+test("works when throwing", () => {
+ const instance = new Error("foo");
+ const fn = mock(f => {
+ throw instance;
+ });
+ expect(() => fn(43)).toThrow("foo");
+ expect(fn.mock.results[0]).toEqual({
+ type: "throw",
+ value: instance,
+ });
+ expect(fn.mock.calls[0]).toEqual([43]);
+});
+
+test("mockReset works", () => {
+ const instance = new Error("foo");
+ const fn = mock(f => {
+ throw instance;
+ });
+ expect(() => fn(43)).toThrow("foo");
+ expect(fn.mock.results[0]).toEqual({
+ type: "throw",
+ value: instance,
+ });
+ expect(fn.mock.calls[0]).toEqual([43]);
+
+ fn.mockReset();
+
+ expect(fn.mock.calls).toBeEmpty();
+ expect(fn.mock.results).toBeEmpty();
+ expect(fn.mock.instances).toBeEmpty();
+ expect(fn).not.toHaveBeenCalled();
+
+ expect(() => fn(43)).toThrow("foo");
+ expect(fn.mock.results[0]).toEqual({
+ type: "throw",
+ value: instance,
+ });
+ expect(fn.mock.calls[0]).toEqual([43]);
+});
+
+test("spyOn works on functions", () => {
+ var obj = {
+ original() {
+ return 42;
+ },
+ };
+ const fn = spyOn(obj, "original");
+ expect(fn).not.toHaveBeenCalled();
+ expect(obj.original()).toBe(42);
+ expect(fn).toHaveBeenCalled();
+ expect(fn).toHaveBeenCalledTimes(1);
+ expect(fn.mock.calls).toHaveLength(1);
+ expect(fn.mock.calls[0]).toBeEmpty();
+ jest.restoreAllMocks();
+ expect(() => expect(obj.original).toHaveBeenCalled()).toThrow();
+});
+
+test("spyOn works on object", () => {
+ var obj = { original: 42 };
+ obj.original = 42;
+ const fn = spyOn(obj, "original");
+ expect(fn).not.toHaveBeenCalled();
+ expect(obj.original).toBe(42);
+ expect(fn).toHaveBeenCalled();
+ expect(fn).toHaveBeenCalledTimes(1);
+ expect(fn.mock.calls).toHaveLength(1);
+ expect(fn.mock.calls[0]).toBeEmpty();
+ jest.restoreAllMocks();
+ expect(() => expect(obj.original).toHaveBeenCalled()).toThrow();
+});
+
+test("spyOn on object doens't crash if object GC'd", () => {
+ const spies = new Array(1000);
+ (() => {
+ for (let i = 0; i < 1000; i++) {
+ var obj = { original: 42 };
+ obj.original = 42;
+ const fn = spyOn(obj, "original");
+ spies[i] = fn;
+ }
+ Bun.gc(true);
+ })();
+ Bun.gc(true);
+
+ jest.restoreAllMocks();
+});
+
+test("spyOn works on globalThis", () => {
+ var obj = globalThis;
+ obj.original = 42;
+ const fn = spyOn(obj, "original");
+ expect(fn).not.toHaveBeenCalled();
+ expect(obj.original).toBe(42);
+ expect(fn).toHaveBeenCalled();
+ expect(fn).toHaveBeenCalledTimes(1);
+ expect(fn.mock.calls).toHaveLength(1);
+ expect(fn.mock.calls[0]).toBeEmpty();
+ jest.restoreAllMocks();
+ expect(() => expect(obj.original).toHaveBeenCalled()).toThrow();
+ obj.original;
+ expect(fn).not.toHaveBeenCalled();
+});
+
+// spyOn does not work with getters/setters yet.