aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/bun-types/bun-test.d.ts41
-rw-r--r--src/bun.js/bindings/JSMockFunction.cpp109
-rw-r--r--test/js/bun/test/mock-test.test.ts93
3 files changed, 216 insertions, 27 deletions
diff --git a/packages/bun-types/bun-test.d.ts b/packages/bun-types/bun-test.d.ts
index 8c429d6fc..bf89b808d 100644
--- a/packages/bun-types/bun-test.d.ts
+++ b/packages/bun-types/bun-test.d.ts
@@ -27,6 +27,38 @@ declare module "bun:test" {
export const mock: {
<T extends AnyFunction>(Function: T): Mock<T>;
+
+ mockClear(): typeof mock;
+ mockReset(): typeof mock;
+ mockRestore(): void;
+ mockReturnValue<T extends JestMock.FunctionLike = JestMock.UnknownFunction>(
+ value: ReturnType<T>,
+ ): JestMock.MockInstance<T>;
+ mockReturnValueOnce<
+ T extends JestMock.FunctionLike = JestMock.UnknownFunction,
+ >(
+ value: ReturnType<T>,
+ ): JestMock.MockInstance<T>;
+ mockResolvedValue<
+ T extends JestMock.FunctionLike = JestMock.UnknownFunction,
+ >(
+ value: JestMock.ResolveType<T>,
+ ): JestMock.MockInstance<T>;
+ mockResolvedValueOnce<
+ T extends JestMock.FunctionLike = JestMock.UnknownFunction,
+ >(
+ value: JestMock.ResolveType<T>,
+ ): JestMock.MockInstance<T>;
+ mockRejectedValue<
+ T extends JestMock.FunctionLike = JestMock.UnknownFunction,
+ >(
+ value: JestMock.RejectType<T>,
+ ): JestMock.MockInstance<T>;
+ mockRejectedValueOnce<
+ T extends JestMock.FunctionLike = JestMock.UnknownFunction,
+ >(
+ value: JestMock.RejectType<T>,
+ ): JestMock.MockInstance<T>;
};
interface Jest {
@@ -1193,7 +1225,7 @@ declare namespace JestMock {
ConstructorLikeKeys<T> | MethodLikeKeys<T>
>;
- type RejectType<T extends FunctionLike> =
+ export type RejectType<T extends FunctionLike> =
ReturnType<T> extends PromiseLike<any> ? unknown : never;
export interface Replaced<T = unknown> {
@@ -1227,11 +1259,8 @@ declare namespace JestMock {
value: V,
) => Replaced<T[K_2]>;
- type ResolveType<T extends FunctionLike> = ReturnType<T> extends PromiseLike<
- infer U
- >
- ? U
- : never;
+ export type ResolveType<T extends FunctionLike> =
+ ReturnType<T> extends PromiseLike<infer U> ? U : never;
export type Spied<T extends ClassLike | FunctionLike> = T extends ClassLike
? SpiedClass<T>
diff --git a/src/bun.js/bindings/JSMockFunction.cpp b/src/bun.js/bindings/JSMockFunction.cpp
index eda0837c9..fcd80102b 100644
--- a/src/bun.js/bindings/JSMockFunction.cpp
+++ b/src/bun.js/bindings/JSMockFunction.cpp
@@ -18,6 +18,7 @@
#include <JavaScriptCore/GetterSetter.h>
#include <JavaScriptCore/WeakMapImpl.h>
#include <JavaScriptCore/WeakMapImplInlines.h>
+#include <JavaScriptCore/FunctionPrototype.h>
namespace Bun {
@@ -207,6 +208,12 @@ public:
JSC::Identifier spyIdentifier;
unsigned spyAttributes = 0;
+ void setName(const WTF::String& name)
+ {
+ auto& vm = this->vm();
+ this->putDirect(vm, vm.propertyNames->name, jsString(vm, name), JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::ReadOnly);
+ }
+
void initMock()
{
mock.initLater(
@@ -330,8 +337,8 @@ static void pushImplInternal(JSMockFunction* fn, JSGlobalObject* jsGlobalObject,
{
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 currentTail = fn->tail.get();
JSValue currentImpl = fn->implementation.get();
if (currentTail) {
if (auto* current = jsDynamicCast<JSMockImplementation*>(currentTail)) {
@@ -501,6 +508,21 @@ extern "C" EncodedJSValue JSMock__spyOn(JSC::JSGlobalObject* lexicalGlobalObject
if (hasValue)
attributes = slot.attributes();
+ {
+ auto catcher = DECLARE_CATCH_SCOPE(vm);
+ WTF::String nameToUse;
+ if (auto* fn = jsDynamicCast<JSFunction*>(value)) {
+ nameToUse = fn->name(vm);
+ } else if (auto* fn = jsDynamicCast<InternalFunction*>(value)) {
+ nameToUse = fn->name();
+ }
+ if (nameToUse.length()) {
+ mock->setName(nameToUse);
+ }
+ if (catcher.exception())
+ catcher.clearException();
+ }
+
attributes |= PropertyAttribute::Function;
object->putDirect(vm, propertyKey, mock, attributes);
RETURN_IF_EXCEPTION(scope, {});
@@ -538,7 +560,7 @@ 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()));
+ auto* prototype = JSMockFunctionPrototype::create(init.vm, init.owner, JSMockFunctionPrototype::createStructure(init.vm, init.owner, init.owner->functionPrototype()));
init.set(JSMockFunction::createStructure(init.vm, init.owner, prototype));
});
@@ -842,6 +864,15 @@ JSC_DEFINE_CUSTOM_GETTER(jsMockFunctionGetter_protoImpl, (JSC::JSGlobalObject *
return JSValue::encode(jsUndefined());
}
+#define HANDLE_STATIC_CALL \
+ if (!thisObject->implementation.get()) { \
+ thisObject = JSMockFunction::create( \
+ vm, \
+ globalObject, \
+ reinterpret_cast<Zig::GlobalObject*>(globalObject)->mockModule.mockFunctionStructure.getInitializedOnMainThread(globalObject), \
+ CallbackKind::Call); \
+ }
+
extern "C" EncodedJSValue JSMockFunction__createObject(Zig::GlobalObject* globalObject)
{
return JSValue::encode(
@@ -951,11 +982,25 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockImplementation, (JSC::JSGlobalObject
}
if (callframe->argumentCount() > 0) {
- JSValue arg = callframe->argument(0);
- if (arg.isCallable()) {
- pushImpl(thisObject, globalObject, JSMockImplementation::Kind::Call, arg);
+ JSValue value = callframe->argument(0);
+ if (value.isCallable()) {
+ {
+ auto catcher = DECLARE_CATCH_SCOPE(vm);
+ WTF::String nameToUse;
+ if (auto* fn = jsDynamicCast<JSFunction*>(value)) {
+ nameToUse = fn->name(vm);
+ } else if (auto* fn = jsDynamicCast<InternalFunction*>(value)) {
+ nameToUse = fn->name();
+ }
+ if (nameToUse.length()) {
+ thisObject->setName(nameToUse);
+ }
+ if (catcher.exception())
+ catcher.clearException();
+ }
+ pushImpl(thisObject, globalObject, JSMockImplementation::Kind::Call, value);
} else {
- pushImpl(thisObject, globalObject, JSMockImplementation::Kind::ReturnValue, arg);
+ pushImpl(thisObject, globalObject, JSMockImplementation::Kind::ReturnValue, value);
}
}
@@ -969,24 +1014,33 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockImplementationOnce, (JSC::JSGlobalObj
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 {};
}
+ HANDLE_STATIC_CALL;
+
if (callframe->argumentCount() > 0) {
- JSValue arg = callframe->argument(0);
- if (arg.isCallable()) {
- pushImpl(thisObject, globalObject, JSMockImplementation::Kind::Call, arg);
+ JSValue value = callframe->argument(0);
+ if (value.isCallable()) {
+ if (!thisObject->implementation) {
+ auto catcher = DECLARE_CATCH_SCOPE(vm);
+ WTF::String nameToUse;
+ if (auto* fn = jsDynamicCast<JSFunction*>(value)) {
+ nameToUse = fn->name(vm);
+ } else if (auto* fn = jsDynamicCast<InternalFunction*>(value)) {
+ nameToUse = fn->name();
+ }
+ if (nameToUse.length()) {
+ thisObject->setName(nameToUse);
+ }
+ if (catcher.exception())
+ catcher.clearException();
+ }
+
+ pushImpl(thisObject, globalObject, JSMockImplementation::Kind::Call, value);
} else {
- pushImpl(thisObject, globalObject, JSMockImplementation::Kind::ReturnValue, arg);
+ pushImpl(thisObject, globalObject, JSMockImplementation::Kind::ReturnValue, value);
}
}
@@ -1003,6 +1057,8 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionWithImplementation, (JSC::JSGlobalObject
RELEASE_AND_RETURN(scope, JSValue::encode(jsUndefined()));
}
+ HANDLE_STATIC_CALL;
+
JSValue arg = callframe->argument(0);
if (callframe->argumentCount() < 1 || arg.isEmpty() || arg.isUndefined()) {
@@ -1025,6 +1081,9 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockName, (JSC::JSGlobalObject * globalOb
throwTypeError(globalObject, scope, "Expected Mock"_s);
return {};
}
+
+ HANDLE_STATIC_CALL;
+
if (callframe->argumentCount() > 0) {
auto* newName = callframe->argument(0).toStringOrNull(globalObject);
if (UNLIKELY(!newName)) {
@@ -1046,6 +1105,7 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockReturnThis, (JSC::JSGlobalObject * gl
throwTypeError(globalObject, scope, "Expected Mock"_s);
}
+ HANDLE_STATIC_CALL;
pushImpl(thisObject, globalObject, JSMockImplementation::Kind::ReturnThis, jsUndefined());
RELEASE_AND_RETURN(scope, JSValue::encode(thisObject));
@@ -1076,6 +1136,7 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockReturnValueOnce, (JSC::JSGlobalObject
throwTypeError(globalObject, scope, "Expected Mock"_s);
}
+ HANDLE_STATIC_CALL;
if (callframe->argumentCount() < 1) {
pushImplOnce(thisObject, globalObject, JSMockImplementation::Kind::ReturnValue, jsUndefined());
} else {
@@ -1084,15 +1145,19 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockReturnValueOnce, (JSC::JSGlobalObject
RELEASE_AND_RETURN(scope, JSValue::encode(thisObject));
}
-JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockResolvedValue, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe))
+JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockResolvedValue, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callframe))
{
+ auto* globalObject = jsCast<Zig::GlobalObject*>(lexicalGlobalObject);
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);
}
+ HANDLE_STATIC_CALL;
+
if (callframe->argumentCount() < 1) {
pushImpl(thisObject, globalObject, JSMockImplementation::Kind::Promise, JSC::JSPromise::resolvedPromise(globalObject, jsUndefined()));
} else {
@@ -1110,6 +1175,8 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockResolvedValueOnce, (JSC::JSGlobalObje
throwTypeError(globalObject, scope, "Expected Mock"_s);
}
+ HANDLE_STATIC_CALL;
+
if (callframe->argumentCount() < 1) {
pushImplOnce(thisObject, globalObject, JSMockImplementation::Kind::Promise, JSC::JSPromise::resolvedPromise(globalObject, jsUndefined()));
} else {
@@ -1127,6 +1194,8 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockRejectedValue, (JSC::JSGlobalObject *
throwTypeError(globalObject, scope, "Expected Mock"_s);
}
+ HANDLE_STATIC_CALL;
+
if (callframe->argumentCount() < 1) {
pushImpl(thisObject, globalObject, JSMockImplementation::Kind::Promise, JSC::JSPromise::rejectedPromise(globalObject, jsUndefined()));
} else {
@@ -1144,6 +1213,8 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockRejectedValueOnce, (JSC::JSGlobalObje
throwTypeError(globalObject, scope, "Expected Mock"_s);
}
+ HANDLE_STATIC_CALL;
+
if (callframe->argumentCount() < 1) {
pushImplOnce(thisObject, globalObject, JSMockImplementation::Kind::Promise, JSC::JSPromise::rejectedPromise(globalObject, jsUndefined()));
} else {
diff --git a/test/js/bun/test/mock-test.test.ts b/test/js/bun/test/mock-test.test.ts
index e51d9fcc7..8c998942c 100644
--- a/test/js/bun/test/mock-test.test.ts
+++ b/test/js/bun/test/mock-test.test.ts
@@ -1,5 +1,19 @@
import { test, mock, expect, spyOn, jest } from "bun:test";
+test("mockResolvedValue", async () => {
+ const fn = mock.mockResolvedValueOnce(42).mockResolvedValue(43);
+ expect(await fn()).toBe(42);
+ expect(await fn()).toBe(43);
+ expect(await fn()).toBe(43);
+});
+
+test("mockRejectedValue", async () => {
+ const fn = mock.mockRejectedValue(42);
+ expect(await fn()).toBe(42);
+ fn.mockRejectedValue(43);
+ expect(await fn()).toBe(43);
+});
+
test("are callable", () => {
const fn = mock(() => 42);
expect(fn()).toBe(42);
@@ -15,6 +29,83 @@ test("are callable", () => {
expect(fn.mock.calls[1]).toBeEmpty();
});
+test(".call works", () => {
+ const fn = mock(function hey() {
+ // @ts-expect-error
+ return this;
+ });
+ expect(fn.call(123)).toBe(123);
+ expect(fn).toHaveBeenCalled();
+ expect(fn).toHaveBeenCalledTimes(1);
+ expect(fn.mock.calls).toHaveLength(1);
+ expect(fn.mock.calls[0]).toBeEmpty();
+
+ expect(fn()).toBe(undefined);
+ expect(fn).toHaveBeenCalledTimes(2);
+
+ expect(fn.mock.calls).toHaveLength(2);
+ expect(fn.mock.calls[1]).toBeEmpty();
+});
+
+test(".apply works", () => {
+ const fn = mock(function hey() {
+ // @ts-expect-error
+ return this;
+ });
+ expect(fn.apply(123)).toBe(123);
+ expect(fn).toHaveBeenCalled();
+ expect(fn).toHaveBeenCalledTimes(1);
+ expect(fn.mock.calls).toHaveLength(1);
+ expect(fn.mock.calls[0]).toBeEmpty();
+
+ expect(fn.apply(undefined)).toBe(undefined);
+ expect(fn).toHaveBeenCalledTimes(2);
+
+ expect(fn.mock.calls).toHaveLength(2);
+ expect(fn.mock.calls[1]).toBeEmpty();
+});
+
+test(".bind works", () => {
+ const fn = mock(function hey() {
+ // @ts-expect-error
+ return this;
+ });
+ expect(fn.bind(123)()).toBe(123);
+ expect(fn).toHaveBeenCalled();
+ expect(fn).toHaveBeenCalledTimes(1);
+ expect(fn.mock.calls).toHaveLength(1);
+ expect(fn.mock.calls[0]).toBeEmpty();
+
+ expect(fn.bind(undefined)()).toBe(undefined);
+ expect(fn).toHaveBeenCalledTimes(2);
+
+ expect(fn.mock.calls).toHaveLength(2);
+ expect(fn.mock.calls[1]).toBeEmpty();
+});
+
+test(".name works", () => {
+ const fn = mock(function hey() {
+ // @ts-expect-error
+ return this;
+ });
+ expect(fn.name).toBe("hey");
+});
+
+test(".name throwing doesnt segfault", () => {
+ function baddie() {
+ // @ts-expect-error
+ return this;
+ }
+ Object.defineProperty(baddie, "name", {
+ get() {
+ throw new Error("foo");
+ },
+ });
+
+ const fn = mock(baddie);
+ fn.name;
+});
+
test("include arguments", () => {
const fn = mock(f => f);
expect(fn(43)).toBe(43);
@@ -131,5 +222,3 @@ test("spyOn works on globalThis", () => {
obj.original;
expect(fn).not.toHaveBeenCalled();
});
-
-// spyOn does not work with getters/setters yet.