From 5fa13625a1ca0ea1a3a1c5bb86d0880dcfac349f Mon Sep 17 00:00:00 2001 From: Dylan Conway <35280289+dylan-conway@users.noreply.github.com> Date: Wed, 21 Jun 2023 23:38:18 -0700 Subject: upgrade zig to `v0.11.0-dev.3737+9eb008717` (#3374) * progress * finish `@memset/@memcpy` update * Update build.zig * change `@enumToInt` to `@intFromEnum` and friends * update zig versions * it was 1 * add link to issue * add `compileError` reminder * fix merge * format * upgrade to llvm 16 * Revert "upgrade to llvm 16" This reverts commit cc930ceb1c5b4db9614a7638596948f704544ab8. --------- Co-authored-by: Jarred Sumner Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> --- src/bun.js/test/jest.zig | 8 ++++---- src/bun.js/test/pretty_format.zig | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) (limited to 'src/bun.js/test') diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig index 00cc954ad..2dc4ae1c4 100644 --- a/src/bun.js/test/jest.zig +++ b/src/bun.js/test/jest.zig @@ -210,13 +210,13 @@ pub const TestRunner = struct { const start = @truncate(Test.ID, this.tests.len); this.tests.len += count; var statuses = this.tests.items(.status)[start..][0..count]; - std.mem.set(Test.Status, statuses, Test.Status.pending); + @memset(statuses, Test.Status.pending); this.callback.onUpdateCount(this.callback, count, count + start); return start; } pub fn getOrPutFile(this: *TestRunner, file_path: string) *DescribeScope { - var entry = this.index.getOrPut(this.allocator, @truncate(u32, std.hash.Wyhash.hash(0, file_path))) catch unreachable; + var entry = this.index.getOrPut(this.allocator, @truncate(u32, bun.hash(file_path))) catch unreachable; if (entry.found_existing) { return this.files.items(.module_scope)[entry.value_ptr.*]; } @@ -317,7 +317,7 @@ pub const Snapshots = struct { name_with_counter[name.len] = ' '; bun.copy(u8, name_with_counter[name.len + 1 ..], counter_string); - const name_hash = std.hash.Wyhash.hash(0, name_with_counter); + const name_hash = bun.hash(name_with_counter); if (this.values.get(name_hash)) |expected| { return expected; } @@ -416,7 +416,7 @@ pub const Snapshots = struct { } const value_clone = try this.allocator.alloc(u8, value.len); bun.copy(u8, value_clone, value); - const name_hash = std.hash.Wyhash.hash(0, key); + const name_hash = bun.hash(key); try this.values.put(name_hash, value_clone); } } diff --git a/src/bun.js/test/pretty_format.zig b/src/bun.js/test/pretty_format.zig index 15ab88799..29e330a04 100644 --- a/src/bun.js/test/pretty_format.zig +++ b/src/bun.js/test/pretty_format.zig @@ -362,7 +362,7 @@ pub const JestPrettyFormat = struct { }; pub fn get(value: JSValue, globalThis: *JSGlobalObject) Result { - switch (@enumToInt(value)) { + switch (@intFromEnum(value)) { 0, 0xa => return Result{ .tag = .Undefined, }, @@ -921,7 +921,7 @@ pub const JestPrettyFormat = struct { this.map = this.map_node.?.data; } - var entry = this.map.getOrPut(@enumToInt(value)) catch unreachable; + var entry = this.map.getOrPut(@intFromEnum(value)) catch unreachable; if (entry.found_existing) { writer.writeAll(comptime Output.prettyFmt("[Circular]", enable_ansi_colors)); return; @@ -930,7 +930,7 @@ pub const JestPrettyFormat = struct { defer { if (comptime Format.canHaveCircularReferences()) { - _ = this.map.remove(@enumToInt(value)); + _ = this.map.remove(@intFromEnum(value)); } } @@ -1049,7 +1049,7 @@ pub const JestPrettyFormat = struct { i = -i; } const digits = if (i != 0) - bun.fmt.fastDigitCount(@intCast(usize, i)) + @as(usize, @boolToInt(is_negative)) + bun.fmt.fastDigitCount(@intCast(usize, i)) + @as(usize, @intFromBool(is_negative)) else 1; this.addForNewLine(digits); @@ -1553,7 +1553,7 @@ pub const JestPrettyFormat = struct { { this.indent += 1; defer this.indent -|= 1; - const count_without_children = props_iter.len - @as(usize, @boolToInt(children_prop != null)); + const count_without_children = props_iter.len - @as(usize, @intFromBool(children_prop != null)); while (props_iter.next()) |prop| { if (prop.eqlComptime("children")) -- cgit v1.2.3 From 217501e180eadd1999f30733e0f13580cd1f0abf Mon Sep 17 00:00:00 2001 From: Ashcon Partovi Date: Thu, 22 Jun 2023 22:27:00 -0700 Subject: `expect().resolves` and `expect().rejects` (#3318) * Move expect and snapshots to their own files * expect().resolves and expect().rejects * Fix promise being added to unhandled rejection list * Handle timeouts in expect() * wip merge * Fix merge issue --------- Co-authored-by: Jarred Sumner Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> --- packages/bun-types/bun-test.d.ts | 14 + src/bun.js/bindings/bindings.cpp | 12 + src/bun.js/bindings/bindings.zig | 13 + src/bun.js/bindings/exports.zig | 14 +- src/bun.js/bindings/generated_classes_list.zig | 10 +- src/bun.js/bindings/headers.h | 2 + src/bun.js/bindings/headers.zig | 2 + src/bun.js/event_loop.zig | 24 + src/bun.js/javascript.zig | 5 + src/bun.js/test/expect.zig | 3335 ++++++++++++++++++ src/bun.js/test/jest.zig | 4264 ++---------------------- src/bun.js/test/pretty_format.zig | 15 +- src/bun.js/test/snapshot.zig | 284 ++ src/cli/test_command.zig | 2 +- src/jsc.zig | 2 + test/cli/test/bun-test.test.ts | 22 +- 16 files changed, 3970 insertions(+), 4050 deletions(-) create mode 100644 src/bun.js/test/expect.zig create mode 100644 src/bun.js/test/snapshot.zig (limited to 'src/bun.js/test') diff --git a/packages/bun-types/bun-test.d.ts b/packages/bun-types/bun-test.d.ts index ba59966ad..156585766 100644 --- a/packages/bun-types/bun-test.d.ts +++ b/packages/bun-types/bun-test.d.ts @@ -424,6 +424,20 @@ declare module "bun:test" { * expect(null).not.toBeNull(); */ not: Expect; + /** + * Expects the value to be a promise that resolves. + * + * @example + * expect(Promise.resolve(1)).resolves.toBe(1); + */ + resolves: Expect; + /** + * Expects the value to be a promise that rejects. + * + * @example + * expect(Promise.reject("error")).rejects.toBe("error"); + */ + rejects: Expect; /** * Asserts that a value equals what is expected. * diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 4eee81f4d..2c2f5c2ea 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -2593,6 +2593,12 @@ bool JSC__JSPromise__isHandled(const JSC__JSPromise* arg0, JSC__VM* arg1) { return arg0->isHandled(reinterpret_cast(arg1)); } +void JSC__JSPromise__setHandled(JSC__JSPromise* promise, JSC__VM* arg1) +{ + auto& vm = *arg1; + auto flags = promise->internalField(JSC::JSPromise::Field::Flags).get().asUInt32(); + promise->internalField(JSC::JSPromise::Field::Flags).set(vm, promise, jsNumber(flags | JSC::JSPromise::isHandledFlag)); +} #pragma mark - JSC::JSInternalPromise @@ -2666,6 +2672,12 @@ bool JSC__JSInternalPromise__isHandled(const JSC__JSInternalPromise* arg0, JSC__ { return arg0->isHandled(reinterpret_cast(arg1)); } +void JSC__JSInternalPromise__setHandled(JSC__JSInternalPromise* promise, JSC__VM* arg1) +{ + auto& vm = *arg1; + auto flags = promise->internalField(JSC::JSPromise::Field::Flags).get().asUInt32(); + promise->internalField(JSC::JSPromise::Field::Flags).set(vm, promise, jsNumber(flags | JSC::JSPromise::isHandledFlag)); +} #pragma mark - JSC::JSGlobalObject diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 581fc6f85..00833c71d 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -2045,6 +2045,9 @@ pub const JSPromise = extern struct { pub fn isHandled(this: *const JSPromise, vm: *VM) bool { return cppFn("isHandled", .{ this, vm }); } + pub fn setHandled(this: *JSPromise, vm: *VM) void { + cppFn("setHandled", .{ this, vm }); + } pub fn rejectWithCaughtException(this: *JSPromise, globalObject: *JSGlobalObject, scope: ThrowScope) void { return cppFn("rejectWithCaughtException", .{ this, globalObject, scope }); @@ -2115,6 +2118,7 @@ pub const JSPromise = extern struct { "asValue", "create", "isHandled", + "setHandled", "reject", "rejectAsHandled", "rejectAsHandledException", @@ -2149,6 +2153,9 @@ pub const JSInternalPromise = extern struct { pub fn isHandled(this: *const JSInternalPromise, vm: *VM) bool { return cppFn("isHandled", .{ this, vm }); } + pub fn setHandled(this: *JSInternalPromise, vm: *VM) void { + cppFn("setHandled", .{ this, vm }); + } pub fn rejectWithCaughtException(this: *JSInternalPromise, globalObject: *JSGlobalObject, scope: ThrowScope) void { return cppFn("rejectWithCaughtException", .{ this, globalObject, scope }); @@ -2332,6 +2339,7 @@ pub const JSInternalPromise = extern struct { "status", "result", "isHandled", + "setHandled", "resolvedPromise", "rejectedPromise", "resolve", @@ -2363,6 +2371,11 @@ pub const AnyPromise = union(enum) { inline else => |promise| promise.isHandled(vm), }; } + pub fn setHandled(this: AnyPromise, vm: *VM) void { + switch (this) { + inline else => |promise| promise.setHandled(vm), + } + } pub fn rejectWithCaughtException(this: AnyPromise, globalObject: *JSGlobalObject, scope: ThrowScope) void { switch (this) { diff --git a/src/bun.js/bindings/exports.zig b/src/bun.js/bindings/exports.zig index de3f819b7..f77b57216 100644 --- a/src/bun.js/bindings/exports.zig +++ b/src/bun.js/bindings/exports.zig @@ -2220,11 +2220,11 @@ pub const ZigConsoleClient = struct { } else if (value.as(JSC.ResolveMessage)) |resolve_log| { resolve_log.msg.writeFormat(writer_, enable_ansi_colors) catch {}; return; - } else if (value.as(JSC.Jest.ExpectAnything) != null) { + } else if (value.as(JSC.Expect.ExpectAnything) != null) { writer.writeAll("Anything"); return; - } else if (value.as(JSC.Jest.ExpectAny) != null) { - const constructor_value = JSC.Jest.ExpectAny.constructorValueGetCached(value) orelse return; + } else if (value.as(JSC.Expect.ExpectAny) != null) { + const constructor_value = JSC.Expect.ExpectAny.constructorValueGetCached(value) orelse return; this.addForNewLine("Any<".len); writer.writeAll("Any<"); @@ -2237,16 +2237,16 @@ pub const ZigConsoleClient = struct { writer.writeAll(">"); return; - } else if (value.as(JSC.Jest.ExpectStringContaining) != null) { - const substring_value = JSC.Jest.ExpectStringContaining.stringValueGetCached(value) orelse return; + } else if (value.as(JSC.Expect.ExpectStringContaining) != null) { + const substring_value = JSC.Expect.ExpectStringContaining.stringValueGetCached(value) orelse return; this.addForNewLine("StringContaining ".len); writer.writeAll("StringContaining "); this.printAs(.String, Writer, writer_, substring_value, .String, enable_ansi_colors); return; - } else if (value.as(JSC.Jest.ExpectStringMatching) != null) { - const test_value = JSC.Jest.ExpectStringMatching.testValueGetCached(value) orelse return; + } else if (value.as(JSC.Expect.ExpectStringMatching) != null) { + const test_value = JSC.Expect.ExpectStringMatching.testValueGetCached(value) orelse return; this.addForNewLine("StringMatching ".len); writer.writeAll("StringMatching "); diff --git a/src/bun.js/bindings/generated_classes_list.zig b/src/bun.js/bindings/generated_classes_list.zig index d5d987dce..c54965093 100644 --- a/src/bun.js/bindings/generated_classes_list.zig +++ b/src/bun.js/bindings/generated_classes_list.zig @@ -4,11 +4,11 @@ pub const Classes = struct { pub const Blob = JSC.WebCore.Blob; pub const CryptoHasher = JSC.API.Bun.Crypto.CryptoHasher; pub const Dirent = JSC.Node.Dirent; - pub const Expect = JSC.Jest.Expect; - pub const ExpectAny = JSC.Jest.ExpectAny; - pub const ExpectAnything = JSC.Jest.ExpectAnything; - pub const ExpectStringContaining = JSC.Jest.ExpectStringContaining; - pub const ExpectStringMatching = JSC.Jest.ExpectStringMatching; + pub const Expect = JSC.Expect.Expect; + pub const ExpectAny = JSC.Expect.ExpectAny; + pub const ExpectAnything = JSC.Expect.ExpectAnything; + pub const ExpectStringContaining = JSC.Expect.ExpectStringContaining; + pub const ExpectStringMatching = JSC.Expect.ExpectStringMatching; pub const FileSystemRouter = JSC.API.FileSystemRouter; pub const Bundler = JSC.API.JSBundler; pub const JSBundler = Bundler; diff --git a/src/bun.js/bindings/headers.h b/src/bun.js/bindings/headers.h index cdf7e05f4..f507121f8 100644 --- a/src/bun.js/bindings/headers.h +++ b/src/bun.js/bindings/headers.h @@ -253,6 +253,7 @@ CPP_DECL JSC__JSPromise* JSC__JSPromise__resolvedPromise(JSC__JSGlobalObject* ar CPP_DECL JSC__JSValue JSC__JSPromise__resolvedPromiseValue(JSC__JSGlobalObject* arg0, JSC__JSValue JSValue1); CPP_DECL void JSC__JSPromise__resolveOnNextTick(JSC__JSPromise* arg0, JSC__JSGlobalObject* arg1, JSC__JSValue JSValue2); CPP_DECL JSC__JSValue JSC__JSPromise__result(JSC__JSPromise* arg0, JSC__VM* arg1); +CPP_DECL void JSC__JSPromise__setHandled(JSC__JSPromise* arg0, JSC__VM* arg1); CPP_DECL uint32_t JSC__JSPromise__status(const JSC__JSPromise* arg0, JSC__VM* arg1); #pragma mark - JSC::JSInternalPromise @@ -267,6 +268,7 @@ CPP_DECL void JSC__JSInternalPromise__rejectWithCaughtException(JSC__JSInternalP CPP_DECL void JSC__JSInternalPromise__resolve(JSC__JSInternalPromise* arg0, JSC__JSGlobalObject* arg1, JSC__JSValue JSValue2); CPP_DECL JSC__JSInternalPromise* JSC__JSInternalPromise__resolvedPromise(JSC__JSGlobalObject* arg0, JSC__JSValue JSValue1); CPP_DECL JSC__JSValue JSC__JSInternalPromise__result(const JSC__JSInternalPromise* arg0, JSC__VM* arg1); +CPP_DECL void JSC__JSInternalPromise__setHandled(JSC__JSInternalPromise* arg0, JSC__VM* arg1); CPP_DECL uint32_t JSC__JSInternalPromise__status(const JSC__JSInternalPromise* arg0, JSC__VM* arg1); #pragma mark - JSC::JSFunction diff --git a/src/bun.js/bindings/headers.zig b/src/bun.js/bindings/headers.zig index 4dda5f30b..666369b21 100644 --- a/src/bun.js/bindings/headers.zig +++ b/src/bun.js/bindings/headers.zig @@ -168,6 +168,7 @@ pub extern fn JSC__JSPromise__resolvedPromise(arg0: *bindings.JSGlobalObject, JS pub extern fn JSC__JSPromise__resolvedPromiseValue(arg0: *bindings.JSGlobalObject, JSValue1: JSC__JSValue) JSC__JSValue; pub extern fn JSC__JSPromise__resolveOnNextTick(arg0: ?*bindings.JSPromise, arg1: *bindings.JSGlobalObject, JSValue2: JSC__JSValue) void; pub extern fn JSC__JSPromise__result(arg0: ?*bindings.JSPromise, arg1: *bindings.VM) JSC__JSValue; +pub extern fn JSC__JSPromise__setHandled(arg0: ?*bindings.JSPromise, arg1: *bindings.VM) void; pub extern fn JSC__JSPromise__status(arg0: [*c]const JSC__JSPromise, arg1: *bindings.VM) u32; pub extern fn JSC__JSInternalPromise__create(arg0: *bindings.JSGlobalObject) [*c]bindings.JSInternalPromise; pub extern fn JSC__JSInternalPromise__isHandled(arg0: [*c]const JSC__JSInternalPromise, arg1: *bindings.VM) bool; @@ -179,6 +180,7 @@ pub extern fn JSC__JSInternalPromise__rejectWithCaughtException(arg0: [*c]bindin pub extern fn JSC__JSInternalPromise__resolve(arg0: [*c]bindings.JSInternalPromise, arg1: *bindings.JSGlobalObject, JSValue2: JSC__JSValue) void; pub extern fn JSC__JSInternalPromise__resolvedPromise(arg0: *bindings.JSGlobalObject, JSValue1: JSC__JSValue) [*c]bindings.JSInternalPromise; pub extern fn JSC__JSInternalPromise__result(arg0: [*c]const JSC__JSInternalPromise, arg1: *bindings.VM) JSC__JSValue; +pub extern fn JSC__JSInternalPromise__setHandled(arg0: [*c]bindings.JSInternalPromise, arg1: *bindings.VM) void; pub extern fn JSC__JSInternalPromise__status(arg0: [*c]const JSC__JSInternalPromise, arg1: *bindings.VM) u32; pub extern fn JSC__JSFunction__optimizeSoon(JSValue0: JSC__JSValue) void; pub extern fn JSC__JSGlobalObject__bunVM(arg0: *bindings.JSGlobalObject) ?*bindings.VirtualMachine; diff --git a/src/bun.js/event_loop.zig b/src/bun.js/event_loop.zig index 8441bd064..0a3459d64 100644 --- a/src/bun.js/event_loop.zig +++ b/src/bun.js/event_loop.zig @@ -666,6 +666,30 @@ pub const EventLoop = struct { } } + pub fn waitForPromiseWithTimeout(this: *EventLoop, promise: JSC.AnyPromise, timeout: u32) bool { + return switch (promise.status(this.global.vm())) { + JSC.JSPromise.Status.Pending => { + if (timeout == 0) { + return false; + } + var start_time = std.time.milliTimestamp(); + while (promise.status(this.global.vm()) == .Pending) { + this.tick(); + + if (std.time.milliTimestamp() - start_time > timeout) { + return false; + } + + if (promise.status(this.global.vm()) == .Pending) { + this.autoTick(); + } + } + return true; + }, + else => true, + }; + } + pub fn waitForTasks(this: *EventLoop) void { this.tick(); while (this.tasks.count > 0) { diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index 545f41f19..bebfbeb18 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -653,6 +653,10 @@ pub const VirtualMachine = struct { this.eventLoop().waitForPromise(promise); } + pub fn waitForPromiseWithTimeout(this: *VirtualMachine, promise: JSC.AnyPromise, timeout: u32) bool { + return this.eventLoop().waitForPromiseWithTimeout(promise, timeout); + } + pub fn waitForTasks(this: *VirtualMachine) void { this.eventLoop().waitForTasks(); } @@ -957,6 +961,7 @@ pub const VirtualMachine = struct { } pub fn refCountedStringWithWasNew(this: *VirtualMachine, new: *bool, input_: []const u8, hash_: ?u32, comptime dupe: bool) *JSC.RefString { + JSC.markBinding(@src()); const hash = hash_ orelse JSC.RefString.computeHash(input_); var entry = this.ref_strings.getOrPut(hash) catch unreachable; diff --git a/src/bun.js/test/expect.zig b/src/bun.js/test/expect.zig new file mode 100644 index 000000000..90fcb2a73 --- /dev/null +++ b/src/bun.js/test/expect.zig @@ -0,0 +1,3335 @@ +const std = @import("std"); +const bun = @import("root").bun; +const default_allocator = bun.default_allocator; +const string = bun.string; +const MutableString = bun.MutableString; +const strings = bun.strings; +const Output = bun.Output; +const jest = bun.JSC.Jest; +const Jest = jest.Jest; +const TestRunner = jest.TestRunner; +const DescribeScope = jest.DescribeScope; +const JSC = bun.JSC; +const VirtualMachine = JSC.VirtualMachine; +const JSGlobalObject = JSC.JSGlobalObject; +const JSValue = JSC.JSValue; +const JSInternalPromise = JSC.JSInternalPromise; +const JSPromise = JSC.JSPromise; +const JSType = JSValue.JSType; +const JSError = JSC.JSError; +const JSObject = JSC.JSObject; +const CallFrame = JSC.CallFrame; +const ZigString = JSC.ZigString; +const Environment = bun.Environment; +const DiffFormatter = @import("./diff_format.zig").DiffFormatter; + +pub const Counter = struct { + expected: u32 = 0, + actual: u32 = 0, +}; + +pub var active_test_expectation_counter: Counter = .{}; + +/// https://jestjs.io/docs/expect +// To support async tests, we need to track the test ID +pub const Expect = struct { + pub usingnamespace JSC.Codegen.JSExpect; + + test_id: TestRunner.Test.ID, + scope: *DescribeScope, + flags: Flags = .{}, + + pub const Flags = packed struct { + promise: enum(u2) { + resolves, + rejects, + none, + } = .none, + not: bool = false, + }; + + pub fn getSignature(comptime matcher_name: string, comptime args: string, comptime not: bool) string { + const received = "expect(received)."; + comptime if (not) { + return received ++ "not." ++ matcher_name ++ "(" ++ args ++ ")"; + }; + return received ++ matcher_name ++ "(" ++ args ++ ")"; + } + + pub fn getFullSignature(comptime matcher: string, comptime args: string, comptime flags: Flags) string { + const fmt = "expect(received)." ++ if (flags.promise != .none) + switch (flags.promise) { + .resolves => if (flags.not) "resolves.not." else "resolves.", + .rejects => if (flags.not) "rejects.not." else "rejects.", + else => unreachable, + } + else if (flags.not) "not." else ""; + return fmt ++ matcher ++ "(" ++ args ++ ")"; + } + + pub fn getNot(this: *Expect, thisValue: JSValue, _: *JSGlobalObject) callconv(.C) JSValue { + this.flags.not = !this.flags.not; + return thisValue; + } + + pub fn getResolves(this: *Expect, thisValue: JSValue, globalThis: *JSGlobalObject) callconv(.C) JSValue { + switch (this.flags.promise) { + .rejects => { + globalThis.throw("Cannot chain .resolves() after .rejects()", .{}); + return .zero; + }, + .resolves => {}, + .none => { + this.flags.promise = .resolves; + }, + } + return thisValue; + } + + pub fn getRejects(this: *Expect, thisValue: JSValue, globalThis: *JSGlobalObject) callconv(.C) JSValue { + switch (this.flags.promise) { + .rejects => { + this.flags.promise = .rejects; + }, + .resolves => { + globalThis.throw("Cannot chain .rejects() after .resolves()", .{}); + return .zero; + }, + .none => { + this.flags.promise = .resolves; + }, + } + + return thisValue; + } + + pub fn getValue(this: *Expect, globalThis: *JSGlobalObject, thisValue: JSValue, comptime matcher_name: string, comptime matcher_args: string) ?JSValue { + if (this.scope.tests.items.len <= this.test_id) { + globalThis.throw("{s}() must be called in a test", .{matcher_name}); + return null; + } + + const value = Expect.capturedValueGetCached(thisValue) orelse { + globalThis.throw("Internal error: the expect(value) was garbage collected but it should not have been!", .{}); + return null; + }; + value.ensureStillAlive(); + + switch (this.flags.promise) { + inline .resolves, .rejects => |resolution| { + if (value.asAnyPromise()) |promise| { + var vm = globalThis.vm(); + promise.setHandled(vm); + + const now = std.time.Instant.now() catch unreachable; + const pending_test = Jest.runner.?.pending_test.?; + const elapsed = @divFloor(now.since(pending_test.started_at), std.time.ns_per_ms); + const remaining = @truncate(u32, Jest.runner.?.last_test_timeout_timer_duration -| elapsed); + + if (!globalThis.bunVM().waitForPromiseWithTimeout(promise, remaining)) { + pending_test.timeout(); + return null; + } + + const newValue = promise.result(vm); + switch (promise.status(vm)) { + .Fulfilled => switch (comptime resolution) { + .resolves => {}, + .rejects => { + if (this.flags.not) { + const signature = comptime getFullSignature(matcher_name, matcher_args, .{ .not = true, .promise = .rejects }); + const fmt = signature ++ "\n\nExpected promise that rejects\nReceived promise that resolved: {any}\n"; + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + globalThis.throwPretty(fmt, .{newValue.toFmt(globalThis, &formatter)}); + return null; + } + const signature = comptime getFullSignature(matcher_name, matcher_args, .{ .not = false, .promise = .rejects }); + const fmt = signature ++ "\n\nExpected promise that rejects\nReceived promise that resolved: {any}\n"; + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + globalThis.throwPretty(fmt, .{newValue.toFmt(globalThis, &formatter)}); + return null; + }, + .none => unreachable, + }, + .Rejected => switch (comptime resolution) { + .rejects => {}, + .resolves => { + if (this.flags.not) { + const signature = comptime getFullSignature(matcher_name, matcher_args, .{ .not = true, .promise = .resolves }); + const fmt = signature ++ "\n\nExpected promise that resolves\nReceived promise that rejected: {any}\n"; + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + globalThis.throwPretty(fmt, .{newValue.toFmt(globalThis, &formatter)}); + return null; + } + const signature = comptime getFullSignature(matcher_name, matcher_args, .{ .not = false, .promise = .resolves }); + const fmt = signature ++ "\n\nExpected promise that resolves\nReceived promise that rejected: {any}\n"; + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + globalThis.throwPretty(fmt, .{newValue.toFmt(globalThis, &formatter)}); + return null; + }, + .none => unreachable, + }, + .Pending => unreachable, + } + + newValue.ensureStillAlive(); + return newValue; + } else { + switch (this.flags.promise) { + .resolves => { + if (this.flags.not) { + const signature = comptime getFullSignature(matcher_name, matcher_args, .{ .not = true, .promise = .resolves }); + const fmt = signature ++ "\n\nExpected promise\nReceived: {any}\n"; + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + globalThis.throwPretty(fmt, .{value.toFmt(globalThis, &formatter)}); + return null; + } + const signature = comptime getFullSignature(matcher_name, matcher_args, .{ .not = false, .promise = .resolves }); + const fmt = signature ++ "\n\nExpected promise\nReceived: {any}\n"; + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + globalThis.throwPretty(fmt, .{value.toFmt(globalThis, &formatter)}); + return null; + }, + .rejects => { + if (this.flags.not) { + const signature = comptime getFullSignature(matcher_name, matcher_args, .{ .not = true, .promise = .rejects }); + const fmt = signature ++ "\n\nExpected promise\nReceived: {any}\n"; + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + globalThis.throwPretty(fmt, .{value.toFmt(globalThis, &formatter)}); + return null; + } + const signature = comptime getFullSignature(matcher_name, matcher_args, .{ .not = false, .promise = .rejects }); + const fmt = signature ++ "\n\nExpected promise\nReceived: {any}\n"; + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + globalThis.throwPretty(fmt, .{value.toFmt(globalThis, &formatter)}); + return null; + }, + .none => unreachable, + } + } + }, + else => {}, + } + + return value; + } + + pub fn getSnapshotName(this: *Expect, allocator: std.mem.Allocator, hint: string) ![]const u8 { + const test_name = this.scope.tests.items[this.test_id].label; + + var length: usize = 0; + var curr_scope: ?*DescribeScope = this.scope; + while (curr_scope) |scope| { + if (scope.label.len > 0) { + length += scope.label.len + 1; + } + curr_scope = scope.parent; + } + length += test_name.len; + if (hint.len > 0) { + length += hint.len + 2; + } + + var buf = try allocator.alloc(u8, length); + + var index = buf.len; + if (hint.len > 0) { + index -= hint.len; + bun.copy(u8, buf[index..], hint); + index -= test_name.len + 2; + bun.copy(u8, buf[index..], test_name); + bun.copy(u8, buf[index + test_name.len ..], ": "); + } else { + index -= test_name.len; + bun.copy(u8, buf[index..], test_name); + } + // copy describe scopes in reverse order + curr_scope = this.scope; + while (curr_scope) |scope| { + if (scope.label.len > 0) { + index -= scope.label.len + 1; + bun.copy(u8, buf[index..], scope.label); + buf[index + scope.label.len] = ' '; + } + curr_scope = scope.parent; + } + + return buf; + } + + pub fn finalize( + this: *Expect, + ) callconv(.C) void { + VirtualMachine.get().allocator.destroy(this); + } + + pub fn call(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSC.JSValue { + const arguments = callframe.arguments(1); + const value = if (arguments.len < 1) JSC.JSValue.jsUndefined() else arguments.ptr[0]; + + var expect = globalObject.bunVM().allocator.create(Expect) catch unreachable; + + if (Jest.runner.?.pending_test == null) { + const err = globalObject.createErrorInstance("expect() must be called in a test", .{}); + err.put(globalObject, ZigString.static("name"), ZigString.init("TestNotRunningError").toValueGC(globalObject)); + globalObject.throwValue(err); + return .zero; + } + + expect.* = .{ + .scope = Jest.runner.?.pending_test.?.describe, + .test_id = Jest.runner.?.pending_test.?.test_id, + }; + const expect_js_value = expect.toJS(globalObject); + expect_js_value.ensureStillAlive(); + Expect.capturedValueSetCached(expect_js_value, globalObject, value); + expect_js_value.ensureStillAlive(); + expect.postMatch(globalObject); + return expect_js_value; + } + + pub fn constructor( + globalObject: *JSC.JSGlobalObject, + _: *JSC.CallFrame, + ) callconv(.C) ?*Expect { + globalObject.throw("expect() cannot be called with new", .{}); + return null; + } + + /// Object.is() + pub fn toBe( + this: *Expect, + globalObject: *JSC.JSGlobalObject, + callframe: *JSC.CallFrame, + ) callconv(.C) JSC.JSValue { + defer this.postMatch(globalObject); + const thisValue = callframe.this(); + const arguments_ = callframe.arguments(1); + const arguments = arguments_.ptr[0..arguments_.len]; + + if (arguments.len < 1) { + globalObject.throwInvalidArguments("toBe() takes 1 argument", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + const right = arguments[0]; + right.ensureStillAlive(); + const left = this.getValue(globalObject, thisValue, "toBe", "expected") orelse return .zero; + + const not = this.flags.not; + var pass = right.isSameValue(left, globalObject); + if (comptime Environment.allow_assert) { + std.debug.assert(pass == JSC.C.JSValueIsStrictEqual(globalObject, right.asObjectRef(), left.asObjectRef())); + } + + 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("toBe", "expected", true); + const fmt = signature ++ "\n\nExpected: not {any}\n"; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{right.toFmt(globalObject, &formatter)}); + return .zero; + } + globalObject.throw(Output.prettyFmt(fmt, false), .{right.toFmt(globalObject, &formatter)}); + return .zero; + } + + const signature = comptime getSignature("toBe", "expected", false); + if (left.deepEquals(right, globalObject) or left.strictDeepEquals(right, globalObject)) { + const fmt = signature ++ + "\n\nIf this test should pass, replace \"toBe\" with \"toEqual\" or \"toStrictEqual\"" ++ + "\n\nExpected: {any}\n" ++ + "Received: serializes to the same string\n"; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{right.toFmt(globalObject, &formatter)}); + return .zero; + } + globalObject.throw(Output.prettyFmt(fmt, false), .{right.toFmt(globalObject, &formatter)}); + return .zero; + } + + if (right.isString() and left.isString()) { + const diff_format = DiffFormatter{ + .expected = right, + .received = left, + .globalObject = globalObject, + .not = not, + }; + const fmt = signature ++ "\n\n{any}\n"; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{diff_format}); + return .zero; + } + globalObject.throw(Output.prettyFmt(fmt, false), .{diff_format}); + return .zero; + } + + const fmt = signature ++ "\n\nExpected: {any}\nReceived: {any}\n"; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{ + right.toFmt(globalObject, &formatter), + left.toFmt(globalObject, &formatter), + }); + return .zero; + } + globalObject.throw(Output.prettyFmt(fmt, false), .{ + right.toFmt(globalObject, &formatter), + left.toFmt(globalObject, &formatter), + }); + return .zero; + } + + pub fn toHaveLength( + this: *Expect, + globalObject: *JSC.JSGlobalObject, + callframe: *JSC.CallFrame, + ) callconv(.C) JSC.JSValue { + defer this.postMatch(globalObject); + const thisValue = callframe.this(); + const arguments_ = callframe.arguments(1); + const arguments = arguments_.ptr[0..arguments_.len]; + + if (arguments.len < 1) { + globalObject.throwInvalidArguments("toHaveLength() takes 1 argument", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const expected: JSValue = arguments[0]; + const value: JSValue = this.getValue(globalObject, thisValue, "toHaveLength", "expected") orelse return .zero; + + if (!value.isObject() and !value.isString()) { + var fmt = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + globalObject.throw("Received value does not have a length property: {any}", .{value.toFmt(globalObject, &fmt)}); + return .zero; + } + + if (!expected.isNumber()) { + var fmt = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + globalObject.throw("Expected value must be a non-negative integer: {any}", .{expected.toFmt(globalObject, &fmt)}); + return .zero; + } + + const expected_length: f64 = expected.asNumber(); + if (@round(expected_length) != expected_length or std.math.isInf(expected_length) or std.math.isNan(expected_length) or expected_length < 0) { + var fmt = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + globalObject.throw("Expected value must be a non-negative integer: {any}", .{expected.toFmt(globalObject, &fmt)}); + return .zero; + } + + const not = this.flags.not; + var pass = false; + + const actual_length = value.getLengthIfPropertyExistsInternal(globalObject); + + if (actual_length == std.math.inf(f64)) { + var fmt = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + globalObject.throw("Received value does not have a length property: {any}", .{value.toFmt(globalObject, &fmt)}); + return .zero; + } else if (std.math.isNan(actual_length)) { + globalObject.throw("Received value has non-number length property: {}", .{actual_length}); + return .zero; + } + + if (actual_length == expected_length) { + pass = true; + } + + if (not) pass = !pass; + if (pass) return thisValue; + + // handle failure + if (not) { + const expected_line = "Expected length: not {d}\n"; + const fmt = comptime getSignature("toHaveLength", "expected", true) ++ "\n\n" ++ expected_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{expected_length}); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{expected_length}); + return .zero; + } + + const expected_line = "Expected length: {d}\n"; + const received_line = "Received length: {d}\n"; + const fmt = comptime getSignature("toHaveLength", "expected", false) ++ "\n\n" ++ + expected_line ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_length, actual_length }); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_length, actual_length }); + return .zero; + } + + pub fn toContain( + this: *Expect, + globalObject: *JSC.JSGlobalObject, + callFrame: *JSC.CallFrame, + ) callconv(.C) JSC.JSValue { + defer this.postMatch(globalObject); + const thisValue = callFrame.this(); + const arguments_ = callFrame.arguments(1); + const arguments = arguments_.ptr[0..arguments_.len]; + + if (arguments.len < 1) { + globalObject.throwInvalidArguments("toContain() takes 1 argument", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const expected = arguments[0]; + expected.ensureStillAlive(); + const value: JSValue = this.getValue(globalObject, thisValue, "toContain", "expected") orelse return .zero; + + const not = this.flags.not; + var pass = false; + + if (value.isIterable(globalObject)) { + var itr = value.arrayIterator(globalObject); + while (itr.next()) |item| { + if (item.isSameValue(expected, globalObject)) { + pass = true; + break; + } + } + } else if (value.isString() and expected.isString()) { + const value_string = value.toString(globalObject).toSlice(globalObject, default_allocator).slice(); + const expected_string = expected.toString(globalObject).toSlice(globalObject, default_allocator).slice(); + if (strings.contains(value_string, expected_string)) { + pass = true; + } else if (value_string.len == 0 and expected_string.len == 0) { // edge case two empty strings are true + pass = true; + } + } else { + globalObject.throw("Received value must be an array type, or both received and expected values must be strings.", .{}); + return .zero; + } + + if (not) pass = !pass; + if (pass) return thisValue; + + // handle failure + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + const value_fmt = value.toFmt(globalObject, &formatter); + const expected_fmt = expected.toFmt(globalObject, &formatter); + if (not) { + const expected_line = "Expected to contain: not {any}\n"; + const fmt = comptime getSignature("toContain", "expected", true) ++ "\n\n" ++ expected_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{expected_fmt}); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{expected_fmt}); + return .zero; + } + + const expected_line = "Expected to contain: {any}\n"; + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toContain", "expected", false) ++ "\n\n" ++ expected_line ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt }); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, value_fmt }); + return .zero; + } + + pub fn toBeTruthy(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { + defer this.postMatch(globalObject); + const thisValue = callFrame.this(); + const value: JSValue = this.getValue(globalObject, thisValue, "toBeTruthy", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + var pass = false; + + const truthy = value.toBooleanSlow(globalObject); + if (truthy) pass = true; + + if (not) pass = !pass; + if (pass) return thisValue; + + // handle failure + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + const value_fmt = value.toFmt(globalObject, &formatter); + if (not) { + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeTruthy", "", true) ++ "\n\n" ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); + return .zero; + } + + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeTruthy", "", false) ++ "\n\n" ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); + return .zero; + } + + pub fn toBeUndefined(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { + defer this.postMatch(globalObject); + const thisValue = callFrame.this(); + const value: JSValue = this.getValue(globalObject, thisValue, "toBeUndefined", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + var pass = false; + if (value.isUndefined()) pass = true; + + if (not) pass = !pass; + if (pass) return thisValue; + + // handle failure + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + const value_fmt = value.toFmt(globalObject, &formatter); + if (not) { + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeUndefined", "", true) ++ "\n\n" ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); + return .zero; + } + + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeUndefined", "", false) ++ "\n\n" ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); + return .zero; + } + + pub fn toBeNaN(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { + defer this.postMatch(globalObject); + + const thisValue = callFrame.this(); + const value: JSValue = this.getValue(globalObject, thisValue, "toBeNaN", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + var pass = false; + if (value.isNumber()) { + const number = value.asNumber(); + if (number != number) pass = true; + } + + if (not) pass = !pass; + if (pass) return thisValue; + + // handle failure + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + const value_fmt = value.toFmt(globalObject, &formatter); + if (not) { + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeNaN", "", true) ++ "\n\n" ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); + return .zero; + } + + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeNaN", "", false) ++ "\n\n" ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); + return .zero; + } + + pub fn toBeNull(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { + defer this.postMatch(globalObject); + + const thisValue = callFrame.this(); + const value: JSValue = this.getValue(globalObject, thisValue, "toBeNull", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + var pass = value.isNull(); + if (not) pass = !pass; + if (pass) return thisValue; + + // handle failure + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + const value_fmt = value.toFmt(globalObject, &formatter); + if (not) { + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeNull", "", true) ++ "\n\n" ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); + return .zero; + } + + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeNull", "", false) ++ "\n\n" ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); + return .zero; + } + + pub fn toBeDefined(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { + defer this.postMatch(globalObject); + + const thisValue = callFrame.this(); + const value: JSValue = this.getValue(globalObject, thisValue, "toBeDefined", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + var pass = !value.isUndefined(); + if (not) pass = !pass; + if (pass) return thisValue; + + // handle failure + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + const value_fmt = value.toFmt(globalObject, &formatter); + if (not) { + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeDefined", "", true) ++ "\n\n" ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); + return .zero; + } + + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeDefined", "", false) ++ "\n\n" ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); + return .zero; + } + + pub fn toBeFalsy(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { + defer this.postMatch(globalObject); + + const thisValue = callFrame.this(); + + const value: JSValue = this.getValue(globalObject, thisValue, "toBeFalsy", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + var pass = false; + + const truthy = value.toBooleanSlow(globalObject); + if (!truthy) pass = true; + + if (not) pass = !pass; + if (pass) return thisValue; + + // handle failure + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + const value_fmt = value.toFmt(globalObject, &formatter); + if (not) { + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeFalsy", "", true) ++ "\n\n" ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); + return .zero; + } + + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeFalsy", "", false) ++ "\n\n" ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); + return .zero; + } + + pub fn toEqual(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { + defer this.postMatch(globalObject); + + const thisValue = callFrame.this(); + const _arguments = callFrame.arguments(1); + const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; + + if (arguments.len < 1) { + globalObject.throwInvalidArguments("toEqual() requires 1 argument", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const expected = arguments[0]; + const value: JSValue = this.getValue(globalObject, thisValue, "toEqual", "expected") orelse return .zero; + + const not = this.flags.not; + var pass = value.jestDeepEquals(expected, globalObject); + + if (not) pass = !pass; + if (pass) return thisValue; + + // handle failure + const diff_formatter = DiffFormatter{ + .received = value, + .expected = expected, + .globalObject = globalObject, + .not = not, + }; + + if (not) { + const signature = comptime getSignature("toEqual", "expected", true); + const fmt = signature ++ "\n\n{any}\n"; + globalObject.throwPretty(fmt, .{diff_formatter}); + return .zero; + } + + const signature = comptime getSignature("toEqual", "expected", false); + const fmt = signature ++ "\n\n{any}\n"; + globalObject.throwPretty(fmt, .{diff_formatter}); + return .zero; + } + + pub fn toStrictEqual(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { + defer this.postMatch(globalObject); + + const thisValue = callFrame.this(); + const _arguments = callFrame.arguments(1); + const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; + + if (arguments.len < 1) { + globalObject.throwInvalidArguments("toStrictEqual() requires 1 argument", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const expected = arguments[0]; + const value: JSValue = this.getValue(globalObject, thisValue, "toStrictEqual", "expected") orelse return .zero; + + const not = this.flags.not; + var pass = value.jestStrictDeepEquals(expected, globalObject); + + if (not) pass = !pass; + if (pass) return thisValue; + + // handle failure + const diff_formatter = DiffFormatter{ .received = value, .expected = expected, .globalObject = globalObject, .not = not }; + + if (not) { + const signature = comptime getSignature("toStrictEqual", "expected", true); + const fmt = signature ++ "\n\n{any}\n"; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{diff_formatter}); + return .zero; + } + globalObject.throw(Output.prettyFmt(fmt, false), .{diff_formatter}); + return .zero; + } + + const signature = comptime getSignature("toStrictEqual", "expected", false); + const fmt = signature ++ "\n\n{any}\n"; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{diff_formatter}); + return .zero; + } + globalObject.throw(Output.prettyFmt(fmt, false), .{diff_formatter}); + return .zero; + } + + pub fn toHaveProperty(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { + defer this.postMatch(globalObject); + + const thisValue = callFrame.this(); + const _arguments = callFrame.arguments(2); + const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; + + if (arguments.len < 1) { + globalObject.throwInvalidArguments("toHaveProperty() requires at least 1 argument", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const expected_property_path = arguments[0]; + expected_property_path.ensureStillAlive(); + const expected_property: ?JSValue = if (arguments.len > 1) arguments[1] else null; + if (expected_property) |ev| ev.ensureStillAlive(); + + const value: JSValue = this.getValue(globalObject, thisValue, "toHaveProperty", "path, value") orelse return .zero; + + if (!expected_property_path.isString() and !expected_property_path.isIterable(globalObject)) { + globalObject.throw("Expected path must be a string or an array", .{}); + return .zero; + } + + const not = this.flags.not; + var path_string = ZigString.Empty; + expected_property_path.toZigString(&path_string, globalObject); + + var pass = !value.isUndefinedOrNull(); + var received_property: JSValue = .zero; + + if (pass) { + received_property = value.getIfPropertyExistsFromPath(globalObject, expected_property_path); + pass = !received_property.isEmpty(); + } + + if (pass and expected_property != null) { + pass = received_property.jestDeepEquals(expected_property.?, globalObject); + } + + if (not) pass = !pass; + if (pass) return thisValue; + + // handle failure + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + if (not) { + if (expected_property != null) { + const signature = comptime getSignature("toHaveProperty", "path, value", true); + if (!received_property.isEmpty()) { + const fmt = signature ++ "\n\nExpected path: {any}\n\nExpected value: not {any}\n"; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{ + expected_property_path.toFmt(globalObject, &formatter), + expected_property.?.toFmt(globalObject, &formatter), + }); + return .zero; + } + globalObject.throw(Output.prettyFmt(fmt, true), .{ + expected_property_path.toFmt(globalObject, &formatter), + expected_property.?.toFmt(globalObject, &formatter), + }); + return .zero; + } + } + + const signature = comptime getSignature("toHaveProperty", "path", true); + const fmt = signature ++ "\n\nExpected path: not {any}\n\nReceived value: {any}\n"; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{ + expected_property_path.toFmt(globalObject, &formatter), + received_property.toFmt(globalObject, &formatter), + }); + return .zero; + } + globalObject.throw(Output.prettyFmt(fmt, false), .{ + expected_property_path.toFmt(globalObject, &formatter), + received_property.toFmt(globalObject, &formatter), + }); + return .zero; + } + + if (expected_property != null) { + const signature = comptime getSignature("toHaveProperty", "path, value", false); + if (!received_property.isEmpty()) { + // deep equal case + const fmt = signature ++ "\n\n{any}\n"; + const diff_format = DiffFormatter{ + .received = received_property, + .expected = expected_property.?, + .globalObject = globalObject, + }; + + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{diff_format}); + return .zero; + } + globalObject.throw(Output.prettyFmt(fmt, false), .{diff_format}); + return .zero; + } + + const fmt = signature ++ "\n\nExpected path: {any}\n\nExpected value: {any}\n\n" ++ + "Unable to find property\n"; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{ + expected_property_path.toFmt(globalObject, &formatter), + expected_property.?.toFmt(globalObject, &formatter), + }); + return .zero; + } + globalObject.throw(Output.prettyFmt(fmt, false), .{ + expected_property_path.toFmt(globalObject, &formatter), + expected_property.?.toFmt(globalObject, &formatter), + }); + return .zero; + } + + const signature = comptime getSignature("toHaveProperty", "path", false); + const fmt = signature ++ "\n\nExpected path: {any}\n\nUnable to find property\n"; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{expected_property_path.toFmt(globalObject, &formatter)}); + return .zero; + } + globalObject.throw(Output.prettyFmt(fmt, false), .{expected_property_path.toFmt(globalObject, &formatter)}); + return .zero; + } + + pub fn toBeEven(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { + defer this.postMatch(globalObject); + + const thisValue = callFrame.this(); + + const value: JSValue = this.getValue(globalObject, thisValue, "toBeEven", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + var pass = false; + + if (value.isAnyInt()) { + const _value = value.toInt64(); + pass = @mod(_value, 2) == 0; + if (_value == -0) { // negative zero is even + pass = true; + } + } else if (value.isBigInt() or value.isBigInt32()) { + const _value = value.toInt64(); + pass = switch (_value == -0) { // negative zero is even + true => true, + else => _value & 1 == 0, + }; + } else if (value.isNumber()) { + const _value = JSValue.asNumber(value); + if (@mod(_value, 1) == 0 and @mod(_value, 2) == 0) { // if the fraction is all zeros and even + pass = true; + } else { + pass = false; + } + } else { + pass = false; + } + + if (not) pass = !pass; + if (pass) return thisValue; + + // handle failure + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + const value_fmt = value.toFmt(globalObject, &formatter); + if (not) { + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeEven", "", true) ++ "\n\n" ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); + return .zero; + } + + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeEven", "", false) ++ "\n\n" ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); + return .zero; + } + + pub fn toBeGreaterThan(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalObject); + + const thisValue = callFrame.this(); + const _arguments = callFrame.arguments(1); + const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; + + if (arguments.len < 1) { + globalObject.throwInvalidArguments("toBeGreaterThan() requires 1 argument", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const other_value = arguments[0]; + other_value.ensureStillAlive(); + + const value: JSValue = this.getValue(globalObject, thisValue, "toBeGreaterThan", "expected") orelse return .zero; + + if ((!value.isNumber() and !value.isBigInt()) or (!other_value.isNumber() and !other_value.isBigInt())) { + globalObject.throw("Expected and actual values must be numbers or bigints", .{}); + return .zero; + } + + const not = this.flags.not; + var pass = false; + + if (!value.isBigInt() and !other_value.isBigInt()) { + pass = value.asNumber() > other_value.asNumber(); + } else if (value.isBigInt()) { + pass = switch (value.asBigIntCompare(globalObject, other_value)) { + .greater_than => true, + else => pass, + }; + } else { + pass = switch (other_value.asBigIntCompare(globalObject, value)) { + .less_than => true, + else => pass, + }; + } + + if (not) pass = !pass; + if (pass) return thisValue; + + // handle failure + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + const value_fmt = value.toFmt(globalObject, &formatter); + const expected_fmt = other_value.toFmt(globalObject, &formatter); + if (not) { + const expected_line = "Expected: not \\> {any}\n"; + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeGreaterThan", "expected", true) ++ "\n\n" ++ expected_line ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt }); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, value_fmt }); + return .zero; + } + + const expected_line = "Expected: \\> {any}\n"; + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeGreaterThan", "expected", false) ++ "\n\n" ++ + expected_line ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(comptime Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt }); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, value_fmt }); + return .zero; + } + + pub fn toBeGreaterThanOrEqual(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalObject); + + const thisValue = callFrame.this(); + const _arguments = callFrame.arguments(1); + const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; + + if (arguments.len < 1) { + globalObject.throwInvalidArguments("toBeGreaterThanOrEqual() requires 1 argument", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const other_value = arguments[0]; + other_value.ensureStillAlive(); + + const value: JSValue = this.getValue(globalObject, thisValue, "toBeGreaterThanOrEqual", "expected") orelse return .zero; + + if ((!value.isNumber() and !value.isBigInt()) or (!other_value.isNumber() and !other_value.isBigInt())) { + globalObject.throw("Expected and actual values must be numbers or bigints", .{}); + return .zero; + } + + const not = this.flags.not; + var pass = false; + + if (!value.isBigInt() and !other_value.isBigInt()) { + pass = value.asNumber() >= other_value.asNumber(); + } else if (value.isBigInt()) { + pass = switch (value.asBigIntCompare(globalObject, other_value)) { + .greater_than, .equal => true, + else => pass, + }; + } else { + pass = switch (other_value.asBigIntCompare(globalObject, value)) { + .less_than, .equal => true, + else => pass, + }; + } + + if (not) pass = !pass; + if (pass) return thisValue; + + // handle failure + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + const value_fmt = value.toFmt(globalObject, &formatter); + const expected_fmt = other_value.toFmt(globalObject, &formatter); + if (not) { + const expected_line = "Expected: not \\>= {any}\n"; + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeGreaterThanOrEqual", "expected", true) ++ "\n\n" ++ expected_line ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt }); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, value_fmt }); + return .zero; + } + + const expected_line = "Expected: \\>= {any}\n"; + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeGreaterThanOrEqual", "expected", false) ++ "\n\n" ++ expected_line ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(comptime Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt }); + return .zero; + } + return .zero; + } + + pub fn toBeLessThan(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalObject); + + const thisValue = callFrame.this(); + const _arguments = callFrame.arguments(1); + const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; + + if (arguments.len < 1) { + globalObject.throwInvalidArguments("toBeLessThan() requires 1 argument", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const other_value = arguments[0]; + other_value.ensureStillAlive(); + + const value: JSValue = this.getValue(globalObject, thisValue, "toBeLessThan", "expected") orelse return .zero; + + if ((!value.isNumber() and !value.isBigInt()) or (!other_value.isNumber() and !other_value.isBigInt())) { + globalObject.throw("Expected and actual values must be numbers or bigints", .{}); + return .zero; + } + + const not = this.flags.not; + var pass = false; + + if (!value.isBigInt() and !other_value.isBigInt()) { + pass = value.asNumber() < other_value.asNumber(); + } else if (value.isBigInt()) { + pass = switch (value.asBigIntCompare(globalObject, other_value)) { + .less_than => true, + else => pass, + }; + } else { + pass = switch (other_value.asBigIntCompare(globalObject, value)) { + .greater_than => true, + else => pass, + }; + } + + if (not) pass = !pass; + if (pass) return thisValue; + + // handle failure + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + const value_fmt = value.toFmt(globalObject, &formatter); + const expected_fmt = other_value.toFmt(globalObject, &formatter); + if (not) { + const expected_line = "Expected: not \\< {any}\n"; + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeLessThan", "expected", true) ++ "\n\n" ++ expected_line ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt }); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, value_fmt }); + return .zero; + } + + const expected_line = "Expected: \\< {any}\n"; + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeLessThan", "expected", false) ++ "\n\n" ++ expected_line ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(comptime Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt }); + return .zero; + } + return .zero; + } + + pub fn toBeLessThanOrEqual(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalObject); + + const thisValue = callFrame.this(); + const _arguments = callFrame.arguments(1); + const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; + + if (arguments.len < 1) { + globalObject.throwInvalidArguments("toBeLessThanOrEqual() requires 1 argument", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const other_value = arguments[0]; + other_value.ensureStillAlive(); + + const value: JSValue = this.getValue(globalObject, thisValue, "toBeLessThanOrEqual", "expected") orelse return .zero; + + if ((!value.isNumber() and !value.isBigInt()) or (!other_value.isNumber() and !other_value.isBigInt())) { + globalObject.throw("Expected and actual values must be numbers or bigints", .{}); + return .zero; + } + + const not = this.flags.not; + var pass = false; + + if (!value.isBigInt() and !other_value.isBigInt()) { + pass = value.asNumber() <= other_value.asNumber(); + } else if (value.isBigInt()) { + pass = switch (value.asBigIntCompare(globalObject, other_value)) { + .less_than, .equal => true, + else => pass, + }; + } else { + pass = switch (other_value.asBigIntCompare(globalObject, value)) { + .greater_than, .equal => true, + else => pass, + }; + } + + if (not) pass = !pass; + if (pass) return thisValue; + + // handle failure + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + const value_fmt = value.toFmt(globalObject, &formatter); + const expected_fmt = other_value.toFmt(globalObject, &formatter); + if (not) { + const expected_line = "Expected: not \\<= {any}\n"; + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeLessThanOrEqual", "expected", true) ++ "\n\n" ++ expected_line ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt }); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, value_fmt }); + return .zero; + } + + const expected_line = "Expected: \\<= {any}\n"; + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeLessThanOrEqual", "expected", false) ++ "\n\n" ++ expected_line ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(comptime Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt }); + return .zero; + } + return .zero; + } + + pub fn toBeCloseTo(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalObject); + + const thisValue = callFrame.this(); + const thisArguments = callFrame.arguments(2); + const arguments = thisArguments.ptr[0..thisArguments.len]; + + if (arguments.len < 1) { + globalObject.throwInvalidArguments("toBeCloseTo() requires at least 1 argument. Expected value must be a number", .{}); + return .zero; + } + + const expected_ = arguments[0]; + if (!expected_.isNumber()) { + globalObject.throwInvalidArgumentType("toBeCloseTo", "expected", "number"); + return .zero; + } + + var precision: f64 = 2.0; + if (arguments.len > 1) { + const precision_ = arguments[1]; + if (!precision_.isNumber()) { + globalObject.throwInvalidArgumentType("toBeCloseTo", "precision", "number"); + return .zero; + } + + precision = precision_.asNumber(); + } + + const received_: JSValue = this.getValue(globalObject, thisValue, "toBeCloseTo", "expected, precision") orelse return .zero; + if (!received_.isNumber()) { + globalObject.throwInvalidArgumentType("expect", "received", "number"); + return .zero; + } + + var expected = expected_.asNumber(); + var received = received_.asNumber(); + + if (std.math.isNegativeInf(expected)) { + expected = -expected; + } + + if (std.math.isNegativeInf(received)) { + received = -received; + } + + if (std.math.isPositiveInf(expected) and std.math.isPositiveInf(received)) { + return thisValue; + } + + const expected_diff = std.math.pow(f64, 10, -precision) / 2; + const actual_diff = std.math.fabs(received - expected); + var pass = actual_diff < expected_diff; + + const not = this.flags.not; + if (not) pass = !pass; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + + const expected_fmt = expected_.toFmt(globalObject, &formatter); + const received_fmt = received_.toFmt(globalObject, &formatter); + + const expected_line = "Expected: {any}\n"; + const received_line = "Received: {any}\n"; + const expected_precision = "Expected precision: {d}\n"; + const expected_difference = "Expected difference: \\< {d}\n"; + const received_difference = "Received difference: {d}\n"; + + const suffix_fmt = "\n\n" ++ expected_line ++ received_line ++ "\n" ++ expected_precision ++ expected_difference ++ received_difference; + + if (not) { + const fmt = comptime getSignature("toBeCloseTo", "expected, precision", true) ++ suffix_fmt; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_fmt, received_fmt, precision, expected_diff, actual_diff }); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, received_fmt, precision, expected_diff, actual_diff }); + return .zero; + } + + const fmt = comptime getSignature("toBeCloseTo", "expected, precision", false) ++ suffix_fmt; + + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_fmt, received_fmt, precision, expected_diff, actual_diff }); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, received_fmt, precision, expected_diff, actual_diff }); + return .zero; + } + + pub fn toBeOdd(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { + defer this.postMatch(globalObject); + + const thisValue = callFrame.this(); + + const value: JSValue = this.getValue(globalObject, thisValue, "toBeOdd", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + var pass = false; + + if (value.isBigInt32()) { + pass = value.toInt32() & 1 == 1; + } else if (value.isBigInt()) { + pass = value.toInt64() & 1 == 1; + } else if (value.isInt32()) { + const _value = value.toInt32(); + pass = @mod(_value, 2) == 1; + } else if (value.isAnyInt()) { + const _value = value.toInt64(); + pass = @mod(_value, 2) == 1; + } else if (value.isNumber()) { + const _value = JSValue.asNumber(value); + if (@mod(_value, 1) == 0 and @mod(_value, 2) == 1) { // if the fraction is all zeros and odd + pass = true; + } else { + pass = false; + } + } else { + pass = false; + } + + if (not) pass = !pass; + if (pass) return thisValue; + + // handle failure + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + const value_fmt = value.toFmt(globalObject, &formatter); + if (not) { + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeOdd", "", true) ++ "\n\n" ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); + return .zero; + } + + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeOdd", "", false) ++ "\n\n" ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); + return .zero; + } + + pub fn toThrow(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalObject); + + const thisValue = callFrame.this(); + const _arguments = callFrame.arguments(1); + const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; + + active_test_expectation_counter.actual += 1; + + const expected_value: JSValue = if (arguments.len > 0) brk: { + const value = arguments[0]; + if (value.isEmptyOrUndefinedOrNull() or !value.isObject() and !value.isString()) { + var fmt = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + globalObject.throw("Expected value must be string or Error: {any}", .{value.toFmt(globalObject, &fmt)}); + return .zero; + } + break :brk value; + } else .zero; + expected_value.ensureStillAlive(); + + const value: JSValue = this.getValue(globalObject, thisValue, "toThrow", "expected") orelse return .zero; + + if (!value.jsType().isFunction()) { + globalObject.throw("Expected value must be a function", .{}); + return .zero; + } + + const not = this.flags.not; + + const result_: ?JSValue = brk: { + var vm = globalObject.bunVM(); + var return_value: JSValue = .zero; + var scope = vm.unhandledRejectionScope(); + var prev_unhandled_pending_rejection_to_capture = vm.unhandled_pending_rejection_to_capture; + vm.unhandled_pending_rejection_to_capture = &return_value; + vm.onUnhandledRejection = &VirtualMachine.onQuietUnhandledRejectionHandlerCaptureValue; + const return_value_from_fucntion: JSValue = value.call(globalObject, &.{}); + vm.unhandled_pending_rejection_to_capture = prev_unhandled_pending_rejection_to_capture; + + if (return_value == .zero) { + return_value = return_value_from_fucntion; + } + + if (return_value.asAnyPromise()) |promise| { + globalObject.bunVM().waitForPromise(promise); + scope.apply(vm); + const promise_result = promise.result(globalObject.vm()); + + switch (promise.status(globalObject.vm())) { + .Fulfilled => { + break :brk null; + }, + .Rejected => { + // since we know for sure it rejected, we should always return the error + break :brk promise_result.toError() orelse promise_result; + }, + .Pending => unreachable, + } + } + scope.apply(vm); + + break :brk return_value.toError(); + }; + + const did_throw = result_ != null; + + if (not) { + const signature = comptime getSignature("toThrow", "expected", true); + + if (!did_throw) return thisValue; + + const result: JSValue = result_.?; + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + + if (expected_value.isEmpty()) { + const signature_no_args = comptime getSignature("toThrow", "", true); + if (result.toError()) |err| { + const name = err.get(globalObject, "name") orelse JSValue.undefined; + const message = err.get(globalObject, "message") orelse JSValue.undefined; + const fmt = signature_no_args ++ "\n\nError name: {any}\nError message: {any}\n"; + globalObject.throwPretty(fmt, .{ + name.toFmt(globalObject, &formatter), + message.toFmt(globalObject, &formatter), + }); + return .zero; + } + + // non error thrown + const fmt = signature_no_args ++ "\n\nThrown value: {any}\n"; + globalObject.throwPretty(fmt, .{result.toFmt(globalObject, &formatter)}); + return .zero; + } + + if (expected_value.isString()) { + const received_message = result.getIfPropertyExistsImpl(globalObject, "message", 7); + + // TODO: remove this allocation + // partial match + { + const expected_slice = expected_value.toSliceOrNull(globalObject) orelse return .zero; + defer expected_slice.deinit(); + const received_slice = received_message.toSliceOrNull(globalObject) orelse return .zero; + defer received_slice.deinit(); + if (!strings.contains(received_slice.slice(), expected_slice.slice())) return thisValue; + } + + const fmt = signature ++ "\n\nExpected substring: not {any}\nReceived message: {any}\n"; + globalObject.throwPretty(fmt, .{ + expected_value.toFmt(globalObject, &formatter), + received_message.toFmt(globalObject, &formatter), + }); + return .zero; + } + + if (expected_value.isRegExp()) { + const received_message = result.getIfPropertyExistsImpl(globalObject, "message", 7); + + // TODO: REMOVE THIS GETTER! Expose a binding to call .test on the RegExp object directly. + if (expected_value.get(globalObject, "test")) |test_fn| { + const matches = test_fn.callWithThis(globalObject, expected_value, &.{received_message}); + if (!matches.toBooleanSlow(globalObject)) return thisValue; + } + + const fmt = signature ++ "\n\nExpected pattern: not {any}\nReceived message: {any}\n"; + globalObject.throwPretty(fmt, .{ + expected_value.toFmt(globalObject, &formatter), + received_message.toFmt(globalObject, &formatter), + }); + return .zero; + } + + if (expected_value.get(globalObject, "message")) |expected_message| { + const received_message = result.getIfPropertyExistsImpl(globalObject, "message", 7); + // no partial match for this case + if (!expected_message.isSameValue(received_message, globalObject)) return thisValue; + + const fmt = signature ++ "\n\nExpected message: not {any}\n"; + globalObject.throwPretty(fmt, .{expected_message.toFmt(globalObject, &formatter)}); + return .zero; + } + + if (!result.isInstanceOf(globalObject, expected_value)) return thisValue; + + var expected_class = ZigString.Empty; + expected_value.getClassName(globalObject, &expected_class); + const received_message = result.getIfPropertyExistsImpl(globalObject, "message", 7); + const fmt = signature ++ "\n\nExpected constructor: not {s}\n\nReceived message: {any}\n"; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_class, received_message.toFmt(globalObject, &formatter) }); + return .zero; + } + globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_class, received_message.toFmt(globalObject, &formatter) }); + return .zero; + } + + const signature = comptime getSignature("toThrow", "expected", false); + if (did_throw) { + if (expected_value.isEmpty()) return thisValue; + + const result: JSValue = if (result_.?.toError()) |r| + r + else + result_.?; + + const _received_message: ?JSValue = if (result.isObject()) + result.get(globalObject, "message") + else if (result.toStringOrNull(globalObject)) |js_str| + JSC.JSValue.fromCell(js_str) + else + null; + + if (expected_value.isString()) { + if (_received_message) |received_message| { + // TODO: remove this allocation + // partial match + const expected_slice = expected_value.toSliceOrNull(globalObject) orelse return .zero; + defer expected_slice.deinit(); + const received_slice = received_message.toSlice(globalObject, globalObject.allocator()); + defer received_slice.deinit(); + if (strings.contains(received_slice.slice(), expected_slice.slice())) return thisValue; + } + + // error: message from received error does not match expected string + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + + if (_received_message) |received_message| { + const expected_value_fmt = expected_value.toFmt(globalObject, &formatter); + const received_message_fmt = received_message.toFmt(globalObject, &formatter); + const fmt = signature ++ "\n\n" ++ "Expected substring: {any}\nReceived message: {any}\n"; + globalObject.throwPretty(fmt, .{ expected_value_fmt, received_message_fmt }); + return .zero; + } + + const expected_fmt = expected_value.toFmt(globalObject, &formatter); + const received_fmt = result.toFmt(globalObject, &formatter); + const fmt = signature ++ "\n\n" ++ "Expected substring: {any}\nReceived value: {any}"; + globalObject.throwPretty(fmt, .{ expected_fmt, received_fmt }); + + return .zero; + } + + if (expected_value.isRegExp()) { + if (_received_message) |received_message| { + // TODO: REMOVE THIS GETTER! Expose a binding to call .test on the RegExp object directly. + if (expected_value.get(globalObject, "test")) |test_fn| { + const matches = test_fn.callWithThis(globalObject, expected_value, &.{received_message}); + if (matches.toBooleanSlow(globalObject)) return thisValue; + } + } + + // error: message from received error does not match expected pattern + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + + if (_received_message) |received_message| { + const expected_value_fmt = expected_value.toFmt(globalObject, &formatter); + const received_message_fmt = received_message.toFmt(globalObject, &formatter); + const fmt = signature ++ "\n\n" ++ "Expected pattern: {any}\nReceived message: {any}\n"; + globalObject.throwPretty(fmt, .{ expected_value_fmt, received_message_fmt }); + + return .zero; + } + + const expected_fmt = expected_value.toFmt(globalObject, &formatter); + const received_fmt = result.toFmt(globalObject, &formatter); + const fmt = signature ++ "\n\n" ++ "Expected pattern: {any}\nReceived value: {any}"; + globalObject.throwPretty(fmt, .{ expected_fmt, received_fmt }); + return .zero; + } + + // If it's not an object, we are going to crash here. + std.debug.assert(expected_value.isObject()); + + if (expected_value.get(globalObject, "message")) |expected_message| { + if (_received_message) |received_message| { + if (received_message.isSameValue(expected_message, globalObject)) return thisValue; + } + + // error: message from received error does not match expected error message. + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + + if (_received_message) |received_message| { + const expected_fmt = expected_message.toFmt(globalObject, &formatter); + const received_fmt = received_message.toFmt(globalObject, &formatter); + const fmt = signature ++ "\n\nExpected message: {any}\nReceived message: {any}\n"; + globalObject.throwPretty(fmt, .{ expected_fmt, received_fmt }); + return .zero; + } + + const expected_fmt = expected_message.toFmt(globalObject, &formatter); + const received_fmt = result.toFmt(globalObject, &formatter); + const fmt = signature ++ "\n\nExpected message: {any}\nReceived value: {any}\n"; + globalObject.throwPretty(fmt, .{ expected_fmt, received_fmt }); + return .zero; + } + + if (result.isInstanceOf(globalObject, expected_value)) return thisValue; + + // error: received error not instance of received error constructor + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + var expected_class = ZigString.Empty; + var received_class = ZigString.Empty; + expected_value.getClassName(globalObject, &expected_class); + result.getClassName(globalObject, &received_class); + const fmt = signature ++ "\n\nExpected constructor: {s}\nReceived constructor: {s}\n\n"; + + if (_received_message) |received_message| { + const message_fmt = fmt ++ "Received message: {any}\n"; + const received_message_fmt = received_message.toFmt(globalObject, &formatter); + + globalObject.throwPretty(message_fmt, .{ + expected_class, + received_class, + received_message_fmt, + }); + return .zero; + } + + const received_fmt = result.toFmt(globalObject, &formatter); + const value_fmt = fmt ++ "Received value: {any}\n"; + + globalObject.throwPretty(value_fmt, .{ + expected_class, + received_class, + received_fmt, + }); + return .zero; + } + + // did not throw + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + const received_line = "Received function did not throw\n"; + + if (expected_value.isEmpty()) { + const fmt = comptime getSignature("toThrow", "", false) ++ "\n\n" ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{}); + return .zero; + } + globalObject.throw(Output.prettyFmt(fmt, false), .{}); + return .zero; + } + + if (expected_value.isString()) { + const expected_fmt = "\n\nExpected substring: {any}\n\n" ++ received_line; + const fmt = signature ++ expected_fmt; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{expected_value.toFmt(globalObject, &formatter)}); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{expected_value.toFmt(globalObject, &formatter)}); + return .zero; + } + + if (expected_value.isRegExp()) { + const expected_fmt = "\n\nExpected pattern: {any}\n\n" ++ received_line; + const fmt = signature ++ expected_fmt; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{expected_value.toFmt(globalObject, &formatter)}); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{expected_value.toFmt(globalObject, &formatter)}); + return .zero; + } + + if (expected_value.get(globalObject, "message")) |expected_message| { + const expected_fmt = "\n\nExpected message: {any}\n\n" ++ received_line; + const fmt = signature ++ expected_fmt; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{expected_message.toFmt(globalObject, &formatter)}); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{expected_message.toFmt(globalObject, &formatter)}); + return .zero; + } + + const expected_fmt = "\n\nExpected constructor: {s}\n\n" ++ received_line; + var expected_class = ZigString.Empty; + expected_value.getClassName(globalObject, &expected_class); + const fmt = signature ++ expected_fmt; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{expected_class}); + return .zero; + } + globalObject.throw(Output.prettyFmt(fmt, true), .{expected_class}); + return .zero; + } + + pub fn toMatchSnapshot(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalObject); + const thisValue = callFrame.this(); + const _arguments = callFrame.arguments(2); + const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + if (not) { + const signature = comptime getSignature("toMatchSnapshot", "", true); + const fmt = signature ++ "\n\nMatcher error: Snapshot matchers cannot be used with not\n"; + globalObject.throwPretty(fmt, .{}); + } + + var hint_string: ZigString = ZigString.Empty; + var property_matchers: ?JSValue = null; + switch (arguments.len) { + 0 => {}, + 1 => { + if (arguments[0].isString()) { + arguments[0].toZigString(&hint_string, globalObject); + } else if (arguments[0].isObject()) { + property_matchers = arguments[0]; + } + }, + else => { + if (!arguments[0].isObject()) { + const signature = comptime getSignature("toMatchSnapshot", "properties, hint", false); + const fmt = signature ++ "\n\nMatcher error: Expected properties must be an object\n"; + globalObject.throwPretty(fmt, .{}); + return .zero; + } + + property_matchers = arguments[0]; + + if (arguments[1].isString()) { + arguments[1].toZigString(&hint_string, globalObject); + } + }, + } + + var hint = hint_string.toSlice(default_allocator); + defer hint.deinit(); + + const value: JSValue = this.getValue(globalObject, thisValue, "toMatchSnapshot", "properties, hint") orelse return .zero; + + if (!value.isObject() and property_matchers != null) { + const signature = comptime getSignature("toMatchSnapshot", "properties, hint", false); + const fmt = signature ++ "\n\nMatcher error: received values must be an object when the matcher has properties\n"; + globalObject.throwPretty(fmt, .{}); + return .zero; + } + + if (property_matchers) |_prop_matchers| { + var prop_matchers = _prop_matchers; + + if (!value.jestDeepMatch(prop_matchers, globalObject, true)) { + // TODO: print diff with properties from propertyMatchers + const signature = comptime getSignature("toMatchSnapshot", "propertyMatchers", false); + const fmt = signature ++ "\n\nExpected propertyMatchers to match properties from received object" ++ + "\n\nReceived: {any}\n"; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject }; + globalObject.throwPretty(fmt, .{value.toFmt(globalObject, &formatter)}); + return .zero; + } + } + + const result = Jest.runner.?.snapshots.getOrPut(this, value, hint.slice(), globalObject) catch |err| { + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject }; + const test_file_path = Jest.runner.?.files.get(this.scope.file_id).source.path.text; + switch (err) { + error.FailedToOpenSnapshotFile => globalObject.throw("Failed to open snapshot file for test file: {s}", .{test_file_path}), + error.FailedToMakeSnapshotDirectory => globalObject.throw("Failed to make snapshot directory for test file: {s}", .{test_file_path}), + error.FailedToWriteSnapshotFile => globalObject.throw("Failed write to snapshot file: {s}", .{test_file_path}), + error.ParseError => globalObject.throw("Failed to parse snapshot file for: {s}", .{test_file_path}), + else => globalObject.throw("Failed to snapshot value: {any}", .{value.toFmt(globalObject, &formatter)}), + } + return .zero; + }; + + if (result) |saved_value| { + var pretty_value: MutableString = MutableString.init(default_allocator, 0) catch unreachable; + value.jestSnapshotPrettyFormat(&pretty_value, globalObject) catch { + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject }; + globalObject.throw("Failed to pretty format value: {s}", .{value.toFmt(globalObject, &formatter)}); + return .zero; + }; + defer pretty_value.deinit(); + + if (strings.eqlLong(pretty_value.toOwnedSliceLeaky(), saved_value, true)) { + Jest.runner.?.snapshots.passed += 1; + return thisValue; + } + + Jest.runner.?.snapshots.failed += 1; + const signature = comptime getSignature("toMatchSnapshot", "expected", false); + const fmt = signature ++ "\n\n{any}\n"; + const diff_format = DiffFormatter{ + .received_string = pretty_value.toOwnedSliceLeaky(), + .expected_string = saved_value, + .globalObject = globalObject, + }; + + globalObject.throwPretty(fmt, .{diff_format}); + return .zero; + } + + return thisValue; + } + + pub fn toBeEmpty(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { + defer this.postMatch(globalObject); + + const thisValue = callFrame.this(); + const value: JSValue = this.getValue(globalObject, thisValue, "toBeEmpty", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + var pass = false; + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + + const actual_length = value.getLengthIfPropertyExistsInternal(globalObject); + + if (actual_length == std.math.inf(f64)) { + if (value.jsTypeLoose().isObject()) { + if (value.isIterable(globalObject)) { + var any_properties_in_iterator = false; + value.forEach(globalObject, &any_properties_in_iterator, struct { + pub fn anythingInIterator( + _: *JSC.VM, + _: *JSGlobalObject, + any_: ?*anyopaque, + _: JSValue, + ) callconv(.C) void { + bun.cast(*bool, any_.?).* = true; + } + }.anythingInIterator); + pass = !any_properties_in_iterator; + } else { + var props_iter = JSC.JSPropertyIterator(.{ + .skip_empty_name = false, + + .include_value = true, + }).init(globalObject, value.asObjectRef()); + defer props_iter.deinit(); + pass = props_iter.len == 0; + } + } else { + const signature = comptime getSignature("toBeEmpty", "", false); + const fmt = signature ++ "\n\nExpected value to be a string, object, or iterable" ++ + "\n\nReceived: {any}\n"; + globalObject.throwPretty(fmt, .{value.toFmt(globalObject, &formatter)}); + return .zero; + } + } else if (std.math.isNan(actual_length)) { + globalObject.throw("Received value has non-number length property: {}", .{actual_length}); + return .zero; + } else { + pass = actual_length == 0; + } + + if (not and pass) { + const signature = comptime getSignature("toBeEmpty", "", true); + const fmt = signature ++ "\n\nExpected value not to be a string, object, or iterable" ++ + "\n\nReceived: {any}\n"; + globalObject.throwPretty(fmt, .{value.toFmt(globalObject, &formatter)}); + return .zero; + } + + if (not) pass = !pass; + if (pass) return thisValue; + + if (not) { + const signature = comptime getSignature("toBeEmpty", "", true); + const fmt = signature ++ "\n\nExpected value not to be empty" ++ + "\n\nReceived: {any}\n"; + globalObject.throwPretty(fmt, .{value.toFmt(globalObject, &formatter)}); + return .zero; + } + + const signature = comptime getSignature("toBeEmpty", "", false); + const fmt = signature ++ "\n\nExpected value to be empty" ++ + "\n\nReceived: {any}\n"; + globalObject.throwPretty(fmt, .{value.toFmt(globalObject, &formatter)}); + return .zero; + } + + pub fn toBeNil(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value: JSValue = this.getValue(globalThis, thisValue, "toBeNil", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + const pass = value.isUndefinedOrNull() != not; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeNil", "", true) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeNil", "", false) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeArray(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value: JSValue = this.getValue(globalThis, thisValue, "toBeArray", "") orelse return .zero; + + if (this.scope.tests.items.len <= this.test_id) { + globalThis.throw("toBeArray() must be called in a test", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + const pass = value.jsType().isArray() != not; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeArray", "", true) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeArray", "", false) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeArrayOfSize(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const _arguments = callFrame.arguments(1); + const arguments = _arguments.ptr[0.._arguments.len]; + + if (arguments.len < 1) { + globalThis.throwInvalidArguments("toBeArrayOfSize() requires 1 argument", .{}); + return .zero; + } + + const value: JSValue = this.getValue(globalThis, thisValue, "toBeArrayOfSize", "") orelse return .zero; + + if (this.scope.tests.items.len <= this.test_id) { + globalThis.throw("toBeArrayOfSize() must be called in a test", .{}); + return .zero; + } + + const size = arguments[0]; + size.ensureStillAlive(); + + if (!size.isAnyInt()) { + globalThis.throw("toBeArrayOfSize() requires the first argument to be a number", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + var pass = value.jsType().isArray() and @intCast(i32, value.getLength(globalThis)) == size.toInt32(); + + if (not) pass = !pass; + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeArrayOfSize", "", true) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeArrayOfSize", "", false) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeBoolean(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value: JSValue = this.getValue(globalThis, thisValue, "toBeBoolean", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + const pass = value.isBoolean() != not; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeBoolean", "", true) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeBoolean", "", false) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeTypeOf(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const _arguments = callFrame.arguments(1); + const arguments = _arguments.ptr[0.._arguments.len]; + + if (arguments.len < 1) { + globalThis.throwInvalidArguments("toBeTypeOf() requires 1 argument", .{}); + return .zero; + } + + if (this.scope.tests.items.len <= this.test_id) { + globalThis.throw("toBeTypeOf() must be called in a test", .{}); + return .zero; + } + + const value: JSValue = this.getValue(globalThis, thisValue, "toBeTypeOf", "") orelse return .zero; + + const expected = arguments[0]; + expected.ensureStillAlive(); + + const expectedAsStr = expected.toString(globalThis).toSlice(globalThis, default_allocator).slice(); + active_test_expectation_counter.actual += 1; + + if (!expected.isString()) { + globalThis.throwInvalidArguments("toBeTypeOf() requires a string argument", .{}); + return .zero; + } + + if (!std.mem.eql(u8, expectedAsStr, "function") and + !std.mem.eql(u8, expectedAsStr, "object") and + !std.mem.eql(u8, expectedAsStr, "bigint") and + !std.mem.eql(u8, expectedAsStr, "boolean") and + !std.mem.eql(u8, expectedAsStr, "number") and + !std.mem.eql(u8, expectedAsStr, "string") and + !std.mem.eql(u8, expectedAsStr, "symbol") and + !std.mem.eql(u8, expectedAsStr, "undefined")) + { + globalThis.throwInvalidArguments("toBeTypeOf() requires a valid type string argument ('function', 'object', 'bigint', 'boolean', 'number', 'string', 'symbol', 'undefined')", .{}); + return .zero; + } + + const not = this.flags.not; + var pass = false; + var whatIsTheType: []const u8 = ""; + + // Checking for function/class should be done before everything else, or it will fail. + if (value.isCallable(globalThis.vm())) { + whatIsTheType = "function"; + } else if (value.isObject() or value.jsType().isArray() or value.isNull()) { + whatIsTheType = "object"; + } else if (value.isBigInt()) { + whatIsTheType = "bigint"; + } else if (value.isBoolean()) { + whatIsTheType = "boolean"; + } else if (value.isNumber()) { + whatIsTheType = "number"; + } else if (value.jsType().isString()) { + whatIsTheType = "string"; + } else if (value.isSymbol()) { + whatIsTheType = "symbol"; + } else if (value.isUndefined()) { + whatIsTheType = "undefined"; + } else { + globalThis.throw("Internal consistency error: unknown JSValue type", .{}); + return .zero; + } + + pass = std.mem.eql(u8, expectedAsStr, whatIsTheType); + + if (not) pass = !pass; + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + const expected_str = expected.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeTypeOf", "", true) ++ "\n\n" ++ "Expected type: not {any}\n" ++ "Received type: \"{s}\"\nReceived value: {any}\n"; + globalThis.throwPretty(fmt, .{ expected_str, whatIsTheType, received }); + return .zero; + } + + const fmt = comptime getSignature("toBeTypeOf", "", false) ++ "\n\n" ++ "Expected type: {any}\n" ++ "Received type: \"{s}\"\nReceived value: {any}\n"; + globalThis.throwPretty(fmt, .{ expected_str, whatIsTheType, received }); + return .zero; + } + + pub fn toBeTrue(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value: JSValue = this.getValue(globalThis, thisValue, "toBeTrue", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + const pass = (value.isBoolean() and value.toBoolean()) != not; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeTrue", "", true) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeTrue", "", false) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeFalse(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value: JSValue = this.getValue(globalThis, thisValue, "toBeFalse", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + const pass = (value.isBoolean() and !value.toBoolean()) != not; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeFalse", "", true) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeFalse", "", false) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeNumber(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value: JSValue = this.getValue(globalThis, thisValue, "toBeNumber", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + const pass = value.isNumber() != not; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeNumber", "", true) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeNumber", "", false) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeInteger(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value: JSValue = this.getValue(globalThis, thisValue, "toBeInteger", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + const pass = value.isAnyInt() != not; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeInteger", "", true) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeInteger", "", false) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeFinite(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value: JSValue = this.getValue(globalThis, thisValue, "toBeFinite", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + var pass = value.isNumber(); + if (pass) { + const num: f64 = value.asNumber(); + pass = std.math.isFinite(num) and !std.math.isNan(num); + } + + const not = this.flags.not; + if (not) pass = !pass; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeFinite", "", true) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeFinite", "", false) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBePositive(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value: JSValue = this.getValue(globalThis, thisValue, "toBePositive", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + var pass = value.isNumber(); + if (pass) { + const num: f64 = value.asNumber(); + pass = @round(num) > 0 and !std.math.isInf(num) and !std.math.isNan(num); + } + + const not = this.flags.not; + if (not) pass = !pass; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBePositive", "", true) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBePositive", "", false) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeNegative(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value: JSValue = this.getValue(globalThis, thisValue, "toBeNegative", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + var pass = value.isNumber(); + if (pass) { + const num: f64 = value.asNumber(); + pass = @round(num) < 0 and !std.math.isInf(num) and !std.math.isNan(num); + } + + const not = this.flags.not; + if (not) pass = !pass; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeNegative", "", true) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeNegative", "", false) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeWithin(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const _arguments = callFrame.arguments(2); + const arguments = _arguments.ptr[0.._arguments.len]; + + if (arguments.len < 1) { + globalThis.throwInvalidArguments("toBeWithin() requires 2 arguments", .{}); + return .zero; + } + + const value: JSValue = this.getValue(globalThis, thisValue, "toBeWithin", "start, end") orelse return .zero; + + const startValue = arguments[0]; + startValue.ensureStillAlive(); + + if (!startValue.isNumber()) { + globalThis.throw("toBeWithin() requires the first argument to be a number", .{}); + return .zero; + } + + const endValue = arguments[1]; + endValue.ensureStillAlive(); + + if (!endValue.isNumber()) { + globalThis.throw("toBeWithin() requires the second argument to be a number", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + var pass = value.isNumber(); + if (pass) { + const num = value.asNumber(); + pass = num >= startValue.asNumber() and num < endValue.asNumber(); + } + + const not = this.flags.not; + if (not) pass = !pass; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const start_fmt = startValue.toFmt(globalThis, &formatter); + const end_fmt = endValue.toFmt(globalThis, &formatter); + const received_fmt = value.toFmt(globalThis, &formatter); + + if (not) { + const expected_line = "Expected: not between {any} (inclusive) and {any} (exclusive)\n"; + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeWithin", "start, end", true) ++ "\n\n" ++ expected_line ++ received_line; + globalThis.throwPretty(fmt, .{ start_fmt, end_fmt, received_fmt }); + return .zero; + } + + const expected_line = "Expected: between {any} (inclusive) and {any} (exclusive)\n"; + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeWithin", "start, end", false) ++ "\n\n" ++ expected_line ++ received_line; + globalThis.throwPretty(fmt, .{ start_fmt, end_fmt, received_fmt }); + return .zero; + } + + pub fn toBeSymbol(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value: JSValue = this.getValue(globalThis, thisValue, "toBeSymbol", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + const pass = value.isSymbol() != not; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeSymbol", "", true) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeSymbol", "", false) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeFunction(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value: JSValue = this.getValue(globalThis, thisValue, "toBeFunction", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + const pass = value.isCallable(globalThis.vm()) != not; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeFunction", "", true) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeFunction", "", false) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeDate(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value: JSValue = this.getValue(globalThis, thisValue, "toBeDate", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + const pass = value.isDate() != not; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeDate", "", true) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeDate", "", false) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeString(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value: JSValue = this.getValue(globalThis, thisValue, "toBeString", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + const pass = value.isString() != not; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeString", "", true) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeString", "", false) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toInclude(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const arguments_ = callFrame.arguments(1); + const arguments = arguments_.ptr[0..arguments_.len]; + + if (arguments.len < 1) { + globalThis.throwInvalidArguments("toInclude() requires 1 argument", .{}); + return .zero; + } + + const expected = arguments[0]; + expected.ensureStillAlive(); + + if (!expected.isString()) { + globalThis.throw("toInclude() requires the first argument to be a string", .{}); + return .zero; + } + + const value: JSValue = this.getValue(globalThis, thisValue, "toInclude", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + var pass = value.isString(); + if (pass) { + const value_string = value.toString(globalThis).toSlice(globalThis, default_allocator).slice(); + const expected_string = expected.toString(globalThis).toSlice(globalThis, default_allocator).slice(); + pass = strings.contains(value_string, expected_string) or expected_string.len == 0; + } + + const not = this.flags.not; + if (not) pass = !pass; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const value_fmt = value.toFmt(globalThis, &formatter); + const expected_fmt = expected.toFmt(globalThis, &formatter); + + if (not) { + const expected_line = "Expected to not include: {any}\n"; + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toInclude", "expected", true) ++ "\n\n" ++ expected_line ++ received_line; + globalThis.throwPretty(fmt, .{ expected_fmt, value_fmt }); + return .zero; + } + + const expected_line = "Expected to include: {any}\n"; + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toInclude", "expected", false) ++ "\n\n" ++ expected_line ++ received_line; + globalThis.throwPretty(fmt, .{ expected_fmt, value_fmt }); + return .zero; + } + + pub fn toStartWith(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const arguments_ = callFrame.arguments(1); + const arguments = arguments_.ptr[0..arguments_.len]; + + if (arguments.len < 1) { + globalThis.throwInvalidArguments("toStartWith() requires 1 argument", .{}); + return .zero; + } + + const expected = arguments[0]; + expected.ensureStillAlive(); + + if (!expected.isString()) { + globalThis.throw("toStartWith() requires the first argument to be a string", .{}); + return .zero; + } + + const value: JSValue = this.getValue(globalThis, thisValue, "toStartWith", "expected") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + var pass = value.isString(); + if (pass) { + const value_string = value.toString(globalThis).toSlice(globalThis, default_allocator).slice(); + const expected_string = expected.toString(globalThis).toSlice(globalThis, default_allocator).slice(); + pass = strings.startsWith(value_string, expected_string) or expected_string.len == 0; + } + + const not = this.flags.not; + if (not) pass = !pass; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const value_fmt = value.toFmt(globalThis, &formatter); + const expected_fmt = expected.toFmt(globalThis, &formatter); + + if (not) { + const expected_line = "Expected to not start with: {any}\n"; + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toStartWith", "expected", true) ++ "\n\n" ++ expected_line ++ received_line; + globalThis.throwPretty(fmt, .{ expected_fmt, value_fmt }); + return .zero; + } + + const expected_line = "Expected to start with: {any}\n"; + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toStartWith", "expected", false) ++ "\n\n" ++ expected_line ++ received_line; + globalThis.throwPretty(fmt, .{ expected_fmt, value_fmt }); + return .zero; + } + + pub fn toEndWith(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const arguments_ = callFrame.arguments(1); + const arguments = arguments_.ptr[0..arguments_.len]; + + if (arguments.len < 1) { + globalThis.throwInvalidArguments("toEndWith() requires 1 argument", .{}); + return .zero; + } + + const expected = arguments[0]; + expected.ensureStillAlive(); + + if (!expected.isString()) { + globalThis.throw("toEndWith() requires the first argument to be a string", .{}); + return .zero; + } + + const value: JSValue = this.getValue(globalThis, thisValue, "toEndWith", "expected") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + var pass = value.isString(); + if (pass) { + const value_string = value.toString(globalThis).toSlice(globalThis, default_allocator).slice(); + const expected_string = expected.toString(globalThis).toSlice(globalThis, default_allocator).slice(); + pass = strings.endsWith(value_string, expected_string) or expected_string.len == 0; + } + + const not = this.flags.not; + if (not) pass = !pass; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const value_fmt = value.toFmt(globalThis, &formatter); + const expected_fmt = expected.toFmt(globalThis, &formatter); + + if (not) { + const expected_line = "Expected to not end with: {any}\n"; + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toEndWith", "expected", true) ++ "\n\n" ++ expected_line ++ received_line; + globalThis.throwPretty(fmt, .{ expected_fmt, value_fmt }); + return .zero; + } + + const expected_line = "Expected to end with: {any}\n"; + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toEndWith", "expected", false) ++ "\n\n" ++ expected_line ++ received_line; + globalThis.throwPretty(fmt, .{ expected_fmt, value_fmt }); + return .zero; + } + + pub fn toBeInstanceOf(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalObject); + + const thisValue = callFrame.this(); + const _arguments = callFrame.arguments(1); + const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; + + if (arguments.len < 1) { + globalObject.throwInvalidArguments("toBeInstanceOf() requires 1 argument", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + + const expected_value = arguments[0]; + if (!expected_value.isConstructor()) { + globalObject.throw("Expected value must be a function: {any}", .{expected_value.toFmt(globalObject, &formatter)}); + return .zero; + } + expected_value.ensureStillAlive(); + + const value: JSValue = this.getValue(globalObject, thisValue, "toBeInstanceOf", "expected") orelse return .zero; + + const not = this.flags.not; + var pass = value.isInstanceOf(globalObject, expected_value); + if (not) pass = !pass; + if (pass) return thisValue; + + // handle failure + const expected_fmt = expected_value.toFmt(globalObject, &formatter); + const value_fmt = value.toFmt(globalObject, &formatter); + if (not) { + const expected_line = "Expected constructor: not {any}\n"; + const received_line = "Received value: {any}\n"; + const fmt = comptime getSignature("toBeInstanceOf", "expected", true) ++ "\n\n" ++ expected_line ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt }); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, value_fmt }); + return .zero; + } + + const expected_line = "Expected constructor: {any}\n"; + const received_line = "Received value: {any}\n"; + const fmt = comptime getSignature("toBeInstanceOf", "expected", false) ++ "\n\n" ++ expected_line ++ received_line; + globalObject.throwPretty(fmt, .{ expected_fmt, value_fmt }); + return .zero; + } + + pub fn toMatch(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { + JSC.markBinding(@src()); + + defer this.postMatch(globalObject); + + const thisValue = callFrame.this(); + const _arguments = callFrame.arguments(1); + const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; + + if (arguments.len < 1) { + globalObject.throwInvalidArguments("toMatch() requires 1 argument", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + + const expected_value = arguments[0]; + if (!expected_value.isString() and !expected_value.isRegExp()) { + globalObject.throw("Expected value must be a string or regular expression: {any}", .{expected_value.toFmt(globalObject, &formatter)}); + return .zero; + } + expected_value.ensureStillAlive(); + + const value: JSValue = this.getValue(globalObject, thisValue, "toMatch", "expected") orelse return .zero; + + if (!value.isString()) { + globalObject.throw("Received value must be a string: {any}", .{value.toFmt(globalObject, &formatter)}); + return .zero; + } + + const not = this.flags.not; + var pass: bool = brk: { + if (expected_value.isString()) { + break :brk value.stringIncludes(globalObject, expected_value); + } else if (expected_value.isRegExp()) { + break :brk expected_value.toMatch(globalObject, value); + } + unreachable; + }; + + if (not) pass = !pass; + if (pass) return thisValue; + + // handle failure + const expected_fmt = expected_value.toFmt(globalObject, &formatter); + const value_fmt = value.toFmt(globalObject, &formatter); + + if (not) { + const expected_line = "Expected substring or pattern: not {any}\n"; + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toMatch", "expected", true) ++ "\n\n" ++ expected_line ++ received_line; + globalObject.throwPretty(fmt, .{ expected_fmt, value_fmt }); + return .zero; + } + + const expected_line = "Expected substring or pattern: {any}\n"; + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toMatch", "expected", false) ++ "\n\n" ++ expected_line ++ received_line; + globalObject.throwPretty(fmt, .{ expected_fmt, value_fmt }); + return .zero; + } + + pub fn toHaveBeenCalled(this: *Expect, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSC.JSValue { + JSC.markBinding(@src()); + const thisValue = callframe.this(); + defer this.postMatch(globalObject); + + const value: JSValue = this.getValue(globalObject, thisValue, "toHaveBeenCalled", "") orelse 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.flags.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", "", true); + const fmt = signature ++ "\n\nExpected: not {any}\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", "", true); + const fmt = signature ++ "\n\nExpected {any}\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 { + JSC.markBinding(@src()); + + 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 = this.getValue(globalObject, thisValue, "toHaveBeenCalledTimes", "expected") orelse 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.flags.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("toHaveBeenCalledTimes", "expected", true); + const fmt = signature ++ "\n\nExpected: not {any}\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("toHaveBeenCalledTimes", "expected", true); + const fmt = signature ++ "\n\nExpected {any}\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 toMatchObject(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { + JSC.markBinding(@src()); + + defer this.postMatch(globalObject); + const thisValue = callFrame.this(); + const args = callFrame.arguments(1).slice(); + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + + const received_object: JSValue = this.getValue(globalObject, thisValue, "toMatchObject", "expected") orelse return .zero; + + if (!received_object.isObject()) { + const matcher_error = "\n\nMatcher error: received value must be a non-null object\n"; + if (not) { + const fmt = comptime getSignature("toMatchObject", "expected", true) ++ matcher_error; + globalObject.throwPretty(fmt, .{}); + return .zero; + } + + const fmt = comptime getSignature("toMatchObject", "expected", false) ++ matcher_error; + globalObject.throwPretty(fmt, .{}); + return .zero; + } + + if (args.len < 1 or !args[0].isObject()) { + const matcher_error = "\n\nMatcher error: expected value must be a non-null object\n"; + if (not) { + const fmt = comptime getSignature("toMatchObject", "", true) ++ matcher_error; + globalObject.throwPretty(fmt, .{}); + return .zero; + } + const fmt = comptime getSignature("toMatchObject", "", false) ++ matcher_error; + globalObject.throwPretty(fmt, .{}); + return .zero; + } + + const property_matchers = args[0]; + + var pass = received_object.jestDeepMatch(property_matchers, globalObject, true); + + if (not) pass = !pass; + if (pass) return thisValue; + + // handle failure + const diff_formatter = DiffFormatter{ + .received = received_object, + .expected = property_matchers, + .globalObject = globalObject, + .not = not, + }; + + if (not) { + const signature = comptime getSignature("toMatchObject", "expected", true); + const fmt = signature ++ "\n\n{any}\n"; + globalObject.throwPretty(fmt, .{diff_formatter}); + return .zero; + } + + const signature = comptime getSignature("toMatchObject", "expected", false); + const fmt = signature ++ "\n\n{any}\n"; + globalObject.throwPretty(fmt, .{diff_formatter}); + return .zero; + } + + pub const toHaveBeenCalledWith = notImplementedJSCFn; + pub const toHaveBeenLastCalledWith = notImplementedJSCFn; + pub const toHaveBeenNthCalledWith = notImplementedJSCFn; + pub const toHaveReturnedTimes = notImplementedJSCFn; + pub const toHaveReturnedWith = notImplementedJSCFn; + pub const toHaveLastReturnedWith = notImplementedJSCFn; + pub const toHaveNthReturnedWith = notImplementedJSCFn; + pub const toContainEqual = notImplementedJSCFn; + pub const toMatchInlineSnapshot = notImplementedJSCFn; + pub const toThrowErrorMatchingSnapshot = notImplementedJSCFn; + pub const toThrowErrorMatchingInlineSnapshot = notImplementedJSCFn; + + pub const getStaticNot = notImplementedStaticProp; + pub const getStaticResolves = notImplementedStaticProp; + pub const getStaticRejects = notImplementedStaticProp; + + pub fn any(globalObject: *JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { + return ExpectAny.call(globalObject, callFrame); + } + + pub fn anything(globalObject: *JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { + return ExpectAnything.call(globalObject, callFrame); + } + + pub fn stringContaining(globalObject: *JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { + return ExpectStringContaining.call(globalObject, callFrame); + } + + pub fn stringMatching(globalObject: *JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { + return ExpectStringMatching.call(globalObject, callFrame); + } + + pub const extend = notImplementedStaticFn; + pub const arrayContaining = notImplementedStaticFn; + pub const assertions = notImplementedStaticFn; + pub const hasAssertions = notImplementedStaticFn; + pub const objectContaining = notImplementedStaticFn; + pub const addSnapshotSerializer = notImplementedStaticFn; + + pub fn notImplementedJSCFn(_: *Expect, globalObject: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue { + globalObject.throw("Not implemented", .{}); + return .zero; + } + + pub fn notImplementedStaticFn(globalObject: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue { + globalObject.throw("Not implemented", .{}); + return .zero; + } + + pub fn notImplementedJSCProp(_: *Expect, _: JSC.JSValue, globalObject: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue { + globalObject.throw("Not implemented", .{}); + return .zero; + } + + pub fn notImplementedStaticProp(globalObject: *JSC.JSGlobalObject, _: JSC.JSValue, _: JSC.JSValue) callconv(.C) JSC.JSValue { + globalObject.throw("Not implemented", .{}); + return .zero; + } + + pub fn postMatch(_: *Expect, globalObject: *JSC.JSGlobalObject) void { + var vm = globalObject.bunVM(); + vm.autoGarbageCollect(); + } +}; + +pub const ExpectAnything = struct { + pub usingnamespace JSC.Codegen.JSExpectAnything; + + pub fn finalize( + this: *ExpectAnything, + ) callconv(.C) void { + VirtualMachine.get().allocator.destroy(this); + } + + pub fn call(globalObject: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSValue { + const anything = globalObject.bunVM().allocator.create(ExpectAnything) catch unreachable; + if (Jest.runner.?.pending_test == null) { + const err = globalObject.createErrorInstance("expect.anything() must be called in a test", .{}); + err.put(globalObject, ZigString.static("name"), ZigString.init("TestNotRunningError").toValueGC(globalObject)); + globalObject.throwValue(err); + return .zero; + } + + const anything_js_value = anything.toJS(globalObject); + anything_js_value.ensureStillAlive(); + + var vm = globalObject.bunVM(); + vm.autoGarbageCollect(); + + return anything_js_value; + } +}; + +pub const ExpectStringMatching = struct { + pub usingnamespace JSC.Codegen.JSExpectStringMatching; + + pub fn finalize( + this: *ExpectStringMatching, + ) callconv(.C) void { + VirtualMachine.get().allocator.destroy(this); + } + + pub fn call(globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { + const args = callFrame.arguments(1).slice(); + + if (args.len == 0 or (!args[0].isString() and !args[0].isRegExp())) { + const fmt = "expect.stringContaining(string)\n\nExpected a string or regular expression\n"; + globalObject.throwPretty(fmt, .{}); + return .zero; + } + + const test_value = args[0]; + const string_matching = globalObject.bunVM().allocator.create(ExpectStringMatching) catch unreachable; + + if (Jest.runner.?.pending_test == null) { + const err = globalObject.createErrorInstance("expect.stringContaining() must be called in a test", .{}); + err.put(globalObject, ZigString.static("name"), ZigString.init("TestNotRunningError").toValueGC(globalObject)); + globalObject.throwValue(err); + return .zero; + } + + const string_matching_js_value = string_matching.toJS(globalObject); + ExpectStringMatching.testValueSetCached(string_matching_js_value, globalObject, test_value); + + var vm = globalObject.bunVM(); + vm.autoGarbageCollect(); + return string_matching_js_value; + } +}; + +pub const ExpectStringContaining = struct { + pub usingnamespace JSC.Codegen.JSExpectStringContaining; + + pub fn finalize( + this: *ExpectStringContaining, + ) callconv(.C) void { + VirtualMachine.get().allocator.destroy(this); + } + + pub fn call(globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { + const args = callFrame.arguments(1).slice(); + + if (args.len == 0 or !args[0].isString()) { + const fmt = "expect.stringContaining(string)\n\nExpected a string\n"; + globalObject.throwPretty(fmt, .{}); + return .zero; + } + + const string_value = args[0]; + + const string_containing = globalObject.bunVM().allocator.create(ExpectStringContaining) catch unreachable; + + if (Jest.runner.?.pending_test == null) { + const err = globalObject.createErrorInstance("expect.stringContaining() must be called in a test", .{}); + err.put(globalObject, ZigString.static("name"), ZigString.init("TestNotRunningError").toValueGC(globalObject)); + globalObject.throwValue(err); + return .zero; + } + + const string_containing_js_value = string_containing.toJS(globalObject); + ExpectStringContaining.stringValueSetCached(string_containing_js_value, globalObject, string_value); + + var vm = globalObject.bunVM(); + vm.autoGarbageCollect(); + return string_containing_js_value; + } +}; + +pub const ExpectAny = struct { + pub usingnamespace JSC.Codegen.JSExpectAny; + + pub fn finalize( + this: *ExpectAny, + ) callconv(.C) void { + VirtualMachine.get().allocator.destroy(this); + } + + pub fn call(globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { + const _arguments = callFrame.arguments(1); + const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; + + if (arguments.len == 0) { + globalObject.throw("any() expects to be passed a constructor function.", .{}); + return .zero; + } + + const constructor = arguments[0]; + constructor.ensureStillAlive(); + if (!constructor.isConstructor()) { + const fmt = "expect.any(constructor)\n\nExpected a constructor\n"; + globalObject.throwPretty(fmt, .{}); + return .zero; + } + + var any = globalObject.bunVM().allocator.create(ExpectAny) catch unreachable; + + if (Jest.runner.?.pending_test == null) { + const err = globalObject.createErrorInstance("expect.any() must be called in a test", .{}); + err.put(globalObject, ZigString.static("name"), ZigString.init("TestNotRunningError").toValueGC(globalObject)); + globalObject.throwValue(err); + return .zero; + } + + any.* = .{}; + const any_js_value = any.toJS(globalObject); + any_js_value.ensureStillAlive(); + ExpectAny.constructorValueSetCached(any_js_value, globalObject, constructor); + any_js_value.ensureStillAlive(); + + var vm = globalObject.bunVM(); + vm.autoGarbageCollect(); + + return any_js_value; + } +}; + +/// 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/bun.js/test/jest.zig b/src/bun.js/test/jest.zig index 2dc4ae1c4..a1a4f2af8 100644 --- a/src/bun.js/test/jest.zig +++ b/src/bun.js/test/jest.zig @@ -8,7 +8,12 @@ const MimeType = @import("../../http.zig").MimeType; const ZigURL = @import("../../url.zig").URL; const HTTPClient = @import("root").bun.HTTP; const NetworkThread = HTTPClient.NetworkThread; -const Environment = @import("../../env.zig"); +const Environment = bun.Environment; + +const Snapshots = @import("./snapshot.zig").Snapshots; +const expect = @import("./expect.zig"); +const Counter = expect.Counter; +const Expect = expect.Expect; const DiffFormatter = @import("./diff_format.zig").DiffFormatter; @@ -28,8 +33,6 @@ const default_allocator = @import("root").bun.default_allocator; const FeatureFlags = @import("root").bun.FeatureFlags; const ArrayBuffer = @import("../base.zig").ArrayBuffer; const Properties = @import("../base.zig").Properties; -const d = @import("../base.zig").d; -const castObj = @import("../base.zig").castObj; const getAllocator = @import("../base.zig").getAllocator; const ZigString = JSC.ZigString; @@ -43,12 +46,10 @@ const JSObject = JSC.JSObject; const CallFrame = JSC.CallFrame; const VirtualMachine = JSC.VirtualMachine; -const Task = @import("../javascript.zig").Task; - -const Fs = @import("../../fs.zig"); +const Fs = bun.fs; const is_bindgen: bool = std.meta.globalOption("bindgen", bool) orelse false; -const ArrayIdentityContext = @import("../../identity_context.zig").ArrayIdentityContext; +const ArrayIdentityContext = bun.ArrayIdentityContext; pub var test_elapsed_timer: ?*std.time.Timer = null; pub const Tag = enum(u3) { @@ -252,4041 +253,256 @@ pub const TestRunner = struct { pass, fail, skip, - todo, - fail_because_todo_passed, - }; - }; -}; - -pub const Snapshots = struct { - const file_header = "// Bun Snapshot v1, https://goo.gl/fbAQLP\n"; - pub const ValuesHashMap = std.HashMap(usize, string, bun.IdentityContext(usize), std.hash_map.default_max_load_percentage); - - allocator: std.mem.Allocator, - update_snapshots: bool, - total: usize = 0, - added: usize = 0, - passed: usize = 0, - failed: usize = 0, - - file_buf: *std.ArrayList(u8), - values: *ValuesHashMap, - counts: *bun.StringHashMap(usize), - _current_file: ?File = null, - snapshot_dir_path: ?string = null, - - const File = struct { - id: TestRunner.File.ID, - file: std.fs.File, - }; - - pub fn getOrPut(this: *Snapshots, expect: *Expect, value: JSValue, hint: string, globalObject: *JSC.JSGlobalObject) !?string { - switch (try this.getSnapshotFile(expect.scope.file_id)) { - .result => {}, - .err => |err| { - return switch (err.syscall) { - .mkdir => error.FailedToMakeSnapshotDirectory, - .open => error.FailedToOpenSnapshotFile, - else => error.SnapshotFailed, - }; - }, - } - - const snapshot_name = try expect.getSnapshotName(this.allocator, hint); - this.total += 1; - - var count_entry = try this.counts.getOrPut(snapshot_name); - const counter = brk: { - if (count_entry.found_existing) { - this.allocator.free(snapshot_name); - count_entry.value_ptr.* += 1; - break :brk count_entry.value_ptr.*; - } - count_entry.value_ptr.* = 1; - break :brk count_entry.value_ptr.*; - }; - - const name = count_entry.key_ptr.*; - - var counter_string_buf = [_]u8{0} ** 32; - var counter_string = try std.fmt.bufPrint(&counter_string_buf, "{d}", .{counter}); - - var name_with_counter = try this.allocator.alloc(u8, name.len + 1 + counter_string.len); - defer this.allocator.free(name_with_counter); - bun.copy(u8, name_with_counter[0..name.len], name); - name_with_counter[name.len] = ' '; - bun.copy(u8, name_with_counter[name.len + 1 ..], counter_string); - - const name_hash = bun.hash(name_with_counter); - if (this.values.get(name_hash)) |expected| { - return expected; - } - - // doesn't exist. append to file bytes and add to hashmap. - var pretty_value = try MutableString.init(this.allocator, 0); - try value.jestSnapshotPrettyFormat(&pretty_value, globalObject); - - const serialized_length = "\nexports[`".len + name_with_counter.len + "`] = `".len + pretty_value.list.items.len + "`;\n".len; - try this.file_buf.ensureUnusedCapacity(serialized_length); - this.file_buf.appendSliceAssumeCapacity("\nexports[`"); - this.file_buf.appendSliceAssumeCapacity(name_with_counter); - this.file_buf.appendSliceAssumeCapacity("`] = `"); - this.file_buf.appendSliceAssumeCapacity(pretty_value.list.items); - this.file_buf.appendSliceAssumeCapacity("`;\n"); - - this.added += 1; - try this.values.put(name_hash, pretty_value.toOwnedSlice()); - return null; - } - - pub fn parseFile(this: *Snapshots) !void { - if (this.file_buf.items.len == 0) return; - - const vm = VirtualMachine.get(); - var opts = js_parser.Parser.Options.init(vm.bundler.options.jsx, .js); - var temp_log = logger.Log.init(this.allocator); - - const test_file = Jest.runner.?.files.get(this._current_file.?.id); - const test_filename = test_file.source.path.name.filename; - const dir_path = test_file.source.path.name.dirWithTrailingSlash(); - - var snapshot_file_path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; - var remain: []u8 = snapshot_file_path_buf[0..bun.MAX_PATH_BYTES]; - bun.copy(u8, remain, dir_path); - remain = remain[dir_path.len..]; - bun.copy(u8, remain, "__snapshots__/"); - remain = remain["__snapshots__/".len..]; - bun.copy(u8, remain, test_filename); - remain = remain[test_filename.len..]; - bun.copy(u8, remain, ".snap"); - remain = remain[".snap".len..]; - remain[0] = 0; - const snapshot_file_path = snapshot_file_path_buf[0 .. snapshot_file_path_buf.len - remain.len :0]; - - const source = logger.Source.initPathString(snapshot_file_path, this.file_buf.items); - - var parser = try js_parser.Parser.init( - opts, - &temp_log, - &source, - vm.bundler.options.define, - this.allocator, - ); - - var parse_result = try parser.parse(); - var ast = if (parse_result == .ast) parse_result.ast else return error.ParseError; - defer ast.deinit(); - - if (ast.exports_ref.isNull()) return; - const exports_ref = ast.exports_ref; - - // TODO: when common js transform changes, keep this updated or add flag to support this version - - const export_default = brk: { - for (ast.parts.slice()) |part| { - for (part.stmts) |stmt| { - if (stmt.data == .s_export_default and stmt.data.s_export_default.value == .expr) { - break :brk stmt.data.s_export_default.value.expr; - } - } - } - - return; - }; - - if (export_default.data == .e_call) { - const function_call = export_default.data.e_call; - if (function_call.args.len == 2 and function_call.args.ptr[0].data == .e_function) { - const arg_function_stmts = function_call.args.ptr[0].data.e_function.func.body.stmts; - for (arg_function_stmts) |stmt| { - switch (stmt.data) { - .s_expr => |expr| { - if (expr.value.data == .e_binary and expr.value.data.e_binary.op == .bin_assign) { - const left = expr.value.data.e_binary.left; - if (left.data == .e_index and left.data.e_index.index.data == .e_string and left.data.e_index.target.data == .e_identifier) { - const target: js_ast.E.Identifier = left.data.e_index.target.data.e_identifier; - var index: *js_ast.E.String = left.data.e_index.index.data.e_string; - if (target.ref.eql(exports_ref) and expr.value.data.e_binary.right.data == .e_string) { - const key = index.slice(this.allocator); - var value_string = expr.value.data.e_binary.right.data.e_string; - const value = value_string.slice(this.allocator); - defer { - if (!index.isUTF8()) this.allocator.free(key); - if (!value_string.isUTF8()) this.allocator.free(value); - } - const value_clone = try this.allocator.alloc(u8, value.len); - bun.copy(u8, value_clone, value); - const name_hash = bun.hash(key); - try this.values.put(name_hash, value_clone); - } - } - } - }, - else => {}, - } - } - } - } - } - - pub fn writeSnapshotFile(this: *Snapshots) !void { - if (this._current_file) |_file| { - var file = _file; - file.file.writeAll(this.file_buf.items) catch { - return error.FailedToWriteSnapshotFile; - }; - file.file.close(); - this.file_buf.clearAndFree(); - - var value_itr = this.values.valueIterator(); - while (value_itr.next()) |value| { - this.allocator.free(value.*); - } - this.values.clearAndFree(); - - var count_key_itr = this.counts.keyIterator(); - while (count_key_itr.next()) |key| { - this.allocator.free(key.*); - } - this.counts.clearAndFree(); - } - } - - fn getSnapshotFile(this: *Snapshots, file_id: TestRunner.File.ID) !JSC.Maybe(void) { - if (this._current_file == null or this._current_file.?.id != file_id) { - try this.writeSnapshotFile(); - - const test_file = Jest.runner.?.files.get(file_id); - const test_filename = test_file.source.path.name.filename; - const dir_path = test_file.source.path.name.dirWithTrailingSlash(); - - var snapshot_file_path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; - var remain: []u8 = snapshot_file_path_buf[0..bun.MAX_PATH_BYTES]; - bun.copy(u8, remain, dir_path); - remain = remain[dir_path.len..]; - bun.copy(u8, remain, "__snapshots__/"); - remain = remain["__snapshots__/".len..]; - - if (this.snapshot_dir_path == null or !strings.eqlLong(dir_path, this.snapshot_dir_path.?, true)) { - remain[0] = 0; - const snapshot_dir_path = snapshot_file_path_buf[0 .. snapshot_file_path_buf.len - remain.len :0]; - switch (JSC.Node.Syscall.mkdir(snapshot_dir_path, 0o777)) { - .result => this.snapshot_dir_path = dir_path, - .err => |err| { - switch (err.getErrno()) { - std.os.E.EXIST => this.snapshot_dir_path = dir_path, - else => return JSC.Maybe(void){ - .err = err, - }, - } - }, - } - } - - bun.copy(u8, remain, test_filename); - remain = remain[test_filename.len..]; - bun.copy(u8, remain, ".snap"); - remain = remain[".snap".len..]; - remain[0] = 0; - const snapshot_file_path = snapshot_file_path_buf[0 .. snapshot_file_path_buf.len - remain.len :0]; - - var flags: JSC.Node.Mode = std.os.O.CREAT | std.os.O.RDWR; - if (this.update_snapshots) flags |= std.os.O.TRUNC; - const fd = switch (JSC.Node.Syscall.open(snapshot_file_path, flags, 0o644)) { - .result => |_fd| _fd, - .err => |err| return JSC.Maybe(void){ - .err = err, - }, - }; - - var file: File = .{ - .id = file_id, - .file = .{ .handle = fd }, - }; - - if (this.update_snapshots) { - try this.file_buf.appendSlice(file_header); - } else { - const length = try file.file.getEndPos(); - if (length == 0) { - try this.file_buf.appendSlice(file_header); - } else { - const buf = try this.allocator.alloc(u8, length); - _ = try file.file.preadAll(buf, 0); - try this.file_buf.appendSlice(buf); - this.allocator.free(buf); - } - } - - this._current_file = file; - try this.parseFile(); - } - - return JSC.Maybe(void).success; - } -}; - -pub const Jest = struct { - pub var runner: ?*TestRunner = null; - - fn globalHook(comptime name: string) JSC.JSHostFunctionType { - return struct { - pub fn appendGlobalFunctionCallback( - globalThis: *JSC.JSGlobalObject, - callframe: *JSC.CallFrame, - ) callconv(.C) JSValue { - const arguments = callframe.arguments(2); - if (arguments.len < 1) { - globalThis.throwNotEnoughArguments("callback", 1, arguments.len); - return .zero; - } - - const function = arguments.ptr[0]; - if (function.isEmptyOrUndefinedOrNull() or !function.isCallable(globalThis.vm())) { - globalThis.throwInvalidArgumentType(name, "callback", "function"); - return .zero; - } - - if (function.getLength(globalThis) > 0) { - globalThis.throw("done() callback is not implemented in global hooks yet. Please make your function take no arguments", .{}); - return .zero; - } - - function.protect(); - @field(Jest.runner.?.global_callbacks, name).append( - bun.default_allocator, - function, - ) catch unreachable; - return JSC.JSValue.jsUndefined(); - } - }.appendGlobalFunctionCallback; - } - - pub fn Bun__Jest__createTestPreloadObject(globalObject: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue { - JSC.markBinding(@src()); - - var global_hooks_object = JSC.JSValue.createEmptyObject(globalObject, 8); - global_hooks_object.ensureStillAlive(); - - const notSupportedHereFn = struct { - pub fn notSupportedHere( - globalThis: *JSC.JSGlobalObject, - _: *JSC.CallFrame, - ) callconv(.C) JSValue { - globalThis.throw("This function can only be used in a test.", .{}); - return .zero; - } - }.notSupportedHere; - const notSupportedHere = JSC.NewFunction(globalObject, null, 0, notSupportedHereFn, false); - notSupportedHere.ensureStillAlive(); - - inline for (.{ - "expect", - "describe", - "it", - "test", - }) |name| { - global_hooks_object.put(globalObject, ZigString.static(name), notSupportedHere); - } - - inline for (.{ "beforeAll", "beforeEach", "afterAll", "afterEach" }) |name| { - const function = JSC.NewFunction(globalObject, null, 1, globalHook(name), false); - function.ensureStillAlive(); - global_hooks_object.put(globalObject, ZigString.static(name), function); - } - return global_hooks_object; - } - - pub fn Bun__Jest__createTestModuleObject(globalObject: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue { - JSC.markBinding(@src()); - - const module = JSC.JSValue.createEmptyObject(globalObject, 11); - - const test_fn = JSC.NewFunction(globalObject, ZigString.static("test"), 2, TestScope.call, false); - module.put( - globalObject, - ZigString.static("test"), - test_fn, - ); - test_fn.put( - globalObject, - ZigString.static("only"), - JSC.NewFunction(globalObject, ZigString.static("only"), 2, TestScope.only, false), - ); - test_fn.put( - globalObject, - ZigString.static("skip"), - JSC.NewFunction(globalObject, ZigString.static("skip"), 2, TestScope.skip, false), - ); - test_fn.put( - globalObject, - ZigString.static("todo"), - JSC.NewFunction(globalObject, ZigString.static("todo"), 2, TestScope.todo, false), - ); - test_fn.put( - globalObject, - ZigString.static("if"), - JSC.NewFunction(globalObject, ZigString.static("if"), 2, TestScope.callIf, false), - ); - test_fn.put( - globalObject, - ZigString.static("skipIf"), - JSC.NewFunction(globalObject, ZigString.static("skipIf"), 2, TestScope.skipIf, false), - ); - - module.put( - globalObject, - ZigString.static("it"), - test_fn, - ); - const describe = JSC.NewFunction(globalObject, ZigString.static("describe"), 2, DescribeScope.call, false); - describe.put( - globalObject, - ZigString.static("only"), - JSC.NewFunction(globalObject, ZigString.static("only"), 2, DescribeScope.only, false), - ); - describe.put( - globalObject, - ZigString.static("skip"), - JSC.NewFunction(globalObject, ZigString.static("skip"), 2, DescribeScope.skip, false), - ); - describe.put( - globalObject, - ZigString.static("todo"), - JSC.NewFunction(globalObject, ZigString.static("todo"), 2, DescribeScope.todo, false), - ); - describe.put( - globalObject, - ZigString.static("if"), - JSC.NewFunction(globalObject, ZigString.static("if"), 2, DescribeScope.callIf, false), - ); - describe.put( - globalObject, - ZigString.static("skipIf"), - JSC.NewFunction(globalObject, ZigString.static("skipIf"), 2, DescribeScope.skipIf, false), - ); - - module.put( - globalObject, - ZigString.static("describe"), - describe, - ); - - module.put( - globalObject, - ZigString.static("beforeAll"), - JSC.NewRuntimeFunction(globalObject, ZigString.static("beforeAll"), 1, DescribeScope.beforeAll, false), - ); - module.put( - globalObject, - ZigString.static("beforeEach"), - JSC.NewRuntimeFunction(globalObject, ZigString.static("beforeEach"), 1, DescribeScope.beforeEach, false), - ); - module.put( - globalObject, - ZigString.static("afterAll"), - JSC.NewRuntimeFunction(globalObject, ZigString.static("afterAll"), 1, DescribeScope.afterAll, false), - ); - module.put( - globalObject, - ZigString.static("afterEach"), - JSC.NewRuntimeFunction(globalObject, ZigString.static("afterEach"), 1, DescribeScope.afterEach, false), - ); - module.put( - globalObject, - ZigString.static("expect"), - Expect.getConstructor(globalObject), - ); - - const mock_fn = 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_fn); - - const jest = JSValue.createEmptyObject(globalObject, 3); - jest.put(globalObject, ZigString.static("fn"), mock_fn); - 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); - - const vi = JSValue.createEmptyObject(globalObject, 1); - vi.put(globalObject, ZigString.static("fn"), mock_fn); - module.put(globalObject, ZigString.static("vi"), vi); - - 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, - ctx: js.JSContextRef, - _: js.JSObjectRef, - _: js.JSObjectRef, - arguments_: []const js.JSValueRef, - exception: js.ExceptionRef, - ) js.JSValueRef { - JSC.markBinding(@src()); - var runner_ = runner orelse { - JSError(getAllocator(ctx), "Run \"bun test\" to run a test", .{}, ctx, exception); - return js.JSValueMakeUndefined(ctx); - }; - const arguments = @ptrCast([]const JSC.JSValue, arguments_); - - if (arguments.len < 1 or !arguments[0].isString()) { - JSError(getAllocator(ctx), "Bun.jest() expects a string filename", .{}, ctx, exception); - return js.JSValueMakeUndefined(ctx); - } - var str = arguments[0].toSlice(ctx, bun.default_allocator); - defer str.deinit(); - var slice = str.slice(); - - if (str.len == 0 or slice[0] != '/') { - JSError(getAllocator(ctx), "Bun.jest() expects an absolute file path", .{}, ctx, exception); - return js.JSValueMakeUndefined(ctx); - } - var vm = ctx.bunVM(); - if (vm.is_in_preload) { - return Bun__Jest__testPreloadObject(ctx).asObjectRef(); - } - - var filepath = Fs.FileSystem.instance.filename_store.append([]const u8, slice) catch unreachable; - - var scope = runner_.getOrPutFile(filepath); - DescribeScope.active = scope; - DescribeScope.module = scope; - - return Bun__Jest__testModuleObject(ctx).asObjectRef(); - } - - comptime { - if (!JSC.is_bindgen) { - @export(Bun__Jest__createTestModuleObject, .{ .name = "Bun__Jest__createTestModuleObject" }); - @export(Bun__Jest__createTestPreloadObject, .{ .name = "Bun__Jest__createTestPreloadObject" }); - } - } -}; - -pub const ExpectAnything = struct { - pub usingnamespace JSC.Codegen.JSExpectAnything; - - pub fn finalize( - this: *ExpectAnything, - ) callconv(.C) void { - VirtualMachine.get().allocator.destroy(this); - } - - pub fn call(globalObject: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSValue { - const anything = globalObject.bunVM().allocator.create(ExpectAnything) catch unreachable; - if (Jest.runner.?.pending_test == null) { - const err = globalObject.createErrorInstance("expect.anything() must be called in a test", .{}); - err.put(globalObject, ZigString.static("name"), ZigString.init("TestNotRunningError").toValueGC(globalObject)); - globalObject.throwValue(err); - return .zero; - } - - const anything_js_value = anything.toJS(globalObject); - anything_js_value.ensureStillAlive(); - - var vm = globalObject.bunVM(); - vm.autoGarbageCollect(); - - return anything_js_value; - } -}; - -pub const ExpectStringMatching = struct { - pub usingnamespace JSC.Codegen.JSExpectStringMatching; - - pub fn finalize( - this: *ExpectStringMatching, - ) callconv(.C) void { - VirtualMachine.get().allocator.destroy(this); - } - - pub fn call(globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { - const args = callFrame.arguments(1).slice(); - - if (args.len == 0 or (!args[0].isString() and !args[0].isRegExp())) { - const fmt = "expect.stringContaining(string)\n\nExpected a string or regular expression\n"; - globalObject.throwPretty(fmt, .{}); - return .zero; - } - - const test_value = args[0]; - const string_matching = globalObject.bunVM().allocator.create(ExpectStringMatching) catch unreachable; - - if (Jest.runner.?.pending_test == null) { - const err = globalObject.createErrorInstance("expect.stringContaining() must be called in a test", .{}); - err.put(globalObject, ZigString.static("name"), ZigString.init("TestNotRunningError").toValueGC(globalObject)); - globalObject.throwValue(err); - return .zero; - } - - const string_matching_js_value = string_matching.toJS(globalObject); - ExpectStringMatching.testValueSetCached(string_matching_js_value, globalObject, test_value); - - var vm = globalObject.bunVM(); - vm.autoGarbageCollect(); - return string_matching_js_value; - } -}; - -pub const ExpectStringContaining = struct { - pub usingnamespace JSC.Codegen.JSExpectStringContaining; - - pub fn finalize( - this: *ExpectStringContaining, - ) callconv(.C) void { - VirtualMachine.get().allocator.destroy(this); - } - - pub fn call(globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { - const args = callFrame.arguments(1).slice(); - - if (args.len == 0 or !args[0].isString()) { - const fmt = "expect.stringContaining(string)\n\nExpected a string\n"; - globalObject.throwPretty(fmt, .{}); - return .zero; - } - - const string_value = args[0]; - - const string_containing = globalObject.bunVM().allocator.create(ExpectStringContaining) catch unreachable; - - if (Jest.runner.?.pending_test == null) { - const err = globalObject.createErrorInstance("expect.stringContaining() must be called in a test", .{}); - err.put(globalObject, ZigString.static("name"), ZigString.init("TestNotRunningError").toValueGC(globalObject)); - globalObject.throwValue(err); - return .zero; - } - - const string_containing_js_value = string_containing.toJS(globalObject); - ExpectStringContaining.stringValueSetCached(string_containing_js_value, globalObject, string_value); - - var vm = globalObject.bunVM(); - vm.autoGarbageCollect(); - return string_containing_js_value; - } -}; -pub const ExpectAny = struct { - pub usingnamespace JSC.Codegen.JSExpectAny; - - pub fn finalize( - this: *ExpectAny, - ) callconv(.C) void { - VirtualMachine.get().allocator.destroy(this); - } - - pub fn call(globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { - const _arguments = callFrame.arguments(1); - const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; - - if (arguments.len == 0) { - globalObject.throw("any() expects to be passed a constructor function.", .{}); - return .zero; - } - - const constructor = arguments[0]; - constructor.ensureStillAlive(); - if (!constructor.isConstructor()) { - const fmt = "expect.any(constructor)\n\nExpected a constructor\n"; - globalObject.throwPretty(fmt, .{}); - return .zero; - } - - var any = globalObject.bunVM().allocator.create(ExpectAny) catch unreachable; - - if (Jest.runner.?.pending_test == null) { - const err = globalObject.createErrorInstance("expect.any() must be called in a test", .{}); - err.put(globalObject, ZigString.static("name"), ZigString.init("TestNotRunningError").toValueGC(globalObject)); - globalObject.throwValue(err); - return .zero; - } - - any.* = .{}; - const any_js_value = any.toJS(globalObject); - any_js_value.ensureStillAlive(); - ExpectAny.constructorValueSetCached(any_js_value, globalObject, constructor); - any_js_value.ensureStillAlive(); - - var vm = globalObject.bunVM(); - vm.autoGarbageCollect(); - - return any_js_value; - } -}; - -/// https://jestjs.io/docs/expect -// To support async tests, we need to track the test ID -pub const Expect = struct { - test_id: TestRunner.Test.ID, - scope: *DescribeScope, - op: Op.Set = Op.Set.init(.{}), - - pub usingnamespace JSC.Codegen.JSExpect; - - pub const Op = enum(u3) { - resolves, - rejects, - not, - pub const Set = std.EnumSet(Op); - }; - - pub fn getSnapshotName(this: *Expect, allocator: std.mem.Allocator, hint: string) ![]const u8 { - const test_name = this.scope.tests.items[this.test_id].label; - - var length: usize = 0; - var curr_scope: ?*DescribeScope = this.scope; - while (curr_scope) |scope| { - if (scope.label.len > 0) { - length += scope.label.len + 1; - } - curr_scope = scope.parent; - } - length += test_name.len; - if (hint.len > 0) { - length += hint.len + 2; - } - - var buf = try allocator.alloc(u8, length); - - var index = buf.len; - if (hint.len > 0) { - index -= hint.len; - bun.copy(u8, buf[index..], hint); - index -= test_name.len + 2; - bun.copy(u8, buf[index..], test_name); - bun.copy(u8, buf[index + test_name.len ..], ": "); - } else { - index -= test_name.len; - bun.copy(u8, buf[index..], test_name); - } - // copy describe scopes in reverse order - curr_scope = this.scope; - while (curr_scope) |scope| { - if (scope.label.len > 0) { - index -= scope.label.len + 1; - bun.copy(u8, buf[index..], scope.label); - buf[index + scope.label.len] = ' '; - } - curr_scope = scope.parent; - } - - return buf; - } - - pub fn finalize( - this: *Expect, - ) callconv(.C) void { - VirtualMachine.get().allocator.destroy(this); - } - - pub fn call(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSC.JSValue { - const arguments = callframe.arguments(1); - const value = if (arguments.len < 1) JSC.JSValue.jsUndefined() else arguments.ptr[0]; - - var expect = globalObject.bunVM().allocator.create(Expect) catch unreachable; - - if (Jest.runner.?.pending_test == null) { - const err = globalObject.createErrorInstance("expect() must be called in a test", .{}); - err.put(globalObject, ZigString.static("name"), ZigString.init("TestNotRunningError").toValueGC(globalObject)); - globalObject.throwValue(err); - return .zero; - } - - expect.* = .{ - .scope = Jest.runner.?.pending_test.?.describe, - .test_id = Jest.runner.?.pending_test.?.test_id, - }; - const expect_js_value = expect.toJS(globalObject); - expect_js_value.ensureStillAlive(); - JSC.Jest.Expect.capturedValueSetCached(expect_js_value, globalObject, value); - expect_js_value.ensureStillAlive(); - expect.postMatch(globalObject); - return expect_js_value; - } - - pub fn constructor( - globalObject: *JSC.JSGlobalObject, - callframe: *JSC.CallFrame, - ) callconv(.C) ?*Expect { - _ = callframe.arguments(1); - globalObject.throw("expect() cannot be called with new", .{}); - return null; - } - - /// Object.is() - pub fn toBe( - this: *Expect, - globalObject: *JSC.JSGlobalObject, - callframe: *JSC.CallFrame, - ) callconv(.C) JSC.JSValue { - defer this.postMatch(globalObject); - const thisValue = callframe.this(); - const arguments_ = callframe.arguments(1); - const arguments = arguments_.ptr[0..arguments_.len]; - - if (arguments.len < 1) { - globalObject.throwInvalidArguments("toBe() takes 1 argument", .{}); - return .zero; - } - - if (this.scope.tests.items.len <= this.test_id) { - globalObject.throw("toBe() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - const right = arguments[0]; - right.ensureStillAlive(); - const left = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - left.ensureStillAlive(); - - const not = this.op.contains(.not); - var pass = right.isSameValue(left, globalObject); - if (comptime Environment.allow_assert) { - std.debug.assert(pass == JSC.C.JSValueIsStrictEqual(globalObject, right.asObjectRef(), left.asObjectRef())); - } - - 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("toBe", "expected", true); - const fmt = signature ++ "\n\nExpected: not {any}\n"; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{right.toFmt(globalObject, &formatter)}); - return .zero; - } - globalObject.throw(Output.prettyFmt(fmt, false), .{right.toFmt(globalObject, &formatter)}); - return .zero; - } - - const signature = comptime getSignature("toBe", "expected", false); - if (left.deepEquals(right, globalObject) or left.strictDeepEquals(right, globalObject)) { - const fmt = signature ++ - "\n\nIf this test should pass, replace \"toBe\" with \"toEqual\" or \"toStrictEqual\"" ++ - "\n\nExpected: {any}\n" ++ - "Received: serializes to the same string\n"; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{right.toFmt(globalObject, &formatter)}); - return .zero; - } - globalObject.throw(Output.prettyFmt(fmt, false), .{right.toFmt(globalObject, &formatter)}); - return .zero; - } - - if (right.isString() and left.isString()) { - const diff_format = DiffFormatter{ - .expected = right, - .received = left, - .globalObject = globalObject, - .not = not, - }; - const fmt = signature ++ "\n\n{any}\n"; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{diff_format}); - return .zero; - } - globalObject.throw(Output.prettyFmt(fmt, false), .{diff_format}); - return .zero; - } - - const fmt = signature ++ "\n\nExpected: {any}\nReceived: {any}\n"; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{ - right.toFmt(globalObject, &formatter), - left.toFmt(globalObject, &formatter), - }); - return .zero; - } - globalObject.throw(Output.prettyFmt(fmt, false), .{ - right.toFmt(globalObject, &formatter), - left.toFmt(globalObject, &formatter), - }); - return .zero; - } - - pub fn getSignature(comptime matcher_name: string, comptime args: string, comptime not: bool) string { - const received = "expect(received)."; - comptime if (not) { - return received ++ "not." ++ matcher_name ++ "(" ++ args ++ ")"; - }; - return received ++ matcher_name ++ "(" ++ args ++ ")"; - } - - pub fn toHaveLength( - this: *Expect, - globalObject: *JSC.JSGlobalObject, - callframe: *JSC.CallFrame, - ) callconv(.C) JSC.JSValue { - defer this.postMatch(globalObject); - const thisValue = callframe.this(); - const arguments_ = callframe.arguments(1); - const arguments = arguments_.ptr[0..arguments_.len]; - - if (arguments.len < 1) { - globalObject.throwInvalidArguments("toHaveLength() takes 1 argument", .{}); - return .zero; - } - - if (this.scope.tests.items.len <= this.test_id) { - globalObject.throw("toHaveLength() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const expected: JSValue = arguments[0]; - 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; - }; - value.ensureStillAlive(); - - if (!value.isObject() and !value.isString()) { - var fmt = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - globalObject.throw("Received value does not have a length property: {any}", .{value.toFmt(globalObject, &fmt)}); - return .zero; - } - - if (!expected.isNumber()) { - var fmt = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - globalObject.throw("Expected value must be a non-negative integer: {any}", .{expected.toFmt(globalObject, &fmt)}); - return .zero; - } - - const expected_length: f64 = expected.asNumber(); - if (@round(expected_length) != expected_length or std.math.isInf(expected_length) or std.math.isNan(expected_length) or expected_length < 0) { - var fmt = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - globalObject.throw("Expected value must be a non-negative integer: {any}", .{expected.toFmt(globalObject, &fmt)}); - return .zero; - } - - const not = this.op.contains(.not); - var pass = false; - - const actual_length = value.getLengthIfPropertyExistsInternal(globalObject); - - if (actual_length == std.math.inf(f64)) { - var fmt = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - globalObject.throw("Received value does not have a length property: {any}", .{value.toFmt(globalObject, &fmt)}); - return .zero; - } else if (std.math.isNan(actual_length)) { - globalObject.throw("Received value has non-number length property: {}", .{actual_length}); - return .zero; - } - - if (actual_length == expected_length) { - pass = true; - } - - if (not) pass = !pass; - if (pass) return thisValue; - - // handle failure - if (not) { - const expected_line = "Expected length: not {d}\n"; - const fmt = comptime getSignature("toHaveLength", "expected", true) ++ "\n\n" ++ expected_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{expected_length}); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{expected_length}); - return .zero; - } - - const expected_line = "Expected length: {d}\n"; - const received_line = "Received length: {d}\n"; - const fmt = comptime getSignature("toHaveLength", "expected", false) ++ "\n\n" ++ - expected_line ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_length, actual_length }); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_length, actual_length }); - return .zero; - } - - pub fn toContain( - this: *Expect, - globalObject: *JSC.JSGlobalObject, - callFrame: *JSC.CallFrame, - ) callconv(.C) JSC.JSValue { - defer this.postMatch(globalObject); - const thisValue = callFrame.this(); - const arguments_ = callFrame.arguments(1); - const arguments = arguments_.ptr[0..arguments_.len]; - - if (arguments.len < 1) { - globalObject.throwInvalidArguments("toContain() takes 1 argument", .{}); - return .zero; - } - - if (this.scope.tests.items.len <= this.test_id) { - globalObject.throw("toContain() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const expected = arguments[0]; - expected.ensureStillAlive(); - 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; - }; - value.ensureStillAlive(); - - const not = this.op.contains(.not); - var pass = false; - - if (value.isIterable(globalObject)) { - var itr = value.arrayIterator(globalObject); - while (itr.next()) |item| { - if (item.isSameValue(expected, globalObject)) { - pass = true; - break; - } - } - } else if (value.isString() and expected.isString()) { - const value_string = value.toString(globalObject).toSlice(globalObject, default_allocator).slice(); - const expected_string = expected.toString(globalObject).toSlice(globalObject, default_allocator).slice(); - if (strings.contains(value_string, expected_string)) { - pass = true; - } else if (value_string.len == 0 and expected_string.len == 0) { // edge case two empty strings are true - pass = true; - } - } else { - globalObject.throw("Received value must be an array type, or both received and expected values must be strings.", .{}); - return .zero; - } - - if (not) pass = !pass; - if (pass) return thisValue; - - // handle failure - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - const value_fmt = value.toFmt(globalObject, &formatter); - const expected_fmt = expected.toFmt(globalObject, &formatter); - if (not) { - const expected_line = "Expected to contain: not {any}\n"; - const fmt = comptime getSignature("toContain", "expected", true) ++ "\n\n" ++ expected_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{expected_fmt}); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{expected_fmt}); - return .zero; - } - - const expected_line = "Expected to contain: {any}\n"; - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toContain", "expected", false) ++ "\n\n" ++ expected_line ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt }); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, value_fmt }); - return .zero; - } - - pub fn toBeTruthy(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { - defer this.postMatch(globalObject); - const thisValue = callFrame.this(); - const value: JSValue = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (this.scope.tests.items.len <= this.test_id) { - globalObject.throw("toBeTruthy() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - var pass = false; - - const truthy = value.toBooleanSlow(globalObject); - if (truthy) pass = true; - - if (not) pass = !pass; - if (pass) return thisValue; - - // handle failure - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - const value_fmt = value.toFmt(globalObject, &formatter); - if (not) { - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeTruthy", "", true) ++ "\n\n" ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); - return .zero; - } - - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeTruthy", "", false) ++ "\n\n" ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); - return .zero; - } - - pub fn toBeUndefined(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { - defer this.postMatch(globalObject); - const thisValue = callFrame.this(); - const value: JSValue = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - var pass = false; - if (value.isUndefined()) pass = true; - - if (not) pass = !pass; - if (pass) return thisValue; - - // handle failure - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - const value_fmt = value.toFmt(globalObject, &formatter); - if (not) { - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeUndefined", "", true) ++ "\n\n" ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); - return .zero; - } - - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeUndefined", "", false) ++ "\n\n" ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); - return .zero; - } - - pub fn toBeNaN(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { - defer this.postMatch(globalObject); - - const thisValue = callFrame.this(); - const value: JSValue = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - var pass = false; - if (value.isNumber()) { - const number = value.asNumber(); - if (number != number) pass = true; - } - - if (not) pass = !pass; - if (pass) return thisValue; - - // handle failure - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - const value_fmt = value.toFmt(globalObject, &formatter); - if (not) { - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeNaN", "", true) ++ "\n\n" ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); - return .zero; - } - - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeNaN", "", false) ++ "\n\n" ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); - return .zero; - } - - pub fn toBeNull(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { - defer this.postMatch(globalObject); - - const thisValue = callFrame.this(); - const value: JSValue = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - var pass = value.isNull(); - if (not) pass = !pass; - if (pass) return thisValue; - - // handle failure - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - const value_fmt = value.toFmt(globalObject, &formatter); - if (not) { - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeNull", "", true) ++ "\n\n" ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); - return .zero; - } - - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeNull", "", false) ++ "\n\n" ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); - return .zero; - } - - pub fn toBeDefined(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { - defer this.postMatch(globalObject); - - const thisValue = callFrame.this(); - const value: JSValue = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - var pass = !value.isUndefined(); - if (not) pass = !pass; - if (pass) return thisValue; - - // handle failure - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - const value_fmt = value.toFmt(globalObject, &formatter); - if (not) { - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeDefined", "", true) ++ "\n\n" ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); - return .zero; - } - - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeDefined", "", false) ++ "\n\n" ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); - return .zero; - } - - pub fn toBeFalsy(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { - defer this.postMatch(globalObject); - - const thisValue = callFrame.this(); - - const value: JSValue = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (this.scope.tests.items.len <= this.test_id) { - globalObject.throw("toBeFalsy() must be called in a test", .{}); - return .zero; - } - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - var pass = false; - - const truthy = value.toBooleanSlow(globalObject); - if (!truthy) pass = true; - - if (not) pass = !pass; - if (pass) return thisValue; - - // handle failure - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - const value_fmt = value.toFmt(globalObject, &formatter); - if (not) { - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeFalsy", "", true) ++ "\n\n" ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); - return .zero; - } - - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeFalsy", "", false) ++ "\n\n" ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); - return .zero; - } - - pub fn toEqual(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { - defer this.postMatch(globalObject); - - const thisValue = callFrame.this(); - const _arguments = callFrame.arguments(1); - const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; - - if (arguments.len < 1) { - globalObject.throwInvalidArguments("toEqual() requires 1 argument", .{}); - return .zero; - } - - if (this.scope.tests.items.len <= this.test_id) { - globalObject.throw("toEqual() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const expected = arguments[0]; - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - const not = this.op.contains(.not); - var pass = value.jestDeepEquals(expected, globalObject); - - if (not) pass = !pass; - if (pass) return thisValue; - - // handle failure - const diff_formatter = DiffFormatter{ - .received = value, - .expected = expected, - .globalObject = globalObject, - .not = not, - }; - - if (not) { - const signature = comptime getSignature("toEqual", "expected", true); - const fmt = signature ++ "\n\n{any}\n"; - globalObject.throwPretty(fmt, .{diff_formatter}); - return .zero; - } - - const signature = comptime getSignature("toEqual", "expected", false); - const fmt = signature ++ "\n\n{any}\n"; - globalObject.throwPretty(fmt, .{diff_formatter}); - return .zero; - } - - pub fn toStrictEqual(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { - defer this.postMatch(globalObject); - - const thisValue = callFrame.this(); - const _arguments = callFrame.arguments(1); - const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; - - if (arguments.len < 1) { - globalObject.throwInvalidArguments("toStrictEqual() requires 1 argument", .{}); - return .zero; - } - - if (this.scope.tests.items.len <= this.test_id) { - globalObject.throw("toStrictEqual() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const expected = arguments[0]; - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - const not = this.op.contains(.not); - var pass = value.jestStrictDeepEquals(expected, globalObject); - - if (not) pass = !pass; - if (pass) return thisValue; - - // handle failure - const diff_formatter = DiffFormatter{ .received = value, .expected = expected, .globalObject = globalObject, .not = not }; - - if (not) { - const signature = comptime getSignature("toStrictEqual", "expected", true); - const fmt = signature ++ "\n\n{any}\n"; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{diff_formatter}); - return .zero; - } - globalObject.throw(Output.prettyFmt(fmt, false), .{diff_formatter}); - return .zero; - } - - const signature = comptime getSignature("toStrictEqual", "expected", false); - const fmt = signature ++ "\n\n{any}\n"; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{diff_formatter}); - return .zero; - } - globalObject.throw(Output.prettyFmt(fmt, false), .{diff_formatter}); - return .zero; - } - - pub fn toHaveProperty(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { - defer this.postMatch(globalObject); - - const thisValue = callFrame.this(); - const _arguments = callFrame.arguments(2); - const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; - - if (arguments.len < 1) { - globalObject.throwInvalidArguments("toHaveProperty() requires at least 1 argument", .{}); - return .zero; - } - - if (this.scope.tests.items.len <= this.test_id) { - globalObject.throw("toHaveProperty must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const expected_property_path = arguments[0]; - expected_property_path.ensureStillAlive(); - const expected_property: ?JSValue = if (arguments.len > 1) arguments[1] else null; - if (expected_property) |ev| ev.ensureStillAlive(); - - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (!expected_property_path.isString() and !expected_property_path.isIterable(globalObject)) { - globalObject.throw("Expected path must be a string or an array", .{}); - return .zero; - } - - const not = this.op.contains(.not); - var path_string = ZigString.Empty; - expected_property_path.toZigString(&path_string, globalObject); - - var pass = !value.isUndefinedOrNull(); - var received_property: JSValue = .zero; - - if (pass) { - received_property = value.getIfPropertyExistsFromPath(globalObject, expected_property_path); - pass = !received_property.isEmpty(); - } - - if (pass and expected_property != null) { - pass = received_property.jestDeepEquals(expected_property.?, globalObject); - } - - if (not) pass = !pass; - if (pass) return thisValue; - - // handle failure - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - if (not) { - if (expected_property != null) { - const signature = comptime getSignature("toHaveProperty", "path, value", true); - if (!received_property.isEmpty()) { - const fmt = signature ++ "\n\nExpected path: {any}\n\nExpected value: not {any}\n"; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{ - expected_property_path.toFmt(globalObject, &formatter), - expected_property.?.toFmt(globalObject, &formatter), - }); - return .zero; - } - globalObject.throw(Output.prettyFmt(fmt, true), .{ - expected_property_path.toFmt(globalObject, &formatter), - expected_property.?.toFmt(globalObject, &formatter), - }); - return .zero; - } - } - - const signature = comptime getSignature("toHaveProperty", "path", true); - const fmt = signature ++ "\n\nExpected path: not {any}\n\nReceived value: {any}\n"; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{ - expected_property_path.toFmt(globalObject, &formatter), - received_property.toFmt(globalObject, &formatter), - }); - return .zero; - } - globalObject.throw(Output.prettyFmt(fmt, false), .{ - expected_property_path.toFmt(globalObject, &formatter), - received_property.toFmt(globalObject, &formatter), - }); - return .zero; - } - - if (expected_property != null) { - const signature = comptime getSignature("toHaveProperty", "path, value", false); - if (!received_property.isEmpty()) { - // deep equal case - const fmt = signature ++ "\n\n{any}\n"; - const diff_format = DiffFormatter{ - .received = received_property, - .expected = expected_property.?, - .globalObject = globalObject, - }; - - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{diff_format}); - return .zero; - } - globalObject.throw(Output.prettyFmt(fmt, false), .{diff_format}); - return .zero; - } - - const fmt = signature ++ "\n\nExpected path: {any}\n\nExpected value: {any}\n\n" ++ - "Unable to find property\n"; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{ - expected_property_path.toFmt(globalObject, &formatter), - expected_property.?.toFmt(globalObject, &formatter), - }); - return .zero; - } - globalObject.throw(Output.prettyFmt(fmt, false), .{ - expected_property_path.toFmt(globalObject, &formatter), - expected_property.?.toFmt(globalObject, &formatter), - }); - return .zero; - } - - const signature = comptime getSignature("toHaveProperty", "path", false); - const fmt = signature ++ "\n\nExpected path: {any}\n\nUnable to find property\n"; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{expected_property_path.toFmt(globalObject, &formatter)}); - return .zero; - } - globalObject.throw(Output.prettyFmt(fmt, false), .{expected_property_path.toFmt(globalObject, &formatter)}); - return .zero; - } - - pub fn toBeEven(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { - defer this.postMatch(globalObject); - - const thisValue = callFrame.this(); - - const value: JSValue = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (this.scope.tests.items.len <= this.test_id) { - globalObject.throw("toBeEven() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - var pass = false; - - if (value.isAnyInt()) { - const _value = value.toInt64(); - pass = @mod(_value, 2) == 0; - if (_value == -0) { // negative zero is even - pass = true; - } - } else if (value.isBigInt() or value.isBigInt32()) { - const _value = value.toInt64(); - pass = switch (_value == -0) { // negative zero is even - true => true, - else => _value & 1 == 0, - }; - } else if (value.isNumber()) { - const _value = JSValue.asNumber(value); - if (@mod(_value, 1) == 0 and @mod(_value, 2) == 0) { // if the fraction is all zeros and even - pass = true; - } else { - pass = false; - } - } else { - pass = false; - } - - if (not) pass = !pass; - if (pass) return thisValue; - - // handle failure - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - const value_fmt = value.toFmt(globalObject, &formatter); - if (not) { - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeEven", "", true) ++ "\n\n" ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); - return .zero; - } - - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeEven", "", false) ++ "\n\n" ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); - return .zero; - } - - pub fn toBeGreaterThan(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalObject); - - const thisValue = callFrame.this(); - const _arguments = callFrame.arguments(1); - const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; - - if (arguments.len < 1) { - globalObject.throwInvalidArguments("toBeGreaterThan() requires 1 argument", .{}); - return .zero; - } - - if (this.scope.tests.items.len <= this.test_id) { - globalObject.throw("toBeGreaterThan() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const other_value = arguments[0]; - other_value.ensureStillAlive(); - - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if ((!value.isNumber() and !value.isBigInt()) or (!other_value.isNumber() and !other_value.isBigInt())) { - globalObject.throw("Expected and actual values must be numbers or bigints", .{}); - return .zero; - } - - const not = this.op.contains(.not); - var pass = false; - - if (!value.isBigInt() and !other_value.isBigInt()) { - pass = value.asNumber() > other_value.asNumber(); - } else if (value.isBigInt()) { - pass = switch (value.asBigIntCompare(globalObject, other_value)) { - .greater_than => true, - else => pass, - }; - } else { - pass = switch (other_value.asBigIntCompare(globalObject, value)) { - .less_than => true, - else => pass, - }; - } - - if (not) pass = !pass; - if (pass) return thisValue; - - // handle failure - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - const value_fmt = value.toFmt(globalObject, &formatter); - const expected_fmt = other_value.toFmt(globalObject, &formatter); - if (not) { - const expected_line = "Expected: not \\> {any}\n"; - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeGreaterThan", "expected", true) ++ "\n\n" ++ expected_line ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt }); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, value_fmt }); - return .zero; - } - - const expected_line = "Expected: \\> {any}\n"; - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeGreaterThan", "expected", false) ++ "\n\n" ++ - expected_line ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(comptime Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt }); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, value_fmt }); - return .zero; - } - - pub fn toBeGreaterThanOrEqual(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalObject); - - const thisValue = callFrame.this(); - const _arguments = callFrame.arguments(1); - const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; - - if (arguments.len < 1) { - globalObject.throwInvalidArguments("toBeGreaterThanOrEqual() requires 1 argument", .{}); - return .zero; - } - - if (this.scope.tests.items.len <= this.test_id) { - globalObject.throw("toBeGreaterThanOrEqual() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const other_value = arguments[0]; - other_value.ensureStillAlive(); - - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if ((!value.isNumber() and !value.isBigInt()) or (!other_value.isNumber() and !other_value.isBigInt())) { - globalObject.throw("Expected and actual values must be numbers or bigints", .{}); - return .zero; - } - - const not = this.op.contains(.not); - var pass = false; - - if (!value.isBigInt() and !other_value.isBigInt()) { - pass = value.asNumber() >= other_value.asNumber(); - } else if (value.isBigInt()) { - pass = switch (value.asBigIntCompare(globalObject, other_value)) { - .greater_than, .equal => true, - else => pass, - }; - } else { - pass = switch (other_value.asBigIntCompare(globalObject, value)) { - .less_than, .equal => true, - else => pass, - }; - } - - if (not) pass = !pass; - if (pass) return thisValue; - - // handle failure - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - const value_fmt = value.toFmt(globalObject, &formatter); - const expected_fmt = other_value.toFmt(globalObject, &formatter); - if (not) { - const expected_line = "Expected: not \\>= {any}\n"; - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeGreaterThanOrEqual", "expected", true) ++ "\n\n" ++ expected_line ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt }); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, value_fmt }); - return .zero; - } - - const expected_line = "Expected: \\>= {any}\n"; - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeGreaterThanOrEqual", "expected", false) ++ "\n\n" ++ expected_line ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(comptime Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt }); - return .zero; - } - return .zero; - } - - pub fn toBeLessThan(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalObject); - - const thisValue = callFrame.this(); - const _arguments = callFrame.arguments(1); - const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; - - if (arguments.len < 1) { - globalObject.throwInvalidArguments("toBeLessThan() requires 1 argument", .{}); - return .zero; - } - - if (this.scope.tests.items.len <= this.test_id) { - globalObject.throw("toBeLessThan() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const other_value = arguments[0]; - other_value.ensureStillAlive(); - - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if ((!value.isNumber() and !value.isBigInt()) or (!other_value.isNumber() and !other_value.isBigInt())) { - globalObject.throw("Expected and actual values must be numbers or bigints", .{}); - return .zero; - } - - const not = this.op.contains(.not); - var pass = false; - - if (!value.isBigInt() and !other_value.isBigInt()) { - pass = value.asNumber() < other_value.asNumber(); - } else if (value.isBigInt()) { - pass = switch (value.asBigIntCompare(globalObject, other_value)) { - .less_than => true, - else => pass, - }; - } else { - pass = switch (other_value.asBigIntCompare(globalObject, value)) { - .greater_than => true, - else => pass, - }; - } - - if (not) pass = !pass; - if (pass) return thisValue; - - // handle failure - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - const value_fmt = value.toFmt(globalObject, &formatter); - const expected_fmt = other_value.toFmt(globalObject, &formatter); - if (not) { - const expected_line = "Expected: not \\< {any}\n"; - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeLessThan", "expected", true) ++ "\n\n" ++ expected_line ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt }); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, value_fmt }); - return .zero; - } - - const expected_line = "Expected: \\< {any}\n"; - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeLessThan", "expected", false) ++ "\n\n" ++ expected_line ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(comptime Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt }); - return .zero; - } - return .zero; - } - - pub fn toBeLessThanOrEqual(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalObject); - - const thisValue = callFrame.this(); - const _arguments = callFrame.arguments(1); - const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; - - if (arguments.len < 1) { - globalObject.throwInvalidArguments("toBeLessThanOrEqual() requires 1 argument", .{}); - return .zero; - } - - if (this.scope.tests.items.len <= this.test_id) { - globalObject.throw("toBeLessThanOrEqual() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const other_value = arguments[0]; - other_value.ensureStillAlive(); - - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if ((!value.isNumber() and !value.isBigInt()) or (!other_value.isNumber() and !other_value.isBigInt())) { - globalObject.throw("Expected and actual values must be numbers or bigints", .{}); - return .zero; - } - - const not = this.op.contains(.not); - var pass = false; - - if (!value.isBigInt() and !other_value.isBigInt()) { - pass = value.asNumber() <= other_value.asNumber(); - } else if (value.isBigInt()) { - pass = switch (value.asBigIntCompare(globalObject, other_value)) { - .less_than, .equal => true, - else => pass, - }; - } else { - pass = switch (other_value.asBigIntCompare(globalObject, value)) { - .greater_than, .equal => true, - else => pass, - }; - } - - if (not) pass = !pass; - if (pass) return thisValue; - - // handle failure - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - const value_fmt = value.toFmt(globalObject, &formatter); - const expected_fmt = other_value.toFmt(globalObject, &formatter); - if (not) { - const expected_line = "Expected: not \\<= {any}\n"; - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeLessThanOrEqual", "expected", true) ++ "\n\n" ++ expected_line ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt }); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, value_fmt }); - return .zero; - } - - const expected_line = "Expected: \\<= {any}\n"; - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeLessThanOrEqual", "expected", false) ++ "\n\n" ++ expected_line ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(comptime Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt }); - return .zero; - } - return .zero; - } - - pub fn toBeCloseTo(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalObject); - - const thisValue = callFrame.this(); - const thisArguments = callFrame.arguments(2); - const arguments = thisArguments.ptr[0..thisArguments.len]; - - if (arguments.len < 1) { - globalObject.throwInvalidArguments("toBeCloseTo() requires at least 1 argument. Expected value must be a number", .{}); - return .zero; - } - - const expected_ = arguments[0]; - if (!expected_.isNumber()) { - globalObject.throwInvalidArgumentType("toBeCloseTo", "expected", "number"); - return .zero; - } - - var precision: f64 = 2.0; - if (arguments.len > 1) { - const precision_ = arguments[1]; - if (!precision_.isNumber()) { - globalObject.throwInvalidArgumentType("toBeCloseTo", "precision", "number"); - return .zero; - } - - precision = precision_.asNumber(); - } - - const received_: JSC.JSValue = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - - if (!received_.isNumber()) { - globalObject.throwInvalidArgumentType("expect", "received", "number"); - return .zero; - } - - var expected = expected_.asNumber(); - var received = received_.asNumber(); - - if (std.math.isNegativeInf(expected)) { - expected = -expected; - } - - if (std.math.isNegativeInf(received)) { - received = -received; - } - - if (std.math.isPositiveInf(expected) and std.math.isPositiveInf(received)) { - return thisValue; - } - - const expected_diff = std.math.pow(f64, 10, -precision) / 2; - const actual_diff = std.math.fabs(received - expected); - var pass = actual_diff < expected_diff; - - const not = this.op.contains(.not); - if (not) pass = !pass; - - if (pass) return thisValue; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - - const expected_fmt = expected_.toFmt(globalObject, &formatter); - const received_fmt = received_.toFmt(globalObject, &formatter); - - const expected_line = "Expected: {any}\n"; - const received_line = "Received: {any}\n"; - const expected_precision = "Expected precision: {d}\n"; - const expected_difference = "Expected difference: \\< {d}\n"; - const received_difference = "Received difference: {d}\n"; - - const suffix_fmt = "\n\n" ++ expected_line ++ received_line ++ "\n" ++ expected_precision ++ expected_difference ++ received_difference; - - if (not) { - const fmt = comptime getSignature("toBeCloseTo", "expected, precision", true) ++ suffix_fmt; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_fmt, received_fmt, precision, expected_diff, actual_diff }); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, received_fmt, precision, expected_diff, actual_diff }); - return .zero; - } - - const fmt = comptime getSignature("toBeCloseTo", "expected, precision", false) ++ suffix_fmt; - - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_fmt, received_fmt, precision, expected_diff, actual_diff }); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, received_fmt, precision, expected_diff, actual_diff }); - return .zero; - } - - pub fn toBeOdd(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { - defer this.postMatch(globalObject); - - const thisValue = callFrame.this(); - - const value: JSValue = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (this.scope.tests.items.len <= this.test_id) { - globalObject.throw("toBeOdd() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - var pass = false; - - if (value.isBigInt32()) { - pass = value.toInt32() & 1 == 1; - } else if (value.isBigInt()) { - pass = value.toInt64() & 1 == 1; - } else if (value.isInt32()) { - const _value = value.toInt32(); - pass = @mod(_value, 2) == 1; - } else if (value.isAnyInt()) { - const _value = value.toInt64(); - pass = @mod(_value, 2) == 1; - } else if (value.isNumber()) { - const _value = JSValue.asNumber(value); - if (@mod(_value, 1) == 0 and @mod(_value, 2) == 1) { // if the fraction is all zeros and odd - pass = true; - } else { - pass = false; - } - } else { - pass = false; - } - - if (not) pass = !pass; - if (pass) return thisValue; - - // handle failure - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - const value_fmt = value.toFmt(globalObject, &formatter); - if (not) { - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeOdd", "", true) ++ "\n\n" ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); - return .zero; - } - - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeOdd", "", false) ++ "\n\n" ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); - return .zero; - } - - pub fn toThrow(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalObject); - - const thisValue = callFrame.this(); - const _arguments = callFrame.arguments(1); - const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; - - if (this.scope.tests.items.len <= this.test_id) { - globalObject.throw("toThrow() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const expected_value: JSValue = if (arguments.len > 0) brk: { - const value = arguments[0]; - if (value.isEmptyOrUndefinedOrNull() or !value.isObject() and !value.isString()) { - var fmt = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - globalObject.throw("Expected value must be string or Error: {any}", .{value.toFmt(globalObject, &fmt)}); - return .zero; - } - break :brk value; - } else .zero; - expected_value.ensureStillAlive(); - - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (!value.jsType().isFunction()) { - globalObject.throw("Expected value must be a function", .{}); - return .zero; - } - - const not = this.op.contains(.not); - - const result_: ?JSValue = brk: { - var vm = globalObject.bunVM(); - var return_value: JSValue = .zero; - var scope = vm.unhandledRejectionScope(); - var prev_unhandled_pending_rejection_to_capture = vm.unhandled_pending_rejection_to_capture; - vm.unhandled_pending_rejection_to_capture = &return_value; - vm.onUnhandledRejection = &VirtualMachine.onQuietUnhandledRejectionHandlerCaptureValue; - const return_value_from_fucntion: JSValue = value.call(globalObject, &.{}); - vm.unhandled_pending_rejection_to_capture = prev_unhandled_pending_rejection_to_capture; - - if (return_value == .zero) { - return_value = return_value_from_fucntion; - } - - if (return_value.asAnyPromise()) |promise| { - globalObject.bunVM().waitForPromise(promise); - scope.apply(vm); - const promise_result = promise.result(globalObject.vm()); - - switch (promise.status(globalObject.vm())) { - .Fulfilled => { - break :brk null; - }, - .Rejected => { - // since we know for sure it rejected, we should always return the error - break :brk promise_result.toError() orelse promise_result; - }, - .Pending => unreachable, - } - } - scope.apply(vm); - - break :brk return_value.toError(); - }; - - const did_throw = result_ != null; - - if (not) { - const signature = comptime getSignature("toThrow", "expected", true); - - if (!did_throw) return thisValue; - - const result: JSValue = result_.?; - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - - if (expected_value.isEmpty()) { - const signature_no_args = comptime getSignature("toThrow", "", true); - if (result.toError()) |err| { - const name = err.get(globalObject, "name") orelse JSValue.undefined; - const message = err.get(globalObject, "message") orelse JSValue.undefined; - const fmt = signature_no_args ++ "\n\nError name: {any}\nError message: {any}\n"; - globalObject.throwPretty(fmt, .{ - name.toFmt(globalObject, &formatter), - message.toFmt(globalObject, &formatter), - }); - return .zero; - } - - // non error thrown - const fmt = signature_no_args ++ "\n\nThrown value: {any}\n"; - globalObject.throwPretty(fmt, .{result.toFmt(globalObject, &formatter)}); - return .zero; - } - - if (expected_value.isString()) { - const received_message = result.getIfPropertyExistsImpl(globalObject, "message", 7); - - // TODO: remove this allocation - // partial match - { - const expected_slice = expected_value.toSliceOrNull(globalObject) orelse return .zero; - defer expected_slice.deinit(); - const received_slice = received_message.toSliceOrNull(globalObject) orelse return .zero; - defer received_slice.deinit(); - if (!strings.contains(received_slice.slice(), expected_slice.slice())) return thisValue; - } - - const fmt = signature ++ "\n\nExpected substring: not {any}\nReceived message: {any}\n"; - globalObject.throwPretty(fmt, .{ - expected_value.toFmt(globalObject, &formatter), - received_message.toFmt(globalObject, &formatter), - }); - return .zero; - } - - if (expected_value.isRegExp()) { - const received_message = result.getIfPropertyExistsImpl(globalObject, "message", 7); - - // TODO: REMOVE THIS GETTER! Expose a binding to call .test on the RegExp object directly. - if (expected_value.get(globalObject, "test")) |test_fn| { - const matches = test_fn.callWithThis(globalObject, expected_value, &.{received_message}); - if (!matches.toBooleanSlow(globalObject)) return thisValue; - } - - const fmt = signature ++ "\n\nExpected pattern: not {any}\nReceived message: {any}\n"; - globalObject.throwPretty(fmt, .{ - expected_value.toFmt(globalObject, &formatter), - received_message.toFmt(globalObject, &formatter), - }); - return .zero; - } - - if (expected_value.get(globalObject, "message")) |expected_message| { - const received_message = result.getIfPropertyExistsImpl(globalObject, "message", 7); - // no partial match for this case - if (!expected_message.isSameValue(received_message, globalObject)) return thisValue; - - const fmt = signature ++ "\n\nExpected message: not {any}\n"; - globalObject.throwPretty(fmt, .{expected_message.toFmt(globalObject, &formatter)}); - return .zero; - } - - if (!result.isInstanceOf(globalObject, expected_value)) return thisValue; - - var expected_class = ZigString.Empty; - expected_value.getClassName(globalObject, &expected_class); - const received_message = result.getIfPropertyExistsImpl(globalObject, "message", 7); - const fmt = signature ++ "\n\nExpected constructor: not {s}\n\nReceived message: {any}\n"; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_class, received_message.toFmt(globalObject, &formatter) }); - return .zero; - } - globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_class, received_message.toFmt(globalObject, &formatter) }); - return .zero; - } - - const signature = comptime getSignature("toThrow", "expected", false); - if (did_throw) { - if (expected_value.isEmpty()) return thisValue; - - const result: JSValue = if (result_.?.toError()) |r| - r - else - result_.?; - - const _received_message: ?JSValue = if (result.isObject()) - result.get(globalObject, "message") - else if (result.toStringOrNull(globalObject)) |js_str| - JSC.JSValue.fromCell(js_str) - else - null; - - if (expected_value.isString()) { - if (_received_message) |received_message| { - // TODO: remove this allocation - // partial match - const expected_slice = expected_value.toSliceOrNull(globalObject) orelse return .zero; - defer expected_slice.deinit(); - const received_slice = received_message.toSlice(globalObject, globalObject.allocator()); - defer received_slice.deinit(); - if (strings.contains(received_slice.slice(), expected_slice.slice())) return thisValue; - } - - // error: message from received error does not match expected string - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - - if (_received_message) |received_message| { - const expected_value_fmt = expected_value.toFmt(globalObject, &formatter); - const received_message_fmt = received_message.toFmt(globalObject, &formatter); - const fmt = signature ++ "\n\n" ++ "Expected substring: {any}\nReceived message: {any}\n"; - globalObject.throwPretty(fmt, .{ expected_value_fmt, received_message_fmt }); - return .zero; - } - - const expected_fmt = expected_value.toFmt(globalObject, &formatter); - const received_fmt = result.toFmt(globalObject, &formatter); - const fmt = signature ++ "\n\n" ++ "Expected substring: {any}\nReceived value: {any}"; - globalObject.throwPretty(fmt, .{ expected_fmt, received_fmt }); - - return .zero; - } - - if (expected_value.isRegExp()) { - if (_received_message) |received_message| { - // TODO: REMOVE THIS GETTER! Expose a binding to call .test on the RegExp object directly. - if (expected_value.get(globalObject, "test")) |test_fn| { - const matches = test_fn.callWithThis(globalObject, expected_value, &.{received_message}); - if (matches.toBooleanSlow(globalObject)) return thisValue; - } - } - - // error: message from received error does not match expected pattern - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - - if (_received_message) |received_message| { - const expected_value_fmt = expected_value.toFmt(globalObject, &formatter); - const received_message_fmt = received_message.toFmt(globalObject, &formatter); - const fmt = signature ++ "\n\n" ++ "Expected pattern: {any}\nReceived message: {any}\n"; - globalObject.throwPretty(fmt, .{ expected_value_fmt, received_message_fmt }); - - return .zero; - } - - const expected_fmt = expected_value.toFmt(globalObject, &formatter); - const received_fmt = result.toFmt(globalObject, &formatter); - const fmt = signature ++ "\n\n" ++ "Expected pattern: {any}\nReceived value: {any}"; - globalObject.throwPretty(fmt, .{ expected_fmt, received_fmt }); - return .zero; - } - - // If it's not an object, we are going to crash here. - std.debug.assert(expected_value.isObject()); - - if (expected_value.get(globalObject, "message")) |expected_message| { - if (_received_message) |received_message| { - if (received_message.isSameValue(expected_message, globalObject)) return thisValue; - } - - // error: message from received error does not match expected error message. - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - - if (_received_message) |received_message| { - const expected_fmt = expected_message.toFmt(globalObject, &formatter); - const received_fmt = received_message.toFmt(globalObject, &formatter); - const fmt = signature ++ "\n\nExpected message: {any}\nReceived message: {any}\n"; - globalObject.throwPretty(fmt, .{ expected_fmt, received_fmt }); - return .zero; - } - - const expected_fmt = expected_message.toFmt(globalObject, &formatter); - const received_fmt = result.toFmt(globalObject, &formatter); - const fmt = signature ++ "\n\nExpected message: {any}\nReceived value: {any}\n"; - globalObject.throwPretty(fmt, .{ expected_fmt, received_fmt }); - return .zero; - } - - if (result.isInstanceOf(globalObject, expected_value)) return thisValue; - - // error: received error not instance of received error constructor - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - var expected_class = ZigString.Empty; - var received_class = ZigString.Empty; - expected_value.getClassName(globalObject, &expected_class); - result.getClassName(globalObject, &received_class); - const fmt = signature ++ "\n\nExpected constructor: {s}\nReceived constructor: {s}\n\n"; - - if (_received_message) |received_message| { - const message_fmt = fmt ++ "Received message: {any}\n"; - const received_message_fmt = received_message.toFmt(globalObject, &formatter); - - globalObject.throwPretty(message_fmt, .{ - expected_class, - received_class, - received_message_fmt, - }); - return .zero; - } - - const received_fmt = result.toFmt(globalObject, &formatter); - const value_fmt = fmt ++ "Received value: {any}\n"; - - globalObject.throwPretty(value_fmt, .{ - expected_class, - received_class, - received_fmt, - }); - return .zero; - } - - // did not throw - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - const received_line = "Received function did not throw\n"; - - if (expected_value.isEmpty()) { - const fmt = comptime getSignature("toThrow", "", false) ++ "\n\n" ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{}); - return .zero; - } - globalObject.throw(Output.prettyFmt(fmt, false), .{}); - return .zero; - } - - if (expected_value.isString()) { - const expected_fmt = "\n\nExpected substring: {any}\n\n" ++ received_line; - const fmt = signature ++ expected_fmt; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{expected_value.toFmt(globalObject, &formatter)}); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{expected_value.toFmt(globalObject, &formatter)}); - return .zero; - } - - if (expected_value.isRegExp()) { - const expected_fmt = "\n\nExpected pattern: {any}\n\n" ++ received_line; - const fmt = signature ++ expected_fmt; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{expected_value.toFmt(globalObject, &formatter)}); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{expected_value.toFmt(globalObject, &formatter)}); - return .zero; - } - - if (expected_value.get(globalObject, "message")) |expected_message| { - const expected_fmt = "\n\nExpected message: {any}\n\n" ++ received_line; - const fmt = signature ++ expected_fmt; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{expected_message.toFmt(globalObject, &formatter)}); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{expected_message.toFmt(globalObject, &formatter)}); - return .zero; - } - - const expected_fmt = "\n\nExpected constructor: {s}\n\n" ++ received_line; - var expected_class = ZigString.Empty; - expected_value.getClassName(globalObject, &expected_class); - const fmt = signature ++ expected_fmt; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{expected_class}); - return .zero; - } - globalObject.throw(Output.prettyFmt(fmt, true), .{expected_class}); - return .zero; - } - - pub fn toMatchSnapshot(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalObject); - const thisValue = callFrame.this(); - const _arguments = callFrame.arguments(2); - const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; - - if (this.scope.tests.items.len <= this.test_id) { - globalObject.throw("toMatchSnapshot() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - if (not) { - const signature = comptime getSignature("toMatchSnapshot", "", true); - const fmt = signature ++ "\n\nMatcher error: Snapshot matchers cannot be used with not\n"; - globalObject.throwPretty(fmt, .{}); - } - - var hint_string: ZigString = ZigString.Empty; - var property_matchers: ?JSValue = null; - switch (arguments.len) { - 0 => {}, - 1 => { - if (arguments[0].isString()) { - arguments[0].toZigString(&hint_string, globalObject); - } else if (arguments[0].isObject()) { - property_matchers = arguments[0]; - } - }, - else => { - if (!arguments[0].isObject()) { - const signature = comptime getSignature("toMatchSnapshot", "properties, hint", false); - const fmt = signature ++ "\n\nMatcher error: Expected properties must be an object\n"; - globalObject.throwPretty(fmt, .{}); - return .zero; - } - - property_matchers = arguments[0]; - - if (arguments[1].isString()) { - arguments[1].toZigString(&hint_string, globalObject); - } - }, - } - - var hint = hint_string.toSlice(default_allocator); - defer hint.deinit(); - - const value: JSValue = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - - if (!value.isObject() and property_matchers != null) { - const signature = comptime getSignature("toMatchSnapshot", "properties, hint", false); - const fmt = signature ++ "\n\nMatcher error: received values must be an object when the matcher has properties\n"; - globalObject.throwPretty(fmt, .{}); - return .zero; - } - - if (property_matchers) |_prop_matchers| { - var prop_matchers = _prop_matchers; - - if (!value.jestDeepMatch(prop_matchers, globalObject, true)) { - // TODO: print diff with properties from propertyMatchers - const signature = comptime getSignature("toMatchSnapshot", "propertyMatchers", false); - const fmt = signature ++ "\n\nExpected propertyMatchers to match properties from received object" ++ - "\n\nReceived: {any}\n"; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject }; - globalObject.throwPretty(fmt, .{value.toFmt(globalObject, &formatter)}); - return .zero; - } - } - - const result = Jest.runner.?.snapshots.getOrPut(this, value, hint.slice(), globalObject) catch |err| { - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject }; - const test_file_path = Jest.runner.?.files.get(this.scope.file_id).source.path.text; - switch (err) { - error.FailedToOpenSnapshotFile => globalObject.throw("Failed to open snapshot file for test file: {s}", .{test_file_path}), - error.FailedToMakeSnapshotDirectory => globalObject.throw("Failed to make snapshot directory for test file: {s}", .{test_file_path}), - error.FailedToWriteSnapshotFile => globalObject.throw("Failed write to snapshot file: {s}", .{test_file_path}), - error.ParseError => globalObject.throw("Failed to parse snapshot file for: {s}", .{test_file_path}), - else => globalObject.throw("Failed to snapshot value: {any}", .{value.toFmt(globalObject, &formatter)}), - } - return .zero; - }; - - if (result) |saved_value| { - var pretty_value: MutableString = MutableString.init(default_allocator, 0) catch unreachable; - value.jestSnapshotPrettyFormat(&pretty_value, globalObject) catch { - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject }; - globalObject.throw("Failed to pretty format value: {s}", .{value.toFmt(globalObject, &formatter)}); - return .zero; - }; - defer pretty_value.deinit(); - - if (strings.eqlLong(pretty_value.toOwnedSliceLeaky(), saved_value, true)) { - Jest.runner.?.snapshots.passed += 1; - return thisValue; - } - - Jest.runner.?.snapshots.failed += 1; - const signature = comptime getSignature("toMatchSnapshot", "expected", false); - const fmt = signature ++ "\n\n{any}\n"; - const diff_format = DiffFormatter{ - .received_string = pretty_value.toOwnedSliceLeaky(), - .expected_string = saved_value, - .globalObject = globalObject, - }; - - globalObject.throwPretty(fmt, .{diff_format}); - return .zero; - } - - return thisValue; - } - - pub fn toBeEmpty(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { - defer this.postMatch(globalObject); - - const thisValue = callFrame.this(); - const value: JSValue = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (this.scope.tests.items.len <= this.test_id) { - globalObject.throw("toBeEmpty() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - var pass = false; - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - - const actual_length = value.getLengthIfPropertyExistsInternal(globalObject); - - if (actual_length == std.math.inf(f64)) { - if (value.jsTypeLoose().isObject()) { - if (value.isIterable(globalObject)) { - var any_properties_in_iterator = false; - value.forEach(globalObject, &any_properties_in_iterator, struct { - pub fn anythingInIterator( - _: *JSC.VM, - _: *JSGlobalObject, - any_: ?*anyopaque, - _: JSValue, - ) callconv(.C) void { - bun.cast(*bool, any_.?).* = true; - } - }.anythingInIterator); - pass = !any_properties_in_iterator; - } else { - var props_iter = JSC.JSPropertyIterator(.{ - .skip_empty_name = false, - - .include_value = true, - }).init(globalObject, value.asObjectRef()); - defer props_iter.deinit(); - pass = props_iter.len == 0; - } - } else { - const signature = comptime getSignature("toBeEmpty", "", false); - const fmt = signature ++ "\n\nExpected value to be a string, object, or iterable" ++ - "\n\nReceived: {any}\n"; - globalObject.throwPretty(fmt, .{value.toFmt(globalObject, &formatter)}); - return .zero; - } - } else if (std.math.isNan(actual_length)) { - globalObject.throw("Received value has non-number length property: {}", .{actual_length}); - return .zero; - } else { - pass = actual_length == 0; - } - - if (not and pass) { - const signature = comptime getSignature("toBeEmpty", "", true); - const fmt = signature ++ "\n\nExpected value not to be a string, object, or iterable" ++ - "\n\nReceived: {any}\n"; - globalObject.throwPretty(fmt, .{value.toFmt(globalObject, &formatter)}); - return .zero; - } - - if (not) pass = !pass; - if (pass) return thisValue; - - if (not) { - const signature = comptime getSignature("toBeEmpty", "", true); - const fmt = signature ++ "\n\nExpected value not to be empty" ++ - "\n\nReceived: {any}\n"; - globalObject.throwPretty(fmt, .{value.toFmt(globalObject, &formatter)}); - return .zero; - } - - const signature = comptime getSignature("toBeEmpty", "", false); - const fmt = signature ++ "\n\nExpected value to be empty" ++ - "\n\nReceived: {any}\n"; - globalObject.throwPretty(fmt, .{value.toFmt(globalObject, &formatter)}); - return .zero; - } - - pub fn toBeNil(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalThis); - - const thisValue = callFrame.this(); - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (this.scope.tests.items.len <= this.test_id) { - globalThis.throw("toBeNil() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - const pass = value.isUndefinedOrNull() != not; - - if (pass) return thisValue; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; - const received = value.toFmt(globalThis, &formatter); - - if (not) { - const fmt = comptime getSignature("toBeNil", "", true) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - const fmt = comptime getSignature("toBeNil", "", false) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - pub fn toBeArray(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalThis); - - const thisValue = callFrame.this(); - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (this.scope.tests.items.len <= this.test_id) { - globalThis.throw("toBeArray() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - const pass = value.jsType().isArray() != not; - - if (pass) return thisValue; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; - const received = value.toFmt(globalThis, &formatter); - - if (not) { - const fmt = comptime getSignature("toBeArray", "", true) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - const fmt = comptime getSignature("toBeArray", "", false) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - pub fn toBeArrayOfSize(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalThis); - - const thisValue = callFrame.this(); - const _arguments = callFrame.arguments(1); - const arguments = _arguments.ptr[0.._arguments.len]; - - if (arguments.len < 1) { - globalThis.throwInvalidArguments("toBeArrayOfSize() requires 1 argument", .{}); - return .zero; - } - - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - - if (this.scope.tests.items.len <= this.test_id) { - globalThis.throw("toBeArrayOfSize() must be called in a test", .{}); - return .zero; - } - - const size = arguments[0]; - size.ensureStillAlive(); - - if (!size.isAnyInt()) { - globalThis.throw("toBeArrayOfSize() requires the first argument to be a number", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - var pass = value.jsType().isArray() and @intCast(i32, value.getLength(globalThis)) == size.toInt32(); - - if (not) pass = !pass; - if (pass) return thisValue; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; - const received = value.toFmt(globalThis, &formatter); - - if (not) { - const fmt = comptime getSignature("toBeArrayOfSize", "", true) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - const fmt = comptime getSignature("toBeArrayOfSize", "", false) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - pub fn toBeBoolean(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalThis); - - const thisValue = callFrame.this(); - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (this.scope.tests.items.len <= this.test_id) { - globalThis.throw("toBeBoolean() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - const pass = value.isBoolean() != not; - - if (pass) return thisValue; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; - const received = value.toFmt(globalThis, &formatter); - - if (not) { - const fmt = comptime getSignature("toBeBoolean", "", true) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - const fmt = comptime getSignature("toBeBoolean", "", false) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - pub fn toBeTrue(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalThis); - - const thisValue = callFrame.this(); - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (this.scope.tests.items.len <= this.test_id) { - globalThis.throw("toBeTrue() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - const pass = (value.isBoolean() and value.toBoolean()) != not; - - if (pass) return thisValue; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; - const received = value.toFmt(globalThis, &formatter); - - if (not) { - const fmt = comptime getSignature("toBeTrue", "", true) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - const fmt = comptime getSignature("toBeTrue", "", false) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - pub fn toBeFalse(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalThis); - - const thisValue = callFrame.this(); - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (this.scope.tests.items.len <= this.test_id) { - globalThis.throw("toBeFalse() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - const pass = (value.isBoolean() and !value.toBoolean()) != not; - - if (pass) return thisValue; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; - const received = value.toFmt(globalThis, &formatter); - - if (not) { - const fmt = comptime getSignature("toBeFalse", "", true) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - const fmt = comptime getSignature("toBeFalse", "", false) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - pub fn toBeNumber(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalThis); - - const thisValue = callFrame.this(); - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (this.scope.tests.items.len <= this.test_id) { - globalThis.throw("toBeNumber() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - const pass = value.isNumber() != not; - - if (pass) return thisValue; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; - const received = value.toFmt(globalThis, &formatter); - - if (not) { - const fmt = comptime getSignature("toBeNumber", "", true) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - const fmt = comptime getSignature("toBeNumber", "", false) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - pub fn toBeInteger(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalThis); - - const thisValue = callFrame.this(); - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (this.scope.tests.items.len <= this.test_id) { - globalThis.throw("toBeInteger() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - const pass = value.isAnyInt() != not; - - if (pass) return thisValue; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; - const received = value.toFmt(globalThis, &formatter); - - if (not) { - const fmt = comptime getSignature("toBeInteger", "", true) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - const fmt = comptime getSignature("toBeInteger", "", false) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - pub fn toBeFinite(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalThis); - - const thisValue = callFrame.this(); - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (this.scope.tests.items.len <= this.test_id) { - globalThis.throw("toBeFinite() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - var pass = value.isNumber(); - if (pass) { - const num: f64 = value.asNumber(); - pass = std.math.isFinite(num) and !std.math.isNan(num); - } - - const not = this.op.contains(.not); - if (not) pass = !pass; - - if (pass) return thisValue; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; - const received = value.toFmt(globalThis, &formatter); - - if (not) { - const fmt = comptime getSignature("toBeFinite", "", true) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - const fmt = comptime getSignature("toBeFinite", "", false) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - pub fn toBePositive(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalThis); - - const thisValue = callFrame.this(); - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (this.scope.tests.items.len <= this.test_id) { - globalThis.throw("toBePositive() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - var pass = value.isNumber(); - if (pass) { - const num: f64 = value.asNumber(); - pass = @round(num) > 0 and !std.math.isInf(num) and !std.math.isNan(num); - } - - const not = this.op.contains(.not); - if (not) pass = !pass; - - if (pass) return thisValue; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; - const received = value.toFmt(globalThis, &formatter); - - if (not) { - const fmt = comptime getSignature("toBePositive", "", true) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - const fmt = comptime getSignature("toBePositive", "", false) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - pub fn toBeNegative(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalThis); - - const thisValue = callFrame.this(); - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (this.scope.tests.items.len <= this.test_id) { - globalThis.throw("toBeNegative() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - var pass = value.isNumber(); - if (pass) { - const num: f64 = value.asNumber(); - pass = @round(num) < 0 and !std.math.isInf(num) and !std.math.isNan(num); - } - - const not = this.op.contains(.not); - if (not) pass = !pass; - - if (pass) return thisValue; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; - const received = value.toFmt(globalThis, &formatter); - - if (not) { - const fmt = comptime getSignature("toBeNegative", "", true) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - const fmt = comptime getSignature("toBeNegative", "", false) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - pub fn toBeTypeOf(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalThis); - - const thisValue = callFrame.this(); - const _arguments = callFrame.arguments(1); - const arguments = _arguments.ptr[0.._arguments.len]; - - if (arguments.len < 1) { - globalThis.throwInvalidArguments("toBeTypeOf() requires 1 argument", .{}); - return .zero; - } - - if (this.scope.tests.items.len <= this.test_id) { - globalThis.throw("toBeTypeOf() must be called in a test", .{}); - return .zero; - } - - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - const expected = arguments[0]; - expected.ensureStillAlive(); - - const expectedAsStr = expected.toString(globalThis).toSlice(globalThis, default_allocator).slice(); - active_test_expectation_counter.actual += 1; - - if (!expected.isString()) { - globalThis.throwInvalidArguments("toBeTypeOf() requires a string argument", .{}); - return .zero; - } - - if (!std.mem.eql(u8, expectedAsStr, "function") and - !std.mem.eql(u8, expectedAsStr, "object") and - !std.mem.eql(u8, expectedAsStr, "bigint") and - !std.mem.eql(u8, expectedAsStr, "boolean") and - !std.mem.eql(u8, expectedAsStr, "number") and - !std.mem.eql(u8, expectedAsStr, "string") and - !std.mem.eql(u8, expectedAsStr, "symbol") and - !std.mem.eql(u8, expectedAsStr, "undefined")) - { - globalThis.throwInvalidArguments("toBeTypeOf() requires a valid type string argument ('function', 'object', 'bigint', 'boolean', 'number', 'string', 'symbol', 'undefined')", .{}); - return .zero; - } - - const not = this.op.contains(.not); - var pass = false; - var whatIsTheType: []const u8 = ""; - - // Checking for function/class should be done before everything else, or it will fail. - if (value.isCallable(globalThis.vm())) { - whatIsTheType = "function"; - } else if (value.isObject() or value.jsType().isArray() or value.isNull()) { - whatIsTheType = "object"; - } else if (value.isBigInt()) { - whatIsTheType = "bigint"; - } else if (value.isBoolean()) { - whatIsTheType = "boolean"; - } else if (value.isNumber()) { - whatIsTheType = "number"; - } else if (value.jsType().isString()) { - whatIsTheType = "string"; - } else if (value.isSymbol()) { - whatIsTheType = "symbol"; - } else if (value.isUndefined()) { - whatIsTheType = "undefined"; - } else { - globalThis.throw("Internal consistency error: unknown JSValue type", .{}); - return .zero; - } - - pass = std.mem.eql(u8, expectedAsStr, whatIsTheType); - - if (not) pass = !pass; - if (pass) return thisValue; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; - const received = value.toFmt(globalThis, &formatter); - const expected_str = expected.toFmt(globalThis, &formatter); - - if (not) { - const fmt = comptime getSignature("toBeTypeOf", "", true) ++ "\n\n" ++ "Expected type: not {any}\n" ++ "Received type: \"{s}\"\nReceived value: {any}\n"; - globalThis.throwPretty(fmt, .{ expected_str, whatIsTheType, received }); - return .zero; - } - - const fmt = comptime getSignature("toBeTypeOf", "", false) ++ "\n\n" ++ "Expected type: {any}\n" ++ "Received type: \"{s}\"\nReceived value: {any}\n"; - globalThis.throwPretty(fmt, .{ expected_str, whatIsTheType, received }); - return .zero; - } - - pub fn toBeWithin(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalThis); - - const thisValue = callFrame.this(); - const _arguments = callFrame.arguments(2); - const arguments = _arguments.ptr[0.._arguments.len]; - - if (arguments.len < 1) { - globalThis.throwInvalidArguments("toBeWithin() requires 2 arguments", .{}); - return .zero; - } - - if (this.scope.tests.items.len <= this.test_id) { - globalThis.throw("toBeWithin() must be called in a test", .{}); - return .zero; - } - - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - const startValue = arguments[0]; - startValue.ensureStillAlive(); - - if (!startValue.isNumber()) { - globalThis.throw("toBeWithin() requires the first argument to be a number", .{}); - return .zero; - } - - const endValue = arguments[1]; - endValue.ensureStillAlive(); - - if (!endValue.isNumber()) { - globalThis.throw("toBeWithin() requires the second argument to be a number", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - var pass = value.isNumber(); - if (pass) { - const num = value.asNumber(); - pass = num >= startValue.asNumber() and num < endValue.asNumber(); - } - - const not = this.op.contains(.not); - if (not) pass = !pass; - - if (pass) return thisValue; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; - const start_fmt = startValue.toFmt(globalThis, &formatter); - const end_fmt = endValue.toFmt(globalThis, &formatter); - const received_fmt = value.toFmt(globalThis, &formatter); - - if (not) { - const expected_line = "Expected: not between {any} (inclusive) and {any} (exclusive)\n"; - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeWithin", "start, end", true) ++ "\n\n" ++ expected_line ++ received_line; - globalThis.throwPretty(fmt, .{ start_fmt, end_fmt, received_fmt }); - return .zero; - } - - const expected_line = "Expected: between {any} (inclusive) and {any} (exclusive)\n"; - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeWithin", "start, end", false) ++ "\n\n" ++ expected_line ++ received_line; - globalThis.throwPretty(fmt, .{ start_fmt, end_fmt, received_fmt }); - return .zero; - } - - pub fn toBeSymbol(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalThis); - - const thisValue = callFrame.this(); - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (this.scope.tests.items.len <= this.test_id) { - globalThis.throw("toBeSymbol() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - const pass = value.isSymbol() != not; - - if (pass) return thisValue; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; - const received = value.toFmt(globalThis, &formatter); - - if (not) { - const fmt = comptime getSignature("toBeSymbol", "", true) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - const fmt = comptime getSignature("toBeSymbol", "", false) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - pub fn toBeFunction(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalThis); - - const thisValue = callFrame.this(); - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (this.scope.tests.items.len <= this.test_id) { - globalThis.throw("toBeFunction() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - const pass = value.isCallable(globalThis.vm()) != not; - - if (pass) return thisValue; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; - const received = value.toFmt(globalThis, &formatter); - - if (not) { - const fmt = comptime getSignature("toBeFunction", "", true) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - const fmt = comptime getSignature("toBeFunction", "", false) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - pub fn toBeDate(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalThis); - - const thisValue = callFrame.this(); - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (this.scope.tests.items.len <= this.test_id) { - globalThis.throw("toBeDate() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - const pass = value.isDate() != not; - - if (pass) return thisValue; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; - const received = value.toFmt(globalThis, &formatter); - - if (not) { - const fmt = comptime getSignature("toBeDate", "", true) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - const fmt = comptime getSignature("toBeDate", "", false) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - pub fn toBeString(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalThis); - - const thisValue = callFrame.this(); - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (this.scope.tests.items.len <= this.test_id) { - globalThis.throw("toBeString() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - const pass = value.isString() != not; - - if (pass) return thisValue; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; - const received = value.toFmt(globalThis, &formatter); - - if (not) { - const fmt = comptime getSignature("toBeString", "", true) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - const fmt = comptime getSignature("toBeString", "", false) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - pub fn toInclude(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalThis); - - const thisValue = callFrame.this(); - const arguments_ = callFrame.arguments(1); - const arguments = arguments_.ptr[0..arguments_.len]; - - if (arguments.len < 1) { - globalThis.throwInvalidArguments("toInclude() requires 1 argument", .{}); - return .zero; - } - - const expected = arguments[0]; - expected.ensureStillAlive(); - - if (!expected.isString()) { - globalThis.throw("toInclude() requires the first argument to be a string", .{}); - return .zero; - } - - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (this.scope.tests.items.len <= this.test_id) { - globalThis.throw("toInclude() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - var pass = value.isString(); - if (pass) { - const value_string = value.toString(globalThis).toSlice(globalThis, default_allocator).slice(); - const expected_string = expected.toString(globalThis).toSlice(globalThis, default_allocator).slice(); - pass = strings.contains(value_string, expected_string) or expected_string.len == 0; - } - - const not = this.op.contains(.not); - if (not) pass = !pass; - - if (pass) return thisValue; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; - const value_fmt = value.toFmt(globalThis, &formatter); - const expected_fmt = expected.toFmt(globalThis, &formatter); - - if (not) { - const expected_line = "Expected to not include: {any}\n"; - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toInclude", "expected", true) ++ "\n\n" ++ expected_line ++ received_line; - globalThis.throwPretty(fmt, .{ expected_fmt, value_fmt }); - return .zero; - } - - const expected_line = "Expected to include: {any}\n"; - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toInclude", "expected", false) ++ "\n\n" ++ expected_line ++ received_line; - globalThis.throwPretty(fmt, .{ expected_fmt, value_fmt }); - return .zero; - } - - pub fn toStartWith(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalThis); - - const thisValue = callFrame.this(); - const arguments_ = callFrame.arguments(1); - const arguments = arguments_.ptr[0..arguments_.len]; - - if (arguments.len < 1) { - globalThis.throwInvalidArguments("toStartWith() requires 1 argument", .{}); - return .zero; - } - - const expected = arguments[0]; - expected.ensureStillAlive(); - - if (!expected.isString()) { - globalThis.throw("toStartWith() requires the first argument to be a string", .{}); - return .zero; - } - - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (this.scope.tests.items.len <= this.test_id) { - globalThis.throw("toStartWith() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - var pass = value.isString(); - if (pass) { - const value_string = value.toString(globalThis).toSlice(globalThis, default_allocator).slice(); - const expected_string = expected.toString(globalThis).toSlice(globalThis, default_allocator).slice(); - pass = strings.startsWith(value_string, expected_string) or expected_string.len == 0; - } - - const not = this.op.contains(.not); - if (not) pass = !pass; - - if (pass) return thisValue; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; - const value_fmt = value.toFmt(globalThis, &formatter); - const expected_fmt = expected.toFmt(globalThis, &formatter); - - if (not) { - const expected_line = "Expected to not start with: {any}\n"; - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toStartWith", "expected", true) ++ "\n\n" ++ expected_line ++ received_line; - globalThis.throwPretty(fmt, .{ expected_fmt, value_fmt }); - return .zero; - } - - const expected_line = "Expected to start with: {any}\n"; - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toStartWith", "expected", false) ++ "\n\n" ++ expected_line ++ received_line; - globalThis.throwPretty(fmt, .{ expected_fmt, value_fmt }); - return .zero; - } - - pub fn toEndWith(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalThis); - - const thisValue = callFrame.this(); - const arguments_ = callFrame.arguments(1); - const arguments = arguments_.ptr[0..arguments_.len]; - - if (arguments.len < 1) { - globalThis.throwInvalidArguments("toEndWith() requires 1 argument", .{}); - return .zero; - } - - const expected = arguments[0]; - expected.ensureStillAlive(); - - if (!expected.isString()) { - globalThis.throw("toEndWith() requires the first argument to be a string", .{}); - return .zero; - } - - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (this.scope.tests.items.len <= this.test_id) { - globalThis.throw("toEndWith() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - var pass = value.isString(); - if (pass) { - const value_string = value.toString(globalThis).toSlice(globalThis, default_allocator).slice(); - const expected_string = expected.toString(globalThis).toSlice(globalThis, default_allocator).slice(); - pass = strings.endsWith(value_string, expected_string) or expected_string.len == 0; - } - - const not = this.op.contains(.not); - if (not) pass = !pass; - - if (pass) return thisValue; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; - const value_fmt = value.toFmt(globalThis, &formatter); - const expected_fmt = expected.toFmt(globalThis, &formatter); - - if (not) { - const expected_line = "Expected to not end with: {any}\n"; - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toEndWith", "expected", true) ++ "\n\n" ++ expected_line ++ received_line; - globalThis.throwPretty(fmt, .{ expected_fmt, value_fmt }); - return .zero; - } - - const expected_line = "Expected to end with: {any}\n"; - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toEndWith", "expected", false) ++ "\n\n" ++ expected_line ++ received_line; - globalThis.throwPretty(fmt, .{ expected_fmt, value_fmt }); - return .zero; - } - - pub fn toBeInstanceOf(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalObject); - - const thisValue = callFrame.this(); - const _arguments = callFrame.arguments(1); - const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; - - if (arguments.len < 1) { - globalObject.throwInvalidArguments("toBeInstanceOf() requires 1 argument", .{}); - return .zero; - } - - if (this.scope.tests.items.len <= this.test_id) { - globalObject.throw("toBeInstanceOf() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - - const expected_value = arguments[0]; - if (!expected_value.isConstructor()) { - globalObject.throw("Expected value must be a function: {any}", .{expected_value.toFmt(globalObject, &formatter)}); - return .zero; - } - expected_value.ensureStillAlive(); - - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - const not = this.op.contains(.not); - var pass = value.isInstanceOf(globalObject, expected_value); - if (not) pass = !pass; - if (pass) return thisValue; - - // handle failure - const expected_fmt = expected_value.toFmt(globalObject, &formatter); - const value_fmt = value.toFmt(globalObject, &formatter); - if (not) { - const expected_line = "Expected constructor: not {any}\n"; - const received_line = "Received value: {any}\n"; - const fmt = comptime getSignature("toBeInstanceOf", "expected", true) ++ "\n\n" ++ expected_line ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt }); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, value_fmt }); - return .zero; - } - - const expected_line = "Expected constructor: {any}\n"; - const received_line = "Received value: {any}\n"; - const fmt = comptime getSignature("toBeInstanceOf", "expected", false) ++ "\n\n" ++ expected_line ++ received_line; - globalObject.throwPretty(fmt, .{ expected_fmt, value_fmt }); - return .zero; - } - - pub fn toMatch(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { - JSC.markBinding(@src()); - - defer this.postMatch(globalObject); - - const thisValue = callFrame.this(); - const _arguments = callFrame.arguments(1); - const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; - - if (this.scope.tests.items.len <= this.test_id) { - globalObject.throw("toMatch() must be called in a test", .{}); - return .zero; - } - - if (arguments.len < 1) { - globalObject.throwInvalidArguments("toMatch() requires 1 argument", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - - const expected_value = arguments[0]; - if (!expected_value.isString() and !expected_value.isRegExp()) { - globalObject.throw("Expected value must be a string or regular expression: {any}", .{expected_value.toFmt(globalObject, &formatter)}); - return .zero; - } - expected_value.ensureStillAlive(); - - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (!value.isString()) { - globalObject.throw("Received value must be a string: {any}", .{value.toFmt(globalObject, &formatter)}); - return .zero; - } - - const not = this.op.contains(.not); - var pass: bool = brk: { - if (expected_value.isString()) { - break :brk value.stringIncludes(globalObject, expected_value); - } else if (expected_value.isRegExp()) { - break :brk expected_value.toMatch(globalObject, value); - } - unreachable; - }; - - if (not) pass = !pass; - if (pass) return thisValue; - - // handle failure - const expected_fmt = expected_value.toFmt(globalObject, &formatter); - const value_fmt = value.toFmt(globalObject, &formatter); - - if (not) { - const expected_line = "Expected substring or pattern: not {any}\n"; - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toMatch", "expected", true) ++ "\n\n" ++ expected_line ++ received_line; - globalObject.throwPretty(fmt, .{ expected_fmt, value_fmt }); - return .zero; - } - - const expected_line = "Expected substring or pattern: {any}\n"; - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toMatch", "expected", false) ++ "\n\n" ++ expected_line ++ received_line; - globalObject.throwPretty(fmt, .{ expected_fmt, value_fmt }); - return .zero; - } - - pub fn toHaveBeenCalled(this: *Expect, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSC.JSValue { - JSC.markBinding(@src()); - 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", "expected", true); - const fmt = signature ++ "\n\nExpected: not {any}\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", "expected", true); - const fmt = signature ++ "\n\nExpected {any}\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 { - JSC.markBinding(@src()); - - 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; - } + todo, + fail_because_todo_passed, + }; + }; +}; - if (arguments.len < 1 or !arguments[0].isAnyInt()) { - globalObject.throwInvalidArguments("toHaveBeenCalledTimes() requires 1 integer argument", .{}); - return .zero; - } +pub const Jest = struct { + pub var runner: ?*TestRunner = null; - const times = arguments[0].coerce(i32, globalObject); + fn globalHook(comptime name: string) JSC.JSHostFunctionType { + return struct { + pub fn appendGlobalFunctionCallback( + globalThis: *JSC.JSGlobalObject, + callframe: *JSC.CallFrame, + ) callconv(.C) JSValue { + const arguments = callframe.arguments(2); + if (arguments.len < 1) { + globalThis.throwNotEnoughArguments("callback", 1, arguments.len); + return .zero; + } - var pass = @intCast(i32, calls.getLength(globalObject)) == times; + const function = arguments.ptr[0]; + if (function.isEmptyOrUndefinedOrNull() or !function.isCallable(globalThis.vm())) { + globalThis.throwInvalidArgumentType(name, "callback", "function"); + return .zero; + } - const not = this.op.contains(.not); - if (not) pass = !pass; - if (pass) return thisValue; + if (function.getLength(globalThis) > 0) { + globalThis.throw("done() callback is not implemented in global hooks yet. Please make your function take no arguments", .{}); + return .zero; + } - // handle failure - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - if (not) { - const signature = comptime getSignature("toHaveBeenCalled", "expected", true); - const fmt = signature ++ "\n\nExpected: not {any}\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", "expected", true); - const fmt = signature ++ "\n\nExpected {any}\n"; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{calls.toFmt(globalObject, &formatter)}); - return .zero; + function.protect(); + @field(Jest.runner.?.global_callbacks, name).append( + bun.default_allocator, + function, + ) catch unreachable; + return JSC.JSValue.jsUndefined(); } - globalObject.throw(Output.prettyFmt(fmt, false), .{calls.toFmt(globalObject, &formatter)}); - return .zero; - } - - unreachable; + }.appendGlobalFunctionCallback; } - pub fn toMatchObject(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { + pub fn Bun__Jest__createTestPreloadObject(globalObject: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue { JSC.markBinding(@src()); - defer this.postMatch(globalObject); - const thisValue = callFrame.this(); - const args = callFrame.arguments(1).slice(); - - if (this.scope.tests.items.len <= this.test_id) { - globalObject.throw("toMatchObject() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - - const received_object = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; + var global_hooks_object = JSC.JSValue.createEmptyObject(globalObject, 8); + global_hooks_object.ensureStillAlive(); - if (!received_object.isObject()) { - const matcher_error = "\n\nMatcher error: received value must be a non-null object\n"; - if (not) { - const fmt = comptime getSignature("toMatchObject", "expected", true) ++ matcher_error; - globalObject.throwPretty(fmt, .{}); + const notSupportedHereFn = struct { + pub fn notSupportedHere( + globalThis: *JSC.JSGlobalObject, + _: *JSC.CallFrame, + ) callconv(.C) JSValue { + globalThis.throw("This function can only be used in a test.", .{}); return .zero; } + }.notSupportedHere; + const notSupportedHere = JSC.NewFunction(globalObject, null, 0, notSupportedHereFn, false); + notSupportedHere.ensureStillAlive(); - const fmt = comptime getSignature("toMatchObject", "expected", false) ++ matcher_error; - globalObject.throwPretty(fmt, .{}); - return .zero; + inline for (.{ + "expect", + "describe", + "it", + "test", + }) |name| { + global_hooks_object.put(globalObject, ZigString.static(name), notSupportedHere); } - if (args.len < 1 or !args[0].isObject()) { - const matcher_error = "\n\nMatcher error: expected value must be a non-null object\n"; - if (not) { - const fmt = comptime getSignature("toMatchObject", "expected", true) ++ matcher_error; - globalObject.throwPretty(fmt, .{}); - return .zero; - } - const fmt = comptime getSignature("toMatchObject", "expected", false) ++ matcher_error; - globalObject.throwPretty(fmt, .{}); - return .zero; + inline for (.{ "beforeAll", "beforeEach", "afterAll", "afterEach" }) |name| { + const function = JSC.NewFunction(globalObject, null, 1, globalHook(name), false); + function.ensureStillAlive(); + global_hooks_object.put(globalObject, ZigString.static(name), function); } + return global_hooks_object; + } - const property_matchers = args[0]; - - var pass = received_object.jestDeepMatch(property_matchers, globalObject, true); + pub fn Bun__Jest__createTestModuleObject(globalObject: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue { + JSC.markBinding(@src()); - if (not) pass = !pass; - if (pass) return thisValue; + const module = JSC.JSValue.createEmptyObject(globalObject, 11); - // handle failure - const diff_formatter = DiffFormatter{ - .received = received_object, - .expected = property_matchers, - .globalObject = globalObject, - .not = not, - }; + const test_fn = JSC.NewFunction(globalObject, ZigString.static("test"), 2, TestScope.call, false); + module.put( + globalObject, + ZigString.static("test"), + test_fn, + ); + test_fn.put( + globalObject, + ZigString.static("only"), + JSC.NewFunction(globalObject, ZigString.static("only"), 2, TestScope.only, false), + ); + test_fn.put( + globalObject, + ZigString.static("skip"), + JSC.NewFunction(globalObject, ZigString.static("skip"), 2, TestScope.skip, false), + ); + test_fn.put( + globalObject, + ZigString.static("todo"), + JSC.NewFunction(globalObject, ZigString.static("todo"), 2, TestScope.todo, false), + ); + test_fn.put( + globalObject, + ZigString.static("if"), + JSC.NewFunction(globalObject, ZigString.static("if"), 2, TestScope.callIf, false), + ); + test_fn.put( + globalObject, + ZigString.static("skipIf"), + JSC.NewFunction(globalObject, ZigString.static("skipIf"), 2, TestScope.skipIf, false), + ); - if (not) { - const signature = comptime getSignature("toMatchObject", "expected", true); - const fmt = signature ++ "\n\n{any}\n"; - globalObject.throwPretty(fmt, .{diff_formatter}); - return .zero; - } + module.put( + globalObject, + ZigString.static("it"), + test_fn, + ); + const describe = JSC.NewFunction(globalObject, ZigString.static("describe"), 2, DescribeScope.call, false); + describe.put( + globalObject, + ZigString.static("only"), + JSC.NewFunction(globalObject, ZigString.static("only"), 2, DescribeScope.only, false), + ); + describe.put( + globalObject, + ZigString.static("skip"), + JSC.NewFunction(globalObject, ZigString.static("skip"), 2, DescribeScope.skip, false), + ); + describe.put( + globalObject, + ZigString.static("todo"), + JSC.NewFunction(globalObject, ZigString.static("todo"), 2, DescribeScope.todo, false), + ); + describe.put( + globalObject, + ZigString.static("if"), + JSC.NewFunction(globalObject, ZigString.static("if"), 2, DescribeScope.callIf, false), + ); + describe.put( + globalObject, + ZigString.static("skipIf"), + JSC.NewFunction(globalObject, ZigString.static("skipIf"), 2, DescribeScope.skipIf, false), + ); - const signature = comptime getSignature("toMatchObject", "expected", false); - const fmt = signature ++ "\n\n{any}\n"; - globalObject.throwPretty(fmt, .{diff_formatter}); - return .zero; - } + module.put( + globalObject, + ZigString.static("describe"), + describe, + ); - pub const toHaveBeenCalledWith = notImplementedJSCFn; - pub const toHaveBeenLastCalledWith = notImplementedJSCFn; - pub const toHaveBeenNthCalledWith = notImplementedJSCFn; - pub const toHaveReturnedTimes = notImplementedJSCFn; - pub const toHaveReturnedWith = notImplementedJSCFn; - pub const toHaveLastReturnedWith = notImplementedJSCFn; - pub const toHaveNthReturnedWith = notImplementedJSCFn; - pub const toContainEqual = notImplementedJSCFn; - pub const toMatchInlineSnapshot = notImplementedJSCFn; - pub const toThrowErrorMatchingSnapshot = notImplementedJSCFn; - pub const toThrowErrorMatchingInlineSnapshot = notImplementedJSCFn; - - pub const getStaticNot = notImplementedStaticProp; - pub const getStaticResolves = notImplementedStaticProp; - pub const getStaticRejects = notImplementedStaticProp; - - pub fn getNot(this: *Expect, thisValue: JSValue, globalObject: *JSGlobalObject) callconv(.C) JSValue { - _ = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; + module.put( + globalObject, + ZigString.static("beforeAll"), + JSC.NewRuntimeFunction(globalObject, ZigString.static("beforeAll"), 1, DescribeScope.beforeAll, false), + ); + module.put( + globalObject, + ZigString.static("beforeEach"), + JSC.NewRuntimeFunction(globalObject, ZigString.static("beforeEach"), 1, DescribeScope.beforeEach, false), + ); + module.put( + globalObject, + ZigString.static("afterAll"), + JSC.NewRuntimeFunction(globalObject, ZigString.static("afterAll"), 1, DescribeScope.afterAll, false), + ); + module.put( + globalObject, + ZigString.static("afterEach"), + JSC.NewRuntimeFunction(globalObject, ZigString.static("afterEach"), 1, DescribeScope.afterEach, false), + ); + module.put( + globalObject, + ZigString.static("expect"), + Expect.getConstructor(globalObject), + ); - this.op.toggle(.not); + const mock_fn = 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_fn); - return thisValue; - } + const jest = JSValue.createEmptyObject(globalObject, 3); + jest.put(globalObject, ZigString.static("fn"), mock_fn); + 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); - pub const getResolves = notImplementedJSCProp; - pub const getRejects = notImplementedJSCProp; + const vi = JSValue.createEmptyObject(globalObject, 1); + vi.put(globalObject, ZigString.static("fn"), mock_fn); + module.put(globalObject, ZigString.static("vi"), vi); - pub fn any(globalObject: *JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { - return ExpectAny.call(globalObject, callFrame); + return module; } - pub fn anything(globalObject: *JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { - return ExpectAnything.call(globalObject, callFrame); - } + extern fn JSMockFunction__createObject(*JSC.JSGlobalObject) JSC.JSValue; - pub fn stringContaining(globalObject: *JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { - return ExpectStringContaining.call(globalObject, callFrame); - } + 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 stringMatching(globalObject: *JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { - return ExpectStringMatching.call(globalObject, callFrame); - } + pub fn call( + _: void, + ctx: js.JSContextRef, + _: js.JSObjectRef, + _: js.JSObjectRef, + arguments_: []const js.JSValueRef, + exception: js.ExceptionRef, + ) js.JSValueRef { + JSC.markBinding(@src()); + var runner_ = runner orelse { + JSError(getAllocator(ctx), "Run \"bun test\" to run a test", .{}, ctx, exception); + return js.JSValueMakeUndefined(ctx); + }; + const arguments = @ptrCast([]const JSC.JSValue, arguments_); - pub const extend = notImplementedStaticFn; - pub const arrayContaining = notImplementedStaticFn; - pub const assertions = notImplementedStaticFn; - pub const hasAssertions = notImplementedStaticFn; - pub const objectContaining = notImplementedStaticFn; - pub const addSnapshotSerializer = notImplementedStaticFn; + if (arguments.len < 1 or !arguments[0].isString()) { + JSError(getAllocator(ctx), "Bun.jest() expects a string filename", .{}, ctx, exception); + return js.JSValueMakeUndefined(ctx); + } + var str = arguments[0].toSlice(ctx, bun.default_allocator); + defer str.deinit(); + var slice = str.slice(); - pub fn notImplementedJSCFn(_: *Expect, globalObject: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue { - globalObject.throw("Not implemented", .{}); - return .zero; - } + if (str.len == 0 or slice[0] != '/') { + JSError(getAllocator(ctx), "Bun.jest() expects an absolute file path", .{}, ctx, exception); + return js.JSValueMakeUndefined(ctx); + } + var vm = ctx.bunVM(); + if (vm.is_in_preload) { + return Bun__Jest__testPreloadObject(ctx).asObjectRef(); + } - pub fn notImplementedStaticFn(globalObject: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue { - globalObject.throw("Not implemented", .{}); - return .zero; - } + var filepath = Fs.FileSystem.instance.filename_store.append([]const u8, slice) catch unreachable; - pub fn notImplementedJSCProp(_: *Expect, _: JSC.JSValue, globalObject: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue { - globalObject.throw("Not implemented", .{}); - return .zero; - } + var scope = runner_.getOrPutFile(filepath); + DescribeScope.active = scope; + DescribeScope.module = scope; - pub fn notImplementedStaticProp(globalObject: *JSC.JSGlobalObject, _: JSC.JSValue, _: JSC.JSValue) callconv(.C) JSC.JSValue { - globalObject.throw("Not implemented", .{}); - return .zero; + return Bun__Jest__testModuleObject(ctx).asObjectRef(); } - pub fn postMatch(_: *Expect, globalObject: *JSC.JSGlobalObject) void { - var vm = globalObject.bunVM(); - vm.autoGarbageCollect(); + comptime { + if (!JSC.is_bindgen) { + @export(Bun__Jest__createTestModuleObject, .{ .name = "Bun__Jest__createTestModuleObject" }); + @export(Bun__Jest__createTestPreloadObject, .{ .name = "Bun__Jest__createTestPreloadObject" }); + } } }; @@ -4338,7 +554,7 @@ pub const TestScope = struct { const err = arguments.ptr[0]; globalThis.bunVM().runErrorHandler(err, null); var task: *TestRunnerTask = arguments.ptr[1].asPromisePtr(TestRunnerTask); - task.handleResult(.{ .fail = active_test_expectation_counter.actual }, .promise); + task.handleResult(.{ .fail = expect.active_test_expectation_counter.actual }, .promise); globalThis.bunVM().autoGarbageCollect(); return JSValue.jsUndefined(); } @@ -4346,7 +562,7 @@ pub const TestScope = struct { pub fn onResolve(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSValue { const arguments = callframe.arguments(2); var task: *TestRunnerTask = arguments.ptr[1].asPromisePtr(TestRunnerTask); - task.handleResult(.{ .pass = active_test_expectation_counter.actual }, .promise); + task.handleResult(.{ .pass = expect.active_test_expectation_counter.actual }, .promise); globalThis.bunVM().autoGarbageCollect(); return JSValue.jsUndefined(); } @@ -4365,13 +581,13 @@ pub const TestScope = struct { if (args.len > 0) { const err = args.ptr[0]; if (err.isEmptyOrUndefinedOrNull()) { - task.handleResult(.{ .pass = active_test_expectation_counter.actual }, .callback); + task.handleResult(.{ .pass = expect.active_test_expectation_counter.actual }, .callback); } else { globalThis.bunVM().runErrorHandlerWithDedupe(err, null); - task.handleResult(.{ .fail = active_test_expectation_counter.actual }, .callback); + task.handleResult(.{ .fail = expect.active_test_expectation_counter.actual }, .callback); } } else { - task.handleResult(.{ .pass = active_test_expectation_counter.actual }, .callback); + task.handleResult(.{ .pass = expect.active_test_expectation_counter.actual }, .callback); } } @@ -4432,7 +648,7 @@ pub const TestScope = struct { return .{ .todo = {} }; } - return .{ .fail = active_test_expectation_counter.actual }; + return .{ .fail = expect.active_test_expectation_counter.actual }; } if (initial_value.asAnyPromise()) |promise| { @@ -4459,7 +675,7 @@ pub const TestScope = struct { return .{ .todo = {} }; } - return .{ .fail = active_test_expectation_counter.actual }; + return .{ .fail = expect.active_test_expectation_counter.actual }; }, .Pending => { task.promise_state = .pending; @@ -4481,15 +697,15 @@ pub const TestScope = struct { return .{ .pending = {} }; } - if (active_test_expectation_counter.expected > 0 and active_test_expectation_counter.expected < active_test_expectation_counter.actual) { + if (expect.active_test_expectation_counter.expected > 0 and expect.active_test_expectation_counter.expected < expect.active_test_expectation_counter.actual) { Output.prettyErrorln("Test fail: {d} / {d} expectations\n (make this better!)", .{ - active_test_expectation_counter.actual, - active_test_expectation_counter.expected, + expect.active_test_expectation_counter.actual, + expect.active_test_expectation_counter.expected, }); - return .{ .fail = active_test_expectation_counter.actual }; + return .{ .fail = expect.active_test_expectation_counter.actual }; } - return .{ .pass = active_test_expectation_counter.actual }; + return .{ .pass = expect.active_test_expectation_counter.actual }; } pub const name = "TestScope"; @@ -4876,8 +1092,6 @@ pub const DescribeScope = struct { }; -var active_test_expectation_counter: TestScope.Counter = undefined; - pub const TestRunnerTask = struct { test_id: TestRunner.Test.ID, describe: *DescribeScope, @@ -4920,7 +1134,7 @@ pub const TestRunnerTask = struct { if (jsc_vm.onUnhandledRejectionCtx) |ctx| { var this = bun.cast(*TestRunnerTask, ctx); jsc_vm.onUnhandledRejectionCtx = null; - this.handleResult(.{ .fail = active_test_expectation_counter.actual }, .unhandledRejection); + this.handleResult(.{ .fail = expect.active_test_expectation_counter.actual }, .unhandledRejection); } } @@ -4932,7 +1146,7 @@ pub const TestRunnerTask = struct { // reset the global state for each test // prior to the run DescribeScope.active = describe; - active_test_expectation_counter = .{}; + expect.active_test_expectation_counter = .{}; jsc_vm.last_reported_error_for_dedupe = .zero; const test_id = this.test_id; @@ -4999,7 +1213,7 @@ pub const TestRunnerTask = struct { this.ref.unref(this.globalThis.bunVM()); this.globalThis.throwTerminationException(); - this.handleResult(.{ .fail = active_test_expectation_counter.actual }, .timeout); + this.handleResult(.{ .fail = expect.active_test_expectation_counter.actual }, .timeout); } pub fn handleResult(this: *TestRunnerTask, result: Result, comptime from: @Type(.EnumLiteral)) void { @@ -5404,11 +1618,3 @@ 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/bun.js/test/pretty_format.zig b/src/bun.js/test/pretty_format.zig index 29e330a04..4a245c3bb 100644 --- a/src/bun.js/test/pretty_format.zig +++ b/src/bun.js/test/pretty_format.zig @@ -15,6 +15,7 @@ const JSPrinter = bun.js_printer; const JSPrivateDataPtr = JSC.JSPrivateDataPtr; const JS = @import("../javascript.zig"); const JSPromise = JSC.JSPromise; +const expect = @import("./expect.zig"); pub const EventType = enum(u8) { Event, @@ -1264,12 +1265,12 @@ pub const JestPrettyFormat = struct { } else if (value.as(JSC.ResolveMessage)) |resolve_log| { resolve_log.msg.writeFormat(writer_, enable_ansi_colors) catch {}; return; - } else if (value.as(JSC.Jest.ExpectAnything) != null) { + } else if (value.as(expect.ExpectAnything) != null) { this.addForNewLine("Anything".len); writer.writeAll("Anything"); return; - } else if (value.as(JSC.Jest.ExpectAny) != null) { - const constructor_value = JSC.Jest.ExpectAny.constructorValueGetCached(value) orelse return; + } else if (value.as(expect.ExpectAny) != null) { + const constructor_value = expect.ExpectAny.constructorValueGetCached(value) orelse return; this.addForNewLine("Any<".len); writer.writeAll("Any<"); @@ -1281,16 +1282,16 @@ pub const JestPrettyFormat = struct { writer.writeAll(">"); return; - } else if (value.as(JSC.Jest.ExpectStringContaining) != null) { - const substring_value = JSC.Jest.ExpectStringContaining.stringValueGetCached(value) orelse return; + } else if (value.as(expect.ExpectStringContaining) != null) { + const substring_value = expect.ExpectStringContaining.stringValueGetCached(value) orelse return; this.addForNewLine("StringContaining ".len); writer.writeAll("StringContaining "); this.printAs(.String, Writer, writer_, substring_value, .String, enable_ansi_colors); return; - } else if (value.as(JSC.Jest.ExpectStringMatching) != null) { - const test_value = JSC.Jest.ExpectStringMatching.testValueGetCached(value) orelse return; + } else if (value.as(expect.ExpectStringMatching) != null) { + const test_value = expect.ExpectStringMatching.testValueGetCached(value) orelse return; this.addForNewLine("StringMatching ".len); writer.writeAll("StringMatching "); diff --git a/src/bun.js/test/snapshot.zig b/src/bun.js/test/snapshot.zig new file mode 100644 index 000000000..12c7b3c36 --- /dev/null +++ b/src/bun.js/test/snapshot.zig @@ -0,0 +1,284 @@ +const std = @import("std"); +const bun = @import("root").bun; +const default_allocator = bun.default_allocator; +const string = bun.string; +const MutableString = bun.MutableString; +const strings = bun.strings; +const logger = bun.logger; +const jest = @import("./jest.zig"); +const Jest = jest.Jest; +const TestRunner = jest.TestRunner; +const js_parser = bun.js_parser; +const js_ast = bun.JSAst; +const JSC = bun.JSC; +const JSValue = JSC.JSValue; +const VirtualMachine = JSC.VirtualMachine; +const Expect = @import("./expect.zig").Expect; + +pub const Snapshots = struct { + const file_header = "// Bun Snapshot v1, https://goo.gl/fbAQLP\n"; + pub const ValuesHashMap = std.HashMap(usize, string, bun.IdentityContext(usize), std.hash_map.default_max_load_percentage); + + allocator: std.mem.Allocator, + update_snapshots: bool, + total: usize = 0, + added: usize = 0, + passed: usize = 0, + failed: usize = 0, + + file_buf: *std.ArrayList(u8), + values: *ValuesHashMap, + counts: *bun.StringHashMap(usize), + _current_file: ?File = null, + snapshot_dir_path: ?string = null, + + const File = struct { + id: TestRunner.File.ID, + file: std.fs.File, + }; + + pub fn getOrPut(this: *Snapshots, expect: *Expect, value: JSValue, hint: string, globalObject: *JSC.JSGlobalObject) !?string { + switch (try this.getSnapshotFile(expect.scope.file_id)) { + .result => {}, + .err => |err| { + return switch (err.syscall) { + .mkdir => error.FailedToMakeSnapshotDirectory, + .open => error.FailedToOpenSnapshotFile, + else => error.SnapshotFailed, + }; + }, + } + + const snapshot_name = try expect.getSnapshotName(this.allocator, hint); + this.total += 1; + + var count_entry = try this.counts.getOrPut(snapshot_name); + const counter = brk: { + if (count_entry.found_existing) { + this.allocator.free(snapshot_name); + count_entry.value_ptr.* += 1; + break :brk count_entry.value_ptr.*; + } + count_entry.value_ptr.* = 1; + break :brk count_entry.value_ptr.*; + }; + + const name = count_entry.key_ptr.*; + + var counter_string_buf = [_]u8{0} ** 32; + var counter_string = try std.fmt.bufPrint(&counter_string_buf, "{d}", .{counter}); + + var name_with_counter = try this.allocator.alloc(u8, name.len + 1 + counter_string.len); + defer this.allocator.free(name_with_counter); + bun.copy(u8, name_with_counter[0..name.len], name); + name_with_counter[name.len] = ' '; + bun.copy(u8, name_with_counter[name.len + 1 ..], counter_string); + + const name_hash = bun.hash(name_with_counter); + if (this.values.get(name_hash)) |expected| { + return expected; + } + + // doesn't exist. append to file bytes and add to hashmap. + var pretty_value = try MutableString.init(this.allocator, 0); + try value.jestSnapshotPrettyFormat(&pretty_value, globalObject); + + const serialized_length = "\nexports[`".len + name_with_counter.len + "`] = `".len + pretty_value.list.items.len + "`;\n".len; + try this.file_buf.ensureUnusedCapacity(serialized_length); + this.file_buf.appendSliceAssumeCapacity("\nexports[`"); + this.file_buf.appendSliceAssumeCapacity(name_with_counter); + this.file_buf.appendSliceAssumeCapacity("`] = `"); + this.file_buf.appendSliceAssumeCapacity(pretty_value.list.items); + this.file_buf.appendSliceAssumeCapacity("`;\n"); + + this.added += 1; + try this.values.put(name_hash, pretty_value.toOwnedSlice()); + return null; + } + + pub fn parseFile(this: *Snapshots) !void { + if (this.file_buf.items.len == 0) return; + + const vm = VirtualMachine.get(); + var opts = js_parser.Parser.Options.init(vm.bundler.options.jsx, .js); + var temp_log = logger.Log.init(this.allocator); + + const test_file = Jest.runner.?.files.get(this._current_file.?.id); + const test_filename = test_file.source.path.name.filename; + const dir_path = test_file.source.path.name.dirWithTrailingSlash(); + + var snapshot_file_path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; + var remain: []u8 = snapshot_file_path_buf[0..bun.MAX_PATH_BYTES]; + bun.copy(u8, remain, dir_path); + remain = remain[dir_path.len..]; + bun.copy(u8, remain, "__snapshots__/"); + remain = remain["__snapshots__/".len..]; + bun.copy(u8, remain, test_filename); + remain = remain[test_filename.len..]; + bun.copy(u8, remain, ".snap"); + remain = remain[".snap".len..]; + remain[0] = 0; + const snapshot_file_path = snapshot_file_path_buf[0 .. snapshot_file_path_buf.len - remain.len :0]; + + const source = logger.Source.initPathString(snapshot_file_path, this.file_buf.items); + + var parser = try js_parser.Parser.init( + opts, + &temp_log, + &source, + vm.bundler.options.define, + this.allocator, + ); + + var parse_result = try parser.parse(); + var ast = if (parse_result == .ast) parse_result.ast else return error.ParseError; + defer ast.deinit(); + + if (ast.exports_ref.isNull()) return; + const exports_ref = ast.exports_ref; + + // TODO: when common js transform changes, keep this updated or add flag to support this version + + const export_default = brk: { + for (ast.parts.slice()) |part| { + for (part.stmts) |stmt| { + if (stmt.data == .s_export_default and stmt.data.s_export_default.value == .expr) { + break :brk stmt.data.s_export_default.value.expr; + } + } + } + + return; + }; + + if (export_default.data == .e_call) { + const function_call = export_default.data.e_call; + if (function_call.args.len == 2 and function_call.args.ptr[0].data == .e_function) { + const arg_function_stmts = function_call.args.ptr[0].data.e_function.func.body.stmts; + for (arg_function_stmts) |stmt| { + switch (stmt.data) { + .s_expr => |expr| { + if (expr.value.data == .e_binary and expr.value.data.e_binary.op == .bin_assign) { + const left = expr.value.data.e_binary.left; + if (left.data == .e_index and left.data.e_index.index.data == .e_string and left.data.e_index.target.data == .e_identifier) { + const target: js_ast.E.Identifier = left.data.e_index.target.data.e_identifier; + var index: *js_ast.E.String = left.data.e_index.index.data.e_string; + if (target.ref.eql(exports_ref) and expr.value.data.e_binary.right.data == .e_string) { + const key = index.slice(this.allocator); + var value_string = expr.value.data.e_binary.right.data.e_string; + const value = value_string.slice(this.allocator); + defer { + if (!index.isUTF8()) this.allocator.free(key); + if (!value_string.isUTF8()) this.allocator.free(value); + } + const value_clone = try this.allocator.alloc(u8, value.len); + bun.copy(u8, value_clone, value); + const name_hash = bun.hash(key); + try this.values.put(name_hash, value_clone); + } + } + } + }, + else => {}, + } + } + } + } + } + + pub fn writeSnapshotFile(this: *Snapshots) !void { + if (this._current_file) |_file| { + var file = _file; + file.file.writeAll(this.file_buf.items) catch { + return error.FailedToWriteSnapshotFile; + }; + file.file.close(); + this.file_buf.clearAndFree(); + + var value_itr = this.values.valueIterator(); + while (value_itr.next()) |value| { + this.allocator.free(value.*); + } + this.values.clearAndFree(); + + var count_key_itr = this.counts.keyIterator(); + while (count_key_itr.next()) |key| { + this.allocator.free(key.*); + } + this.counts.clearAndFree(); + } + } + + fn getSnapshotFile(this: *Snapshots, file_id: TestRunner.File.ID) !JSC.Maybe(void) { + if (this._current_file == null or this._current_file.?.id != file_id) { + try this.writeSnapshotFile(); + + const test_file = Jest.runner.?.files.get(file_id); + const test_filename = test_file.source.path.name.filename; + const dir_path = test_file.source.path.name.dirWithTrailingSlash(); + + var snapshot_file_path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; + var remain: []u8 = snapshot_file_path_buf[0..bun.MAX_PATH_BYTES]; + bun.copy(u8, remain, dir_path); + remain = remain[dir_path.len..]; + bun.copy(u8, remain, "__snapshots__/"); + remain = remain["__snapshots__/".len..]; + + if (this.snapshot_dir_path == null or !strings.eqlLong(dir_path, this.snapshot_dir_path.?, true)) { + remain[0] = 0; + const snapshot_dir_path = snapshot_file_path_buf[0 .. snapshot_file_path_buf.len - remain.len :0]; + switch (JSC.Node.Syscall.mkdir(snapshot_dir_path, 0o777)) { + .result => this.snapshot_dir_path = dir_path, + .err => |err| { + switch (err.getErrno()) { + std.os.E.EXIST => this.snapshot_dir_path = dir_path, + else => return JSC.Maybe(void){ + .err = err, + }, + } + }, + } + } + + bun.copy(u8, remain, test_filename); + remain = remain[test_filename.len..]; + bun.copy(u8, remain, ".snap"); + remain = remain[".snap".len..]; + remain[0] = 0; + const snapshot_file_path = snapshot_file_path_buf[0 .. snapshot_file_path_buf.len - remain.len :0]; + + var flags: JSC.Node.Mode = std.os.O.CREAT | std.os.O.RDWR; + if (this.update_snapshots) flags |= std.os.O.TRUNC; + const fd = switch (JSC.Node.Syscall.open(snapshot_file_path, flags, 0o644)) { + .result => |_fd| _fd, + .err => |err| return JSC.Maybe(void){ + .err = err, + }, + }; + + var file: File = .{ + .id = file_id, + .file = .{ .handle = fd }, + }; + + if (this.update_snapshots) { + try this.file_buf.appendSlice(file_header); + } else { + const length = try file.file.getEndPos(); + if (length == 0) { + try this.file_buf.appendSlice(file_header); + } else { + const buf = try this.allocator.alloc(u8, length); + _ = try file.file.preadAll(buf, 0); + try this.file_buf.appendSlice(buf); + this.allocator.free(buf); + } + } + + this._current_file = file; + try this.parseFile(); + } + + return JSC.Maybe(void).success; + } +}; diff --git a/src/cli/test_command.zig b/src/cli/test_command.zig index b7712c0de..445473839 100644 --- a/src/cli/test_command.zig +++ b/src/cli/test_command.zig @@ -41,7 +41,7 @@ const HTTPThread = @import("root").bun.HTTP.HTTPThread; const JSC = @import("root").bun.JSC; const jest = JSC.Jest; const TestRunner = JSC.Jest.TestRunner; -const Snapshots = JSC.Jest.Snapshots; +const Snapshots = JSC.Snapshot.Snapshots; const Test = TestRunner.Test; const NetworkThread = @import("root").bun.HTTP.NetworkThread; const uws = @import("root").bun.uws; diff --git a/src/jsc.zig b/src/jsc.zig index 26ad7cc5f..67cf3f05c 100644 --- a/src/jsc.zig +++ b/src/jsc.zig @@ -26,6 +26,8 @@ pub const Cloudflare = struct { pub const AttributeIterator = @import("./bun.js/api/html_rewriter.zig").AttributeIterator; }; pub const Jest = @import("./bun.js/test/jest.zig"); +pub const Expect = @import("./bun.js/test/expect.zig"); +pub const Snapshot = @import("./bun.js/test/snapshot.zig"); pub const API = struct { pub const JSBundler = @import("./bun.js/api/JSBundler.zig").JSBundler; pub const BuildArtifact = @import("./bun.js/api/JSBundler.zig").BuildArtifact; diff --git a/test/cli/test/bun-test.test.ts b/test/cli/test/bun-test.test.ts index 47ea17db3..534c17513 100644 --- a/test/cli/test/bun-test.test.ts +++ b/test/cli/test/bun-test.test.ts @@ -192,14 +192,34 @@ describe("bun test", () => { }); expect(stderr).toContain("Invalid timeout"); }); + test("timeout can be set to 0ms", () => { + const stderr = runTest({ + args: ["--timeout", "0"], + input: ` + import { test, expect } from "bun:test"; + import { sleep } from "bun"; + test("ok", async () => { + await expect(Promise.resolve()).resolves.toBeUndefined(); + await expect(Promise.reject()).rejects.toBeUndefined(); + }); + test("timeout", async () => { + await expect(sleep(1)).resolves.toBeUndefined(); + }); + `, + }); + expect(stderr).toContain("timed out after 0ms"); + }); test("timeout can be set to 1ms", () => { const stderr = runTest({ args: ["--timeout", "1"], input: ` import { test, expect } from "bun:test"; import { sleep } from "bun"; + test("ok", async () => { + await expect(sleep(1)).resolves.toBeUndefined(); + }); test("timeout", async () => { - await sleep(2); + await expect(sleep(2)).resolves.toBeUndefined(); }); `, }); -- cgit v1.2.3 From ceec1afec2bb187fecef0f5006dfe27ee3e1a9a6 Mon Sep 17 00:00:00 2001 From: dave caruso Date: Sat, 24 Jun 2023 02:24:05 -0400 Subject: Add vi.spyOn and clean up some mock function binding calls (#3376) * Add vi.spyOn and clean up some binding calls * add vi.restoreAllMocks * remove junk file --------- Co-authored-by: Jarred Sumner --- src/bun.js/bindings/JSMockFunction.cpp | 12 ++++-------- src/bun.js/test/jest.zig | 25 +++++++++++++------------ test/bun.lockb | Bin 56172 -> 86272 bytes test/js/bun/test/mock-fn.test.js | 12 ++++++++++-- test/js/bun/test/test-interop.js | 19 +++++++++++++++++++ test/package.json | 3 ++- 6 files changed, 48 insertions(+), 23 deletions(-) (limited to 'src/bun.js/test') diff --git a/src/bun.js/bindings/JSMockFunction.cpp b/src/bun.js/bindings/JSMockFunction.cpp index b7c2659b4..fbfcf0c9e 100644 --- a/src/bun.js/bindings/JSMockFunction.cpp +++ b/src/bun.js/bindings/JSMockFunction.cpp @@ -391,6 +391,7 @@ void JSMockFunction::visitChildrenImpl(JSCell* cell, Visitor& visitor) visitor.append(fn->instances); visitor.append(fn->returnValues); visitor.append(fn->invocationCallOrder); + visitor.append(fn->spyOriginal); fn->mock.visit(visitor); } DEFINE_VISIT_CHILDREN(JSMockFunction); @@ -526,13 +527,13 @@ extern "C" void JSMock__resetSpies(Zig::GlobalObject* globalObject) globalObject->mockModule.activeSpies.clear(); } -extern "C" EncodedJSValue jsFunctionResetSpies(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callframe) +extern "C" EncodedJSValue JSMock__jsRestoreAllMocks(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callframe) { JSMock__resetSpies(jsCast(globalObject)); return JSValue::encode(jsUndefined()); } -extern "C" EncodedJSValue JSMock__spyOn(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callframe) +extern "C" EncodedJSValue JSMock__jsSpyOn(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callframe) { auto& vm = lexicalGlobalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); @@ -963,7 +964,7 @@ JSC_DEFINE_CUSTOM_GETTER(jsMockFunctionGetter_protoImpl, (JSC::JSGlobalObject * return JSValue::encode(jsUndefined()); } -JSC_DEFINE_HOST_FUNCTION(jsMockFunctionConstructor, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callframe)) +extern "C" EncodedJSValue JSMock__jsMockFn(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callframe) { auto& vm = lexicalGlobalObject->vm(); auto* globalObject = jsCast(lexicalGlobalObject); @@ -997,11 +998,6 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionConstructor, (JSC::JSGlobalObject * lexic return JSValue::encode(thisObject); } -extern "C" EncodedJSValue JSMockFunction__createObject(Zig::GlobalObject* globalObject) -{ - auto& vm = globalObject->vm(); - return JSValue::encode(JSC::JSFunction::create(vm, globalObject, 0, "mock"_s, jsMockFunctionConstructor, ImplementationVisibility::Public)); -} extern "C" EncodedJSValue JSMockFunction__getCalls(EncodedJSValue encodedValue) { JSValue value = JSValue::decode(encodedValue); diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig index a1a4f2af8..f43afb1a2 100644 --- a/src/bun.js/test/jest.zig +++ b/src/bun.js/test/jest.zig @@ -333,7 +333,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, 11); + const module = JSC.JSValue.createEmptyObject(globalObject, 12); const test_fn = JSC.NewFunction(globalObject, ZigString.static("test"), 2, TestScope.call, false); module.put( @@ -431,31 +431,32 @@ pub const Jest = struct { Expect.getConstructor(globalObject), ); - const mock_fn = 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_fn); + const mockFn = JSC.NewFunction(globalObject, ZigString.static("fn"), 1, JSMock__jsMockFn, false); + const spyOn = JSC.NewFunction(globalObject, ZigString.static("spyOn"), 2, JSMock__jsSpyOn, false); + const restoreAllMocks = JSC.NewFunction(globalObject, ZigString.static("restoreAllMocks"), 2, JSMock__jsRestoreAllMocks, false); + module.put(globalObject, ZigString.static("mock"), mockFn); const jest = JSValue.createEmptyObject(globalObject, 3); - jest.put(globalObject, ZigString.static("fn"), mock_fn); + jest.put(globalObject, ZigString.static("fn"), mockFn); 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); - const vi = JSValue.createEmptyObject(globalObject, 1); - vi.put(globalObject, ZigString.static("fn"), mock_fn); + const vi = JSValue.createEmptyObject(globalObject, 3); + vi.put(globalObject, ZigString.static("fn"), mockFn); + vi.put(globalObject, ZigString.static("spyOn"), spyOn); + vi.put(globalObject, ZigString.static("restoreAllMocks"), restoreAllMocks); module.put(globalObject, ZigString.static("vi"), vi); 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; + extern fn JSMock__jsMockFn(*JSC.JSGlobalObject, *JSC.CallFrame) JSC.JSValue; + extern fn JSMock__jsRestoreAllMocks(*JSC.JSGlobalObject, *JSC.CallFrame) JSC.JSValue; + extern fn JSMock__jsSpyOn(*JSC.JSGlobalObject, *JSC.CallFrame) JSC.JSValue; pub fn call( _: void, diff --git a/test/bun.lockb b/test/bun.lockb index fc86845e1..a26f2fdfe 100755 Binary files a/test/bun.lockb and b/test/bun.lockb differ diff --git a/test/js/bun/test/mock-fn.test.js b/test/js/bun/test/mock-fn.test.js index eac981fd1..8504e3d70 100644 --- a/test/js/bun/test/mock-fn.test.js +++ b/test/js/bun/test/mock-fn.test.js @@ -2,7 +2,16 @@ * This file is meant to be runnable in both Jest and Bun. * `bunx jest mock-fn.test.js` */ -var { isBun, test, describe, expect, jest, vi, mock, bunTest, spyOn } = require("./test-interop.js")(); +var { isBun, expect, jest, vi, mock, spyOn } = require("./test-interop.js")(); + +// if you want to test vitest, comment the above and uncomment the below + +// import { expect, describe, test, vi } from "vitest"; +// const isBun = false; +// const jest = { fn: vi.fn, restoreAllMocks: vi.restoreAllMocks }; +// const spyOn = vi.spyOn; +// import * as extended from "jest-extended"; +// expect.extend(extended); async function expectResolves(promise) { expect(promise).toBeInstanceOf(Promise); @@ -434,7 +443,6 @@ describe("mock()", () => { return "3"; }, ); - expect(result).toBe(undefined); expect(fn()).toBe("1"); }); test("withImplementation (async)", async () => { diff --git a/test/js/bun/test/test-interop.js b/test/js/bun/test/test-interop.js index 5c41082d6..4b2199ae9 100644 --- a/test/js/bun/test/test-interop.js +++ b/test/js/bun/test/test-interop.js @@ -20,6 +20,25 @@ module.exports = () => { vi: bunTest.vi, spyOn: bunTest.spyOn, }; + } else if (process.env.VITEST) { + const vi = require("vitest"); + + return { + isBun: false, + bunTest: null, + test: vi.test, + describe: vi.describe, + it: vi.it, + expect: vi.expect, + beforeEach: vi.beforeEach, + afterEach: vi.afterEach, + beforeAll: vi.beforeAll, + afterAll: vi.afterAll, + jest: { fn: vi.fn }, + mock: null, + vi, + spyOn: vi.spyOn, + }; } else { const globals = require("@jest/globals"); const extended = require("jest-extended"); diff --git a/test/package.json b/test/package.json index a9b8db913..480305013 100644 --- a/test/package.json +++ b/test/package.json @@ -22,7 +22,8 @@ "supertest": "^6.1.6", "svelte": "^3.55.1", "typescript": "^5.0.2", - "undici": "^5.20.0" + "undici": "^5.20.0", + "vitest": "^0.32.2" }, "private": true, "scripts": { -- cgit v1.2.3 From a5100ad380f099907d4b3a9bec696f481b8e7821 Mon Sep 17 00:00:00 2001 From: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> Date: Mon, 26 Jun 2023 15:49:14 -0700 Subject: Fix .rejects --- src/bun.js/test/expect.zig | 21 +++++++-------------- test/js/bun/test/expect.test.js | 22 ++++++++++++++++++++++ 2 files changed, 29 insertions(+), 14 deletions(-) (limited to 'src/bun.js/test') diff --git a/src/bun.js/test/expect.zig b/src/bun.js/test/expect.zig index 90fcb2a73..e0833f8ed 100644 --- a/src/bun.js/test/expect.zig +++ b/src/bun.js/test/expect.zig @@ -73,32 +73,25 @@ pub const Expect = struct { } pub fn getResolves(this: *Expect, thisValue: JSValue, globalThis: *JSGlobalObject) callconv(.C) JSValue { - switch (this.flags.promise) { + this.flags.promise = switch (this.flags.promise) { + .resolves, .none => .resolves, .rejects => { globalThis.throw("Cannot chain .resolves() after .rejects()", .{}); return .zero; }, - .resolves => {}, - .none => { - this.flags.promise = .resolves; - }, - } + }; + return thisValue; } pub fn getRejects(this: *Expect, thisValue: JSValue, globalThis: *JSGlobalObject) callconv(.C) JSValue { - switch (this.flags.promise) { - .rejects => { - this.flags.promise = .rejects; - }, + this.flags.promise = switch (this.flags.promise) { + .none, .rejects => .rejects, .resolves => { globalThis.throw("Cannot chain .rejects() after .resolves()", .{}); return .zero; }, - .none => { - this.flags.promise = .resolves; - }, - } + }; return thisValue; } diff --git a/test/js/bun/test/expect.test.js b/test/js/bun/test/expect.test.js index d42ec2219..e7195d6f8 100644 --- a/test/js/bun/test/expect.test.js +++ b/test/js/bun/test/expect.test.js @@ -6,6 +6,28 @@ var { isBun, test, describe, expect, jest, vi, mock, bunTest, spyOn } = require("./test-interop.js")(); describe("expect()", () => { + test("rejects", async () => { + await expect(Promise.reject(1)).rejects.toBe(1); + + // Different task + await expect( + new Promise((_, reject) => { + setTimeout(() => reject(1), 0); + }), + ).rejects.toBe(1); + }); + + test("resolves", async () => { + await expect(Promise.resolve(1)).resolves.toBe(1); + + // Different task + await expect( + new Promise(resolve => { + setTimeout(() => resolve(1), 0); + }), + ).resolves.toBe(1); + }); + test("can call without an argument", () => { expect().toBe(undefined); }); -- cgit v1.2.3 From 182e8aa1392af9ba92beccce06f49f8e4593fe5c Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Thu, 29 Jun 2023 02:27:15 +0300 Subject: [jest] fix lifecycle hook execution order (#3447) --- src/bun.js/test/jest.zig | 162 +++++++++++++++++++++--------------- test/js/bun/test/jest-hooks.test.ts | 31 +++++++ test/js/bun/test/test-test.test.ts | 6 +- 3 files changed, 130 insertions(+), 69 deletions(-) (limited to 'src/bun.js/test') diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig index f43afb1a2..d3bb90747 100644 --- a/src/bun.js/test/jest.zig +++ b/src/bun.js/test/jest.zig @@ -493,8 +493,7 @@ pub const Jest = struct { var filepath = Fs.FileSystem.instance.filename_store.append([]const u8, slice) catch unreachable; var scope = runner_.getOrPutFile(filepath); - DescribeScope.active = scope; - DescribeScope.module = scope; + scope.push(); return Bun__Jest__testModuleObject(ctx).asObjectRef(); } @@ -751,17 +750,16 @@ pub const DescribeScope = struct { } pub fn push(new: *DescribeScope) void { - if (comptime is_bindgen) return undefined; - if (new == DescribeScope.active) return; - - new.parent = DescribeScope.active; + if (comptime is_bindgen) return; + std.debug.assert(DescribeScope.active != new); + if (new.parent) |scope| std.debug.assert(scope == DescribeScope.active); DescribeScope.active = new; } pub fn pop(this: *DescribeScope) void { - if (comptime is_bindgen) return undefined; - if (DescribeScope.active == this) - DescribeScope.active = this.parent orelse DescribeScope.active; + if (comptime is_bindgen) return; + std.debug.assert(DescribeScope.active == this); + DescribeScope.active = this.parent; } pub const LifecycleHook = enum { @@ -771,8 +769,7 @@ pub const DescribeScope = struct { afterAll, }; - pub threadlocal var active: *DescribeScope = undefined; - pub threadlocal var module: *DescribeScope = undefined; + pub threadlocal var active: ?*DescribeScope = null; const CallbackFn = *const fn ( *JSC.JSGlobalObject, @@ -781,21 +778,24 @@ pub const DescribeScope = struct { fn createCallback(comptime hook: LifecycleHook) CallbackFn { return struct { - const this_hook = hook; pub fn run( globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame, ) callconv(.C) JSC.JSValue { - const arguments_ = callframe.arguments(2); - const arguments = arguments_.ptr[0..arguments_.len]; - if (arguments.len == 0 or !arguments[0].isObject() or !arguments[0].isCallable(globalThis.vm())) { - globalThis.throwInvalidArgumentType(@tagName(this_hook), "callback", "function"); + const arguments = callframe.arguments(2); + if (arguments.len < 1) { + globalThis.throwNotEnoughArguments("callback", 1, arguments.len); return .zero; } - arguments[0].protect(); - const name = comptime @as(string, @tagName(this_hook)); - @field(DescribeScope.active, name).append(getAllocator(globalThis), arguments[0]) catch unreachable; + const cb = arguments.ptr[0]; + if (!cb.isObject() or !cb.isCallable(globalThis.vm())) { + globalThis.throwInvalidArgumentType(@tagName(hook), "callback", "function"); + return .zero; + } + + cb.protect(); + @field(DescribeScope.active.?, @tagName(hook)).append(getAllocator(globalThis), cb) catch unreachable; return JSC.JSValue.jsBoolean(true); } }.run; @@ -829,11 +829,22 @@ pub const DescribeScope = struct { pub const beforeAll = createCallback(.beforeAll); pub const beforeEach = createCallback(.beforeEach); - pub fn execCallback(this: *DescribeScope, globalObject: *JSC.JSGlobalObject, comptime hook: LifecycleHook) JSValue { - const name = comptime @as(string, @tagName(hook)); - var hooks: []JSC.JSValue = @field(this, name).items; - for (hooks, 0..) |cb, i| { - if (cb.isEmpty()) continue; + pub fn execCallback(this: *DescribeScope, globalObject: *JSC.JSGlobalObject, comptime hook: LifecycleHook) ?JSValue { + var hooks = &@field(this, @tagName(hook)); + defer { + if (comptime hook == .beforeAll or hook == .afterAll) { + hooks.clearAndFree(getAllocator(globalObject)); + } + } + + for (hooks.items) |cb| { + std.debug.assert(cb.isObject()); + std.debug.assert(cb.isCallable(globalObject.vm())); + defer { + if (comptime hook == .beforeAll or hook == .afterAll) { + cb.unprotect(); + } + } const pending_test = Jest.runner.?.pending_test; // forbid `expect()` within hooks @@ -843,20 +854,23 @@ pub const DescribeScope = struct { Jest.runner.?.did_pending_test_fail = false; const vm = VirtualMachine.get(); - var result: JSC.JSValue = if (cb.getLength(globalObject) > 0) brk: { - this.done = false; - const done_func = JSC.NewFunctionWithData( - globalObject, - ZigString.static("done"), - 0, - DescribeScope.onDone, - false, - this, - ); - var result = cb.call(globalObject, &.{done_func}); - vm.waitFor(&this.done); - break :brk result; - } else cb.call(globalObject, &.{}); + var result: JSC.JSValue = switch (cb.getLength(globalObject)) { + 0 => cb.call(globalObject, &.{}), + else => brk: { + this.done = false; + const done_func = JSC.NewFunctionWithData( + globalObject, + ZigString.static("done"), + 0, + DescribeScope.onDone, + false, + this, + ); + var result = cb.call(globalObject, &.{done_func}); + vm.waitFor(&this.done); + break :brk result; + }, + }; if (result.asAnyPromise()) |promise| { if (promise.status(globalObject.vm()) == .Pending) { result.protect(); @@ -870,19 +884,28 @@ pub const DescribeScope = struct { Jest.runner.?.pending_test = pending_test; Jest.runner.?.did_pending_test_fail = orig_did_pending_test_fail; if (result.isAnyError()) return result; - - if (comptime hook == .beforeAll or hook == .afterAll) { - hooks[i] = JSC.JSValue.zero; - } } - return JSValue.zero; + return null; } pub fn runGlobalCallbacks(globalThis: *JSC.JSGlobalObject, comptime hook: LifecycleHook) ?JSValue { // global callbacks - for (@field(Jest.runner.?.global_callbacks, @tagName(hook)).items) |cb| { - if (cb.isEmpty()) continue; + var hooks = &@field(Jest.runner.?.global_callbacks, @tagName(hook)); + defer { + if (comptime hook == .beforeAll or hook == .afterAll) { + hooks.clearAndFree(getAllocator(globalThis)); + } + } + + for (hooks.items) |cb| { + std.debug.assert(cb.isObject()); + std.debug.assert(cb.isCallable(globalThis.vm())); + defer { + if (comptime hook == .beforeAll or hook == .afterAll) { + cb.unprotect(); + } + } const pending_test = Jest.runner.?.pending_test; // forbid `expect()` within hooks @@ -909,28 +932,40 @@ pub const DescribeScope = struct { if (result.isAnyError()) return result; } - if (comptime hook == .beforeAll or hook == .afterAll) { - @field(Jest.runner.?.global_callbacks, @tagName(hook)).items.len = 0; - } - return null; } + fn runBeforeCallbacks(this: *DescribeScope, globalObject: *JSC.JSGlobalObject, comptime hook: LifecycleHook) ?JSValue { + if (this.parent) |scope| { + if (scope.runBeforeCallbacks(globalObject, hook)) |err| { + return err; + } + } + return this.execCallback(globalObject, hook); + } + pub fn runCallback(this: *DescribeScope, globalObject: *JSC.JSGlobalObject, comptime hook: LifecycleHook) JSValue { + if (comptime hook == .afterAll or hook == .afterEach) { + var parent: ?*DescribeScope = this; + while (parent) |scope| { + if (scope.execCallback(globalObject, hook)) |err| { + return err; + } + parent = scope.parent; + } + } + if (runGlobalCallbacks(globalObject, hook)) |err| { return err; } - var parent = this.parent; - while (parent) |scope| { - const ret = scope.execCallback(globalObject, hook); - if (!ret.isEmpty()) { - return ret; + if (comptime hook == .beforeAll or hook == .beforeEach) { + if (this.runBeforeCallbacks(globalObject, hook)) |err| { + return err; } - parent = scope.parent; } - return this.execCallback(globalObject, hook); + return .zero; } pub fn call(globalThis: *JSGlobalObject, callframe: *CallFrame) callconv(.C) JSValue { @@ -961,11 +996,8 @@ pub const DescribeScope = struct { if (comptime is_bindgen) return undefined; callback.protect(); defer callback.unprotect(); - var original_active = active; - defer active = original_active; - if (this != module) - this.parent = this.parent orelse active; - active = this; + this.push(); + defer this.pop(); if (callback == .zero) { this.runTests(globalObject); @@ -1064,9 +1096,8 @@ pub const DescribeScope = struct { if (!this.isAllSkipped()) { // Run the afterAll callbacks, in reverse order // unless there were no tests for this scope - const afterAll_result = this.execCallback(globalThis, .afterAll); - if (!afterAll_result.isEmpty()) { - globalThis.bunVM().runErrorHandler(afterAll_result, null); + if (this.execCallback(globalThis, .afterAll)) |err| { + globalThis.bunVM().runErrorHandler(err, null); } } @@ -1146,7 +1177,6 @@ pub const TestRunnerTask = struct { // reset the global state for each test // prior to the run - DescribeScope.active = describe; expect.active_test_expectation_counter = .{}; jsc_vm.last_reported_error_for_dedupe = .zero; @@ -1417,7 +1447,7 @@ inline fn createScope( return .zero; } - const parent = DescribeScope.active; + const parent = DescribeScope.active.?; const allocator = getAllocator(globalThis); const label = if (description == .zero) "" diff --git a/test/js/bun/test/jest-hooks.test.ts b/test/js/bun/test/jest-hooks.test.ts index c99dc7759..618cdc4c6 100644 --- a/test/js/bun/test/jest-hooks.test.ts +++ b/test/js/bun/test/jest-hooks.test.ts @@ -1,5 +1,36 @@ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "bun:test"; +let hooks_run: string[] = []; + +beforeAll(() => hooks_run.push("global beforeAll")); +beforeEach(() => hooks_run.push("global beforeEach")); +afterAll(() => hooks_run.push("global afterAll")); +afterEach(() => hooks_run.push("global afterEach")); + +describe("describe scope", () => { + beforeAll(() => hooks_run.push("describe beforeAll")); + beforeEach(() => hooks_run.push("describe beforeEach")); + afterAll(() => hooks_run.push("describe afterAll")); + afterEach(() => hooks_run.push("describe afterEach")); + + it("should run after beforeAll/beforeEach in the correct order", () => { + expect(hooks_run).toEqual(["global beforeAll", "describe beforeAll", "global beforeEach", "describe beforeEach"]); + }); + + it("should run after afterEach/afterAll in the correct order", () => { + expect(hooks_run).toEqual([ + "global beforeAll", + "describe beforeAll", + "global beforeEach", + "describe beforeEach", + "describe afterEach", + "global afterEach", + "global beforeEach", + "describe beforeEach", + ]); + }); +}); + describe("test jest hooks in bun-test", () => { describe("test beforeAll hook", () => { let animal = "tiger"; diff --git a/test/js/bun/test/test-test.test.ts b/test/js/bun/test/test-test.test.ts index 7ecfdef11..5f732bb82 100644 --- a/test/js/bun/test/test-test.test.ts +++ b/test/js/bun/test/test-test.test.ts @@ -540,18 +540,18 @@ beforeEach: #2 beforeEach: TEST-FILE beforeEach: one describe scope -- inside one describe scope -- +afterEach: one describe scope +afterEach: TEST-FILE afterEach: #1 afterEach: #2 -afterEach: TEST-FILE -afterEach: one describe scope afterAll: one describe scope beforeEach: #1 beforeEach: #2 beforeEach: TEST-FILE -- the top-level test -- +afterEach: TEST-FILE afterEach: #1 afterEach: #2 -afterEach: TEST-FILE afterAll: TEST-FILE afterAll: #1 afterAll: #2 -- cgit v1.2.3 From 68e6fe00a4be7857f474cb3aa80e0d876499e3f8 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Wed, 28 Jun 2023 21:11:06 -0700 Subject: Use `bun.String` for `ZigException` (#3451) * Use `bun.String` for `ZigException` * woopsie --------- Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> --- src/bun.js/bindings/ZigGlobalObject.cpp | 4 +- src/bun.js/bindings/bindings.cpp | 69 +++++----------- src/bun.js/bindings/exports.zig | 130 ++++++++++++++++++++---------- src/bun.js/bindings/headers-handwritten.h | 16 ++-- src/bun.js/javascript.zig | 72 ++++++++++------- src/bun.js/test/jest.zig | 18 ++--- src/js/out/modules/node/path.js | 2 +- src/js/out/modules/node/stream.web.js | 2 +- src/logger.zig | 4 +- src/string.zig | 24 ++++++ 10 files changed, 198 insertions(+), 143 deletions(-) (limited to 'src/bun.js/test') diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index 4b4edf097..9ff3ebf93 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -390,7 +390,7 @@ static String computeErrorInfoWithoutPrepareStackTrace(JSC::VM& vm, Vectorline.zeroBasedInt(); remappedFrames[i].position.column_start = sourcePositions->startColumn.zeroBasedInt() + 1; diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 68aef29dd..141215ebe 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -3303,7 +3303,7 @@ bool JSC__JSValue__stringIncludes(JSC__JSValue value, JSC__JSGlobalObject* globa static void populateStackFrameMetadata(JSC::VM& vm, const JSC::StackFrame* stackFrame, ZigStackFrame* frame) { - frame->source_url = Zig::toZigString(stackFrame->sourceURL(vm)); + frame->source_url = Bun::toString(stackFrame->sourceURL(vm)); if (stackFrame->isWasmFrame()) { frame->code_type = ZigStackFrameCodeWasm; @@ -3340,37 +3340,11 @@ static void populateStackFrameMetadata(JSC::VM& vm, const JSC::StackFrame* stack JSC::JSObject* callee = JSC::jsCast(calleeCell); - // Does the code block have a user-defined name property? - JSC::JSValue name = callee->getDirect(vm, vm.propertyNames->name); - if (name && name.isString()) { - auto str = name.toWTFString(m_codeBlock->globalObject()); - frame->function_name = Zig::toZigString(str); - return; - } - - /* For functions (either JSFunction or InternalFunction), fallback to their "native" name - * property. Based on JSC::getCalculatedDisplayName, "inlining" the - * JSFunction::calculatedDisplayName\InternalFunction::calculatedDisplayName calls */ - if (JSC::JSFunction* function = JSC::jsDynamicCast(callee)) { - - WTF::String actualName = function->name(vm); - if (!actualName.isEmpty() || function->isHostOrBuiltinFunction()) { - frame->function_name = Zig::toZigString(actualName); - return; - } - - auto inferred_name = function->jsExecutable()->name(); - frame->function_name = Zig::toZigString(inferred_name.string()); - } - - if (JSC::InternalFunction* function = JSC::jsDynamicCast(callee)) { - // Based on JSC::InternalFunction::calculatedDisplayName, skipping the "displayName" property - frame->function_name = Zig::toZigString(function->name()); - } + frame->function_name = Bun::toString(JSC::getCalculatedDisplayName(vm, callee)); } // Based on // https://github.com/mceSystems/node-jsc/blob/master/deps/jscshim/src/shim/JSCStackTrace.cpp#L298 -static void populateStackFramePosition(const JSC::StackFrame* stackFrame, ZigString* source_lines, +static void populateStackFramePosition(const JSC::StackFrame* stackFrame, BunString* source_lines, int32_t* source_line_numbers, uint8_t source_lines_count, ZigStackFramePosition* position) { @@ -3440,7 +3414,7 @@ static void populateStackFramePosition(const JSC::StackFrame* stackFrame, ZigStr // Most of the time, when you look at a stack trace, you want a couple lines above - source_lines[0] = { &chars[lineStart], lineStop - lineStart }; + source_lines[0] = Bun::toString(sourceString.substring(lineStart, lineStop - lineStart).toStringWithoutCopying()); source_line_numbers[0] = line; if (lineStart > 0) { @@ -3457,8 +3431,7 @@ static void populateStackFramePosition(const JSC::StackFrame* stackFrame, ZigStr } // We are at the beginning of the line - source_lines[source_line_i] = { &chars[byte_offset_in_source_string], - end_of_line_offset - byte_offset_in_source_string + 1 }; + source_lines[source_line_i] = Bun::toString(sourceString.substring(byte_offset_in_source_string, end_of_line_offset - byte_offset_in_source_string + 1).toStringWithoutCopying()); source_line_numbers[source_line_i] = line - source_line_i; source_line_i++; @@ -3546,30 +3519,30 @@ static void fromErrorInstance(ZigException* except, JSC::JSGlobalObject* global, except->code = 8; } if (except->code == SYNTAX_ERROR_CODE) { - except->message = Zig::toZigString(err->sanitizedMessageString(global)); + except->message = Bun::toString(err->sanitizedMessageString(global)); } else if (JSC::JSValue message = obj->getIfPropertyExists(global, vm.propertyNames->message)) { - except->message = Zig::toZigString(message, global); + except->message = Bun::toString(global, message); } else { - except->message = Zig::toZigString(err->sanitizedMessageString(global)); + except->message = Bun::toString(err->sanitizedMessageString(global)); } - except->name = Zig::toZigString(err->sanitizedNameString(global)); + except->name = Bun::toString(err->sanitizedNameString(global)); except->runtime_type = err->runtimeTypeForCause(); auto clientData = WebCore::clientData(vm); if (except->code != SYNTAX_ERROR_CODE) { if (JSC::JSValue syscall = obj->getIfPropertyExists(global, clientData->builtinNames().syscallPublicName())) { - except->syscall = Zig::toZigString(syscall, global); + except->syscall = Bun::toString(global, syscall); } if (JSC::JSValue code = obj->getIfPropertyExists(global, clientData->builtinNames().codePublicName())) { - except->code_ = Zig::toZigString(code, global); + except->code_ = Bun::toString(global, code); } if (JSC::JSValue path = obj->getIfPropertyExists(global, clientData->builtinNames().pathPublicName())) { - except->path = Zig::toZigString(path, global); + except->path = Bun::toString(global, path); } if (JSC::JSValue fd = obj->getIfPropertyExists(global, Identifier::fromString(vm, "fd"_s))) { @@ -3585,7 +3558,7 @@ static void fromErrorInstance(ZigException* except, JSC::JSGlobalObject* global, if (getFromSourceURL) { if (JSC::JSValue sourceURL = obj->getIfPropertyExists(global, vm.propertyNames->sourceURL)) { - except->stack.frames_ptr[0].source_url = Zig::toZigString(sourceURL, global); + except->stack.frames_ptr[0].source_url = Bun::toString(global, sourceURL); if (JSC::JSValue column = obj->getIfPropertyExists(global, vm.propertyNames->column)) { except->stack.frames_ptr[0].position.column_start = column.toInt32(global); @@ -3597,7 +3570,7 @@ static void fromErrorInstance(ZigException* except, JSC::JSGlobalObject* global, if (JSC::JSValue lineText = obj->getIfPropertyExists(global, JSC::Identifier::fromString(vm, "lineText"_s))) { if (JSC::JSString* jsStr = lineText.toStringOrNull(global)) { auto str = jsStr->value(global); - except->stack.source_lines_ptr[0] = Zig::toZigString(str); + except->stack.source_lines_ptr[0] = Bun::toString(str); except->stack.source_lines_numbers[0] = except->stack.frames_ptr[0].position.line; except->stack.source_lines_len = 1; except->remapped = true; @@ -3620,7 +3593,7 @@ void exceptionFromString(ZigException* except, JSC::JSValue value, JSC::JSGlobal if (JSC::JSObject* obj = JSC::jsDynamicCast(value)) { if (obj->hasProperty(global, global->vm().propertyNames->name)) { auto name_str = obj->getIfPropertyExists(global, global->vm().propertyNames->name).toWTFString(global); - except->name = Zig::toZigString(name_str); + except->name = Bun::toString(name_str); if (name_str == "Error"_s) { except->code = JSErrorCodeError; } else if (name_str == "EvalError"_s) { @@ -3642,14 +3615,14 @@ void exceptionFromString(ZigException* except, JSC::JSValue value, JSC::JSGlobal if (JSC::JSValue message = obj->getIfPropertyExists(global, global->vm().propertyNames->message)) { if (message) { - except->message = Zig::toZigString( + except->message = Bun::toString( message.toWTFString(global)); } } if (JSC::JSValue sourceURL = obj->getIfPropertyExists(global, global->vm().propertyNames->sourceURL)) { if (sourceURL) { - except->stack.frames_ptr[0].source_url = Zig::toZigString( + except->stack.frames_ptr[0].source_url = Bun::toString( sourceURL.toWTFString(global)); except->stack.frames_len = 1; } @@ -3678,9 +3651,7 @@ void exceptionFromString(ZigException* except, JSC::JSValue value, JSC::JSGlobal } scope.release(); - auto ref = OpaqueJSString::tryCreate(str); - except->message = ZigString { ref->characters8(), ref->length() }; - ref->ref(); + except->message = Bun::toString(str); } void JSC__VM__releaseWeakRefs(JSC__VM* arg0) @@ -3790,8 +3761,8 @@ void JSC__JSValue__toZigException(JSC__JSValue JSValue0, JSC__JSGlobalObject* ar JSC::JSValue value = JSC::JSValue::decode(JSValue0); if (value == JSC::JSValue {}) { exception->code = JSErrorCodeError; - exception->name = Zig::toZigString("Error"_s); - exception->message = Zig::toZigString("Unknown error"_s); + exception->name = Bun::toString("Error"_s); + exception->message = Bun::toString("Unknown error"_s); return; } diff --git a/src/bun.js/bindings/exports.zig b/src/bun.js/bindings/exports.zig index 213291e7b..73a26e4be 100644 --- a/src/bun.js/bindings/exports.zig +++ b/src/bun.js/bindings/exports.zig @@ -29,6 +29,7 @@ const Backtrace = @import("../../crash_reporter.zig"); const JSPrinter = bun.js_printer; const JSLexer = bun.js_lexer; const typeBaseName = @import("../../meta.zig").typeBaseName; +const String = bun.String; pub const ZigGlobalObject = extern struct { pub const shim = Shimmer("Zig", "GlobalObject", @This()); @@ -438,7 +439,7 @@ pub const Process = extern struct { }; pub const ZigStackTrace = extern struct { - source_lines_ptr: [*c]ZigString, + source_lines_ptr: [*c]bun.String, source_lines_numbers: [*c]i32, source_lines_len: u8, source_lines_to_collect: u8, @@ -456,23 +457,24 @@ pub const ZigStackTrace = extern struct { { var source_lines_iter = this.sourceLineIterator(); - var source_line_len: usize = 0; - var count: usize = 0; - while (source_lines_iter.next()) |source| { - count += 1; - source_line_len += source.text.len; - } + var source_line_len = source_lines_iter.getLength(); - if (count > 0 and source_line_len > 0) { - var source_lines = try allocator.alloc(Api.SourceLine, count); + if (source_line_len > 0) { + var source_lines = try allocator.alloc(Api.SourceLine, @intCast(usize, @max(source_lines_iter.i, 0))); var source_line_buf = try allocator.alloc(u8, source_line_len); source_lines_iter = this.sourceLineIterator(); var remain_buf = source_line_buf[0..]; var i: usize = 0; while (source_lines_iter.next()) |source| { - bun.copy(u8, remain_buf, source.text); - const copied_line = remain_buf[0..source.text.len]; - remain_buf = remain_buf[source.text.len..]; + const text = source.text.slice(); + defer source.text.deinit(); + defer bun.copy( + u8, + remain_buf, + text, + ); + const copied_line = remain_buf[0..text.len]; + remain_buf = remain_buf[text.len..]; source_lines[i] = .{ .text = copied_line, .line = source.line }; i += 1; } @@ -508,9 +510,18 @@ pub const ZigStackTrace = extern struct { pub const SourceLine = struct { line: i32, - text: string, + text: ZigString.Slice, }; + pub fn getLength(this: *SourceLineIterator) usize { + var count: usize = 0; + for (this.trace.source_lines_ptr[0..@intCast(usize, this.i)]) |*line| { + count += line.length(); + } + + return count; + } + pub fn untilLast(this: *SourceLineIterator) ?SourceLine { if (this.i < 1) return null; return this.next(); @@ -522,7 +533,7 @@ pub const ZigStackTrace = extern struct { const source_line = this.trace.source_lines_ptr[@intCast(usize, this.i)]; const result = SourceLine{ .line = this.trace.source_lines_numbers[@intCast(usize, this.i)], - .text = source_line.slice(), + .text = source_line.toUTF8(bun.default_allocator), }; this.i -= 1; return result; @@ -541,21 +552,28 @@ pub const ZigStackTrace = extern struct { }; pub const ZigStackFrame = extern struct { - function_name: ZigString, - source_url: ZigString, + function_name: String, + source_url: String, position: ZigStackFramePosition, code_type: ZigStackFrameCode, /// This informs formatters whether to display as a blob URL or not remapped: bool = false, + pub fn deinit(this: *ZigStackFrame) void { + this.function_name.deref(); + this.source_url.deref(); + } + pub fn toAPI(this: *const ZigStackFrame, root_path: string, origin: ?*const ZigURL, allocator: std.mem.Allocator) !Api.StackFrame { var frame: Api.StackFrame = comptime std.mem.zeroes(Api.StackFrame); - if (this.function_name.len > 0) { - frame.function_name = try allocator.dupe(u8, this.function_name.slice()); + if (!this.function_name.isEmpty()) { + var slicer = this.function_name.toUTF8(allocator); + defer slicer.deinit(); + frame.function_name = (try slicer.clone(allocator)).slice(); } - if (this.source_url.len > 0) { + if (!this.source_url.isEmpty()) { frame.file = try std.fmt.allocPrint(allocator, "{any}", .{this.sourceURLFormatter(root_path, origin, true, false)}); } @@ -576,7 +594,7 @@ pub const ZigStackFrame = extern struct { } pub const SourceURLFormatter = struct { - source_url: ZigString, + source_url: bun.String, position: ZigStackFramePosition, enable_color: bool, origin: ?*const ZigURL, @@ -588,7 +606,9 @@ pub const ZigStackFrame = extern struct { try writer.writeAll(Output.prettyFmt("", true)); } - var source_slice = this.source_url.slice(); + var source_slice_ = this.source_url.toUTF8(bun.default_allocator); + var source_slice = source_slice_.slice(); + defer source_slice_.deinit(); if (!this.remapped) { if (this.origin) |origin| { @@ -647,12 +667,12 @@ pub const ZigStackFrame = extern struct { }; pub const NameFormatter = struct { - function_name: ZigString, + function_name: String, code_type: ZigStackFrameCode, enable_color: bool, pub fn format(this: NameFormatter, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { - const name = this.function_name.slice(); + const name = this.function_name; switch (this.code_type) { .Eval => { @@ -662,26 +682,26 @@ pub const ZigStackFrame = extern struct { // try writer.writeAll("(esm)"); }, .Function => { - if (name.len > 0) { + if (!name.isEmpty()) { if (this.enable_color) { - try std.fmt.format(writer, comptime Output.prettyFmt("{s}", true), .{name}); + try std.fmt.format(writer, comptime Output.prettyFmt("{}", true), .{name}); } else { - try std.fmt.format(writer, "{s}", .{name}); + try std.fmt.format(writer, "{}", .{name}); } } }, .Global => { - if (name.len > 0) { - try std.fmt.format(writer, "globalThis {s}", .{name}); + if (!name.isEmpty()) { + try std.fmt.format(writer, "globalThis {}", .{name}); } else { try writer.writeAll("globalThis"); } }, .Wasm => { - try std.fmt.format(writer, "WASM {s}", .{name}); + try std.fmt.format(writer, "WASM {}", .{name}); }, .Constructor => { - try std.fmt.format(writer, "new {s}", .{name}); + try std.fmt.format(writer, "new {}", .{name}); }, else => {}, } @@ -689,9 +709,9 @@ pub const ZigStackFrame = extern struct { }; pub const Zero: ZigStackFrame = ZigStackFrame{ - .function_name = ZigString{ ._unsafe_ptr_do_not_use = "", .len = 0 }, + .function_name = String.empty, .code_type = ZigStackFrameCode.None, - .source_url = ZigString{ ._unsafe_ptr_do_not_use = "", .len = 0 }, + .source_url = String.empty, .position = ZigStackFramePosition.Invalid, }; @@ -744,14 +764,14 @@ pub const ZigException = extern struct { /// SystemError only errno: c_int = 0, /// SystemError only - syscall: ZigString = ZigString.Empty, + syscall: String = String.empty, /// SystemError only - system_code: ZigString = ZigString.Empty, + system_code: String = String.empty, /// SystemError only - path: ZigString = ZigString.Empty, + path: String = String.empty, - name: ZigString, - message: ZigString, + name: String, + message: String, stack: ZigStackTrace, exception: ?*anyopaque, @@ -760,6 +780,19 @@ pub const ZigException = extern struct { fd: i32 = -1, + pub fn deinit(this: *ZigException) void { + this.syscall.deref(); + this.system_code.deref(); + this.path.deref(); + + this.name.deref(); + this.message.deref(); + + for (this.stack.frames_ptr[0..this.stack.frames_len]) |*frame| { + frame.deinit(); + } + } + pub const shim = Shimmer("Zig", "Exception", @This()); pub const name = "ZigException"; pub const namespace = shim.namespace; @@ -768,7 +801,7 @@ pub const ZigException = extern struct { const frame_count = 32; pub const source_lines_count = 6; source_line_numbers: [source_lines_count]i32, - source_lines: [source_lines_count]ZigString, + source_lines: [source_lines_count]String, frames: [frame_count]ZigStackFrame, loaded: bool, zig_exception: ZigException, @@ -786,8 +819,8 @@ pub const ZigException = extern struct { }, .source_lines = brk: { - var lines: [source_lines_count]ZigString = undefined; - @memset(&lines, ZigString.Empty); + var lines: [source_lines_count]String = undefined; + @memset(&lines, String.empty); break :brk lines; }, .zig_exception = undefined, @@ -798,13 +831,17 @@ pub const ZigException = extern struct { return Holder.Zero; } + pub fn deinit(this: *Holder) void { + this.zigException().deinit(); + } + pub fn zigException(this: *Holder) *ZigException { if (!this.loaded) { this.zig_exception = ZigException{ .code = @enumFromInt(JSErrorCode, 255), .runtime_type = JSRuntimeType.Nothing, - .name = ZigString.Empty, - .message = ZigString.Empty, + .name = String.empty, + .message = String.empty, .exception = null, .stack = ZigStackTrace{ .source_lines_ptr = &this.source_lines, @@ -832,8 +869,13 @@ pub const ZigException = extern struct { root_path: string, origin: ?*const ZigURL, ) !void { - const _name: string = @field(this, "name").slice(); - const message: string = @field(this, "message").slice(); + const name_slice = @field(this, "name").toUTF8(bun.default_allocator); + const message_slice = @field(this, "message").toUTF8(bun.default_allocator); + + const _name = name_slice.slice(); + defer name_slice.deinit(); + const message = message_slice.slice(); + defer message_slice.deinit(); var is_empty = true; var api_exception = Api.JsException{ diff --git a/src/bun.js/bindings/headers-handwritten.h b/src/bun.js/bindings/headers-handwritten.h index db1e38d3e..c7429b633 100644 --- a/src/bun.js/bindings/headers-handwritten.h +++ b/src/bun.js/bindings/headers-handwritten.h @@ -120,15 +120,15 @@ typedef struct ZigStackFramePosition { } ZigStackFramePosition; typedef struct ZigStackFrame { - ZigString function_name; - ZigString source_url; + BunString function_name; + BunString source_url; ZigStackFramePosition position; ZigStackFrameCode code_type; bool remapped; } ZigStackFrame; typedef struct ZigStackTrace { - ZigString* source_lines_ptr; + BunString* source_lines_ptr; int32_t* source_lines_numbers; uint8_t source_lines_len; uint8_t source_lines_to_collect; @@ -140,11 +140,11 @@ typedef struct ZigException { unsigned char code; uint16_t runtime_type; int errno_; - ZigString syscall; - ZigString code_; - ZigString path; - ZigString name; - ZigString message; + BunString syscall; + BunString code_; + BunString path; + BunString name; + BunString message; ZigStackTrace stack; void* exception; bool remapped; diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index cf6a65841..605cc0c25 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -1805,6 +1805,7 @@ pub const VirtualMachine = struct { if (exception) |exception_| { var holder = ZigException.Holder.init(); var zig_exception: *ZigException = holder.zigException(); + defer zig_exception.deinit(); exception_.getStackTrace(&zig_exception.stack); if (zig_exception.stack.frames_len > 0) { if (allow_ansi_color) { @@ -1932,8 +1933,14 @@ pub const VirtualMachine = struct { while (i < stack.len) : (i += 1) { const frame = stack[@intCast(usize, i)]; - const file = frame.source_url.slice(); - const func = frame.function_name.slice(); + const file_slice = frame.source_url.toSlice(bun.default_allocator); + defer file_slice.deinit(); + const func_slice = frame.function_name.toSlice(bun.default_allocator); + defer func_slice.deinit(); + + const file = file_slice.slice(); + const func = func_slice.slice(); + if (file.len == 0 and func.len == 0) continue; const has_name = std.fmt.count("{any}", .{frame.nameFormatter( @@ -1985,7 +1992,7 @@ pub const VirtualMachine = struct { pub fn remapStackFramePositions(this: *VirtualMachine, frames: [*]JSC.ZigStackFrame, frames_count: usize) void { for (frames[0..frames_count]) |*frame| { if (frame.position.isInvalid() or frame.remapped) continue; - var sourceURL = frame.source_url.toSlice(bun.default_allocator); + var sourceURL = frame.source_url.toUTF8(bun.default_allocator); defer sourceURL.deinit(); if (this.source_mappings.resolveMapping( @@ -2049,8 +2056,10 @@ pub const VirtualMachine = struct { if (frames.len == 0) return; var top = &frames[0]; + var top_source_url = top.source_url.toUTF8(bun.default_allocator); + defer top_source_url.deinit(); if (this.source_mappings.resolveMapping( - top.source_url.slice(), + top_source_url.slice(), @max(top.position.line, 0), @max(top.position.column_start, 0), )) |mapping| { @@ -2078,18 +2087,18 @@ pub const VirtualMachine = struct { )) |lines| { var source_lines = exception.stack.source_lines_ptr[0..JSC.ZigException.Holder.source_lines_count]; var source_line_numbers = exception.stack.source_lines_numbers[0..JSC.ZigException.Holder.source_lines_count]; - @memset(source_lines, ZigString.Empty); + @memset(source_lines, String.empty); @memset(source_line_numbers, 0); var lines_ = lines[0..@min(lines.len, source_lines.len)]; for (lines_, 0..) |line, j| { - source_lines[(lines_.len - 1) - j] = ZigString.init(line); + source_lines[(lines_.len - 1) - j] = String.init(line); source_line_numbers[j] = top.position.line - @intCast(i32, j) + 1; } exception.stack.source_lines_len = @intCast(u8, lines_.len); - top.position.column_stop = @intCast(i32, source_lines[lines_.len - 1].len); + top.position.column_stop = @intCast(i32, source_lines[lines_.len - 1].length()); top.position.line_stop = top.position.column_stop; // This expression range is no longer accurate @@ -2101,8 +2110,10 @@ pub const VirtualMachine = struct { if (frames.len > 1) { for (frames[1..]) |*frame| { if (frame.position.isInvalid()) continue; + const source_url = frame.source_url.toUTF8(bun.default_allocator); + defer source_url.deinit(); if (this.source_mappings.resolveMapping( - frame.source_url.slice(), + source_url.slice(), @max(frame.position.line, 0), @max(frame.position.column_start, 0), )) |mapping| { @@ -2117,6 +2128,7 @@ pub const VirtualMachine = struct { pub fn printErrorInstance(this: *VirtualMachine, error_instance: JSValue, exception_list: ?*ExceptionList, comptime Writer: type, writer: Writer, comptime allow_ansi_color: bool, comptime allow_side_effects: bool) !void { var exception_holder = ZigException.Holder.init(); var exception = exception_holder.zigException(); + defer exception_holder.deinit(); this.remapZigException(exception, error_instance, exception_list); this.had_errors = true; @@ -2134,15 +2146,18 @@ pub const VirtualMachine = struct { var source_lines = exception.stack.sourceLineIterator(); var last_pad: u64 = 0; while (source_lines.untilLast()) |source| { + defer source.text.deinit(); + const int_size = std.fmt.count("{d}", .{source.line}); const pad = max_line_number_pad - int_size; last_pad = pad; try writer.writeByteNTimes(' ', pad); + try writer.print( comptime Output.prettyFmt("{d} | {s}\n", allow_ansi_color), .{ source.line, - std.mem.trim(u8, source.text, "\n"), + std.mem.trim(u8, source.text.slice(), "\n"), }, ); } @@ -2158,7 +2173,8 @@ pub const VirtualMachine = struct { const top_frame = if (exception.stack.frames_len > 0) exception.stack.frames()[0] else null; if (top_frame == null or top_frame.?.position.isInvalid()) { defer did_print_name = true; - var text = std.mem.trim(u8, source.text, "\n"); + defer source.text.deinit(); + var text = std.mem.trim(u8, source.text.slice(), "\n"); try writer.print( comptime Output.prettyFmt( @@ -2176,7 +2192,9 @@ pub const VirtualMachine = struct { const int_size = std.fmt.count("{d}", .{source.line}); const pad = max_line_number_pad - int_size; try writer.writeByteNTimes(' ', pad); - var remainder = std.mem.trim(u8, source.text, "\n"); + defer source.text.deinit(); + const text = source.text.slice(); + var remainder = std.mem.trim(u8, text, "\n"); try writer.print( comptime Output.prettyFmt( @@ -2188,7 +2206,7 @@ pub const VirtualMachine = struct { if (!top.position.isInvalid()) { var first_non_whitespace = @intCast(u32, top.position.column_start); - while (first_non_whitespace < source.text.len and source.text[first_non_whitespace] == ' ') { + while (first_non_whitespace < text.len and text[first_non_whitespace] == ' ') { first_non_whitespace += 1; } const indent = @intCast(usize, pad) + " | ".len + first_non_whitespace; @@ -2219,10 +2237,10 @@ pub const VirtualMachine = struct { }; var show = Show{ - .system_code = exception.system_code.len > 0 and !strings.eql(exception.system_code.slice(), name.slice()), - .syscall = exception.syscall.len > 0, + .system_code = !exception.system_code.eql(name) and !exception.system_code.isEmpty(), + .syscall = !exception.syscall.isEmpty(), .errno = exception.errno < 0, - .path = exception.path.len > 0, + .path = !exception.path.isEmpty(), .fd = exception.fd != -1, }; @@ -2262,7 +2280,7 @@ pub const VirtualMachine = struct { } else if (show.errno) { try writer.writeAll(" "); } - try writer.print(comptime Output.prettyFmt(" path: \"{s}\"\n", allow_ansi_color), .{exception.path}); + try writer.print(comptime Output.prettyFmt(" path: \"{}\"\n", allow_ansi_color), .{exception.path}); } if (show.fd) { @@ -2281,12 +2299,12 @@ pub const VirtualMachine = struct { } else if (show.errno) { try writer.writeAll(" "); } - try writer.print(comptime Output.prettyFmt(" code: \"{s}\"\n", allow_ansi_color), .{exception.system_code}); + try writer.print(comptime Output.prettyFmt(" code: \"{}\"\n", allow_ansi_color), .{exception.system_code}); add_extra_line = true; } if (show.syscall) { - try writer.print(comptime Output.prettyFmt(" syscall: \"{s}\"\n", allow_ansi_color), .{exception.syscall}); + try writer.print(comptime Output.prettyFmt(" syscall: \"{}\"\n", allow_ansi_color), .{exception.syscall}); add_extra_line = true; } @@ -2303,22 +2321,22 @@ pub const VirtualMachine = struct { try printStackTrace(@TypeOf(writer), writer, exception.stack, allow_ansi_color); } - fn printErrorNameAndMessage(_: *VirtualMachine, name: ZigString, message: ZigString, comptime Writer: type, writer: Writer, comptime allow_ansi_color: bool) !void { - if (name.len > 0 and message.len > 0) { - const display_name: ZigString = if (!name.is16Bit() and strings.eqlComptime(name.slice(), "Error")) ZigString.init("error") else name; + fn printErrorNameAndMessage(_: *VirtualMachine, name: String, message: String, comptime Writer: type, writer: Writer, comptime allow_ansi_color: bool) !void { + if (!name.isEmpty() and !message.isEmpty()) { + const display_name: String = if (name.eqlComptime("Error")) String.init("error") else name; try writer.print(comptime Output.prettyFmt("{any}: {s}\n", allow_ansi_color), .{ display_name, message, }); - } else if (name.len > 0) { - if (name.is16Bit() or !strings.hasPrefixComptime(name.slice(), "error")) { - try writer.print(comptime Output.prettyFmt("error: {s}\n", allow_ansi_color), .{name}); + } else if (!name.isEmpty()) { + if (!name.hasPrefixComptime("error")) { + try writer.print(comptime Output.prettyFmt("error: {}\n", allow_ansi_color), .{name}); } else { - try writer.print(comptime Output.prettyFmt("{s}\n", allow_ansi_color), .{name}); + try writer.print(comptime Output.prettyFmt("{}\n", allow_ansi_color), .{name}); } - } else if (message.len > 0) { - try writer.print(comptime Output.prettyFmt("error: {s}\n", allow_ansi_color), .{message}); + } else if (!message.isEmpty()) { + try writer.print(comptime Output.prettyFmt("error: {}\n", allow_ansi_color), .{message}); } else { try writer.print(comptime Output.prettyFmt("error\n", allow_ansi_color), .{}); } diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig index d3bb90747..5ae2337f9 100644 --- a/src/bun.js/test/jest.zig +++ b/src/bun.js/test/jest.zig @@ -1532,8 +1532,8 @@ inline fn createIfScope( // In Github Actions, emit an annotation that renders the error and location. // https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-error-message pub fn printGithubAnnotation(exception: *JSC.ZigException) void { - const name = exception.name; - const message = exception.message; + const name = @field(exception, "name"); + const message = @field(exception, "message"); const frames = exception.stack.frames(); const top_frame = if (frames.len > 0) frames[0] else null; const dir = bun.getenvZ("GITHUB_WORKSPACE") orelse bun.fs.FileSystem.instance.top_level_dir; @@ -1543,7 +1543,7 @@ pub fn printGithubAnnotation(exception: *JSC.ZigException) void { if (top_frame) |frame| { if (!frame.position.isInvalid()) { - const source_url = frame.source_url.toSlice(allocator); + const source_url = frame.source_url.toUTF8(allocator); defer source_url.deinit(); const file = bun.path.relative(dir, source_url.slice()); Output.printError("\n::error file={s},line={d},col={d},title=", .{ @@ -1559,14 +1559,14 @@ pub fn printGithubAnnotation(exception: *JSC.ZigException) void { Output.printError("\n::error title=", .{}); } - if (name.len == 0 or name.eqlComptime("Error")) { + if (name.isEmpty() or name.eqlComptime("Error")) { Output.printError("error", .{}); } else { Output.printError("{s}", .{name.githubAction()}); } - if (message.len > 0) { - const message_slice = message.toSlice(allocator); + if (!message.isEmpty()) { + const message_slice = message.toUTF8(allocator); defer message_slice.deinit(); const msg = message_slice.slice(); @@ -1574,7 +1574,7 @@ pub fn printGithubAnnotation(exception: *JSC.ZigException) void { while (strings.indexOfNewlineOrNonASCIIOrANSI(msg, cursor)) |i| { cursor = i + 1; if (msg[i] == '\n') { - const first_line = ZigString.init(msg[0..i]); + const first_line = bun.String.fromUTF8(msg[0..i]); Output.printError(": {s}::", .{first_line.githubAction()}); break; } @@ -1605,10 +1605,10 @@ pub fn printGithubAnnotation(exception: *JSC.ZigException) void { var i: i16 = 0; while (i < frames.len) : (i += 1) { const frame = frames[@intCast(usize, i)]; - const source_url = frame.source_url.toSlice(allocator); + const source_url = frame.source_url.toUTF8(allocator); defer source_url.deinit(); const file = bun.path.relative(dir, source_url.slice()); - const func = frame.function_name.toSlice(allocator); + const func = frame.function_name.toUTF8(allocator); if (file.len == 0 and func.len == 0) continue; diff --git a/src/js/out/modules/node/path.js b/src/js/out/modules/node/path.js index f8cc1ec08..3a3a75207 100644 --- a/src/js/out/modules/node/path.js +++ b/src/js/out/modules/node/path.js @@ -1 +1 @@ -var g=function(i){var f=m({basename:i.basename.bind(i),dirname:i.dirname.bind(i),extname:i.extname.bind(i),format:i.format.bind(i),isAbsolute:i.isAbsolute.bind(i),join:i.join.bind(i),normalize:i.normalize.bind(i),parse:i.parse.bind(i),relative:i.relative.bind(i),resolve:i.resolve.bind(i),toNamespacedPath:i.toNamespacedPath.bind(i),sep:i.sep,delimiter:i.delimiter});return f.default=f,f},m=(i)=>Object.assign(Object.create(null),i),k=g(Bun._Path()),q=g(Bun._Path(!1)),v=g(Bun._Path(!0));k.win32=v;k.posix=q;var{basename:y,dirname:z,extname:A,format:B,isAbsolute:C,join:D,normalize:E,parse:F,relative:G,resolve:H,toNamespacedPath:I,sep:J,delimiter:K,__esModule:L}=k;k[Symbol.for("CommonJS")]=0;k.__esModule=!0;var O=k;export{v as win32,I as toNamespacedPath,J as sep,H as resolve,G as relative,q as posix,F as parse,E as normalize,D as join,C as isAbsolute,B as format,A as extname,z as dirname,K as delimiter,O as default,m as createModule,y as basename,L as __esModule}; +var i=function(N){var m=g({basename:N.basename.bind(N),dirname:N.dirname.bind(N),extname:N.extname.bind(N),format:N.format.bind(N),isAbsolute:N.isAbsolute.bind(N),join:N.join.bind(N),normalize:N.normalize.bind(N),parse:N.parse.bind(N),relative:N.relative.bind(N),resolve:N.resolve.bind(N),toNamespacedPath:N.toNamespacedPath.bind(N),sep:N.sep,delimiter:N.delimiter});return m.default=m,m},g=(N)=>Object.assign(Object.create(null),N),f=i(Bun._Path()),J=i(Bun._Path(!1)),k=i(Bun._Path(!0));f.win32=k;f.posix=J;var{basename:q,dirname:v,extname:y,format:z,isAbsolute:A,join:B,normalize:K,parse:C,relative:D,resolve:E,toNamespacedPath:F,sep:G,delimiter:H,__esModule:I}=f;f[Symbol.for("CommonJS")]=0;f.__esModule=!0;var O=f;export{k as win32,F as toNamespacedPath,G as sep,E as resolve,D as relative,J as posix,C as parse,K as normalize,B as join,A as isAbsolute,z as format,y as extname,v as dirname,H as delimiter,O as default,g as createModule,q as basename,I as __esModule}; diff --git a/src/js/out/modules/node/stream.web.js b/src/js/out/modules/node/stream.web.js index bb906418c..f91ee03b4 100644 --- a/src/js/out/modules/node/stream.web.js +++ b/src/js/out/modules/node/stream.web.js @@ -1 +1 @@ -var{ReadableStream:c,ReadableStreamDefaultController:j,WritableStream:k,WritableStreamDefaultController:p,WritableStreamDefaultWriter:v,TransformStream:w,TransformStreamDefaultController:x,ByteLengthQueuingStrategy:z,CountQueuingStrategy:A,ReadableStreamBYOBReader:E,ReadableStreamBYOBRequest:F,ReadableStreamDefaultReader:G}=globalThis,H={ReadableStream:c,ReadableStreamDefaultController:j,WritableStream:k,WritableStreamDefaultController:p,WritableStreamDefaultWriter:v,TransformStream:w,TransformStreamDefaultController:x,ByteLengthQueuingStrategy:z,CountQueuingStrategy:A,ReadableStreamBYOBReader:E,ReadableStreamBYOBRequest:F,ReadableStreamDefaultReader:G,[Symbol.for("CommonJS")]:0};export{H as default,v as WritableStreamDefaultWriter,p as WritableStreamDefaultController,k as WritableStream,x as TransformStreamDefaultController,w as TransformStream,G as ReadableStreamDefaultReader,j as ReadableStreamDefaultController,F as ReadableStreamBYOBRequest,E as ReadableStreamBYOBReader,c as ReadableStream,A as CountQueuingStrategy,z as ByteLengthQueuingStrategy}; +var{ReadableStream:k,ReadableStreamDefaultController:v,WritableStream:w,WritableStreamDefaultController:x,WritableStreamDefaultWriter:z,TransformStream:A,TransformStreamDefaultController:E,ByteLengthQueuingStrategy:F,CountQueuingStrategy:c,ReadableStreamBYOBReader:j,ReadableStreamBYOBRequest:G,ReadableStreamDefaultReader:p}=globalThis,H={ReadableStream:k,ReadableStreamDefaultController:v,WritableStream:w,WritableStreamDefaultController:x,WritableStreamDefaultWriter:z,TransformStream:A,TransformStreamDefaultController:E,ByteLengthQueuingStrategy:F,CountQueuingStrategy:c,ReadableStreamBYOBReader:j,ReadableStreamBYOBRequest:G,ReadableStreamDefaultReader:p,[Symbol.for("CommonJS")]:0};export{H as default,z as WritableStreamDefaultWriter,x as WritableStreamDefaultController,w as WritableStream,E as TransformStreamDefaultController,A as TransformStream,p as ReadableStreamDefaultReader,v as ReadableStreamDefaultController,G as ReadableStreamBYOBRequest,j as ReadableStreamBYOBReader,k as ReadableStream,c as CountQueuingStrategy,F as ByteLengthQueuingStrategy}; diff --git a/src/logger.zig b/src/logger.zig index 3279e9fd5..fc25541de 100644 --- a/src/logger.zig +++ b/src/logger.zig @@ -421,12 +421,12 @@ pub const Msg = struct { if (err.toError()) |value| { value.toZigException(globalObject, zig_exception_holder.zigException()); } else { - zig_exception_holder.zig_exception.message = JSC.ZigString.fromUTF8(err.toSlice(globalObject, allocator).slice()); + zig_exception_holder.zig_exception.message = err.toBunString(globalObject); } return Msg{ .data = .{ - .text = zig_exception_holder.zigException().message.toSliceClone(allocator).slice(), + .text = try zig_exception_holder.zigException().message.toOwnedSlice(allocator), .location = Location{ .file = file, }, diff --git a/src/string.zig b/src/string.zig index 54af2ba68..3c0c99ce5 100644 --- a/src/string.zig +++ b/src/string.zig @@ -247,6 +247,26 @@ pub const String = extern struct { extern fn BunString__fromLatin1(bytes: [*]const u8, len: usize) String; extern fn BunString__fromBytes(bytes: [*]const u8, len: usize) String; + pub fn toOwnedSlice(this: String, allocator: std.mem.Allocator) ![]u8 { + switch (this.tag) { + .ZigString => return try this.value.ZigString.toOwnedSlice(allocator), + .WTFStringImpl => { + var utf8_slice = this.value.WTFStringImpl.toUTF8(allocator); + + if (utf8_slice.allocator.get()) |alloc| { + if (isWTFAllocator(alloc)) { + return @constCast((try utf8_slice.clone(allocator)).slice()); + } + } + + return @constCast(utf8_slice.slice()); + }, + .StaticZigString => return try this.value.StaticZigString.toOwnedSlice(allocator), + .Empty => return &[_]u8{}, + else => unreachable, + } + } + pub fn createLatin1(bytes: []const u8) String { JSC.markBinding(@src()); return BunString__fromLatin1(bytes.ptr, bytes.len); @@ -429,6 +449,10 @@ pub const String = extern struct { return .latin1; } + pub fn githubAction(self: String) ZigString.GithubActionFormatter { + return self.toZigString().githubAction(); + } + pub fn byteSlice(this: String) []const u8 { return switch (this.tag) { .ZigString, .StaticZigString => this.value.ZigString.byteSlice(), -- cgit v1.2.3 From 853e3771595fd46bb09641c8dbdab738843a45cd Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Thu, 29 Jun 2023 08:30:21 -0700 Subject: Revert "[jest] fix lifecycle hook execution order (#3447)" (#3455) This reverts commit 182e8aa1392af9ba92beccce06f49f8e4593fe5c. --- src/bun.js/test/jest.zig | 162 +++++++++++++++--------------------- test/js/bun/test/jest-hooks.test.ts | 31 ------- test/js/bun/test/test-test.test.ts | 6 +- 3 files changed, 69 insertions(+), 130 deletions(-) (limited to 'src/bun.js/test') diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig index 5ae2337f9..d53194e00 100644 --- a/src/bun.js/test/jest.zig +++ b/src/bun.js/test/jest.zig @@ -493,7 +493,8 @@ pub const Jest = struct { var filepath = Fs.FileSystem.instance.filename_store.append([]const u8, slice) catch unreachable; var scope = runner_.getOrPutFile(filepath); - scope.push(); + DescribeScope.active = scope; + DescribeScope.module = scope; return Bun__Jest__testModuleObject(ctx).asObjectRef(); } @@ -750,16 +751,17 @@ pub const DescribeScope = struct { } pub fn push(new: *DescribeScope) void { - if (comptime is_bindgen) return; - std.debug.assert(DescribeScope.active != new); - if (new.parent) |scope| std.debug.assert(scope == DescribeScope.active); + if (comptime is_bindgen) return undefined; + if (new == DescribeScope.active) return; + + new.parent = DescribeScope.active; DescribeScope.active = new; } pub fn pop(this: *DescribeScope) void { - if (comptime is_bindgen) return; - std.debug.assert(DescribeScope.active == this); - DescribeScope.active = this.parent; + if (comptime is_bindgen) return undefined; + if (DescribeScope.active == this) + DescribeScope.active = this.parent orelse DescribeScope.active; } pub const LifecycleHook = enum { @@ -769,7 +771,8 @@ pub const DescribeScope = struct { afterAll, }; - pub threadlocal var active: ?*DescribeScope = null; + pub threadlocal var active: *DescribeScope = undefined; + pub threadlocal var module: *DescribeScope = undefined; const CallbackFn = *const fn ( *JSC.JSGlobalObject, @@ -778,24 +781,21 @@ pub const DescribeScope = struct { fn createCallback(comptime hook: LifecycleHook) CallbackFn { return struct { + const this_hook = hook; pub fn run( globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame, ) callconv(.C) JSC.JSValue { - const arguments = callframe.arguments(2); - if (arguments.len < 1) { - globalThis.throwNotEnoughArguments("callback", 1, arguments.len); + const arguments_ = callframe.arguments(2); + const arguments = arguments_.ptr[0..arguments_.len]; + if (arguments.len == 0 or !arguments[0].isObject() or !arguments[0].isCallable(globalThis.vm())) { + globalThis.throwInvalidArgumentType(@tagName(this_hook), "callback", "function"); return .zero; } - const cb = arguments.ptr[0]; - if (!cb.isObject() or !cb.isCallable(globalThis.vm())) { - globalThis.throwInvalidArgumentType(@tagName(hook), "callback", "function"); - return .zero; - } - - cb.protect(); - @field(DescribeScope.active.?, @tagName(hook)).append(getAllocator(globalThis), cb) catch unreachable; + arguments[0].protect(); + const name = comptime @as(string, @tagName(this_hook)); + @field(DescribeScope.active, name).append(getAllocator(globalThis), arguments[0]) catch unreachable; return JSC.JSValue.jsBoolean(true); } }.run; @@ -829,22 +829,11 @@ pub const DescribeScope = struct { pub const beforeAll = createCallback(.beforeAll); pub const beforeEach = createCallback(.beforeEach); - pub fn execCallback(this: *DescribeScope, globalObject: *JSC.JSGlobalObject, comptime hook: LifecycleHook) ?JSValue { - var hooks = &@field(this, @tagName(hook)); - defer { - if (comptime hook == .beforeAll or hook == .afterAll) { - hooks.clearAndFree(getAllocator(globalObject)); - } - } - - for (hooks.items) |cb| { - std.debug.assert(cb.isObject()); - std.debug.assert(cb.isCallable(globalObject.vm())); - defer { - if (comptime hook == .beforeAll or hook == .afterAll) { - cb.unprotect(); - } - } + pub fn execCallback(this: *DescribeScope, globalObject: *JSC.JSGlobalObject, comptime hook: LifecycleHook) JSValue { + const name = comptime @as(string, @tagName(hook)); + var hooks: []JSC.JSValue = @field(this, name).items; + for (hooks, 0..) |cb, i| { + if (cb.isEmpty()) continue; const pending_test = Jest.runner.?.pending_test; // forbid `expect()` within hooks @@ -854,23 +843,20 @@ pub const DescribeScope = struct { Jest.runner.?.did_pending_test_fail = false; const vm = VirtualMachine.get(); - var result: JSC.JSValue = switch (cb.getLength(globalObject)) { - 0 => cb.call(globalObject, &.{}), - else => brk: { - this.done = false; - const done_func = JSC.NewFunctionWithData( - globalObject, - ZigString.static("done"), - 0, - DescribeScope.onDone, - false, - this, - ); - var result = cb.call(globalObject, &.{done_func}); - vm.waitFor(&this.done); - break :brk result; - }, - }; + var result: JSC.JSValue = if (cb.getLength(globalObject) > 0) brk: { + this.done = false; + const done_func = JSC.NewFunctionWithData( + globalObject, + ZigString.static("done"), + 0, + DescribeScope.onDone, + false, + this, + ); + var result = cb.call(globalObject, &.{done_func}); + vm.waitFor(&this.done); + break :brk result; + } else cb.call(globalObject, &.{}); if (result.asAnyPromise()) |promise| { if (promise.status(globalObject.vm()) == .Pending) { result.protect(); @@ -884,28 +870,19 @@ pub const DescribeScope = struct { Jest.runner.?.pending_test = pending_test; Jest.runner.?.did_pending_test_fail = orig_did_pending_test_fail; if (result.isAnyError()) return result; + + if (comptime hook == .beforeAll or hook == .afterAll) { + hooks[i] = JSC.JSValue.zero; + } } - return null; + return JSValue.zero; } pub fn runGlobalCallbacks(globalThis: *JSC.JSGlobalObject, comptime hook: LifecycleHook) ?JSValue { // global callbacks - var hooks = &@field(Jest.runner.?.global_callbacks, @tagName(hook)); - defer { - if (comptime hook == .beforeAll or hook == .afterAll) { - hooks.clearAndFree(getAllocator(globalThis)); - } - } - - for (hooks.items) |cb| { - std.debug.assert(cb.isObject()); - std.debug.assert(cb.isCallable(globalThis.vm())); - defer { - if (comptime hook == .beforeAll or hook == .afterAll) { - cb.unprotect(); - } - } + for (@field(Jest.runner.?.global_callbacks, @tagName(hook)).items) |cb| { + if (cb.isEmpty()) continue; const pending_test = Jest.runner.?.pending_test; // forbid `expect()` within hooks @@ -932,40 +909,28 @@ pub const DescribeScope = struct { if (result.isAnyError()) return result; } - return null; - } - - fn runBeforeCallbacks(this: *DescribeScope, globalObject: *JSC.JSGlobalObject, comptime hook: LifecycleHook) ?JSValue { - if (this.parent) |scope| { - if (scope.runBeforeCallbacks(globalObject, hook)) |err| { - return err; - } + if (comptime hook == .beforeAll or hook == .afterAll) { + @field(Jest.runner.?.global_callbacks, @tagName(hook)).items.len = 0; } - return this.execCallback(globalObject, hook); + + return null; } pub fn runCallback(this: *DescribeScope, globalObject: *JSC.JSGlobalObject, comptime hook: LifecycleHook) JSValue { - if (comptime hook == .afterAll or hook == .afterEach) { - var parent: ?*DescribeScope = this; - while (parent) |scope| { - if (scope.execCallback(globalObject, hook)) |err| { - return err; - } - parent = scope.parent; - } - } - if (runGlobalCallbacks(globalObject, hook)) |err| { return err; } - if (comptime hook == .beforeAll or hook == .beforeEach) { - if (this.runBeforeCallbacks(globalObject, hook)) |err| { - return err; + var parent = this.parent; + while (parent) |scope| { + const ret = scope.execCallback(globalObject, hook); + if (!ret.isEmpty()) { + return ret; } + parent = scope.parent; } - return .zero; + return this.execCallback(globalObject, hook); } pub fn call(globalThis: *JSGlobalObject, callframe: *CallFrame) callconv(.C) JSValue { @@ -996,8 +961,11 @@ pub const DescribeScope = struct { if (comptime is_bindgen) return undefined; callback.protect(); defer callback.unprotect(); - this.push(); - defer this.pop(); + var original_active = active; + defer active = original_active; + if (this != module) + this.parent = this.parent orelse active; + active = this; if (callback == .zero) { this.runTests(globalObject); @@ -1096,8 +1064,9 @@ pub const DescribeScope = struct { if (!this.isAllSkipped()) { // Run the afterAll callbacks, in reverse order // unless there were no tests for this scope - if (this.execCallback(globalThis, .afterAll)) |err| { - globalThis.bunVM().runErrorHandler(err, null); + const afterAll_result = this.execCallback(globalThis, .afterAll); + if (!afterAll_result.isEmpty()) { + globalThis.bunVM().runErrorHandler(afterAll_result, null); } } @@ -1177,6 +1146,7 @@ pub const TestRunnerTask = struct { // reset the global state for each test // prior to the run + DescribeScope.active = describe; expect.active_test_expectation_counter = .{}; jsc_vm.last_reported_error_for_dedupe = .zero; @@ -1447,7 +1417,7 @@ inline fn createScope( return .zero; } - const parent = DescribeScope.active.?; + const parent = DescribeScope.active; const allocator = getAllocator(globalThis); const label = if (description == .zero) "" diff --git a/test/js/bun/test/jest-hooks.test.ts b/test/js/bun/test/jest-hooks.test.ts index 618cdc4c6..c99dc7759 100644 --- a/test/js/bun/test/jest-hooks.test.ts +++ b/test/js/bun/test/jest-hooks.test.ts @@ -1,36 +1,5 @@ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "bun:test"; -let hooks_run: string[] = []; - -beforeAll(() => hooks_run.push("global beforeAll")); -beforeEach(() => hooks_run.push("global beforeEach")); -afterAll(() => hooks_run.push("global afterAll")); -afterEach(() => hooks_run.push("global afterEach")); - -describe("describe scope", () => { - beforeAll(() => hooks_run.push("describe beforeAll")); - beforeEach(() => hooks_run.push("describe beforeEach")); - afterAll(() => hooks_run.push("describe afterAll")); - afterEach(() => hooks_run.push("describe afterEach")); - - it("should run after beforeAll/beforeEach in the correct order", () => { - expect(hooks_run).toEqual(["global beforeAll", "describe beforeAll", "global beforeEach", "describe beforeEach"]); - }); - - it("should run after afterEach/afterAll in the correct order", () => { - expect(hooks_run).toEqual([ - "global beforeAll", - "describe beforeAll", - "global beforeEach", - "describe beforeEach", - "describe afterEach", - "global afterEach", - "global beforeEach", - "describe beforeEach", - ]); - }); -}); - describe("test jest hooks in bun-test", () => { describe("test beforeAll hook", () => { let animal = "tiger"; diff --git a/test/js/bun/test/test-test.test.ts b/test/js/bun/test/test-test.test.ts index 5f732bb82..7ecfdef11 100644 --- a/test/js/bun/test/test-test.test.ts +++ b/test/js/bun/test/test-test.test.ts @@ -540,18 +540,18 @@ beforeEach: #2 beforeEach: TEST-FILE beforeEach: one describe scope -- inside one describe scope -- -afterEach: one describe scope -afterEach: TEST-FILE afterEach: #1 afterEach: #2 +afterEach: TEST-FILE +afterEach: one describe scope afterAll: one describe scope beforeEach: #1 beforeEach: #2 beforeEach: TEST-FILE -- the top-level test -- -afterEach: TEST-FILE afterEach: #1 afterEach: #2 +afterEach: TEST-FILE afterAll: TEST-FILE afterAll: #1 afterAll: #2 -- cgit v1.2.3 From 02f707f231622066782066ba2c88aefab7f49377 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sat, 1 Jul 2023 21:44:04 +0300 Subject: [jest] fix lifecycle hook execution order (#3461) * [jest] fix lifecycle hook execution order * strip `std.debug.assert()` from release build --- src/bun.js/test/jest.zig | 201 +++++++++++++++++++++--------------- test/js/bun/test/jest-hooks.test.ts | 31 ++++++ test/js/bun/test/test-test.test.ts | 6 +- 3 files changed, 153 insertions(+), 85 deletions(-) (limited to 'src/bun.js/test') diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig index d53194e00..6e4cb827c 100644 --- a/src/bun.js/test/jest.zig +++ b/src/bun.js/test/jest.zig @@ -493,8 +493,7 @@ pub const Jest = struct { var filepath = Fs.FileSystem.instance.filename_store.append([]const u8, slice) catch unreachable; var scope = runner_.getOrPutFile(filepath); - DescribeScope.active = scope; - DescribeScope.module = scope; + scope.push(); return Bun__Jest__testModuleObject(ctx).asObjectRef(); } @@ -751,17 +750,23 @@ pub const DescribeScope = struct { } pub fn push(new: *DescribeScope) void { - if (comptime is_bindgen) return undefined; - if (new == DescribeScope.active) return; - - new.parent = DescribeScope.active; + if (comptime is_bindgen) return; + if (new.parent) |scope| { + if (comptime Environment.allow_assert) { + std.debug.assert(DescribeScope.active != new); + std.debug.assert(scope == DescribeScope.active); + } + } else if (DescribeScope.active) |scope| { + // calling Bun.jest() within (already active) module + if (scope.parent != null) return; + } DescribeScope.active = new; } pub fn pop(this: *DescribeScope) void { - if (comptime is_bindgen) return undefined; - if (DescribeScope.active == this) - DescribeScope.active = this.parent orelse DescribeScope.active; + if (comptime is_bindgen) return; + if (comptime Environment.allow_assert) std.debug.assert(DescribeScope.active == this); + DescribeScope.active = this.parent; } pub const LifecycleHook = enum { @@ -771,8 +776,7 @@ pub const DescribeScope = struct { afterAll, }; - pub threadlocal var active: *DescribeScope = undefined; - pub threadlocal var module: *DescribeScope = undefined; + pub threadlocal var active: ?*DescribeScope = null; const CallbackFn = *const fn ( *JSC.JSGlobalObject, @@ -781,21 +785,24 @@ pub const DescribeScope = struct { fn createCallback(comptime hook: LifecycleHook) CallbackFn { return struct { - const this_hook = hook; pub fn run( globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame, ) callconv(.C) JSC.JSValue { - const arguments_ = callframe.arguments(2); - const arguments = arguments_.ptr[0..arguments_.len]; - if (arguments.len == 0 or !arguments[0].isObject() or !arguments[0].isCallable(globalThis.vm())) { - globalThis.throwInvalidArgumentType(@tagName(this_hook), "callback", "function"); + const arguments = callframe.arguments(2); + if (arguments.len < 1) { + globalThis.throwNotEnoughArguments("callback", 1, arguments.len); + return .zero; + } + + const cb = arguments.ptr[0]; + if (!cb.isObject() or !cb.isCallable(globalThis.vm())) { + globalThis.throwInvalidArgumentType(@tagName(hook), "callback", "function"); return .zero; } - arguments[0].protect(); - const name = comptime @as(string, @tagName(this_hook)); - @field(DescribeScope.active, name).append(getAllocator(globalThis), arguments[0]) catch unreachable; + cb.protect(); + @field(DescribeScope.active.?, @tagName(hook)).append(getAllocator(globalThis), cb) catch unreachable; return JSC.JSValue.jsBoolean(true); } }.run; @@ -829,11 +836,24 @@ pub const DescribeScope = struct { pub const beforeAll = createCallback(.beforeAll); pub const beforeEach = createCallback(.beforeEach); - pub fn execCallback(this: *DescribeScope, globalObject: *JSC.JSGlobalObject, comptime hook: LifecycleHook) JSValue { - const name = comptime @as(string, @tagName(hook)); - var hooks: []JSC.JSValue = @field(this, name).items; - for (hooks, 0..) |cb, i| { - if (cb.isEmpty()) continue; + pub fn execCallback(this: *DescribeScope, globalObject: *JSC.JSGlobalObject, comptime hook: LifecycleHook) ?JSValue { + var hooks = &@field(this, @tagName(hook)); + defer { + if (comptime hook == .beforeAll or hook == .afterAll) { + hooks.clearAndFree(getAllocator(globalObject)); + } + } + + for (hooks.items) |cb| { + if (comptime Environment.allow_assert) { + std.debug.assert(cb.isObject()); + std.debug.assert(cb.isCallable(globalObject.vm())); + } + defer { + if (comptime hook == .beforeAll or hook == .afterAll) { + cb.unprotect(); + } + } const pending_test = Jest.runner.?.pending_test; // forbid `expect()` within hooks @@ -843,20 +863,23 @@ pub const DescribeScope = struct { Jest.runner.?.did_pending_test_fail = false; const vm = VirtualMachine.get(); - var result: JSC.JSValue = if (cb.getLength(globalObject) > 0) brk: { - this.done = false; - const done_func = JSC.NewFunctionWithData( - globalObject, - ZigString.static("done"), - 0, - DescribeScope.onDone, - false, - this, - ); - var result = cb.call(globalObject, &.{done_func}); - vm.waitFor(&this.done); - break :brk result; - } else cb.call(globalObject, &.{}); + var result: JSC.JSValue = switch (cb.getLength(globalObject)) { + 0 => cb.call(globalObject, &.{}), + else => brk: { + this.done = false; + const done_func = JSC.NewFunctionWithData( + globalObject, + ZigString.static("done"), + 0, + DescribeScope.onDone, + false, + this, + ); + var result = cb.call(globalObject, &.{done_func}); + vm.waitFor(&this.done); + break :brk result; + }, + }; if (result.asAnyPromise()) |promise| { if (promise.status(globalObject.vm()) == .Pending) { result.protect(); @@ -870,19 +893,30 @@ pub const DescribeScope = struct { Jest.runner.?.pending_test = pending_test; Jest.runner.?.did_pending_test_fail = orig_did_pending_test_fail; if (result.isAnyError()) return result; - - if (comptime hook == .beforeAll or hook == .afterAll) { - hooks[i] = JSC.JSValue.zero; - } } - return JSValue.zero; + return null; } pub fn runGlobalCallbacks(globalThis: *JSC.JSGlobalObject, comptime hook: LifecycleHook) ?JSValue { // global callbacks - for (@field(Jest.runner.?.global_callbacks, @tagName(hook)).items) |cb| { - if (cb.isEmpty()) continue; + var hooks = &@field(Jest.runner.?.global_callbacks, @tagName(hook)); + defer { + if (comptime hook == .beforeAll or hook == .afterAll) { + hooks.clearAndFree(getAllocator(globalThis)); + } + } + + for (hooks.items) |cb| { + if (comptime Environment.allow_assert) { + std.debug.assert(cb.isObject()); + std.debug.assert(cb.isCallable(globalThis.vm())); + } + defer { + if (comptime hook == .beforeAll or hook == .afterAll) { + cb.unprotect(); + } + } const pending_test = Jest.runner.?.pending_test; // forbid `expect()` within hooks @@ -909,28 +943,40 @@ pub const DescribeScope = struct { if (result.isAnyError()) return result; } - if (comptime hook == .beforeAll or hook == .afterAll) { - @field(Jest.runner.?.global_callbacks, @tagName(hook)).items.len = 0; - } - return null; } - pub fn runCallback(this: *DescribeScope, globalObject: *JSC.JSGlobalObject, comptime hook: LifecycleHook) JSValue { + fn runBeforeCallbacks(this: *DescribeScope, globalObject: *JSC.JSGlobalObject, comptime hook: LifecycleHook) ?JSValue { + if (this.parent) |scope| { + if (scope.runBeforeCallbacks(globalObject, hook)) |err| { + return err; + } + } + return this.execCallback(globalObject, hook); + } + + pub fn runCallback(this: *DescribeScope, globalObject: *JSC.JSGlobalObject, comptime hook: LifecycleHook) ?JSValue { + if (comptime hook == .afterAll or hook == .afterEach) { + var parent: ?*DescribeScope = this; + while (parent) |scope| { + if (scope.execCallback(globalObject, hook)) |err| { + return err; + } + parent = scope.parent; + } + } + if (runGlobalCallbacks(globalObject, hook)) |err| { return err; } - var parent = this.parent; - while (parent) |scope| { - const ret = scope.execCallback(globalObject, hook); - if (!ret.isEmpty()) { - return ret; + if (comptime hook == .beforeAll or hook == .beforeEach) { + if (this.runBeforeCallbacks(globalObject, hook)) |err| { + return err; } - parent = scope.parent; } - return this.execCallback(globalObject, hook); + return null; } pub fn call(globalThis: *JSGlobalObject, callframe: *CallFrame) callconv(.C) JSValue { @@ -961,11 +1007,8 @@ pub const DescribeScope = struct { if (comptime is_bindgen) return undefined; callback.protect(); defer callback.unprotect(); - var original_active = active; - defer active = original_active; - if (this != module) - this.parent = this.parent orelse active; - active = this; + this.push(); + defer this.pop(); if (callback == .zero) { this.runTests(globalObject); @@ -1019,8 +1062,7 @@ pub const DescribeScope = struct { var i: TestRunner.Test.ID = 0; if (!this.isAllSkipped()) { - const beforeAllCallback = this.runCallback(globalObject, .beforeAll); - if (!beforeAllCallback.isEmpty()) { + if (this.runCallback(globalObject, .beforeAll)) |_| { while (i < end) { Jest.runner.?.reportFailure(i + this.test_id_start, source.path.text, tests[i].label, 0, 0, this); i += 1; @@ -1051,9 +1093,8 @@ pub const DescribeScope = struct { this.pending_tests.unset(test_id); if (!skipped) { - const afterEach_result = this.runCallback(globalThis, .afterEach); - if (!afterEach_result.isEmpty()) { - globalThis.bunVM().runErrorHandler(afterEach_result, null); + if (this.runCallback(globalThis, .afterEach)) |err| { + globalThis.bunVM().runErrorHandler(err, null); } } @@ -1064,9 +1105,8 @@ pub const DescribeScope = struct { if (!this.isAllSkipped()) { // Run the afterAll callbacks, in reverse order // unless there were no tests for this scope - const afterAll_result = this.execCallback(globalThis, .afterAll); - if (!afterAll_result.isEmpty()) { - globalThis.bunVM().runErrorHandler(afterAll_result, null); + if (this.execCallback(globalThis, .afterAll)) |err| { + globalThis.bunVM().runErrorHandler(err, null); } } @@ -1146,7 +1186,6 @@ pub const TestRunnerTask = struct { // reset the global state for each test // prior to the run - DescribeScope.active = describe; expect.active_test_expectation_counter = .{}; jsc_vm.last_reported_error_for_dedupe = .zero; @@ -1178,11 +1217,9 @@ pub const TestRunnerTask = struct { this.needs_before_each = false; const label = test_.label; - const beforeEach = this.describe.runCallback(globalThis, .beforeEach); - - if (!beforeEach.isEmpty()) { + if (this.describe.runCallback(globalThis, .beforeEach)) |err| { Jest.runner.?.reportFailure(test_id, this.source_file_path, label, 0, 0, this.describe); - jsc_vm.runErrorHandler(beforeEach, null); + jsc_vm.runErrorHandler(err, null); return false; } } @@ -1210,7 +1247,7 @@ pub const TestRunnerTask = struct { } pub fn timeout(this: *TestRunnerTask) void { - std.debug.assert(!this.reported); + if (comptime Environment.allow_assert) std.debug.assert(!this.reported); this.ref.unref(this.globalThis.bunVM()); this.globalThis.throwTerminationException(); @@ -1223,7 +1260,7 @@ pub const TestRunnerTask = struct { switch (comptime from) { .promise => { - std.debug.assert(this.promise_state == .pending); + if (comptime Environment.allow_assert) std.debug.assert(this.promise_state == .pending); this.promise_state = .fulfilled; if (this.done_callback_state == .pending and result == .pass) { @@ -1231,7 +1268,7 @@ pub const TestRunnerTask = struct { } }, .callback => { - std.debug.assert(this.done_callback_state == .pending); + if (comptime Environment.allow_assert) std.debug.assert(this.done_callback_state == .pending); this.done_callback_state = .fulfilled; if (this.promise_state == .pending and result == .pass) { @@ -1239,7 +1276,7 @@ pub const TestRunnerTask = struct { } }, .sync => { - std.debug.assert(this.sync_state == .pending); + if (comptime Environment.allow_assert) std.debug.assert(this.sync_state == .pending); this.sync_state = .fulfilled; }, .timeout, .unhandledRejection => {}, @@ -1417,7 +1454,7 @@ inline fn createScope( return .zero; } - const parent = DescribeScope.active; + const parent = DescribeScope.active.?; const allocator = getAllocator(globalThis); const label = if (description == .zero) "" @@ -1502,8 +1539,8 @@ inline fn createIfScope( // In Github Actions, emit an annotation that renders the error and location. // https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-error-message pub fn printGithubAnnotation(exception: *JSC.ZigException) void { - const name = @field(exception, "name"); - const message = @field(exception, "message"); + const name = exception.name; + const message = exception.message; const frames = exception.stack.frames(); const top_frame = if (frames.len > 0) frames[0] else null; const dir = bun.getenvZ("GITHUB_WORKSPACE") orelse bun.fs.FileSystem.instance.top_level_dir; diff --git a/test/js/bun/test/jest-hooks.test.ts b/test/js/bun/test/jest-hooks.test.ts index c99dc7759..618cdc4c6 100644 --- a/test/js/bun/test/jest-hooks.test.ts +++ b/test/js/bun/test/jest-hooks.test.ts @@ -1,5 +1,36 @@ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "bun:test"; +let hooks_run: string[] = []; + +beforeAll(() => hooks_run.push("global beforeAll")); +beforeEach(() => hooks_run.push("global beforeEach")); +afterAll(() => hooks_run.push("global afterAll")); +afterEach(() => hooks_run.push("global afterEach")); + +describe("describe scope", () => { + beforeAll(() => hooks_run.push("describe beforeAll")); + beforeEach(() => hooks_run.push("describe beforeEach")); + afterAll(() => hooks_run.push("describe afterAll")); + afterEach(() => hooks_run.push("describe afterEach")); + + it("should run after beforeAll/beforeEach in the correct order", () => { + expect(hooks_run).toEqual(["global beforeAll", "describe beforeAll", "global beforeEach", "describe beforeEach"]); + }); + + it("should run after afterEach/afterAll in the correct order", () => { + expect(hooks_run).toEqual([ + "global beforeAll", + "describe beforeAll", + "global beforeEach", + "describe beforeEach", + "describe afterEach", + "global afterEach", + "global beforeEach", + "describe beforeEach", + ]); + }); +}); + describe("test jest hooks in bun-test", () => { describe("test beforeAll hook", () => { let animal = "tiger"; diff --git a/test/js/bun/test/test-test.test.ts b/test/js/bun/test/test-test.test.ts index 7ecfdef11..5f732bb82 100644 --- a/test/js/bun/test/test-test.test.ts +++ b/test/js/bun/test/test-test.test.ts @@ -540,18 +540,18 @@ beforeEach: #2 beforeEach: TEST-FILE beforeEach: one describe scope -- inside one describe scope -- +afterEach: one describe scope +afterEach: TEST-FILE afterEach: #1 afterEach: #2 -afterEach: TEST-FILE -afterEach: one describe scope afterAll: one describe scope beforeEach: #1 beforeEach: #2 beforeEach: TEST-FILE -- the top-level test -- +afterEach: TEST-FILE afterEach: #1 afterEach: #2 -afterEach: TEST-FILE afterAll: TEST-FILE afterAll: #1 afterAll: #2 -- cgit v1.2.3 From aa38e51afb73dab3071addc07f82fd96153ff450 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 2 Jul 2023 22:09:50 -0700 Subject: Support mocking `new Date()` & `Date.now()` in bun:test (#3501) * Support changing the time * Bump WebKit * Update bun.lockb --------- Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> --- .github/workflows/bun-linux-aarch64.yml | 2 +- .github/workflows/bun-linux-build.yml | 4 ++-- .github/workflows/bun-mac-aarch64.yml | 16 ++++++++-------- .github/workflows/bun-mac-x64-baseline.yml | 16 ++++++++-------- .github/workflows/bun-mac-x64.yml | 16 ++++++++-------- Dockerfile | 2 +- bun.lockb | Bin 72925 -> 72925 bytes package.json | 2 +- src/bun.js/WebKit | 2 +- src/bun.js/bindings/JSMockFunction.cpp | 24 ++++++++++++++++++++++++ src/bun.js/test/jest.zig | 5 +++++ test/js/bun/test/test-timers.test.ts | 11 +++++++++++ 12 files changed, 70 insertions(+), 30 deletions(-) create mode 100644 test/js/bun/test/test-timers.test.ts (limited to 'src/bun.js/test') diff --git a/.github/workflows/bun-linux-aarch64.yml b/.github/workflows/bun-linux-aarch64.yml index 01714460b..13dddece4 100644 --- a/.github/workflows/bun-linux-aarch64.yml +++ b/.github/workflows/bun-linux-aarch64.yml @@ -36,7 +36,7 @@ jobs: arch: aarch64 build_arch: arm64 runner: linux-arm64 - webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-linux-arm64-lto.tar.gz" + webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-3/bun-webkit-linux-arm64-lto.tar.gz" webkit_basename: "bun-webkit-linux-arm64-lto" build_machine_arch: aarch64 diff --git a/.github/workflows/bun-linux-build.yml b/.github/workflows/bun-linux-build.yml index 798ffaf33..e992770f1 100644 --- a/.github/workflows/bun-linux-build.yml +++ b/.github/workflows/bun-linux-build.yml @@ -46,7 +46,7 @@ jobs: arch: x86_64 build_arch: amd64 runner: big-ubuntu - webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-linux-amd64-lto.tar.gz" + webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-3/bun-webkit-linux-amd64-lto.tar.gz" webkit_basename: "bun-webkit-linux-amd64-lto" build_machine_arch: x86_64 - cpu: nehalem @@ -54,7 +54,7 @@ jobs: arch: x86_64 build_arch: amd64 runner: big-ubuntu - webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-linux-amd64-lto.tar.gz" + webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-3/bun-webkit-linux-amd64-lto.tar.gz" webkit_basename: "bun-webkit-linux-amd64-lto" build_machine_arch: x86_64 diff --git a/.github/workflows/bun-mac-aarch64.yml b/.github/workflows/bun-mac-aarch64.yml index f281783bd..484c7035b 100644 --- a/.github/workflows/bun-mac-aarch64.yml +++ b/.github/workflows/bun-mac-aarch64.yml @@ -117,7 +117,7 @@ jobs: # obj: bun-obj-darwin-x64-baseline # runner: macos-11 # artifact: bun-obj-darwin-x64-baseline - # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-amd64-lto.tar.gz" + # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-3/bun-webkit-macos-amd64-lto.tar.gz" # dependencies: true # compile_obj: false # - cpu: haswell @@ -126,7 +126,7 @@ jobs: # obj: bun-obj-darwin-x64 # runner: macos-11 # artifact: bun-obj-darwin-x64 - # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-amd64-lto.tar.gz" + # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-3/bun-webkit-macos-amd64-lto.tar.gz" # dependencies: true # compile_obj: false # - cpu: nehalem @@ -135,7 +135,7 @@ jobs: # obj: bun-obj-darwin-x64-baseline # runner: macos-11 # artifact: bun-obj-darwin-x64-baseline - # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-amd64-lto.tar.gz" + # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-3/bun-webkit-macos-amd64-lto.tar.gz" # dependencies: false # compile_obj: true # - cpu: haswell @@ -144,7 +144,7 @@ jobs: # obj: bun-obj-darwin-x64 # runner: macos-11 # artifact: bun-obj-darwin-x64 - # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-amd64-lto.tar.gz" + # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-3/bun-webkit-macos-amd64-lto.tar.gz" # dependencies: false # compile_obj: true - cpu: native @@ -152,7 +152,7 @@ jobs: tag: bun-darwin-aarch64 obj: bun-obj-darwin-aarch64 artifact: bun-obj-darwin-aarch64 - webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-arm64-lto.tar.gz" + webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-3/bun-webkit-macos-arm64-lto.tar.gz" runner: macos-arm64 dependencies: true compile_obj: true @@ -257,7 +257,7 @@ jobs: # package: bun-darwin-x64 # runner: macos-11 # artifact: bun-obj-darwin-x64-baseline - # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-amd64-lto.tar.gz" + # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-3/bun-webkit-macos-amd64-lto.tar.gz" # - cpu: haswell # arch: x86_64 # tag: bun-darwin-x64 @@ -265,14 +265,14 @@ jobs: # package: bun-darwin-x64 # runner: macos-11 # artifact: bun-obj-darwin-x64 - # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-amd64-lto.tar.gz" + # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-3/bun-webkit-macos-amd64-lto.tar.gz" - cpu: native arch: aarch64 tag: bun-darwin-aarch64 obj: bun-obj-darwin-aarch64 package: bun-darwin-aarch64 artifact: bun-obj-darwin-aarch64 - webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-arm64-lto.tar.gz" + webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-3/bun-webkit-macos-arm64-lto.tar.gz" runner: macos-arm64 steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/bun-mac-x64-baseline.yml b/.github/workflows/bun-mac-x64-baseline.yml index b5f79a757..fe8f0b396 100644 --- a/.github/workflows/bun-mac-x64-baseline.yml +++ b/.github/workflows/bun-mac-x64-baseline.yml @@ -117,7 +117,7 @@ jobs: obj: bun-obj-darwin-x64-baseline runner: macos-11 artifact: bun-obj-darwin-x64-baseline - webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-amd64-lto.tar.gz" + webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-3/bun-webkit-macos-amd64-lto.tar.gz" dependencies: true compile_obj: false # - cpu: haswell @@ -126,7 +126,7 @@ jobs: # obj: bun-obj-darwin-x64 # runner: macos-11 # artifact: bun-obj-darwin-x64 - # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-amd64-lto.tar.gz" + # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-3/bun-webkit-macos-amd64-lto.tar.gz" # dependencies: true # compile_obj: false - cpu: nehalem @@ -135,7 +135,7 @@ jobs: obj: bun-obj-darwin-x64-baseline runner: macos-11 artifact: bun-obj-darwin-x64-baseline - webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-amd64-lto.tar.gz" + webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-3/bun-webkit-macos-amd64-lto.tar.gz" dependencies: false compile_obj: true # - cpu: haswell @@ -144,7 +144,7 @@ jobs: # obj: bun-obj-darwin-x64 # runner: macos-11 # artifact: bun-obj-darwin-x64 - # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-amd64-lto.tar.gz" + # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-3/bun-webkit-macos-amd64-lto.tar.gz" # dependencies: false # compile_obj: true # - cpu: native @@ -152,7 +152,7 @@ jobs: # tag: bun-darwin-aarch64 # obj: bun-obj-darwin-aarch64 # artifact: bun-obj-darwin-aarch64 - # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-amd64-lto.tar.gz" + # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-3/bun-webkit-macos-amd64-lto.tar.gz" # runner: macos-arm64 # dependencies: true # compile_obj: true @@ -258,7 +258,7 @@ jobs: package: bun-darwin-x64 runner: macos-11 artifact: bun-obj-darwin-x64-baseline - webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-amd64-lto.tar.gz" + webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-3/bun-webkit-macos-amd64-lto.tar.gz" # - cpu: haswell # arch: x86_64 # tag: bun-darwin-x64 @@ -266,14 +266,14 @@ jobs: # package: bun-darwin-x64 # runner: macos-11 # artifact: bun-obj-darwin-x64 - # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-amd64-lto.tar.gz" + # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-3/bun-webkit-macos-amd64-lto.tar.gz" # - cpu: native # arch: aarch64 # tag: bun-darwin-aarch64 # obj: bun-obj-darwin-aarch64 # package: bun-darwin-aarch64 # artifact: bun-obj-darwin-aarch64 - # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-amd64-lto.tar.gz" + # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-3/bun-webkit-macos-amd64-lto.tar.gz" # runner: macos-arm64 steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/bun-mac-x64.yml b/.github/workflows/bun-mac-x64.yml index 8b30321f0..ae265adab 100644 --- a/.github/workflows/bun-mac-x64.yml +++ b/.github/workflows/bun-mac-x64.yml @@ -117,7 +117,7 @@ jobs: # obj: bun-obj-darwin-x64-baseline # runner: macos-11 # artifact: bun-obj-darwin-x64-baseline - # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-amd64-lto.tar.gz" + # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-3/bun-webkit-macos-amd64-lto.tar.gz" # dependencies: true # compile_obj: false - cpu: haswell @@ -126,7 +126,7 @@ jobs: obj: bun-obj-darwin-x64 runner: macos-11 artifact: bun-obj-darwin-x64 - webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-amd64-lto.tar.gz" + webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-3/bun-webkit-macos-amd64-lto.tar.gz" dependencies: true compile_obj: false # - cpu: nehalem @@ -135,7 +135,7 @@ jobs: # obj: bun-obj-darwin-x64-baseline # runner: macos-11 # artifact: bun-obj-darwin-x64-baseline - # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-amd64-lto.tar.gz" + # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-3/bun-webkit-macos-amd64-lto.tar.gz" # dependencies: false # compile_obj: true - cpu: haswell @@ -144,7 +144,7 @@ jobs: obj: bun-obj-darwin-x64 runner: macos-11 artifact: bun-obj-darwin-x64 - webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-amd64-lto.tar.gz" + webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-3/bun-webkit-macos-amd64-lto.tar.gz" dependencies: false compile_obj: true # - cpu: native @@ -152,7 +152,7 @@ jobs: # tag: bun-darwin-aarch64 # obj: bun-obj-darwin-aarch64 # artifact: bun-obj-darwin-aarch64 - # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-arm64-lto.tar.gz" + # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-3/bun-webkit-macos-arm64-lto.tar.gz" # runner: macos-arm64 # dependencies: true # compile_obj: true @@ -260,7 +260,7 @@ jobs: # package: bun-darwin-x64 # runner: macos-11 # artifact: bun-obj-darwin-x64-baseline - # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-amd64-lto.tar.gz" + # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-3/bun-webkit-macos-amd64-lto.tar.gz" - cpu: haswell arch: x86_64 tag: bun-darwin-x64 @@ -268,14 +268,14 @@ jobs: package: bun-darwin-x64 runner: macos-11 artifact: bun-obj-darwin-x64 - webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-amd64-lto.tar.gz" + webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-3/bun-webkit-macos-amd64-lto.tar.gz" # - cpu: native # arch: aarch64 # tag: bun-darwin-aarch64 # obj: bun-obj-darwin-aarch64 # package: bun-darwin-aarch64 # artifact: bun-obj-darwin-aarch64 - # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-arm64-lto.tar.gz" + # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-3/bun-webkit-macos-arm64-lto.tar.gz" # runner: macos-arm64 steps: - uses: actions/checkout@v3 diff --git a/Dockerfile b/Dockerfile index 5f66a5e04..66c3360af 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,7 @@ ARG ARCH=x86_64 ARG BUILD_MACHINE_ARCH=x86_64 ARG TRIPLET=${ARCH}-linux-gnu ARG BUILDARCH=amd64 -ARG WEBKIT_TAG=may20-2 +ARG WEBKIT_TAG=may20-3 ARG ZIG_TAG=jul1 ARG ZIG_VERSION="0.11.0-dev.3737+9eb008717" ARG WEBKIT_BASENAME="bun-webkit-linux-$BUILDARCH" diff --git a/bun.lockb b/bun.lockb index e53db751b..69f9ded0b 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 4fe213747..f4bd86e76 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "@types/react": "^18.0.25", "@typescript-eslint/eslint-plugin": "^5.31.0", "@typescript-eslint/parser": "^5.31.0", - "bun-webkit": "0.0.1-8a03cf746abef8a48c932ab25f8821390632f2e2" + "bun-webkit": "0.0.1-26c819733315f0ab64ae8e8e65b77d77d31211e1" }, "version": "0.0.0", "prettier": "./.prettierrc.cjs" diff --git a/src/bun.js/WebKit b/src/bun.js/WebKit index 8a03cf746..26c819733 160000 --- a/src/bun.js/WebKit +++ b/src/bun.js/WebKit @@ -1 +1 @@ -Subproject commit 8a03cf746abef8a48c932ab25f8821390632f2e2 +Subproject commit 26c819733315f0ab64ae8e8e65b77d77d31211e1 diff --git a/src/bun.js/bindings/JSMockFunction.cpp b/src/bun.js/bindings/JSMockFunction.cpp index 33922c2b7..0e24e761c 100644 --- a/src/bun.js/bindings/JSMockFunction.cpp +++ b/src/bun.js/bindings/JSMockFunction.cpp @@ -19,6 +19,7 @@ #include #include #include +#include namespace Bun { @@ -65,6 +66,29 @@ JSC_DECLARE_HOST_FUNCTION(jsMockFunctionMockRejectedValueOnce); JSC_DECLARE_HOST_FUNCTION(jsMockFunctionWithImplementationCleanup); JSC_DECLARE_HOST_FUNCTION(jsMockFunctionWithImplementation); +extern "C" EncodedJSValue JSMock__jsNow(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame) +{ + return JSValue::encode(jsNumber(globalObject->jsDateNow())); +} +extern "C" EncodedJSValue JSMock__jsSetSystemTime(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame) +{ + JSValue argument0 = callFrame->argument(0); + + if (auto* dateInstance = jsDynamicCast(argument0)) { + if (std::isnormal(dateInstance->internalNumber())) { + globalObject->overridenDateNow = dateInstance->internalNumber(); + } + return JSValue::encode(callFrame->thisValue()); + } + + if (argument0.isNumber() && argument0.asNumber() > 0) { + globalObject->overridenDateNow = argument0.asNumber(); + } + + globalObject->overridenDateNow = -1; + return JSValue::encode(callFrame->thisValue()); +} + uint64_t JSMockModule::s_nextInvocationId = 0; // This is taken from JSWeakSet diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig index 6e4cb827c..727466835 100644 --- a/src/bun.js/test/jest.zig +++ b/src/bun.js/test/jest.zig @@ -440,6 +440,9 @@ pub const Jest = struct { jest.put(globalObject, ZigString.static("fn"), mockFn); jest.put(globalObject, ZigString.static("spyOn"), spyOn); jest.put(globalObject, ZigString.static("restoreAllMocks"), restoreAllMocks); + jest.put(globalObject, ZigString.static("now"), JSC.NewFunction(globalObject, ZigString.static("now"), 0, JSMock__jsNow, false)); + jest.put(globalObject, ZigString.static("setSystemTime"), JSC.NewFunction(globalObject, ZigString.static("setSystemTime"), 0, JSMock__jsSetSystemTime, false)); + module.put(globalObject, ZigString.static("jest"), jest); module.put(globalObject, ZigString.static("spyOn"), spyOn); @@ -455,6 +458,8 @@ pub const Jest = struct { extern fn Bun__Jest__testPreloadObject(*JSC.JSGlobalObject) JSC.JSValue; extern fn Bun__Jest__testModuleObject(*JSC.JSGlobalObject) JSC.JSValue; extern fn JSMock__jsMockFn(*JSC.JSGlobalObject, *JSC.CallFrame) JSC.JSValue; + extern fn JSMock__jsNow(*JSC.JSGlobalObject, *JSC.CallFrame) JSC.JSValue; + extern fn JSMock__jsSetSystemTime(*JSC.JSGlobalObject, *JSC.CallFrame) JSC.JSValue; extern fn JSMock__jsRestoreAllMocks(*JSC.JSGlobalObject, *JSC.CallFrame) JSC.JSValue; extern fn JSMock__jsSpyOn(*JSC.JSGlobalObject, *JSC.CallFrame) JSC.JSValue; diff --git a/test/js/bun/test/test-timers.test.ts b/test/js/bun/test/test-timers.test.ts new file mode 100644 index 000000000..64da0abda --- /dev/null +++ b/test/js/bun/test/test-timers.test.ts @@ -0,0 +1,11 @@ +test("we can go back in time", () => { + const dateNow = Date.now; + // jest.useFakeTimers(); + jest.setSystemTime(new Date("2020-01-01T00:00:00.000Z")); + expect(new Date().toISOString()).toBe("2020-01-01T00:00:00.000Z"); + expect(Date.now()).toBe(1577836800000); + expect(dateNow).toBe(Date.now); + + jest.setSystemTime(); + expect(new Date().toISOString()).not.toBe("2020-01-01T00:00:00.000Z"); +}); -- cgit v1.2.3 From f0a795b568b741f71945e1078b87355d3217cc23 Mon Sep 17 00:00:00 2001 From: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> Date: Sun, 2 Jul 2023 22:42:13 -0700 Subject: Stub out `useFakeTimers` and `useRealTimers` --- packages/bun-types/bun-test.d.ts | 30 +++++++++++++++++++++++++++++ src/bun.js/bindings/JSMockFunction.cpp | 12 ++++++++++++ src/bun.js/test/jest.zig | 31 +++++++++++++++++++++++++++--- test/js/bun/test/test-timers.test.ts | 35 +++++++++++++++++++++++++--------- 4 files changed, 96 insertions(+), 12 deletions(-) (limited to 'src/bun.js/test') diff --git a/packages/bun-types/bun-test.d.ts b/packages/bun-types/bun-test.d.ts index 156585766..5182f7e93 100644 --- a/packages/bun-types/bun-test.d.ts +++ b/packages/bun-types/bun-test.d.ts @@ -29,6 +29,36 @@ declare module "bun:test" { (Function: T): Mock; }; + /** + * Control the system time used by: + * - `Date.now()` + * - `new Date()` + * - `Intl.DateTimeFormat().format()` + * + * In the future, we may add support for more functions, but we haven't done that yet. + * + * @param now The time to set the system time to. If not provided, the system time will be reset. + * @returns `this` + * @since v0.6.13 + * + * ## Set Date to a specific time + * + * ```js + * import { setSystemTime } from 'bun:test'; + * + * setSystemTime(new Date('2020-01-01T00:00:00.000Z')); + * console.log(new Date().toISOString()); // 2020-01-01T00:00:00.000Z + * ``` + * ## Reset Date to the current time + * + * ```js + * import { setSystemTime } from 'bun:test'; + * + * setSystemTime(); + * ``` + */ + export function setSystemTime(now?: Date | number): ThisType; + interface Jest { restoreAllMocks(): void; fn(func?: T): Mock; diff --git a/src/bun.js/bindings/JSMockFunction.cpp b/src/bun.js/bindings/JSMockFunction.cpp index 0e24e761c..3a84f0139 100644 --- a/src/bun.js/bindings/JSMockFunction.cpp +++ b/src/bun.js/bindings/JSMockFunction.cpp @@ -66,6 +66,18 @@ JSC_DECLARE_HOST_FUNCTION(jsMockFunctionMockRejectedValueOnce); JSC_DECLARE_HOST_FUNCTION(jsMockFunctionWithImplementationCleanup); JSC_DECLARE_HOST_FUNCTION(jsMockFunctionWithImplementation); +// This is a stub. Exists so that the same code can be run in Jest +extern "C" EncodedJSValue JSMock__jsUseFakeTimers(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame) +{ + return JSValue::encode(callFrame->thisValue()); +} + +extern "C" EncodedJSValue JSMock__jsUseRealTimers(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame) +{ + globalObject->overridenDateNow = -1; + return JSValue::encode(callFrame->thisValue()); +} + extern "C" EncodedJSValue JSMock__jsNow(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame) { return JSValue::encode(jsNumber(globalObject->jsDateNow())); diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig index 727466835..55600ded8 100644 --- a/src/bun.js/test/jest.zig +++ b/src/bun.js/test/jest.zig @@ -333,7 +333,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, 12); + const module = JSC.JSValue.createEmptyObject(globalObject, 13); const test_fn = JSC.NewFunction(globalObject, ZigString.static("test"), 2, TestScope.call, false); module.put( @@ -431,17 +431,40 @@ pub const Jest = struct { Expect.getConstructor(globalObject), ); + const setSystemTime = JSC.NewFunction(globalObject, ZigString.static("setSystemTime"), 0, JSMock__jsSetSystemTime, false); + module.put( + globalObject, + ZigString.static("setSystemTime"), + setSystemTime, + ); + const useFakeTimers = JSC.NewFunction(globalObject, ZigString.static("useFakeTimers"), 0, JSMock__jsUseFakeTimers, false); + const useRealTimers = JSC.NewFunction(globalObject, ZigString.static("useRealTimers"), 0, JSMock__jsUseRealTimers, false); + const mockFn = JSC.NewFunction(globalObject, ZigString.static("fn"), 1, JSMock__jsMockFn, false); const spyOn = JSC.NewFunction(globalObject, ZigString.static("spyOn"), 2, JSMock__jsSpyOn, false); const restoreAllMocks = JSC.NewFunction(globalObject, ZigString.static("restoreAllMocks"), 2, JSMock__jsRestoreAllMocks, false); module.put(globalObject, ZigString.static("mock"), mockFn); - const jest = JSValue.createEmptyObject(globalObject, 3); + const jest = JSValue.createEmptyObject(globalObject, 7); jest.put(globalObject, ZigString.static("fn"), mockFn); jest.put(globalObject, ZigString.static("spyOn"), spyOn); jest.put(globalObject, ZigString.static("restoreAllMocks"), restoreAllMocks); + jest.put( + globalObject, + ZigString.static("setSystemTime"), + setSystemTime, + ); + jest.put( + globalObject, + ZigString.static("useFakeTimers"), + useFakeTimers, + ); + jest.put( + globalObject, + ZigString.static("useRealTimers"), + useRealTimers, + ); jest.put(globalObject, ZigString.static("now"), JSC.NewFunction(globalObject, ZigString.static("now"), 0, JSMock__jsNow, false)); - jest.put(globalObject, ZigString.static("setSystemTime"), JSC.NewFunction(globalObject, ZigString.static("setSystemTime"), 0, JSMock__jsSetSystemTime, false)); module.put(globalObject, ZigString.static("jest"), jest); module.put(globalObject, ZigString.static("spyOn"), spyOn); @@ -462,6 +485,8 @@ pub const Jest = struct { extern fn JSMock__jsSetSystemTime(*JSC.JSGlobalObject, *JSC.CallFrame) JSC.JSValue; extern fn JSMock__jsRestoreAllMocks(*JSC.JSGlobalObject, *JSC.CallFrame) JSC.JSValue; extern fn JSMock__jsSpyOn(*JSC.JSGlobalObject, *JSC.CallFrame) JSC.JSValue; + extern fn JSMock__jsUseFakeTimers(*JSC.JSGlobalObject, *JSC.CallFrame) JSC.JSValue; + extern fn JSMock__jsUseRealTimers(*JSC.JSGlobalObject, *JSC.CallFrame) JSC.JSValue; pub fn call( _: void, diff --git a/test/js/bun/test/test-timers.test.ts b/test/js/bun/test/test-timers.test.ts index 64da0abda..963467dee 100644 --- a/test/js/bun/test/test-timers.test.ts +++ b/test/js/bun/test/test-timers.test.ts @@ -1,11 +1,28 @@ test("we can go back in time", () => { - const dateNow = Date.now; - // jest.useFakeTimers(); - jest.setSystemTime(new Date("2020-01-01T00:00:00.000Z")); - expect(new Date().toISOString()).toBe("2020-01-01T00:00:00.000Z"); - expect(Date.now()).toBe(1577836800000); - expect(dateNow).toBe(Date.now); - - jest.setSystemTime(); - expect(new Date().toISOString()).not.toBe("2020-01-01T00:00:00.000Z"); + const DateBeforeMocked = Date; + const orig = new Date(); + orig.setHours(0, 0, 0, 0); + jest.useFakeTimers(); + jest.setSystemTime(new Date("1995-12-19T00:00:00.000Z")); + + expect(new Date().toISOString()).toBe("1995-12-19T00:00:00.000Z"); + expect(Date.now()).toBe(819331200000); + + if (typeof Bun !== "undefined") { + // In bun, the Date object remains the same despite being mocked. + // This prevents a whole bunch of subtle bugs in tests. + expect(DateBeforeMocked).toBe(Date); + expect(DateBeforeMocked.now).toBe(Date.now); + + // Jest doesn't property mock new Intl.DateTimeFormat().format() + expect(new Intl.DateTimeFormat().format()).toBe("12/19/1995"); + } else { + expect(DateBeforeMocked).not.toBe(Date); + expect(DateBeforeMocked.now).not.toBe(Date.now); + } + + jest.useRealTimers(); + const now = new Date(); + now.setHours(0, 0, 0, 0); + expect(now.toISOString()).toBe(orig.toISOString()); }); -- cgit v1.2.3 From 31ab56d36238528c6f44c52a6cbf600744a91f0a Mon Sep 17 00:00:00 2001 From: dave caruso Date: Tue, 11 Jul 2023 12:48:19 -0700 Subject: Fix: console.log with class constructors (#3602) * Fix console.log with class constructors * oops * fix it * lol * fix test --- src/bun.js/bindings/bindings.cpp | 14 ++++++++-- src/bun.js/bindings/exports.zig | 37 +++++++++++------------- src/bun.js/test/pretty_format.zig | 42 +++++++++++++++------------- test/js/web/console/console-log.expected.txt | 8 +++++- test/js/web/console/console-log.js | 6 ++++ 5 files changed, 65 insertions(+), 42 deletions(-) (limited to 'src/bun.js/test') diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 9f9b20c1e..d311072e4 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -2775,8 +2775,18 @@ void JSC__JSValue__put(JSC__JSValue JSValue0, JSC__JSGlobalObject* arg1, const Z bool JSC__JSValue__isClass(JSC__JSValue JSValue0, JSC__JSGlobalObject* arg1) { - JSC::JSValue value = JSC::JSValue::decode(JSValue0); - return value.isConstructor(); + JSValue value = JSValue::decode(JSValue0); + auto callData = getCallData(value); + + switch (callData.type) { + case CallData::Type::JS: + return callData.js.functionExecutable->isClassConstructorFunction(); + case CallData::Type::Native: + if (callData.native.isBoundFunction) + return false; + return value.isConstructor(); + } + return false; } bool JSC__JSValue__isCell(JSC__JSValue JSValue0) { return JSC::JSValue::decode(JSValue0).isCell(); } bool JSC__JSValue__isCustomGetterSetter(JSC__JSValue JSValue0) diff --git a/src/bun.js/bindings/exports.zig b/src/bun.js/bindings/exports.zig index 73a26e4be..db6de2ef3 100644 --- a/src/bun.js/bindings/exports.zig +++ b/src/bun.js/bindings/exports.zig @@ -1417,23 +1417,20 @@ pub const ZigConsoleClient = struct { // If we check an Object has a method table and it does not // it will crash - const callable = js_type != .Object and value.isCallable(globalThis.vm()); - - if (value.isClass(globalThis) and !callable) { - return .{ - .tag = .Object, - .cell = js_type, - }; - } + if (js_type != .Object and value.isCallable(globalThis.vm())) { + if (value.isClass(globalThis)) { + return .{ + .tag = .Class, + .cell = js_type, + }; + } - if (callable and js_type == .JSFunction) { - return .{ - .tag = .Function, - .cell = js_type, - }; - } else if (callable and js_type == .InternalFunction) { return .{ - .tag = .Object, + // TODO: we print InternalFunction as Object because we have a lot of + // callable namespaces and printing the contents of it is better than [Function: namespace] + // ideally, we would print [Function: namespace] { ... } on all functions, internal and js. + // what we'll do later is rid of .Function and .Class and handle the prefix in the .Object formatter + .tag = if (js_type == .InternalFunction) .Object else .Function, .cell = js_type, }; } @@ -1756,7 +1753,7 @@ pub const ZigConsoleClient = struct { parent: JSValue, const enable_ansi_colors = enable_ansi_colors_; pub fn handleFirstProperty(this: *@This(), globalThis: *JSC.JSGlobalObject, value: JSValue) void { - if (!value.jsType().isFunction() and !value.isClass(globalThis)) { + if (!value.jsType().isFunction()) { var writer = WrappedWriter(Writer){ .ctx = this.writer, .failed = false, @@ -2094,9 +2091,9 @@ pub const ZigConsoleClient = struct { this.addForNewLine(printable.len); if (printable.len == 0) { - writer.print(comptime Output.prettyFmt("[class]", enable_ansi_colors), .{}); + writer.print(comptime Output.prettyFmt("[class]", enable_ansi_colors), .{}); } else { - writer.print(comptime Output.prettyFmt("[class {}]", enable_ansi_colors), .{printable}); + writer.print(comptime Output.prettyFmt("[class {}]", enable_ansi_colors), .{printable}); } }, .Function => { @@ -2106,7 +2103,7 @@ pub const ZigConsoleClient = struct { if (printable.len == 0) { writer.print(comptime Output.prettyFmt("[Function]", enable_ansi_colors), .{}); } else { - writer.print(comptime Output.prettyFmt("[Function: {}]", enable_ansi_colors), .{printable}); + writer.print(comptime Output.prettyFmt("[Function: {}]", enable_ansi_colors), .{printable}); } }, .Getter => { @@ -2802,7 +2799,7 @@ pub const ZigConsoleClient = struct { } if (iter.i == 0) { - if (value.isClass(this.globalThis) and !value.isCallable(this.globalThis.vm())) + if (value.isClass(this.globalThis)) this.printAs(.Class, Writer, writer_, value, jsType, enable_ansi_colors) else if (value.isCallable(this.globalThis.vm())) this.printAs(.Function, Writer, writer_, value, jsType, enable_ansi_colors) diff --git a/src/bun.js/test/pretty_format.zig b/src/bun.js/test/pretty_format.zig index 4a245c3bb..a6c6aa631 100644 --- a/src/bun.js/test/pretty_format.zig +++ b/src/bun.js/test/pretty_format.zig @@ -423,23 +423,20 @@ pub const JestPrettyFormat = struct { // If we check an Object has a method table and it does not // it will crash - const callable = js_type != .Object and value.isCallable(globalThis.vm()); + if (js_type != .Object and value.isCallable(globalThis.vm())) { + if (value.isClass(globalThis)) { + return .{ + .tag = .Class, + .cell = js_type, + }; + } - if (value.isClass(globalThis) and !callable) { return .{ - .tag = .Object, - .cell = js_type, - }; - } - - if (callable and js_type == .JSFunction) { - return .{ - .tag = .Function, - .cell = js_type, - }; - } else if (callable and js_type == .InternalFunction) { - return .{ - .tag = .Object, + // TODO: we print InternalFunction as Object because we have a lot of + // callable namespaces and printing the contents of it is better than [Function: namespace] + // ideally, we would print [Function: namespace] { ... } on all functions, internal and js. + // what we'll do later is rid of .Function and .Class and handle the prefix in the .Object formatter + .tag = if (js_type == .InternalFunction) .Object else .Function, .cell = js_type, }; } @@ -750,7 +747,7 @@ pub const JestPrettyFormat = struct { parent: JSValue, const enable_ansi_colors = enable_ansi_colors_; pub fn handleFirstProperty(this: *@This(), globalThis: *JSC.JSGlobalObject, value: JSValue) void { - if (!value.jsType().isFunction() and !value.isClass(globalThis)) { + if (!value.jsType().isFunction()) { var writer = WrappedWriter(Writer){ .ctx = this.writer, .failed = false, @@ -1126,13 +1123,20 @@ pub const JestPrettyFormat = struct { this.addForNewLine(printable.len); if (printable.len == 0) { - writer.print(comptime Output.prettyFmt("[class]", enable_ansi_colors), .{}); + writer.print(comptime Output.prettyFmt("[class]", enable_ansi_colors), .{}); } else { - writer.print(comptime Output.prettyFmt("[class {}]", enable_ansi_colors), .{printable}); + writer.print(comptime Output.prettyFmt("[class {}]", enable_ansi_colors), .{printable}); } }, .Function => { - writer.writeAll("[Function]"); + var printable = ZigString.init(&name_buf); + value.getNameProperty(this.globalThis, &printable); + + if (printable.len == 0) { + writer.print(comptime Output.prettyFmt("[Function]", enable_ansi_colors), .{}); + } else { + writer.print(comptime Output.prettyFmt("[Function: {}]", enable_ansi_colors), .{printable}); + } }, .Array => { const len = @truncate(u32, value.getLength(this.globalThis)); diff --git a/test/js/web/console/console-log.expected.txt b/test/js/web/console/console-log.expected.txt index 97191c8be..332322665 100644 --- a/test/js/web/console/console-log.expected.txt +++ b/test/js/web/console/console-log.expected.txt @@ -1,4 +1,6 @@ Hello World! +0 +-0 123 -123 123.567 @@ -7,6 +9,8 @@ true false null undefined +Infinity +-Infinity Symbol(Symbol Description) 2000-06-27T02:24:34.304Z [ 123, 456, 789 ] @@ -29,7 +33,9 @@ Symbol(Symbol Description) } Promise { } [Function] -[Function: Foo] +[Function] +[class Foo] +[class] {} [Function: foooo] /FooRegex/ diff --git a/test/js/web/console/console-log.js b/test/js/web/console/console-log.js index e23a3e9cb..4db40aaac 100644 --- a/test/js/web/console/console-log.js +++ b/test/js/web/console/console-log.js @@ -1,4 +1,6 @@ console.log("Hello World!"); +console.log(0); +console.log(-0); console.log(123); console.log(-123); console.log(123.567); @@ -7,6 +9,8 @@ console.log(true); console.log(false); console.log(null); console.log(undefined); +console.log(Infinity); +console.log(-Infinity); console.log(Symbol("Symbol Description")); console.log(new Date(Math.pow(2, 34) * 56)); console.log([123, 456, 789]); @@ -27,7 +31,9 @@ console.log(new Promise(() => {})); class Foo {} console.log(() => {}); +console.log(function () {}); console.log(Foo); +console.log(class {}); console.log(new Foo()); console.log(function foooo() {}); -- cgit v1.2.3