diff options
author | 2023-06-09 16:38:06 -0700 | |
---|---|---|
committer | 2023-06-09 16:38:06 -0700 | |
commit | bf518222d456c913fc5e6b6e0d14952d76c0ce91 (patch) | |
tree | 99431a14fefb1b52b75ea0528b68232e8af2cb7c | |
parent | 6565bd89d533b17d0d975f2790a4b4d37d8aecc1 (diff) | |
download | bun-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.ts | 43 | ||||
-rw-r--r-- | src/bun.js/bindings/JSMockFunction.cpp | 1154 | ||||
-rw-r--r-- | src/bun.js/bindings/JSMockFunction.h | 30 | ||||
-rw-r--r-- | src/bun.js/bindings/ZigGeneratedClasses.cpp | 31 | ||||
-rw-r--r-- | src/bun.js/bindings/ZigGlobalObject.cpp | 8 | ||||
-rw-r--r-- | src/bun.js/bindings/ZigGlobalObject.h | 3 | ||||
-rw-r--r-- | src/bun.js/bindings/generated_classes.zig | 3 | ||||
-rw-r--r-- | src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h | 3 | ||||
-rw-r--r-- | src/bun.js/bindings/webcore/DOMIsoSubspaces.h | 2 | ||||
-rw-r--r-- | src/bun.js/test/jest.classes.ts | 4 | ||||
-rw-r--r-- | src/bun.js/test/jest.zig | 143 | ||||
-rw-r--r-- | src/js_parser.zig | 2 | ||||
-rw-r--r-- | test/js/bun/test/mock-test.test.ts | 131 |
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. |