diff options
Diffstat (limited to 'src/bun.js/test')
-rw-r--r-- | src/bun.js/test/jest.classes.ts | 12 | ||||
-rw-r--r-- | src/bun.js/test/jest.zig | 659 | ||||
-rw-r--r-- | src/bun.js/test/pretty_format.zig | 1963 |
3 files changed, 2623 insertions, 11 deletions
diff --git a/src/bun.js/test/jest.classes.ts b/src/bun.js/test/jest.classes.ts index 78848b287..9182c8cc6 100644 --- a/src/bun.js/test/jest.classes.ts +++ b/src/bun.js/test/jest.classes.ts @@ -2,6 +2,18 @@ import { define } from "../scripts/class-definitions"; export default [ define({ + name: "ExpectAny", + construct: false, + noConstructor: true, + call: true, + finalize: true, + JSType: "0b11101110", + values: ["constructorValue"], + configurable: false, + klass: {}, + proto: {}, + }), + define({ name: "Expect", construct: true, call: true, diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig index 2cb86c4f7..d8d4fb246 100644 --- a/src/bun.js/test/jest.zig +++ b/src/bun.js/test/jest.zig @@ -1,5 +1,7 @@ const std = @import("std"); const bun = @import("bun"); +const js_parser = bun.js_parser; +const js_ast = bun.JSAst; const Api = @import("../../api/schema.zig").Api; const RequestContext = @import("../../http.zig").RequestContext; const MimeType = @import("../../http.zig").MimeType; @@ -64,12 +66,75 @@ fn notImplementedProp( } pub const DiffFormatter = struct { - received: JSValue, - expected: JSValue, + received_string: ?string = null, + expected_string: ?string = null, + received: ?JSValue = null, + expected: ?JSValue = null, globalObject: *JSC.JSGlobalObject, not: bool = false, pub fn format(this: DiffFormatter, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + if (this.expected_string != null and this.received_string != null) { + const received = this.received_string.?; + const expected = this.expected_string.?; + + var dmp = DiffMatchPatch.default; + dmp.diff_timeout = 200; + var diffs = try dmp.diff(default_allocator, received, expected, false); + defer diffs.deinit(default_allocator); + + const equal_fmt = "<d>{s}<r>"; + const delete_fmt = "<red>{s}<r>"; + const insert_fmt = "<green>{s}<r>"; + + try writer.writeAll("Expected: "); + for (diffs.items) |df| { + switch (df.operation) { + .delete => continue, + .insert => { + if (Output.enable_ansi_colors) { + try writer.print(Output.prettyFmt(insert_fmt, true), .{df.text}); + } else { + try writer.print(Output.prettyFmt(insert_fmt, false), .{df.text}); + } + }, + .equal => { + if (Output.enable_ansi_colors) { + try writer.print(Output.prettyFmt(equal_fmt, true), .{df.text}); + } else { + try writer.print(Output.prettyFmt(equal_fmt, false), .{df.text}); + } + }, + } + } + + try writer.writeAll("\nReceived: "); + for (diffs.items) |df| { + switch (df.operation) { + .insert => continue, + .delete => { + if (Output.enable_ansi_colors) { + try writer.print(Output.prettyFmt(delete_fmt, true), .{df.text}); + } else { + try writer.print(Output.prettyFmt(delete_fmt, false), .{df.text}); + } + }, + .equal => { + if (Output.enable_ansi_colors) { + try writer.print(Output.prettyFmt(equal_fmt, true), .{df.text}); + } else { + try writer.print(Output.prettyFmt(equal_fmt, false), .{df.text}); + } + }, + } + } + return; + } + + if (this.received == null or this.expected == null) return; + + const received = this.received.?; + const expected = this.expected.?; var received_buf = MutableString.init(default_allocator, 0) catch unreachable; var expected_buf = MutableString.init(default_allocator, 0) catch unreachable; defer { @@ -94,7 +159,7 @@ pub const DiffFormatter = struct { JSC.ZigConsoleClient.format( .Debug, this.globalObject, - @ptrCast([*]const JSValue, &this.received), + @ptrCast([*]const JSValue, &received), 1, Writer, Writer, @@ -131,21 +196,21 @@ pub const DiffFormatter = struct { return; } - switch (this.received.determineDiffMethod(this.expected, this.globalObject)) { + switch (received.determineDiffMethod(expected, this.globalObject)) { .none => { const fmt = "Expected: <green>{any}<r>\nReceived: <red>{any}<r>"; var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = this.globalObject, .quote_strings = true }; if (Output.enable_ansi_colors) { try writer.print(Output.prettyFmt(fmt, true), .{ - this.expected.toFmt(this.globalObject, &formatter), - this.received.toFmt(this.globalObject, &formatter), + expected.toFmt(this.globalObject, &formatter), + received.toFmt(this.globalObject, &formatter), }); return; } try writer.print(Output.prettyFmt(fmt, true), .{ - this.expected.toFmt(this.globalObject, &formatter), - this.received.toFmt(this.globalObject, &formatter), + expected.toFmt(this.globalObject, &formatter), + received.toFmt(this.globalObject, &formatter), }); return; }, @@ -305,6 +370,8 @@ pub const TestRunner = struct { /// This silences TestNotRunningError when expect() is used to halt a running test. did_pending_test_fail: bool = false, + snapshots: Snapshots, + pub const Drainer = JSC.AnyTask.New(TestRunner, drain); pub fn enqueue(this: *TestRunner, task: *TestRunnerTask) void { @@ -427,6 +494,274 @@ pub const TestRunner = struct { }; }; +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 = std.hash.Wyhash.hash(0, 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.ok) parse_result.ast else return error.ParseError; + defer ast.deinit(); + + if (ast.exports_ref == null) 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) |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 = std.hash.Wyhash.hash(0, 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; @@ -465,6 +800,54 @@ pub const Jest = struct { } }; +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 = "<d>expect.<r>any<d>(<r>constructor<d>)<r>\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(); + JSC.Jest.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 { @@ -481,6 +864,49 @@ pub const Expect = struct { 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 { @@ -2080,6 +2506,215 @@ pub const Expect = struct { 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\n<b>Matcher error<r>: Snapshot matchers cannot be used with <b>not<r>\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", "<green>properties<r><d>, <r>hint", false); + const fmt = signature ++ "\n\nMatcher error: Expected <green>properties<r> 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", "<green>properties<r><d>, <r>hint", false); + const fmt = signature ++ "\n\n<b>Matcher error: <red>received<r> values must be an object when the matcher has <green>properties<r>\n"; + globalObject.throwPretty(fmt, .{}); + return .zero; + } + + if (property_matchers) |_prop_matchers| { + var prop_matchers = _prop_matchers; + + var itr = PropertyMatcherIterator{ + .received_object = value, + .failed = false, + }; + + prop_matchers.forEachProperty(globalObject, &itr, PropertyMatcherIterator.forEach); + + if (itr.failed) { + // TODO: print diff with properties from propertyMatchers + const signature = comptime getSignature("toMatchSnapshot", "<green>propertyMatchers<r>", false); + const fmt = signature ++ "\n\nExpected <green>propertyMatchers<r> 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", "<green>expected<r>", 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 const PropertyMatcherIterator = struct { + received_object: JSValue, + failed: bool, + i: usize = 0, + + pub fn forEach( + globalObject: *JSGlobalObject, + ctx_ptr: ?*anyopaque, + key_: [*c]ZigString, + value: JSValue, + _: bool, + ) callconv(.C) void { + const key: ZigString = key_.?[0]; + if (key.eqlComptime("constructor")) return; + if (key.eqlComptime("call")) return; + + var ctx: *@This() = bun.cast(*@This(), ctx_ptr orelse return); + defer ctx.i += 1; + var received_object: JSValue = ctx.received_object; + + if (received_object.get(globalObject, key.slice())) |received_value| { + if (JSC.Jest.ExpectAny.fromJS(value)) |_| { + var constructor_value = JSC.Jest.ExpectAny.constructorValueGetCached(value) orelse { + globalObject.throw("Internal consistency error: the expect.any(constructor value) was garbage collected but it should not have been!", .{}); + ctx.failed = true; + return; + }; + + if (received_value.isCell() and received_value.isInstanceOf(globalObject, constructor_value)) { + received_object.put(globalObject, &key, value); + return; + } + + // check primitives + // TODO: check the constructor for primitives by reading it from JSGlobalObject through a binding. + var constructor_name = ZigString.Empty; + constructor_value.getNameProperty(globalObject, &constructor_name); + if (received_value.isNumber() and constructor_name.eqlComptime("Number")) { + received_object.put(globalObject, &key, value); + return; + } + if (received_value.isBoolean() and constructor_name.eqlComptime("Boolean")) { + received_object.put(globalObject, &key, value); + return; + } + if (received_value.isString() and constructor_name.eqlComptime("String")) { + received_object.put(globalObject, &key, value); + return; + } + if (received_value.isBigInt() and constructor_name.eqlComptime("BigInt")) { + received_object.put(globalObject, &key, value); + return; + } + + ctx.failed = true; + return; + } + + if (value.isObject()) { + if (received_object.get(globalObject, key.slice())) |new_object| { + var itr = PropertyMatcherIterator{ + .received_object = new_object, + .failed = false, + }; + value.forEachProperty(globalObject, &itr, PropertyMatcherIterator.forEach); + if (itr.failed) { + ctx.failed = true; + } + } else { + ctx.failed = true; + } + + return; + } + + if (value.isSameValue(received_value, globalObject)) return; + } + + ctx.failed = true; + } + }; + pub fn toBeInstanceOf(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { defer this.postMatch(globalObject); @@ -2098,7 +2733,6 @@ pub const Expect = struct { } active_test_expectation_counter.actual += 1; - const expected_value = arguments[0]; if (!expected_value.jsType().isFunction()) { var fmt = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; @@ -2156,7 +2790,6 @@ pub const Expect = struct { pub const toContainEqual = notImplementedJSCFn; pub const toMatch = notImplementedJSCFn; pub const toMatchObject = notImplementedJSCFn; - pub const toMatchSnapshot = notImplementedJSCFn; pub const toMatchInlineSnapshot = notImplementedJSCFn; pub const toThrowErrorMatchingSnapshot = notImplementedJSCFn; pub const toThrowErrorMatchingInlineSnapshot = notImplementedJSCFn; @@ -2179,9 +2812,12 @@ pub const Expect = struct { pub const getResolves = notImplementedJSCProp; pub const getRejects = notImplementedJSCProp; + pub fn any(globalObject: *JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { + return ExpectAny.call(globalObject, callFrame); + } + pub const extend = notImplementedStaticFn; pub const anything = notImplementedStaticFn; - pub const any = notImplementedStaticFn; pub const arrayContaining = notImplementedStaticFn; pub const assertions = notImplementedStaticFn; pub const hasAssertions = notImplementedStaticFn; @@ -2225,6 +2861,7 @@ pub const TestScope = struct { ran: bool = false, task: ?*TestRunnerTask = null, skipped: bool = false, + snapshot_count: usize = 0, pub const Class = NewClass( void, diff --git a/src/bun.js/test/pretty_format.zig b/src/bun.js/test/pretty_format.zig new file mode 100644 index 000000000..dd5814aca --- /dev/null +++ b/src/bun.js/test/pretty_format.zig @@ -0,0 +1,1963 @@ +const std = @import("std"); +const bun = @import("bun"); +const Output = bun.Output; +const JSC = bun.JSC; +const JSGlobalObject = JSC.JSGlobalObject; +const JSValue = JSC.JSValue; +const is_bindgen: bool = std.meta.globalOption("bindgen", bool) orelse false; +const default_allocator = bun.default_allocator; +const CAPI = JSC.C; +const ZigString = JSC.ZigString; +const strings = bun.strings; +const string = bun.string; +const JSLexer = bun.js_lexer; +const JSPrinter = bun.js_printer; +const JSPrivateDataPtr = JSC.JSPrivateDataPtr; +const JS = @import("../javascript.zig"); +const JSPromise = JSC.JSPromise; + +pub const EventType = enum(u8) { + Event, + MessageEvent, + CloseEvent, + ErrorEvent, + OpenEvent, + unknown = 254, + _, + + pub const map = bun.ComptimeStringMap(EventType, .{ + .{ EventType.Event.label(), EventType.Event }, + .{ EventType.MessageEvent.label(), EventType.MessageEvent }, + .{ EventType.CloseEvent.label(), EventType.CloseEvent }, + .{ EventType.ErrorEvent.label(), EventType.ErrorEvent }, + .{ EventType.OpenEvent.label(), EventType.OpenEvent }, + }); + + pub fn label(this: EventType) string { + return switch (this) { + .Event => "event", + .MessageEvent => "message", + .CloseEvent => "close", + .ErrorEvent => "error", + .OpenEvent => "open", + else => "event", + }; + } +}; + +pub const JestPrettyFormat = struct { + pub const Type = *anyopaque; + const Counter = std.AutoHashMapUnmanaged(u64, u32); + + counts: Counter = .{}, + + pub const MessageLevel = enum(u32) { + Log = 0, + Warning = 1, + Error = 2, + Debug = 3, + Info = 4, + _, + }; + + pub const MessageType = enum(u32) { + Log = 0, + Dir = 1, + DirXML = 2, + Table = 3, + Trace = 4, + StartGroup = 5, + StartGroupCollapsed = 6, + EndGroup = 7, + Clear = 8, + Assert = 9, + Timing = 10, + Profile = 11, + ProfileEnd = 12, + Image = 13, + _, + }; + + pub const FormatOptions = struct { + enable_colors: bool, + add_newline: bool, + flush: bool, + quote_strings: bool = false, + }; + + pub fn format( + level: MessageLevel, + global: *JSGlobalObject, + vals: [*]const JSValue, + len: usize, + comptime RawWriter: type, + comptime Writer: type, + writer: Writer, + options: FormatOptions, + ) void { + var fmt: JestPrettyFormat.Formatter = undefined; + defer { + if (fmt.map_node) |node| { + node.data = fmt.map; + node.data.clearRetainingCapacity(); + node.release(); + } + } + + if (len == 1) { + fmt = JestPrettyFormat.Formatter{ + .remaining_values = &[_]JSValue{}, + .globalThis = global, + .quote_strings = options.quote_strings, + }; + const tag = JestPrettyFormat.Formatter.Tag.get(vals[0], global); + + var unbuffered_writer = if (comptime Writer != RawWriter) + writer.context.unbuffered_writer.context.writer() + else + writer; + + if (tag.tag == .String) { + if (options.enable_colors) { + if (level == .Error) { + unbuffered_writer.writeAll(comptime Output.prettyFmt("<r><red>", true)) catch unreachable; + } + fmt.format( + tag, + @TypeOf(unbuffered_writer), + unbuffered_writer, + vals[0], + global, + true, + ); + if (level == .Error) { + unbuffered_writer.writeAll(comptime Output.prettyFmt("<r>", true)) catch unreachable; + } + } else { + fmt.format( + tag, + @TypeOf(unbuffered_writer), + unbuffered_writer, + vals[0], + global, + false, + ); + } + if (options.add_newline) _ = unbuffered_writer.write("\n") catch 0; + } else { + defer { + if (comptime Writer != RawWriter) { + if (options.flush) writer.context.flush() catch {}; + } + } + if (options.enable_colors) { + fmt.format( + tag, + Writer, + writer, + vals[0], + global, + true, + ); + } else { + fmt.format( + tag, + Writer, + writer, + vals[0], + global, + false, + ); + } + if (options.add_newline) _ = writer.write("\n") catch 0; + } + + return; + } + + defer { + if (comptime Writer != RawWriter) { + if (options.flush) writer.context.flush() catch {}; + } + } + + var this_value: JSValue = vals[0]; + fmt = JestPrettyFormat.Formatter{ + .remaining_values = vals[0..len][1..], + .globalThis = global, + .quote_strings = options.quote_strings, + }; + var tag: JestPrettyFormat.Formatter.Tag.Result = undefined; + + var any = false; + if (options.enable_colors) { + if (level == .Error) { + writer.writeAll(comptime Output.prettyFmt("<r><red>", true)) catch unreachable; + } + while (true) { + if (any) { + _ = writer.write(" ") catch 0; + } + any = true; + + tag = JestPrettyFormat.Formatter.Tag.get(this_value, global); + if (tag.tag == .String and fmt.remaining_values.len > 0) { + tag.tag = .StringPossiblyFormatted; + } + + fmt.format(tag, Writer, writer, this_value, global, true); + if (fmt.remaining_values.len == 0) { + break; + } + + this_value = fmt.remaining_values[0]; + fmt.remaining_values = fmt.remaining_values[1..]; + } + if (level == .Error) { + writer.writeAll(comptime Output.prettyFmt("<r>", true)) catch unreachable; + } + } else { + while (true) { + if (any) { + _ = writer.write(" ") catch 0; + } + any = true; + tag = JestPrettyFormat.Formatter.Tag.get(this_value, global); + if (tag.tag == .String and fmt.remaining_values.len > 0) { + tag.tag = .StringPossiblyFormatted; + } + + fmt.format(tag, Writer, writer, this_value, global, false); + if (fmt.remaining_values.len == 0) + break; + + this_value = fmt.remaining_values[0]; + fmt.remaining_values = fmt.remaining_values[1..]; + } + } + + if (options.add_newline) _ = writer.write("\n") catch 0; + } + + pub const Formatter = struct { + remaining_values: []const JSValue = &[_]JSValue{}, + map: Visited.Map = undefined, + map_node: ?*Visited.Pool.Node = null, + hide_native: bool = false, + globalThis: *JSGlobalObject, + indent: u32 = 0, + quote_strings: bool = false, + failed: bool = false, + estimated_line_length: usize = 0, + always_newline_scope: bool = false, + + pub fn goodTimeForANewLine(this: *@This()) bool { + if (this.estimated_line_length > 80) { + this.resetLine(); + return true; + } + return false; + } + + pub fn resetLine(this: *@This()) void { + this.estimated_line_length = this.indent * 2; + } + + pub fn addForNewLine(this: *@This(), len: usize) void { + this.estimated_line_length +|= len; + } + + pub const ZigFormatter = struct { + formatter: *JestPrettyFormat.Formatter, + global: *JSGlobalObject, + value: JSValue, + + pub const WriteError = error{UhOh}; + pub fn format(self: ZigFormatter, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + self.formatter.remaining_values = &[_]JSValue{self.value}; + defer { + self.formatter.remaining_values = &[_]JSValue{}; + } + self.formatter.globalThis = self.global; + self.formatter.format( + Tag.get(self.value, self.global), + @TypeOf(writer), + writer, + self.value, + self.formatter.globalThis, + false, + ); + } + }; + + // For detecting circular references + pub const Visited = struct { + const ObjectPool = @import("../../pool.zig").ObjectPool; + pub const Map = std.AutoHashMap(JSValue.Type, void); + pub const Pool = ObjectPool( + Map, + struct { + pub fn init(allocator: std.mem.Allocator) anyerror!Map { + return Map.init(allocator); + } + }.init, + true, + 16, + ); + }; + + pub const Tag = enum { + StringPossiblyFormatted, + String, + Undefined, + Double, + Integer, + Null, + Boolean, + Array, + Object, + Function, + Class, + Error, + TypedArray, + Map, + Set, + Symbol, + BigInt, + + GlobalObject, + Private, + Promise, + + JSON, + NativeCode, + ArrayBuffer, + + JSX, + Event, + + pub fn isPrimitive(this: Tag) bool { + return switch (this) { + .String, + .StringPossiblyFormatted, + .Undefined, + .Double, + .Integer, + .Null, + .Boolean, + .Symbol, + .BigInt, + => true, + else => false, + }; + } + + pub inline fn canHaveCircularReferences(tag: Tag) bool { + return tag == .Array or tag == .Object or tag == .Map or tag == .Set; + } + + const Result = struct { + tag: Tag, + cell: JSValue.JSType = JSValue.JSType.Cell, + }; + + pub fn get(value: JSValue, globalThis: *JSGlobalObject) Result { + switch (@enumToInt(value)) { + 0, 0xa => return Result{ + .tag = .Undefined, + }, + 0x2 => return Result{ + .tag = .Null, + }, + else => {}, + } + + if (value.isInt32()) { + return .{ + .tag = .Integer, + }; + } else if (value.isNumber()) { + return .{ + .tag = .Double, + }; + } else if (value.isBoolean()) { + return .{ + .tag = .Boolean, + }; + } + + if (!value.isCell()) + return .{ + .tag = .NativeCode, + }; + + const js_type = value.jsType(); + + if (js_type.isHidden()) return .{ + .tag = .NativeCode, + .cell = js_type, + }; + + // Cell is the "unknown" type + // if we call JSObjectGetPrivate, it can segfault + if (js_type == .Cell) { + return .{ + .tag = .NativeCode, + .cell = js_type, + }; + } + + if (js_type == .DOMWrapper) { + return .{ + .tag = .Private, + .cell = js_type, + }; + } + + if (CAPI.JSObjectGetPrivate(value.asObjectRef()) != null) + return .{ + .tag = .Private, + .cell = js_type, + }; + + // 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 (callable and js_type == .JSFunction) { + return .{ + .tag = .Function, + .cell = js_type, + }; + } else if (callable and js_type == .InternalFunction) { + return .{ + .tag = .Object, + .cell = js_type, + }; + } + + if (js_type == .PureForwardingProxy) { + return Tag.get( + JSC.JSValue.c(JSC.C.JSObjectGetProxyTarget(value.asObjectRef())), + globalThis, + ); + } + + // Is this a react element? + if (js_type.isObject()) { + if (value.get(globalThis, "$$typeof")) |typeof_symbol| { + var reactElement = ZigString.init("react.element"); + var react_fragment = ZigString.init("react.fragment"); + + if (JSValue.isSameValue(typeof_symbol, JSValue.symbolFor(globalThis, &reactElement), globalThis) or JSValue.isSameValue(typeof_symbol, JSValue.symbolFor(globalThis, &react_fragment), globalThis)) { + return .{ .tag = .JSX, .cell = js_type }; + } + } + } + + return .{ + .tag = switch (js_type) { + JSValue.JSType.ErrorInstance => .Error, + JSValue.JSType.NumberObject => .Double, + JSValue.JSType.DerivedArray, JSValue.JSType.Array => .Array, + JSValue.JSType.DerivedStringObject, JSValue.JSType.String, JSValue.JSType.StringObject => .String, + JSValue.JSType.RegExpObject => .String, + JSValue.JSType.Symbol => .Symbol, + JSValue.JSType.BooleanObject => .Boolean, + JSValue.JSType.JSFunction => .Function, + JSValue.JSType.JSWeakMap, JSValue.JSType.JSMap => .Map, + JSValue.JSType.JSWeakSet, JSValue.JSType.JSSet => .Set, + JSValue.JSType.JSDate => .JSON, + JSValue.JSType.JSPromise => .Promise, + JSValue.JSType.Object, + JSValue.JSType.FinalObject, + .ModuleNamespaceObject, + .GlobalObject, + => .Object, + + .ArrayBuffer, + JSValue.JSType.Int8Array, + JSValue.JSType.Uint8Array, + JSValue.JSType.Uint8ClampedArray, + JSValue.JSType.Int16Array, + JSValue.JSType.Uint16Array, + JSValue.JSType.Int32Array, + JSValue.JSType.Uint32Array, + JSValue.JSType.Float32Array, + JSValue.JSType.Float64Array, + JSValue.JSType.BigInt64Array, + JSValue.JSType.BigUint64Array, + .DataView, + => .TypedArray, + + .HeapBigInt => .BigInt, + + // None of these should ever exist here + // But we're going to check anyway + .GetterSetter, + .CustomGetterSetter, + .APIValueWrapper, + .NativeExecutable, + .ProgramExecutable, + .ModuleProgramExecutable, + .EvalExecutable, + .FunctionExecutable, + .UnlinkedFunctionExecutable, + .UnlinkedProgramCodeBlock, + .UnlinkedModuleProgramCodeBlock, + .UnlinkedEvalCodeBlock, + .UnlinkedFunctionCodeBlock, + .CodeBlock, + .JSImmutableButterfly, + .JSSourceCode, + .JSScriptFetcher, + .JSScriptFetchParameters, + .JSCallee, + .GlobalLexicalEnvironment, + .LexicalEnvironment, + .ModuleEnvironment, + .StrictEvalActivation, + .WithScope, + => .NativeCode, + + .Event => .Event, + + else => .JSON, + }, + .cell = js_type, + }; + } + }; + + const CellType = CAPI.CellType; + threadlocal var name_buf: [512]u8 = undefined; + + fn writeWithFormatting( + this: *JestPrettyFormat.Formatter, + comptime Writer: type, + writer_: Writer, + comptime Slice: type, + slice_: Slice, + globalThis: *JSGlobalObject, + comptime enable_ansi_colors: bool, + ) void { + var writer = WrappedWriter(Writer){ .ctx = writer_ }; + var slice = slice_; + var i: u32 = 0; + var len: u32 = @truncate(u32, slice.len); + var any_non_ascii = false; + while (i < len) : (i += 1) { + switch (slice[i]) { + '%' => { + i += 1; + if (i >= len) + break; + + const token = switch (slice[i]) { + 's' => Tag.String, + 'f' => Tag.Double, + 'o' => Tag.Undefined, + 'O' => Tag.Object, + 'd', 'i' => Tag.Integer, + else => continue, + }; + + // Flush everything up to the % + const end = slice[0 .. i - 1]; + if (!any_non_ascii) + writer.writeAll(end) + else + writer.writeAll(end); + any_non_ascii = false; + slice = slice[@min(slice.len, i + 1)..]; + i = 0; + len = @truncate(u32, slice.len); + const next_value = this.remaining_values[0]; + this.remaining_values = this.remaining_values[1..]; + switch (token) { + Tag.String => this.printAs(Tag.String, Writer, writer_, next_value, next_value.jsType(), enable_ansi_colors), + Tag.Double => this.printAs(Tag.Double, Writer, writer_, next_value, next_value.jsType(), enable_ansi_colors), + Tag.Object => this.printAs(Tag.Object, Writer, writer_, next_value, next_value.jsType(), enable_ansi_colors), + Tag.Integer => this.printAs(Tag.Integer, Writer, writer_, next_value, next_value.jsType(), enable_ansi_colors), + + // undefined is overloaded to mean the '%o" field + Tag.Undefined => this.format(Tag.get(next_value, globalThis), Writer, writer_, next_value, globalThis, enable_ansi_colors), + + else => unreachable, + } + if (this.remaining_values.len == 0) break; + }, + '\\' => { + i += 1; + if (i >= len) + break; + if (slice[i] == '%') i += 2; + }, + 128...255 => { + any_non_ascii = true; + }, + else => {}, + } + } + + if (slice.len > 0) writer.writeAll(slice); + } + + pub fn WrappedWriter(comptime Writer: type) type { + return struct { + ctx: Writer, + failed: bool = false, + estimated_line_length: *usize = undefined, + + pub fn print(self: *@This(), comptime fmt: string, args: anytype) void { + self.ctx.print(fmt, args) catch { + self.failed = true; + }; + } + + pub fn writeLatin1(self: *@This(), buf: []const u8) void { + var remain = buf; + while (remain.len > 0) { + if (strings.firstNonASCII(remain)) |i| { + if (i > 0) { + self.ctx.writeAll(remain[0..i]) catch { + self.failed = true; + return; + }; + } + self.ctx.writeAll(&strings.latin1ToCodepointBytesAssumeNotASCII(remain[i])) catch { + self.failed = true; + }; + remain = remain[i + 1 ..]; + } else { + break; + } + } + + self.ctx.writeAll(remain) catch return; + } + + pub inline fn writeAll(self: *@This(), buf: []const u8) void { + self.ctx.writeAll(buf) catch { + self.failed = true; + }; + } + + pub inline fn writeString(self: *@This(), str: ZigString) void { + self.print("{}", .{str}); + } + + pub inline fn write16Bit(self: *@This(), input: []const u16) void { + strings.formatUTF16Type([]const u16, input, self.ctx) catch { + self.failed = true; + }; + } + }; + } + + pub fn writeIndent( + this: *JestPrettyFormat.Formatter, + comptime Writer: type, + writer: Writer, + ) !void { + const indent = @min(this.indent, 32); + var buf = [_]u8{' '} ** 64; + var total_remain: usize = indent; + while (total_remain > 0) { + const written = @min(32, total_remain); + try writer.writeAll(buf[0 .. written * 2]); + total_remain -|= written; + } + } + + pub fn printComma(this: *JestPrettyFormat.Formatter, comptime Writer: type, writer: Writer, comptime enable_ansi_colors: bool) !void { + try writer.writeAll(comptime Output.prettyFmt("<r><d>,<r>", enable_ansi_colors)); + this.estimated_line_length += 1; + } + + pub fn MapIterator(comptime Writer: type, comptime enable_ansi_colors: bool) type { + return struct { + formatter: *JestPrettyFormat.Formatter, + writer: Writer, + pub fn forEach(_: [*c]JSC.VM, globalObject: [*c]JSGlobalObject, ctx: ?*anyopaque, nextValue: JSValue) callconv(.C) void { + var this: *@This() = bun.cast(*@This(), ctx orelse return); + const key = JSC.JSObject.getIndex(nextValue, globalObject, 0); + const value = JSC.JSObject.getIndex(nextValue, globalObject, 1); + this.formatter.writeIndent(Writer, this.writer) catch unreachable; + const key_tag = Tag.get(key, globalObject); + + this.formatter.format( + key_tag, + Writer, + this.writer, + key, + this.formatter.globalThis, + enable_ansi_colors, + ); + this.writer.writeAll(" => ") catch unreachable; + const value_tag = Tag.get(value, globalObject); + this.formatter.format( + value_tag, + Writer, + this.writer, + value, + this.formatter.globalThis, + enable_ansi_colors, + ); + this.formatter.printComma(Writer, this.writer, enable_ansi_colors) catch unreachable; + this.writer.writeAll("\n") catch unreachable; + } + }; + } + + pub fn SetIterator(comptime Writer: type, comptime enable_ansi_colors: bool) type { + return struct { + formatter: *JestPrettyFormat.Formatter, + writer: Writer, + pub fn forEach(_: [*c]JSC.VM, globalObject: [*c]JSGlobalObject, ctx: ?*anyopaque, nextValue: JSValue) callconv(.C) void { + var this: *@This() = bun.cast(*@This(), ctx orelse return); + this.formatter.writeIndent(Writer, this.writer) catch {}; + const key_tag = Tag.get(nextValue, globalObject); + this.formatter.format( + key_tag, + Writer, + this.writer, + nextValue, + this.formatter.globalThis, + enable_ansi_colors, + ); + + this.formatter.printComma(Writer, this.writer, enable_ansi_colors) catch unreachable; + this.writer.writeAll("\n") catch unreachable; + } + }; + } + + pub fn PropertyIterator(comptime Writer: type, comptime enable_ansi_colors_: bool) type { + return struct { + formatter: *JestPrettyFormat.Formatter, + writer: Writer, + i: usize = 0, + always_newline: bool = false, + 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)) { + var writer = WrappedWriter(Writer){ + .ctx = this.writer, + .failed = false, + }; + var name_str = ZigString.init(""); + + value.getNameProperty(globalThis, &name_str); + if (name_str.len > 0 and !strings.eqlComptime(name_str.slice(), "Object")) { + writer.print("{} ", .{name_str}); + } else { + value.getPrototype(globalThis).getNameProperty(globalThis, &name_str); + if (name_str.len > 0 and !strings.eqlComptime(name_str.slice(), "Object")) { + writer.print("{} ", .{name_str}); + } + } + } + + this.always_newline = true; + this.formatter.estimated_line_length = this.formatter.indent * 2 + 1; + + if (this.formatter.indent == 0) this.writer.writeAll("\n") catch {}; + var classname = ZigString.Empty; + value.getClassName(globalThis, &classname); + if (!strings.eqlComptime(classname.slice(), "Object")) { + this.writer.print("{} ", .{classname}) catch {}; + } + + this.writer.writeAll("{\n") catch {}; + this.formatter.indent += 1; + this.formatter.writeIndent(Writer, this.writer) catch {}; + } + + pub fn forEach( + globalThis: *JSGlobalObject, + ctx_ptr: ?*anyopaque, + key_: [*c]ZigString, + value: JSValue, + is_symbol: bool, + ) callconv(.C) void { + const key = key_.?[0]; + if (key.eqlComptime("constructor")) return; + if (key.eqlComptime("call")) return; + + var ctx: *@This() = bun.cast(*@This(), ctx_ptr orelse return); + var this = ctx.formatter; + var writer_ = ctx.writer; + var writer = WrappedWriter(Writer){ + .ctx = writer_, + .failed = false, + }; + + const tag = Tag.get(value, globalThis); + + if (tag.cell.isHidden()) return; + if (ctx.i == 0) { + handleFirstProperty(ctx, globalThis, ctx.parent); + } else { + this.printComma(Writer, writer_, enable_ansi_colors) catch unreachable; + } + + defer ctx.i += 1; + if (ctx.i > 0) { + if (ctx.always_newline or this.always_newline_scope or this.goodTimeForANewLine()) { + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch {}; + this.resetLine(); + } else { + this.estimated_line_length += 1; + writer.writeAll(" "); + } + } + + if (!is_symbol) { + + // TODO: make this one pass? + if (!key.is16Bit() and JSLexer.isLatin1Identifier(@TypeOf(key.slice()), key.slice())) { + this.addForNewLine(key.len + 1); + + writer.print( + comptime Output.prettyFmt("\"{}\"<d>:<r> ", enable_ansi_colors), + .{key}, + ); + } else if (key.is16Bit() and JSLexer.isLatin1Identifier(@TypeOf(key.utf16SliceAligned()), key.utf16SliceAligned())) { + this.addForNewLine(key.len + 1); + + writer.print( + comptime Output.prettyFmt("\"{}\"<d>:<r> ", enable_ansi_colors), + .{key}, + ); + } else if (key.is16Bit()) { + var utf16Slice = key.utf16SliceAligned(); + + this.addForNewLine(utf16Slice.len + 2); + + if (comptime enable_ansi_colors) { + writer.writeAll(comptime Output.prettyFmt("<r><green>", true)); + } + + writer.writeAll("'"); + + while (strings.indexOfAny16(utf16Slice, "\"")) |j| { + writer.write16Bit(utf16Slice[0..j]); + writer.writeAll("\""); + utf16Slice = utf16Slice[j + 1 ..]; + } + + writer.write16Bit(utf16Slice); + + writer.print( + comptime Output.prettyFmt("\"<r><d>:<r> ", enable_ansi_colors), + .{}, + ); + } else { + this.addForNewLine(key.len + 1); + + writer.print( + comptime Output.prettyFmt("{s}<d>:<r> ", enable_ansi_colors), + .{JSPrinter.formatJSONString(key.slice())}, + ); + } + } else { + this.addForNewLine(1 + "[Symbol()]:".len + key.len); + writer.print( + comptime Output.prettyFmt("<r><d>[<r><blue>Symbol({any})<r><d>]:<r> ", enable_ansi_colors), + .{ + key, + }, + ); + } + + if (tag.cell.isStringLike()) { + if (comptime enable_ansi_colors) { + writer.writeAll(comptime Output.prettyFmt("<r><green>", true)); + } + } + + this.format(tag, Writer, ctx.writer, value, globalThis, enable_ansi_colors); + + if (tag.cell.isStringLike()) { + if (comptime enable_ansi_colors) { + writer.writeAll(comptime Output.prettyFmt("<r>", true)); + } + } + } + }; + } + + pub fn printAs( + this: *JestPrettyFormat.Formatter, + comptime Format: JestPrettyFormat.Formatter.Tag, + comptime Writer: type, + writer_: Writer, + value: JSValue, + jsType: JSValue.JSType, + comptime enable_ansi_colors: bool, + ) void { + if (this.failed) + return; + var writer = WrappedWriter(Writer){ .ctx = writer_, .estimated_line_length = &this.estimated_line_length }; + defer { + if (writer.failed) { + this.failed = true; + } + } + if (comptime Format.canHaveCircularReferences()) { + if (this.map_node == null) { + this.map_node = Visited.Pool.get(default_allocator); + this.map_node.?.data.clearRetainingCapacity(); + this.map = this.map_node.?.data; + } + + var entry = this.map.getOrPut(@enumToInt(value)) catch unreachable; + if (entry.found_existing) { + writer.writeAll(comptime Output.prettyFmt("<r><cyan>[Circular]<r>", enable_ansi_colors)); + return; + } + } + + defer { + if (comptime Format.canHaveCircularReferences()) { + _ = this.map.remove(@enumToInt(value)); + } + } + + switch (comptime Format) { + .StringPossiblyFormatted => { + var str = value.toSlice(this.globalThis, bun.default_allocator); + defer str.deinit(); + this.addForNewLine(str.len); + const slice = str.slice(); + this.writeWithFormatting(Writer, writer_, @TypeOf(slice), slice, this.globalThis, enable_ansi_colors); + }, + .String => { + var str = ZigString.init(""); + value.toZigString(&str, this.globalThis); + this.addForNewLine(str.len); + + if (value.jsType() == .StringObject or value.jsType() == .DerivedStringObject) { + if (str.len == 0) { + writer.writeAll("String {}"); + return; + } + if (this.indent == 0 and str.len > 0) { + writer.writeAll("\n"); + } + writer.writeAll("String {\n"); + this.indent += 1; + defer this.indent -|= 1; + this.resetLine(); + this.writeIndent(Writer, writer_) catch unreachable; + const length = str.len; + for (str.slice(), 0..) |c, i| { + writer.print("\"{d}\": \"{c}\",\n", .{ i, c }); + if (i != length - 1) this.writeIndent(Writer, writer_) catch unreachable; + } + this.resetLine(); + writer.writeAll("}\n"); + return; + } + + if (this.quote_strings and jsType != .RegExpObject) { + if (str.len == 0) { + writer.writeAll("\"\""); + return; + } + + if (comptime enable_ansi_colors) { + writer.writeAll(Output.prettyFmt("<r><green>", true)); + } + + defer if (comptime enable_ansi_colors) + writer.writeAll(Output.prettyFmt("<r>", true)); + + if (str.is16Bit()) { + this.printAs(.JSON, Writer, writer_, value, .StringObject, enable_ansi_colors); + return; + } + + var has_newline = false; + if (strings.indexOfAny(str.slice(), "\n\r")) |_| { + has_newline = true; + writer.writeAll("\n"); + } + + writer.writeAll("\""); + var remaining = str.slice(); + while (strings.indexOfAny(remaining, "\\\r")) |i| { + switch (remaining[i]) { + '\\' => { + writer.print("{s}\\", .{remaining[0 .. i + 1]}); + remaining = remaining[i + 1 ..]; + }, + '\r' => { + if (i + 1 < remaining.len and remaining[i + 1] == '\n') { + writer.print("{s}", .{remaining[0..i]}); + } else { + writer.print("{s}\n", .{remaining[0..i]}); + } + remaining = remaining[i + 1 ..]; + }, + else => unreachable, + } + } + + writer.writeAll(remaining); + writer.writeAll("\""); + if (has_newline) writer.writeAll("\n"); + return; + } + + if (jsType == .RegExpObject and enable_ansi_colors) { + writer.print(comptime Output.prettyFmt("<r><red>", enable_ansi_colors), .{}); + } + + if (str.is16Bit()) { + // streaming print + writer.print("{s}", .{str}); + } else if (strings.isAllASCII(str.slice())) { + // fast path + writer.writeAll(str.slice()); + } else if (str.len > 0) { + // slow path + var buf = strings.allocateLatin1IntoUTF8(bun.default_allocator, []const u8, str.slice()) catch &[_]u8{}; + if (buf.len > 0) { + defer bun.default_allocator.free(buf); + writer.writeAll(buf); + } + } + + if (jsType == .RegExpObject and enable_ansi_colors) { + writer.print(comptime Output.prettyFmt("<r>", enable_ansi_colors), .{}); + } + }, + .Integer => { + const int = value.toInt64(); + if (int < std.math.maxInt(u32)) { + var i = int; + const is_negative = i < 0; + if (is_negative) { + i = -i; + } + const digits = if (i != 0) + bun.fmt.fastDigitCount(@intCast(usize, i)) + @as(usize, @boolToInt(is_negative)) + else + 1; + this.addForNewLine(digits); + } else { + this.addForNewLine(bun.fmt.count("{d}", .{int})); + } + writer.print(comptime Output.prettyFmt("<r><yellow>{d}<r>", enable_ansi_colors), .{int}); + }, + .BigInt => { + var out_str = value.getZigString(this.globalThis).slice(); + this.addForNewLine(out_str.len); + + writer.print(comptime Output.prettyFmt("<r><yellow>{s}n<r>", enable_ansi_colors), .{out_str}); + }, + .Double => { + if (value.isCell()) { + this.printAs(.Object, Writer, writer_, value, .Object, enable_ansi_colors); + return; + } + + const num = value.asNumber(); + + if (std.math.isPositiveInf(num)) { + this.addForNewLine("Infinity".len); + writer.print(comptime Output.prettyFmt("<r><yellow>Infinity<r>", enable_ansi_colors), .{}); + } else if (std.math.isNegativeInf(num)) { + this.addForNewLine("-Infinity".len); + writer.print(comptime Output.prettyFmt("<r><yellow>-Infinity<r>", enable_ansi_colors), .{}); + } else if (std.math.isNan(num)) { + this.addForNewLine("NaN".len); + writer.print(comptime Output.prettyFmt("<r><yellow>NaN<r>", enable_ansi_colors), .{}); + } else { + this.addForNewLine(std.fmt.count("{d}", .{num})); + writer.print(comptime Output.prettyFmt("<r><yellow>{d}<r>", enable_ansi_colors), .{num}); + } + }, + .Undefined => { + this.addForNewLine(9); + writer.print(comptime Output.prettyFmt("<r><d>undefined<r>", enable_ansi_colors), .{}); + }, + .Null => { + this.addForNewLine(4); + writer.print(comptime Output.prettyFmt("<r><yellow>null<r>", enable_ansi_colors), .{}); + }, + .Symbol => { + const description = value.getDescription(this.globalThis); + this.addForNewLine("Symbol".len); + + if (description.len > 0) { + this.addForNewLine(description.len + "()".len); + writer.print(comptime Output.prettyFmt("<r><blue>Symbol({any})<r>", enable_ansi_colors), .{description}); + } else { + writer.print(comptime Output.prettyFmt("<r><blue>Symbol<r>", enable_ansi_colors), .{}); + } + }, + .Error => { + var classname = ZigString.Empty; + value.getClassName(this.globalThis, &classname); + var message_string = ZigString.Empty; + if (value.get(this.globalThis, "message")) |message_prop| { + message_prop.toZigString(&message_string, this.globalThis); + } + if (message_string.len == 0) { + writer.print("[{s}]", .{classname}); + return; + } + writer.print("[{s}: {s}]", .{ classname, message_string }); + return; + }, + .Class => { + var printable = ZigString.init(&name_buf); + value.getClassName(this.globalThis, &printable); + this.addForNewLine(printable.len); + + if (printable.len == 0) { + writer.print(comptime Output.prettyFmt("[class]", enable_ansi_colors), .{}); + } else { + writer.print(comptime Output.prettyFmt("[class <cyan>{}<r>]", enable_ansi_colors), .{printable}); + } + }, + .Function => { + writer.writeAll("[Function]"); + }, + .Array => { + const len = @truncate(u32, value.getLengthOfArray(this.globalThis)); + if (len == 0) { + writer.writeAll("[]"); + this.addForNewLine(2); + return; + } + + if (this.indent == 0) { + writer.writeAll("\n"); + } + + var was_good_time = this.always_newline_scope; + { + this.indent += 1; + defer this.indent -|= 1; + + this.addForNewLine(2); + + var ref = value.asObjectRef(); + + var prev_quote_strings = this.quote_strings; + this.quote_strings = true; + defer this.quote_strings = prev_quote_strings; + + { + const element = JSValue.fromRef(CAPI.JSObjectGetPropertyAtIndex(this.globalThis, ref, 0, null)); + const tag = Tag.get(element, this.globalThis); + + was_good_time = was_good_time or !tag.tag.isPrimitive() or this.goodTimeForANewLine(); + + this.resetLine(); + writer.writeAll("["); + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch unreachable; + this.addForNewLine(1); + + this.format(tag, Writer, writer_, element, this.globalThis, enable_ansi_colors); + + if (tag.cell.isStringLike()) { + if (comptime enable_ansi_colors) { + writer.writeAll(comptime Output.prettyFmt("<r>", true)); + } + } + + if (len == 1) { + this.printComma(Writer, writer_, enable_ansi_colors) catch unreachable; + } + } + + var i: u32 = 1; + while (i < len) : (i += 1) { + this.printComma(Writer, writer_, enable_ansi_colors) catch unreachable; + + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch unreachable; + + const element = JSValue.fromRef(CAPI.JSObjectGetPropertyAtIndex(this.globalThis, ref, i, null)); + const tag = Tag.get(element, this.globalThis); + + this.format(tag, Writer, writer_, element, this.globalThis, enable_ansi_colors); + + if (tag.cell.isStringLike()) { + if (comptime enable_ansi_colors) { + writer.writeAll(comptime Output.prettyFmt("<r>", true)); + } + } + + if (i == len - 1) { + this.printComma(Writer, writer_, enable_ansi_colors) catch unreachable; + } + } + } + + this.resetLine(); + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch {}; + writer.writeAll("]"); + if (this.indent == 0) { + writer.writeAll("\n"); + } + this.resetLine(); + this.addForNewLine(1); + }, + .Private => { + if (value.as(JSC.WebCore.Response)) |response| { + response.writeFormat(Formatter, this, writer_, enable_ansi_colors) catch {}; + return; + } else if (value.as(JSC.WebCore.Request)) |request| { + request.writeFormat(Formatter, this, writer_, enable_ansi_colors) catch {}; + return; + } else if (value.as(JSC.WebCore.Blob)) |blob| { + blob.writeFormat(Formatter, this, writer_, enable_ansi_colors) catch {}; + return; + } else if (value.as(JSC.DOMFormData) != null) { + const toJSONFunction = value.get(this.globalThis, "toJSON").?; + + this.addForNewLine("FormData (entries) ".len); + writer.writeAll(comptime Output.prettyFmt("<r><blue>FormData<r> <d>(entries)<r> ", enable_ansi_colors)); + + return this.printAs( + .Object, + Writer, + writer_, + toJSONFunction.callWithThis(this.globalThis, value, &.{}), + .Object, + enable_ansi_colors, + ); + } else if (value.as(JSC.API.Bun.Timer.TimerObject)) |timer| { + this.addForNewLine("Timeout(# ) ".len + bun.fmt.fastDigitCount(@intCast(u64, @max(timer.id, 0)))); + if (timer.kind == .setInterval) { + this.addForNewLine("repeats ".len + bun.fmt.fastDigitCount(@intCast(u64, @max(timer.id, 0)))); + writer.print(comptime Output.prettyFmt("<r><blue>Timeout<r> <d>(#<yellow>{d}<r><d>, repeats)<r>", enable_ansi_colors), .{ + timer.id, + }); + } else { + writer.print(comptime Output.prettyFmt("<r><blue>Timeout<r> <d>(#<yellow>{d}<r><d>)<r>", enable_ansi_colors), .{ + timer.id, + }); + } + + return; + } else if (jsType != .DOMWrapper) { + if (CAPI.JSObjectGetPrivate(value.asRef())) |private_data_ptr| { + const priv_data = JSPrivateDataPtr.from(private_data_ptr); + switch (priv_data.tag()) { + .BuildError => { + const build_error = priv_data.as(JS.BuildError); + build_error.msg.writeFormat(writer_, enable_ansi_colors) catch {}; + return; + }, + .ResolveError => { + const resolve_error = priv_data.as(JS.ResolveError); + resolve_error.msg.writeFormat(writer_, enable_ansi_colors) catch {}; + return; + }, + else => {}, + } + } + + if (value.isCallable(this.globalThis.vm())) { + return this.printAs(.Function, Writer, writer_, value, jsType, enable_ansi_colors); + } + + return this.printAs(.Object, Writer, writer_, value, jsType, enable_ansi_colors); + } + return this.printAs(.Object, Writer, writer_, value, .Event, enable_ansi_colors); + }, + .NativeCode => { + this.addForNewLine("[native code]".len); + writer.writeAll("[native code]"); + }, + .Promise => { + if (this.goodTimeForANewLine()) { + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch {}; + } + writer.writeAll("Promise {}"); + }, + .Boolean => { + if (value.isCell()) { + this.printAs(.Object, Writer, writer_, value, .Object, enable_ansi_colors); + return; + } + if (value.toBoolean()) { + this.addForNewLine(4); + writer.writeAll(comptime Output.prettyFmt("<r><yellow>true<r>", enable_ansi_colors)); + } else { + this.addForNewLine(5); + writer.writeAll(comptime Output.prettyFmt("<r><yellow>false<r>", enable_ansi_colors)); + } + }, + .GlobalObject => { + const fmt = "[this.globalThis]"; + this.addForNewLine(fmt.len); + writer.writeAll(comptime Output.prettyFmt("<cyan>" ++ fmt ++ "<r>", enable_ansi_colors)); + }, + .Map => { + const length_value = value.get(this.globalThis, "size") orelse JSC.JSValue.jsNumberFromInt32(0); + const length = length_value.toInt32(); + + const prev_quote_strings = this.quote_strings; + this.quote_strings = true; + defer this.quote_strings = prev_quote_strings; + + const map_name = if (value.jsType() == .JSWeakMap) "WeakMap" else "Map"; + + if (length == 0) { + return writer.print("{s} {{}}", .{map_name}); + } + + writer.print("\n{s} {{\n", .{map_name}); + { + this.indent += 1; + defer this.indent -|= 1; + var iter = MapIterator(Writer, enable_ansi_colors){ + .formatter = this, + .writer = writer_, + }; + value.forEach(this.globalThis, &iter, @TypeOf(iter).forEach); + } + this.writeIndent(Writer, writer_) catch {}; + writer.writeAll("}"); + writer.writeAll("\n"); + }, + .Set => { + const length_value = value.get(this.globalThis, "size") orelse JSC.JSValue.jsNumberFromInt32(0); + const length = length_value.toInt32(); + + const prev_quote_strings = this.quote_strings; + this.quote_strings = true; + defer this.quote_strings = prev_quote_strings; + + this.writeIndent(Writer, writer_) catch {}; + + const set_name = if (value.jsType() == .JSWeakSet) "WeakSet" else "Set"; + + if (length == 0) { + return writer.print("{s} {{}}", .{set_name}); + } + + writer.print("\n{s} {{\n", .{set_name}); + { + this.indent += 1; + defer this.indent -|= 1; + var iter = SetIterator(Writer, enable_ansi_colors){ + .formatter = this, + .writer = writer_, + }; + value.forEach(this.globalThis, &iter, @TypeOf(iter).forEach); + } + this.writeIndent(Writer, writer_) catch {}; + writer.writeAll("}"); + writer.writeAll("\n"); + }, + .JSON => { + var str = ZigString.init(""); + value.jsonStringify(this.globalThis, this.indent, &str); + this.addForNewLine(str.len); + if (jsType == JSValue.JSType.JSDate) { + // in the code for printing dates, it never exceeds this amount + var iso_string_buf: [36]u8 = undefined; + var out_buf: []const u8 = std.fmt.bufPrint(&iso_string_buf, "{}", .{str}) catch ""; + if (out_buf.len > 2) { + // trim the quotes + out_buf = out_buf[1 .. out_buf.len - 1]; + } + + writer.print(comptime Output.prettyFmt("<r><magenta>{s}<r>", enable_ansi_colors), .{out_buf}); + return; + } + + writer.print("{}", .{str}); + }, + .Event => { + const event_type = EventType.map.getWithEql(value.get(this.globalThis, "type").?.getZigString(this.globalThis), ZigString.eqlComptime) orelse EventType.unknown; + if (event_type != .MessageEvent and event_type != .ErrorEvent) { + return this.printAs(.Object, Writer, writer_, value, .Event, enable_ansi_colors); + } + + writer.print( + comptime Output.prettyFmt("<r><cyan>{s}<r> {{\n", enable_ansi_colors), + .{ + @tagName(event_type), + }, + ); + { + this.indent += 1; + defer this.indent -|= 1; + const old_quote_strings = this.quote_strings; + this.quote_strings = true; + defer this.quote_strings = old_quote_strings; + this.writeIndent(Writer, writer_) catch unreachable; + + writer.print( + comptime Output.prettyFmt("<r>type: <green>\"{s}\"<r><d>,<r>\n", enable_ansi_colors), + .{ + event_type.label(), + }, + ); + this.writeIndent(Writer, writer_) catch unreachable; + + switch (event_type) { + .MessageEvent => { + writer.print( + comptime Output.prettyFmt("<r><blue>data<d>:<r> ", enable_ansi_colors), + .{}, + ); + const data = value.get(this.globalThis, "data").?; + const tag = Tag.get(data, this.globalThis); + if (tag.cell.isStringLike()) { + this.format(tag, Writer, writer_, data, this.globalThis, enable_ansi_colors); + } else { + this.format(tag, Writer, writer_, data, this.globalThis, enable_ansi_colors); + } + }, + .ErrorEvent => { + writer.print( + comptime Output.prettyFmt("<r><blue>error<d>:<r>\n", enable_ansi_colors), + .{}, + ); + + const data = value.get(this.globalThis, "error").?; + const tag = Tag.get(data, this.globalThis); + this.format(tag, Writer, writer_, data, this.globalThis, enable_ansi_colors); + }, + else => unreachable, + } + writer.writeAll("\n"); + } + + this.writeIndent(Writer, writer_) catch unreachable; + writer.writeAll("}"); + }, + .JSX => { + writer.writeAll(comptime Output.prettyFmt("<r>", enable_ansi_colors)); + + writer.writeAll("<"); + + var needs_space = false; + var tag_name_str = ZigString.init(""); + + var tag_name_slice: ZigString.Slice = ZigString.Slice.empty; + var is_tag_kind_primitive = false; + + defer if (tag_name_slice.isAllocated()) tag_name_slice.deinit(); + + if (value.get(this.globalThis, "type")) |type_value| { + const _tag = Tag.get(type_value, this.globalThis); + + if (_tag.cell == .Symbol) {} else if (_tag.cell.isStringLike()) { + type_value.toZigString(&tag_name_str, this.globalThis); + is_tag_kind_primitive = true; + } else if (_tag.cell.isObject() or type_value.isCallable(this.globalThis.vm())) { + type_value.getNameProperty(this.globalThis, &tag_name_str); + if (tag_name_str.len == 0) { + tag_name_str = ZigString.init("NoName"); + } + } else { + type_value.toZigString(&tag_name_str, this.globalThis); + } + + tag_name_slice = tag_name_str.toSlice(default_allocator); + needs_space = true; + } else { + tag_name_slice = ZigString.init("unknown").toSlice(default_allocator); + + needs_space = true; + } + + if (!is_tag_kind_primitive) + writer.writeAll(comptime Output.prettyFmt("<cyan>", enable_ansi_colors)) + else + writer.writeAll(comptime Output.prettyFmt("<green>", enable_ansi_colors)); + writer.writeAll(tag_name_slice.slice()); + if (enable_ansi_colors) writer.writeAll(comptime Output.prettyFmt("<r>", enable_ansi_colors)); + + if (value.get(this.globalThis, "key")) |key_value| { + if (!key_value.isUndefinedOrNull()) { + if (needs_space) + writer.writeAll(" key=") + else + writer.writeAll("key="); + + const old_quote_strings = this.quote_strings; + this.quote_strings = true; + defer this.quote_strings = old_quote_strings; + + this.format(Tag.get(key_value, this.globalThis), Writer, writer_, key_value, this.globalThis, enable_ansi_colors); + + needs_space = true; + } + } + + if (value.get(this.globalThis, "props")) |props| { + const prev_quote_strings = this.quote_strings; + this.quote_strings = true; + defer this.quote_strings = prev_quote_strings; + + var props_iter = JSC.JSPropertyIterator(.{ + .skip_empty_name = true, + + .include_value = true, + }).init(this.globalThis, props.asObjectRef()); + defer props_iter.deinit(); + + var children_prop = props.get(this.globalThis, "children"); + if (props_iter.len > 0) { + { + this.indent += 1; + defer this.indent -|= 1; + const count_without_children = props_iter.len - @as(usize, @boolToInt(children_prop != null)); + + while (props_iter.next()) |prop| { + if (prop.eqlComptime("children")) + continue; + + var property_value = props_iter.value; + const tag = Tag.get(property_value, this.globalThis); + + if (tag.cell.isHidden()) continue; + + if (needs_space) writer.writeAll(" "); + needs_space = false; + + writer.print( + comptime Output.prettyFmt("<r><blue>{s}<d>=<r>", enable_ansi_colors), + .{prop.trunc(128)}, + ); + + if (tag.cell.isStringLike()) { + if (comptime enable_ansi_colors) { + writer.writeAll(comptime Output.prettyFmt("<r><green>", true)); + } + } + + this.format(tag, Writer, writer_, property_value, this.globalThis, enable_ansi_colors); + + if (tag.cell.isStringLike()) { + if (comptime enable_ansi_colors) { + writer.writeAll(comptime Output.prettyFmt("<r>", true)); + } + } + + if ( + // count_without_children is necessary to prevent printing an extra newline + // if there are children and one prop and the child prop is the last prop + props_iter.i + 1 < count_without_children and + // 3 is arbitrary but basically + // <input type="text" value="foo" /> + // ^ should be one line + // <input type="text" value="foo" bar="true" baz={false} /> + // ^ should be multiple lines + props_iter.i > 3) + { + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch unreachable; + } else if (props_iter.i + 1 < count_without_children) { + writer.writeAll(" "); + } + } + } + + if (children_prop) |children| { + const tag = Tag.get(children, this.globalThis); + + const print_children = switch (tag.tag) { + .String, .JSX, .Array => true, + else => false, + }; + + if (print_children) { + print_children: { + switch (tag.tag) { + .String => { + var children_string = children.getZigString(this.globalThis); + if (children_string.len == 0) break :print_children; + if (comptime enable_ansi_colors) writer.writeAll(comptime Output.prettyFmt("<r>", true)); + + writer.writeAll(">"); + if (children_string.len < 128) { + writer.writeString(children_string); + } else { + this.indent += 1; + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch unreachable; + this.indent -|= 1; + writer.writeString(children_string); + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch unreachable; + } + }, + .JSX => { + writer.writeAll(">\n"); + + { + this.indent += 1; + this.writeIndent(Writer, writer_) catch unreachable; + defer this.indent -|= 1; + this.format(Tag.get(children, this.globalThis), Writer, writer_, children, this.globalThis, enable_ansi_colors); + } + + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch unreachable; + }, + .Array => { + const length = children.getLengthOfArray(this.globalThis); + if (length == 0) break :print_children; + writer.writeAll(">\n"); + + { + this.indent += 1; + this.writeIndent(Writer, writer_) catch unreachable; + const _prev_quote_strings = this.quote_strings; + this.quote_strings = false; + defer this.quote_strings = _prev_quote_strings; + + defer this.indent -|= 1; + + var j: usize = 0; + while (j < length) : (j += 1) { + const child = JSC.JSObject.getIndex(children, this.globalThis, @intCast(u32, j)); + this.format(Tag.get(child, this.globalThis), Writer, writer_, child, this.globalThis, enable_ansi_colors); + if (j + 1 < length) { + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch unreachable; + } + } + } + + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch unreachable; + }, + else => unreachable, + } + + writer.writeAll("</"); + if (!is_tag_kind_primitive) + writer.writeAll(comptime Output.prettyFmt("<r><cyan>", enable_ansi_colors)) + else + writer.writeAll(comptime Output.prettyFmt("<r><green>", enable_ansi_colors)); + writer.writeAll(tag_name_slice.slice()); + if (enable_ansi_colors) writer.writeAll(comptime Output.prettyFmt("<r>", enable_ansi_colors)); + writer.writeAll(">"); + } + + return; + } + } + } + } + + writer.writeAll(" />"); + }, + .Object => { + const prev_quote_strings = this.quote_strings; + this.quote_strings = true; + defer this.quote_strings = prev_quote_strings; + + const Iterator = PropertyIterator(Writer, enable_ansi_colors); + + // We want to figure out if we should print this object + // on one line or multiple lines + // + // The 100% correct way would be to print everything to + // a temporary buffer and then check how long each line was + // + // But it's important that console.log() is fast. So we + // do a small compromise to avoid multiple passes over input + // + // We say: + // + // If the object has at least 2 properties and ANY of the following conditions are met: + // - total length of all the property names is more than + // 14 characters + // - the parent object is printing each property on a new line + // - The first property is a DOM object, ESM namespace, Map, Set, or Blob + // + // Then, we print it each property on a new line, recursively. + // + const prev_always_newline_scope = this.always_newline_scope; + defer this.always_newline_scope = prev_always_newline_scope; + var iter = Iterator{ + .formatter = this, + .writer = writer_, + .always_newline = this.always_newline_scope or this.goodTimeForANewLine(), + .parent = value, + }; + + value.forEachPropertyOrdered(this.globalThis, &iter, Iterator.forEach); + + if (iter.i == 0) { + var object_name = ZigString.Empty; + value.getClassName(this.globalThis, &object_name); + + if (!strings.eqlComptime(object_name.slice(), "Object")) { + if (value.as(JSC.Jest.ExpectAny)) |_| { + var constructor = JSC.Jest.ExpectAny.constructorValueGetCached(value) orelse unreachable; + var constructor_name = ZigString.Empty; + constructor.getNameProperty(this.globalThis, &constructor_name); + writer.print("Any<{s}>", .{constructor_name}); + } else { + writer.print("{s} {{}}", .{object_name}); + } + } else { + // don't write "Object" + writer.writeAll("{}"); + } + } else { + this.printComma(Writer, writer_, enable_ansi_colors) catch unreachable; + + if (iter.always_newline) { + this.indent -|= 1; + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch {}; + writer.writeAll("}"); + this.estimated_line_length += 1; + } else { + this.estimated_line_length += 2; + writer.writeAll(" }"); + } + + if (this.indent == 0) { + writer.writeAll("\n"); + } + } + }, + .TypedArray => { + const arrayBuffer = value.asArrayBuffer(this.globalThis).?; + const slice = arrayBuffer.byteSlice(); + + if (this.indent == 0 and slice.len > 0) { + writer.writeAll("\n"); + } + + if (jsType == .Uint8Array) { + var buffer_name = ZigString.Empty; + value.getClassName(this.globalThis, &buffer_name); + if (strings.eqlComptime(buffer_name.slice(), "Buffer")) { + // special formatting for 'Buffer' snapshots only + if (slice.len == 0 and this.indent == 0) writer.writeAll("\n"); + writer.writeAll("{\n"); + this.indent += 1; + this.writeIndent(Writer, writer_) catch {}; + writer.writeAll("\"data\": ["); + + this.indent += 1; + for (slice) |el| { + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch {}; + writer.print("{d},", .{el}); + } + this.indent -|= 1; + + if (slice.len > 0) { + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch {}; + writer.writeAll("],\n"); + } else { + writer.writeAll("],\n"); + } + + this.writeIndent(Writer, writer_) catch {}; + writer.writeAll("\"type\": \"Buffer\",\n"); + + this.indent -|= 1; + this.writeIndent(Writer, writer_) catch {}; + writer.writeAll("}"); + + if (this.indent == 0) { + writer.writeAll("\n"); + } + + return; + } + writer.writeAll(bun.asByteSlice(@tagName(arrayBuffer.typed_array_type))); + } else { + writer.writeAll(bun.asByteSlice(@tagName(arrayBuffer.typed_array_type))); + } + + writer.writeAll(" ["); + + if (slice.len > 0) { + switch (jsType) { + .Int8Array => { + const slice_with_type = @alignCast(std.meta.alignment([]i8), std.mem.bytesAsSlice(i8, slice)); + this.indent += 1; + defer this.indent -|= 1; + for (slice_with_type) |el| { + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch {}; + writer.print("{d},", .{el}); + } + }, + .Int16Array => { + const slice_with_type = @alignCast(std.meta.alignment([]i16), std.mem.bytesAsSlice(i16, slice)); + this.indent += 1; + defer this.indent -|= 1; + for (slice_with_type) |el| { + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch {}; + writer.print("{d},", .{el}); + } + }, + .Uint16Array => { + const slice_with_type = @alignCast(std.meta.alignment([]u16), std.mem.bytesAsSlice(u16, slice)); + this.indent += 1; + defer this.indent -|= 1; + for (slice_with_type) |el| { + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch {}; + writer.print("{d},", .{el}); + } + }, + .Int32Array => { + const slice_with_type = @alignCast(std.meta.alignment([]i32), std.mem.bytesAsSlice(i32, slice)); + this.indent += 1; + defer this.indent -|= 1; + for (slice_with_type) |el| { + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch {}; + writer.print("{d},", .{el}); + } + }, + .Uint32Array => { + const slice_with_type = @alignCast(std.meta.alignment([]u32), std.mem.bytesAsSlice(u32, slice)); + this.indent += 1; + defer this.indent -|= 1; + for (slice_with_type) |el| { + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch {}; + writer.print("{d},", .{el}); + } + }, + .Float32Array => { + const slice_with_type = @alignCast(std.meta.alignment([]f32), std.mem.bytesAsSlice(f32, slice)); + this.indent += 1; + defer this.indent -|= 1; + for (slice_with_type) |el| { + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch {}; + writer.print("{d},", .{el}); + } + }, + .Float64Array => { + const slice_with_type = @alignCast(std.meta.alignment([]f64), std.mem.bytesAsSlice(f64, slice)); + this.indent += 1; + defer this.indent -|= 1; + for (slice_with_type) |el| { + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch {}; + writer.print("{d},", .{el}); + } + }, + .BigInt64Array => { + const slice_with_type = @alignCast(std.meta.alignment([]i64), std.mem.bytesAsSlice(i64, slice)); + this.indent += 1; + defer this.indent -|= 1; + for (slice_with_type) |el| { + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch {}; + writer.print("{d},", .{el}); + } + }, + .BigUint64Array => { + const slice_with_type = @alignCast(std.meta.alignment([]u64), std.mem.bytesAsSlice(u64, slice)); + this.indent += 1; + defer this.indent -|= 1; + for (slice_with_type) |el| { + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch {}; + writer.print("{d},", .{el}); + } + }, + + // Uint8Array, Uint8ClampedArray, DataView, ArrayBuffer + else => { + var slice_with_type = @alignCast(std.meta.alignment([]u8), std.mem.bytesAsSlice(u8, slice)); + this.indent += 1; + defer this.indent -|= 1; + for (slice_with_type) |el| { + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch {}; + writer.print("{d},", .{el}); + } + }, + } + } + + if (slice.len > 0) { + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch {}; + writer.writeAll("]"); + if (this.indent == 0) { + writer.writeAll("\n"); + } + } else { + writer.writeAll("]"); + } + }, + else => {}, + } + } + + pub fn format(this: *JestPrettyFormat.Formatter, result: Tag.Result, comptime Writer: type, writer: Writer, value: JSValue, globalThis: *JSGlobalObject, comptime enable_ansi_colors: bool) void { + if (comptime is_bindgen) { + return; + } + var prevGlobalThis = this.globalThis; + defer this.globalThis = prevGlobalThis; + this.globalThis = globalThis; + + // This looks incredibly redundant. We make the JestPrettyFormat.Formatter.Tag a + // comptime var so we have to repeat it here. The rationale there is + // it _should_ limit the stack usage because each version of the + // function will be relatively small + return switch (result.tag) { + .StringPossiblyFormatted => this.printAs(.StringPossiblyFormatted, Writer, writer, value, result.cell, enable_ansi_colors), + .String => this.printAs(.String, Writer, writer, value, result.cell, enable_ansi_colors), + .Undefined => this.printAs(.Undefined, Writer, writer, value, result.cell, enable_ansi_colors), + .Double => this.printAs(.Double, Writer, writer, value, result.cell, enable_ansi_colors), + .Integer => this.printAs(.Integer, Writer, writer, value, result.cell, enable_ansi_colors), + .Null => this.printAs(.Null, Writer, writer, value, result.cell, enable_ansi_colors), + .Boolean => this.printAs(.Boolean, Writer, writer, value, result.cell, enable_ansi_colors), + .Array => this.printAs(.Array, Writer, writer, value, result.cell, enable_ansi_colors), + .Object => this.printAs(.Object, Writer, writer, value, result.cell, enable_ansi_colors), + .Function => this.printAs(.Function, Writer, writer, value, result.cell, enable_ansi_colors), + .Class => this.printAs(.Class, Writer, writer, value, result.cell, enable_ansi_colors), + .Error => this.printAs(.Error, Writer, writer, value, result.cell, enable_ansi_colors), + .ArrayBuffer, .TypedArray => this.printAs(.TypedArray, Writer, writer, value, result.cell, enable_ansi_colors), + .Map => this.printAs(.Map, Writer, writer, value, result.cell, enable_ansi_colors), + .Set => this.printAs(.Set, Writer, writer, value, result.cell, enable_ansi_colors), + .Symbol => this.printAs(.Symbol, Writer, writer, value, result.cell, enable_ansi_colors), + .BigInt => this.printAs(.BigInt, Writer, writer, value, result.cell, enable_ansi_colors), + .GlobalObject => this.printAs(.GlobalObject, Writer, writer, value, result.cell, enable_ansi_colors), + .Private => this.printAs(.Private, Writer, writer, value, result.cell, enable_ansi_colors), + .Promise => this.printAs(.Promise, Writer, writer, value, result.cell, enable_ansi_colors), + .JSON => this.printAs(.JSON, Writer, writer, value, result.cell, enable_ansi_colors), + .NativeCode => this.printAs(.NativeCode, Writer, writer, value, result.cell, enable_ansi_colors), + .JSX => this.printAs(.JSX, Writer, writer, value, result.cell, enable_ansi_colors), + .Event => this.printAs(.Event, Writer, writer, value, result.cell, enable_ansi_colors), + }; + } + }; +}; |