From b82656d9fc34674fb248c883ad6cbe3c14bc6bd7 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 21 Jan 2024 04:47:36 -0800 Subject: [PATCH] Introduce `Bun.stringWidth` (#8327) * Introduce `Bun.stringWidth` * [autofix.ci] apply automated fixes * Update utils.md --------- Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- bench/bun.lockb | Bin 47676 -> 49490 bytes bench/package.json | 3 +- bench/snippets/string-width.mjs | 44 ++++ docs/api/utils.md | 18 ++ packages/bun-types/bun.d.ts | 24 +++ src/bun.js/ConsoleObject.zig | 12 +- src/bun.js/api/bun.zig | 32 ++- src/bun.js/bindings/BunObject+exports.h | 1 + src/bun.js/bindings/BunObject.cpp | 1 + src/string.zig | 16 +- src/string_immutable.zig | 198 ++++++++++++------ test/bun.lockb | Bin 291026 -> 292445 bytes .../__snapshots__/console-table.test.ts.snap | 11 + test/js/bun/console/console-table.test.ts | 26 +++ test/js/bun/util/stringWidth.test.ts | 85 ++++++++ test/package.json | 1 + 16 files changed, 391 insertions(+), 81 deletions(-) create mode 100644 bench/snippets/string-width.mjs create mode 100644 test/js/bun/util/stringWidth.test.ts diff --git a/bench/bun.lockb b/bench/bun.lockb index 679b4cb92bfda755f23ed10ef17705165b7a01ad..2b23aa48aeda1f02739e32a1a35e0b92bf03ae2f 100755 GIT binary patch delta 9054 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 F_+O-S_DcW& diff --git a/bench/package.json b/bench/package.json index d62970fbdf..3cafcffe82 100644 --- a/bench/package.json +++ b/bench/package.json @@ -10,7 +10,8 @@ "eventemitter3": "^5.0.0", "fast-glob": "3.3.1", "fdir": "^6.1.0", - "mitata": "^0.1.6" + "mitata": "^0.1.6", + "string-width": "^7.0.0" }, "scripts": { "ffi": "cd ffi && bun run deps && bun run build && bun run bench", diff --git a/bench/snippets/string-width.mjs b/bench/snippets/string-width.mjs new file mode 100644 index 0000000000..63e1f4ecb5 --- /dev/null +++ b/bench/snippets/string-width.mjs @@ -0,0 +1,44 @@ +import { bench, run } from "./runner.mjs"; +import npmStringWidth from "string-width"; + +const bunStringWidth = globalThis?.Bun?.stringWidth; + +bench("npm/string-width (ansi + emoji + ascii)", () => { + npmStringWidth("hello there! 😀\u001b[31m😀😀"); +}); + +bench("npm/string-width (ansi + emoji)", () => { + npmStringWidth("😀\u001b[31m😀😀"); +}); + +bench("npm/string-width (ansi + ascii)", () => { + npmStringWidth("\u001b[31mhello there!"); +}); + +if (bunStringWidth) { + bench("Bun.stringWidth (ansi + emoji + ascii)", () => { + bunStringWidth("hello there! 😀\u001b[31m😀😀"); + }); + + bench("Bun.stringWidth (ansi + emoji)", () => { + bunStringWidth("😀\u001b[31m😀😀"); + }); + + bench("Bun.stringWidth (ansi + ascii)", () => { + bunStringWidth("\u001b[31mhello there!"); + }); + + if (npmStringWidth("😀\u001b[31m😀😀") !== bunStringWidth("😀\u001b[31m😀😀")) { + console.error("string-width mismatch"); + } + + if (npmStringWidth("hello there! 😀\u001b[31m😀😀") !== bunStringWidth("hello there! 😀\u001b[31m😀😀")) { + console.error("string-width mismatch"); + } + + if (npmStringWidth("\u001b[31mhello there!") !== bunStringWidth("\u001b[31mhello there!")) { + console.error("string-width mismatch"); + } +} + +await run(); diff --git a/docs/api/utils.md b/docs/api/utils.md index 0347da8752..b6089338b2 100644 --- a/docs/api/utils.md +++ b/docs/api/utils.md @@ -261,6 +261,24 @@ This function is optimized for large input. On an M1X, it processes 480 MB/s - 20 GB/s, depending on how much data is being escaped and whether there is non-ascii text. Non-string types will be converted to a string before escaping. +## `Bun.stringWidth()` + +```ts +Bun.stringWidth(input: string, options?: { countAnsiEscapeCodes?: boolean = false }): number +``` + +Returns the number of columns required to display a string. This is useful for aligning text in a terminal. By default, ANSI escape codes are removed before measuring the string. To include them, pass `{ countAnsiEscapeCodes: true }` as the second argument. + +```ts +Bun.stringWidth("hello"); // => 5 +Bun.stringWidth("\u001b[31mhello\u001b[0m"); // => 5 +Bun.stringWidth("\u001b[31mhello\u001b[0m", { countAnsiEscapeCodes: true }); // => 12 +``` + +Compared with the popular `string-width` npm package, `bun`'s implementation is > [100x faster](https://github.com/oven-sh/bun/blob/8abd1fb088bcf2e78bd5d0d65ba4526872d2ab61/bench/snippets/string-width.mjs#L22) + + + ## `Bun.fileURLToPath()` diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 3d7af020cb..86c9d98127 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -4497,6 +4497,30 @@ declare module "bun" { "ignore" | "inherit" | null | undefined >; + /** + * + * Count the visible width of a string, as it would be displayed in a terminal. + * + * By default, strips ANSI escape codes before measuring the string. This is + * because ANSI escape codes are not visible characters. If passed a non-string, + * it will return 0. + * + * @param str The string to measure + * @param options + */ + function stringWidth( + str: string, + options?: { + /** + * Whether to include ANSI escape codes in the width calculation + * + * Slightly faster if set to `false`, but less accurate if the string contains ANSI escape codes. + * @default false + */ + countAnsiEscapeCodes?: boolean; + }, + ): number; + class FileSystemRouter { /** * Create a new {@link FileSystemRouter}. diff --git a/src/bun.js/ConsoleObject.zig b/src/bun.js/ConsoleObject.zig index 1369d0bf98..a332cd3bc5 100644 --- a/src/bun.js/ConsoleObject.zig +++ b/src/bun.js/ConsoleObject.zig @@ -284,12 +284,12 @@ const TablePrinter = struct { ); pub fn write(this: VisibleCharacterCounter, bytes: []const u8) WriteError!usize { - this.width.* += strings.visibleUTF8Width(bytes); + this.width.* += strings.visible.width.exclude_ansi_colors.utf8(bytes); return bytes.len; } pub fn writeAll(this: VisibleCharacterCounter, bytes: []const u8) WriteError!void { - this.width.* += strings.visibleUTF8Width(bytes); + this.width.* += strings.width.exclude_ansi_colors.utf8(bytes); } }; @@ -320,7 +320,7 @@ const TablePrinter = struct { fn updateColumnsForRow(this: *TablePrinter, columns: *std.ArrayList(Column), row_key: RowKey, row_value: JSValue) !void { // update size of "(index)" column const row_key_len: u32 = switch (row_key) { - .str => |value| @intCast(value.visibleWidth()), + .str => |value| @intCast(value.visibleWidthExcludeANSIColors()), .num => |value| @truncate(bun.fmt.fastDigitCount(value)), }; columns.items[0].width = @max(columns.items[0].width, row_key_len); @@ -400,7 +400,7 @@ const TablePrinter = struct { try writer.writeAll("│"); { const len: u32 = switch (row_key) { - .str => |value| @truncate(value.visibleWidth()), + .str => |value| @truncate(value.visibleWidthExcludeANSIColors()), .num => |value| @truncate(bun.fmt.fastDigitCount(value)), }; const needed = columns.items[0].width -| len; @@ -544,7 +544,7 @@ const TablePrinter = struct { { for (columns.items) |*col| { // also update the col width with the length of the column name itself - col.width = @max(col.width, @as(u32, @intCast(col.name.visibleWidth()))); + col.width = @max(col.width, @as(u32, @intCast(col.name.visibleWidthExcludeANSIColors()))); } try writer.writeAll("┌"); @@ -557,7 +557,7 @@ const TablePrinter = struct { for (columns.items, 0..) |col, i| { if (i > 0) try writer.writeAll("│"); - const len = col.name.visibleWidth(); + const len = col.name.visibleWidthExcludeANSIColors(); const needed = col.width -| len; try writer.writeByteNTimes(' ', 1); if (comptime enable_ansi_colors) { diff --git a/src/bun.js/api/bun.zig b/src/bun.js/api/bun.zig index 9765b074a8..d07823a6d7 100644 --- a/src/bun.js/api/bun.zig +++ b/src/bun.js/api/bun.zig @@ -41,7 +41,7 @@ pub const BunObject = struct { pub const spawnSync = JSC.wrapStaticMethod(JSC.Subprocess, "spawnSync", false); pub const which = Bun.which; pub const write = JSC.WebCore.Blob.writeFile; - // pub const @"$" = Bun.shell; + pub const stringWidth = Bun.stringWidth; pub const shellParse = Bun.shellParse; pub const shellLex = Bun.shellLex; pub const braces = Bun.braces; @@ -161,7 +161,7 @@ pub const BunObject = struct { @export(BunObject.spawnSync, .{ .name = callbackName("spawnSync") }); @export(BunObject.which, .{ .name = callbackName("which") }); @export(BunObject.write, .{ .name = callbackName("write") }); - // @export(BunObject.@"$", .{ .name = callbackName("$") }); + @export(BunObject.stringWidth, .{ .name = callbackName("stringWidth") }); @export(BunObject.shellParse, .{ .name = callbackName("shellParse") }); @export(BunObject.shellLex, .{ .name = callbackName("shellLex") }); @export(BunObject.shellEscape, .{ .name = callbackName("shellEscape") }); @@ -5108,6 +5108,34 @@ pub const FFIObject = struct { } }; +fn stringWidth(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSC.JSValue { + const arguments = callframe.arguments(2).slice(); + const value = if (arguments.len > 0) arguments[0] else JSC.JSValue.jsUndefined(); + const options_object = if (arguments.len > 1) arguments[1] else JSC.JSValue.jsUndefined(); + + if (!value.isString()) { + return JSC.jsNumber(0); + } + + const str = value.toBunString(globalObject); + defer str.deref(); + + var count_ansi_escapes = false; + + if (options_object.isObject()) { + if (options_object.getTruthy(globalObject, "countAnsiEscapeCodes")) |count_ansi_escapes_value| { + if (count_ansi_escapes_value.isBoolean()) + count_ansi_escapes = count_ansi_escapes_value.toBoolean(); + } + } + + if (count_ansi_escapes) { + return JSC.jsNumber(str.visibleWidth()); + } + + return JSC.jsNumber(str.visibleWidthExcludeANSIColors()); +} + /// EnvironmentVariables is runtime defined. /// Also, you can't iterate over process.env normally since it only exists at build-time otherwise // This is aliased to Bun.env diff --git a/src/bun.js/bindings/BunObject+exports.h b/src/bun.js/bindings/BunObject+exports.h index 63c71a0fee..d590963521 100644 --- a/src/bun.js/bindings/BunObject+exports.h +++ b/src/bun.js/bindings/BunObject+exports.h @@ -66,6 +66,7 @@ macro(spawnSync) \ macro(which) \ macro(write) \ + macro(stringWidth) \ macro(shellParse) \ macro(shellLex) \ macro(shellEscape) \ diff --git a/src/bun.js/bindings/BunObject.cpp b/src/bun.js/bindings/BunObject.cpp index 5a8958925f..91e412de10 100644 --- a/src/bun.js/bindings/BunObject.cpp +++ b/src/bun.js/bindings/BunObject.cpp @@ -621,6 +621,7 @@ JSC_DEFINE_HOST_FUNCTION(functionHashCode, stdin BunObject_getter_wrap_stdin DontDelete|PropertyCallback stdout BunObject_getter_wrap_stdout DontDelete|PropertyCallback stringHashCode functionHashCode DontDelete|Function 1 + stringWidth BunObject_callback_stringWidth DontDelete|Function 2 unsafe BunObject_getter_wrap_unsafe DontDelete|PropertyCallback version constructBunVersion ReadOnly|DontDelete|PropertyCallback which BunObject_callback_which DontDelete|Function 1 diff --git a/src/string.zig b/src/string.zig index 65f56b9347..9addb67072 100644 --- a/src/string.zig +++ b/src/string.zig @@ -940,11 +940,21 @@ pub const String = extern struct { pub fn visibleWidth(this: *const String) usize { if (this.isUTF8()) { - return bun.strings.visibleUTF8Width(this.utf8()); + return bun.strings.visible.width.utf8(this.utf8()); } else if (this.isUTF16()) { - return bun.strings.visibleUTF16Width(this.utf16()); + return bun.strings.visible.width.utf16(this.utf16()); } else { - return bun.strings.visibleLatin1Width(this.latin1()); + return bun.strings.visible.width.ascii(this.latin1()); + } + } + + pub fn visibleWidthExcludeANSIColors(this: *const String) usize { + if (this.isUTF8()) { + return bun.strings.visible.width.exclude_ansi_colors.utf8(this.utf8()); + } else if (this.isUTF16()) { + return bun.strings.visible.width.exclude_ansi_colors.utf16(this.utf16()); + } else { + return bun.strings.visible.width.exclude_ansi_colors.ascii(this.latin1()); } } diff --git a/src/string_immutable.zig b/src/string_immutable.zig index 5824765d72..92c59a9cb3 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -3793,6 +3793,10 @@ pub fn indexOfCharUsize(slice: []const u8, char: u8) ?usize { return i; } +pub fn indexOfChar16Usize(slice: []const u16, char: u16) ?usize { + return std.mem.indexOfScalar(u16, slice, char); +} + test "indexOfChar" { const pairs = .{ .{ @@ -5659,89 +5663,145 @@ pub fn visibleCodepointWidthType(comptime T: type, cp: T) usize { return 1; } -pub fn visibleASCIIWidth(input_: anytype) usize { - var length: usize = 0; - var input = input_; +pub const visible = struct { + fn visibleASCIIWidth(input_: anytype) usize { + var length: usize = 0; + var input = input_; + + if (comptime Environment.enableSIMD) { + // https://zig.godbolt.org/z/hxhjncvq7 + const ElementType = std.meta.Child(@TypeOf(input_)); + const simd = 16 / @sizeOf(ElementType); + if (input.len >= simd) { + const input_end = input.ptr + input.len - (input.len % simd); + while (input.ptr != input_end) { + const chunk: @Vector(simd, ElementType) = input[0..simd].*; + input = input[simd..]; + + const cmp: @Vector(simd, ElementType) = @splat(0x1f); + const match1: @Vector(simd, u1) = @bitCast(chunk >= cmp); + const match: @Vector(simd, ElementType) = match1; + + length += @reduce(.Add, match); + } + } + + // this is a deliberate compiler optimization + // it disables auto-vectorizing the "input" for loop. + if (!(input.len < simd)) unreachable; + } + + for (input) |c| { + length += if (c > 0x1f) 1 else 0; + } + + return length; + } + + fn visibleASCIIWidthExcludeANSIColors(input_: anytype) usize { + var length: usize = 0; + var input = input_; - if (comptime Environment.enableSIMD) { - // https://zig.godbolt.org/z/hxhjncvq7 const ElementType = std.meta.Child(@TypeOf(input_)); - const simd = 16 / @sizeOf(ElementType); - if (input.len >= simd) { - const input_end = input.ptr + input.len - (input.len % simd); - while (input.ptr != input_end) { - const chunk: @Vector(simd, ElementType) = input[0..simd].*; - input = input[simd..]; + const indexFn = if (comptime ElementType == u8) strings.indexOfCharUsize else strings.indexOfChar16Usize; - const cmp: @Vector(simd, ElementType) = @splat(0x1f); - const match1: @Vector(simd, u1) = @bitCast(chunk >= cmp); - const match: @Vector(simd, ElementType) = match1; + while (indexFn(input, '\x1b')) |i| { + length += visibleASCIIWidth(input[0..i]); + input = input[i..]; - length += @reduce(.Add, match); + if (input.len < 3) return length; + + if (input[1] == '[') { + const end = indexFn(input[2..], 'm') orelse return length; + input = input[end + 3 ..]; + } else { + input = input[1..]; } } - // this is a deliberate compiler optimization - // it disables auto-vectorizing the "input" for loop. - if (!(input.len < simd)) unreachable; + length += visibleASCIIWidth(input); + + return length; } - for (input) |c| { - length += if (c > 0x1f) 1 else 0; + fn visibleUTF8WidthFn(input: []const u8, comptime asciiFn: anytype) usize { + var bytes = input; + var len: usize = 0; + while (bun.strings.firstNonASCII(bytes)) |i| { + len += asciiFn(bytes[0..i]); + + const byte = bytes[i]; + const skip = bun.strings.wtf8ByteSequenceLengthWithInvalid(byte); + const cp_bytes: [4]u8 = switch (skip) { + inline 1, 2, 3, 4 => |cp_len| .{ + byte, + if (comptime cp_len > 1) bytes[1] else 0, + if (comptime cp_len > 2) bytes[2] else 0, + if (comptime cp_len > 3) bytes[3] else 0, + }, + else => unreachable, + }; + + const cp = decodeWTF8RuneTMultibyte(&cp_bytes, skip, u32, unicode_replacement); + len += visibleCodepointWidthType(u32, cp); + + bytes = bytes[@min(i + skip, bytes.len)..]; + } + + len += asciiFn(bytes); + + return len; } - return length; -} + fn visibleUTF16WidthFn(input: []const u16, comptime asciiFn: anytype) usize { + var bytes = input; + var len: usize = 0; + while (bun.strings.firstNonASCII16CheckMin([]const u16, bytes, false)) |i| { + len += asciiFn(bytes[0..i]); + bytes = bytes[i..]; -pub fn visibleUTF8Width(input: []const u8) usize { - var bytes = input; - var len: usize = 0; - while (bun.strings.firstNonASCII(bytes)) |i| { - len += visibleASCIIWidth(bytes[0..i]); + const utf8 = utf16CodepointWithFFFD([]const u16, bytes); + len += visibleCodepointWidthType(u32, utf8.code_point); + bytes = bytes[@min(@as(usize, utf8.len), bytes.len)..]; + } - const byte = bytes[i]; - const skip = bun.strings.wtf8ByteSequenceLengthWithInvalid(byte); - const cp_bytes: [4]u8 = switch (skip) { - inline 1, 2, 3, 4 => |cp_len| .{ - byte, - if (comptime cp_len > 1) bytes[1] else 0, - if (comptime cp_len > 2) bytes[2] else 0, - if (comptime cp_len > 3) bytes[3] else 0, - }, - else => unreachable, + len += asciiFn(bytes); + + return len; + } + + fn visibleLatin1WidthFn(input: []const u8) usize { + return visibleASCIIWidth(input); + } + + pub const width = struct { + pub fn ascii(input: []const u8) usize { + return visibleASCIIWidth(input); + } + + pub fn utf8(input: []const u8) usize { + return visibleUTF8WidthFn(input, visibleASCIIWidth); + } + + pub fn utf16(input: []const u16) usize { + return visibleUTF16WidthFn(input, visibleASCIIWidth); + } + + pub const exclude_ansi_colors = struct { + pub fn ascii(input: []const u8) usize { + return visibleASCIIWidthExcludeANSIColors(input); + } + + pub fn utf8(input: []const u8) usize { + return visibleUTF8WidthFn(input, visibleASCIIWidthExcludeANSIColors); + } + + pub fn utf16(input: []const u16) usize { + return visibleUTF16WidthFn(input, visibleASCIIWidthExcludeANSIColors); + } }; - - const cp = decodeWTF8RuneTMultibyte(&cp_bytes, skip, u32, unicode_replacement); - len += visibleCodepointWidthType(u32, cp); - - bytes = bytes[@min(i + skip, bytes.len)..]; - } - - len += visibleASCIIWidth(bytes); - - return len; -} - -pub fn visibleUTF16Width(input: []const u16) usize { - var bytes = input; - var len: usize = 0; - while (bun.strings.firstNonASCII16CheckMin([]const u16, bytes, false)) |i| { - len += visibleASCIIWidth(bytes[0..i]); - bytes = bytes[i..]; - - const utf8 = utf16CodepointWithFFFD([]const u16, bytes); - len += visibleCodepointWidthType(u32, utf8.code_point); - bytes = bytes[@min(@as(usize, utf8.len), bytes.len)..]; - } - - len += visibleASCIIWidth(bytes); - - return len; -} - -pub fn visibleLatin1Width(input: []const u8) usize { - return visibleASCIIWidth(input); -} + }; +}; pub const QuoteEscapeFormat = struct { data: []const u8, diff --git a/test/bun.lockb b/test/bun.lockb index 4289360c49a3c0b2534635def0aace959dd84648..9484b06ecaf26f52cf5870f1ae79dac5469c5c98 100755 GIT binary patch delta 29691 zcmeHwd3+7m|Nh*$3Ay$qkwxq+LK6~|MTl5yi>f$PvwI)R=t|;*fr>6UvLN{7Wlk=H6ZJeFIe*WUEkyHRlaRb95`iCh>6YXY^ zHq7O0?VX?~jz}7=DoR1H2SSU1pClT580-%Jy7XUuqbMcd4~o+pq1=Yg`0~hS0au$~ zw4wr-e)p)TzTv2{TWDOE?}y<%jzDKUJHdq&MO#rcz&d=gQO3SWM%p@XDa72EXp}W$ zilP*Se-X^xh=_^^8_XU+I{hBuJ$te>w)nWvNc>lnxZa_$(a;-CH}=sJ?1XrCFnh{( zhF-2UzJFXqcsB&vA;AR+12sh{56%R;fjfXJf`0=$g8jkllwDvp)D2t~yh{2nry2fC z_>8{*pYeOaWx&h8<-n7{PKs5D9}L48LL`{I(>*F8Di#g;cCMlnf`1=e1l)L@ksccr z5itO}X1~BF@aBA@f+Jv7uo3JEo&jdV;=tU24q#SXTe36Ltq`7~U?!Xev!%PirNPDI zaEOkLij3|br(9lQba~&1umNmfbZmHBWT>LdUuqnp=%~2b-AJ(r20+&I+xQL$#<{l?VMuzZy{r#0KD2P1- zKRPNZLa|k}T2`=dnO3btQ}slW7F(jSwQ`biq~qdZ!`Y0;@P6TJK%>=0EwN$!V~0Qw zi;eZ{9@Qhv2Rd8x9+~8l^{OcX2WUiAV ztN!;!a}y*-dq#$GNSfCh{nZ`J-R%VCZk<8KV&IKIM!~%zqBt;>8yk%DI-eW*KKQIS zWupBt$E}dkA@{tly~$|t3k0yDO(=jBeOJZsPoSr$A43DUhqtjuEU-zk z?7^t+eR15B82x={x6zH|_88|$ zA2}`c8H6#3EfwHil-O&q1L7aKMpF1+UurQ<=PD@&o#g^=D2TofZEK5U=1%IUH7;BjLzS}S=Ln3K;ia2dvf zdCoVN><#8*<_6~6k`K&N@=m&;4@2E3KYqtaqw5z)9w|8#%yOH6F(p{zE5fJ*!3^eH zapgCI_k%gfB!QV>GT03qBlSpB#16vH)h`{zGK`Lhz_Es;yn@dT$&#EQ`4E_6YYVtI zMpOJs-B98`0dpRo0_Gl$05h;JxCB_q&<;75^*MLZIGD9CC|N!_M`s*(y@R{Q#c>W& zN?tZ}_3}nHm!n0rwk|HkI;>lm{z0(|H*WX%tYb#qU5z*1sNBhJW83Pd>`W0GJ!*~~ zU<%eeYXw?{j0xgXb6{S9s3urO5{4As4HG^lT zt#9sU$^VVb%bxCMX#=k#(#$N#vP`Ds)3V_e8Lud zA_D>~*J0JkDLrt4qO{QSpalCd@akw;@qw0;us(#YXfa*=ECnYjioYI(lGR2NwN)#F z?1w=N)FNvKT7HD(gXDaA#pX$h(ion-w!XceM_W@b(EdkQ z9hhV(FjG;wqi7TR#Xb^VCtcqM3&UON9|&2zWiTw98B+fRURyoB(kw;!B1iuUo}nNA zgYG@sR?Y}mIptVR!Rw;uUVp^jVUD6;K9p6=glFu}6^ z$g&2aFGLI-438{$b>U$Q`1n~omKp=etgR3BvxLLrZrbaEY&E=~94`}|KRgUhKXkz| z93^-t6JtAenYL<6kmU@-h7eIS#;E)9oWX{Q+QMUYRm*PVXTJwt3w_=w^sUhVJ1x7Z zpM4O#hFWBcKubKV#(G&i=~AR;%nJ@Hj2*(j^F|(cL7HbQrYNbSiN1dJ8{lCe)eE$! zD{bv!S&gNqm!*zfsrA_&WJ!k@fa(h9XKte;Tcem^fb>u-c54MZFGOO(!by4op0Qgv z$1N_aY<-m70lO?cd!8R^!YZv=K#*lWM0T;UC(m;-u%de38E0;Oz351Ixsi+Eai%D& zPeN^1D~h+?Tpq+x@Yo3CYvyO!0*^-lHDD5Y0k0N3MOz=?XYpE-(_9?xp72^A4O1EB zg5AV_NSRSrXJ^a)M8??;*L6$}vjfPtoVn2eOmenE9 z@+&M>fF14cXDRiA5#_*PYT26O@e=k3UN9o{dEU}!lhG85K8cTl$4=4RKl%8>39)QQ8U|cd^z^emK-&xBi$;Lr6TDAxtcM;PYF1+X9 zv1K^9PvuuXv z3(vSl58H{4+VHS9v3{02@OZH-$f06ieHX74T(t~>RTH`~*jB)61P==e^oM!3qI?Vw zJ%AJC6L^e7>Dafi@Oa!XoZ+2@XAIfwuKsp=^h-tiKucd(UI;SA;B4s`-FpCDLtC1C zxxJ`Zi|iO^=?04mQfT+3fm=BadeO^iHa{p>5IvRAp1jDf|@MpR8d`($`1%O}wESPMU0-`?{O znyE#e4z!Pf)l|2VVPRzm?M+T4+`03#?Xp-lQBY8&+#wtDj~| z9S=*E0?Vk)^{B19F0c%3IV_{RY*q-N;?%h>Z6%zjJZ z@ubHDgpqO*p3&b}fvAPkwaoKDmR9Mu={y^2@~_jiJ{N-2&FR{z3qkgmj_d2O3xW1^ zez958VBwMo?Gmi6x)uB@mJGTzGsn6O3u`>Y1)tCtPOzrI!ZH!oIaqqC#pgF;(T~H9 zWzo5l#^THH>YvK%Ez0ZJn@(c|r>(hM*8xUnV+^2xP4Ify68oZ81S{;7}IJtq1&l8zG{El73C(5hVzvJ^gNoc{J4b(VVY zIG&Bue@I@>eiJ=yjc=eO8y3%TJ<7h;d7K~GnlW`9XynN2;9)6^@JFy(<>(DAAhVX$ zEYN;5EUZ0!1MT<1!tkgSs204a^~nsfw7Y0r^^GOOyo*|9W{~Nw=5edOrO_qZ!k)9i z*YG%z;1WBxNb7uA>vKEEG8ZDx^P<{%M4rE_W!^?7T+ym! z)v?x8^iELngZ%(*dD#pf^S@#OFp25gf5Oa9mR0G5Le)VUEpc*E3pgCC-mc7O{I z4sau@Kwmo8@^U`-F@PN~0ASo8fZJa& z^{)XoBtiO#U~d1CnaQlZx>HVj?{0Z3i<-=Q*#3&y_NjW5!OS;J`gxgg)1^*kZLb?b7e3DEjI{@nerf&q;jAXzb*amPT zQ{M$}+XXOwkK`2I{h+WP;6^rUBknnC^X@sgQo&TOZ)ElergTJXzvrws9FwWOA)+S~< zcV_fW8T~e9dM3a=eZWX=A575$_u1rPrfXa11PC^zYoX_m&1 zeBcV;g5VD*N~YUp^o^rlkJ!^TOUM?lzH1OG?5QUBXomQx{zVhh=FGWRe< z`ed5?#S^!@%zW)-`o}Vz%v0cV>3?2G2KiDS}xy-=d zWdUS1;5@jnmiDNMcKeaTTVV9#9hr+v^DchyDBYJjnWa6J{@a+fKSetExs3Za*s2c$ z6LVqvD`uH0;tGL_fZ3oDVCp4h{Y?WldA6P5=#F?|9@*Yx6 zn&%T^GOi+H$!tV*>60Dd*Oxw-jSB`-YAE%;!B+jSG(kaZS8FgUXd??I(`+YwGSk~j zpG^N_$sNGlflgo^?$5#8{+w~vF{X#@pCxGyWZB-Vyl0J&Fgj0i$GrWbWBm>Hifo zB*BQQtQ)ky!4DQV-bm1yBYPTs?dmgQ!82W^=4DDVrJk2L66Q!fFSD5orB3GVERjB$ z{j^&8WSVPLtU7tY-2#uz{7D9o>2H<(Uok_r%Q!L{xLf*UHY7#*WR|;6@_tobC-=($ zG7C5)eKPm#XD~A!12gHk)KAjEMy7dM`gxi8&PY8k^OAFkLzNX=2Q%=749v^aZ$f9% zEg5%Pau!8wZ(}Ci#SeD*U>>Pa(*H`P=dwIYa|J|W%j>hC(K6#-F^AMdSF`< zF04|RC561qif2om%=v7Yj9)HwG7DZI{k+WlD`k8Vm?zH~>93V+T_=t2!QApPS328d z`gX}XWE`0l>;$s`yQQ9&nZ8HrWFCqGGCmcIbgObm3P-@)$n3J?U`i(>pQ3~9ZOjIo zk?CYs{JZ3{U{-t)jQ`3N{9ybwI=nYw7zQ)rbs2C|@-00O6J#Fdv~XYg{|$4jykz+t zf3InP^#dfC4Kd+>Q8)9z{S!0Y4m#Ohrjr?;AI!EDkX%@%lW7*0x+7R0|1?;T6H_I# zAZMw&fZ2s*z|_mhI5P8;X@=R`4mc#SeB_J1_^!=M?`RGwZYVk_G=SnLE-O^{}c4 z{9wg{je4y%=g8BEaq6;deKH(NXVi0HYJG0Pk)9^Zdc~zq>&!FPVQ;0ncUL_~l0CEcWgO zFgoGg4d8#i3HCn>@?i{b{Bk36a`_U>`6g8Ae}`G#yBolH zZwRwf-`xN#alQj}9g*RB_)KfF+)pyZy=<>Tqb5x3zDYvv{?`^ye|407fjpZSRABw4X53HSU>74}hOH+oh>@ge;Mm0%6vE z2w4>Fihz9(njL^Je;I;BEba{$5<@eG3L zsc3f)^h_j?o(oed=!NJE61@*2czY^>UkS@0=&wa6>5bR|QWaH{IBfbvHL0QxSPV`> zc-mncX|pQajv&QO6|rEGy+}QReECF$G*EsqgybO7NfzO86jVTrA{7*;Nri;>&!EC0 zkyJ!nBo!3_$3VrzR8nzq6C|Dv7S^$SWbY-icnHHv4vD#6#orWL4=bkiru72!u2E?Q1vt#FybT{ z@V-c;aEgNGDF{`>kW&y6&OpeZ;2}ItL-79{!o<@Ms*BSUZcwOq27;$ZJOg3YSqNDa zYKVZ}AvDW?F#mT5-r^>OrxZfYLZ~HXpM|jU9E4XCYK!I>5IUWQuqFe7pLj;W;R1v% z=O6@#q;n89Qz(2MLZIk;9zySn5VljOCoC5rxLkq|c>zLwv4z4x3S}-rXduEbLKu7* zLK=le!u1k_s#hS4xCEhzNTqO!g6CxjABiECAtYRdkU^oj@VElO{~CmeS0J<$rzzZ^ zQ12>)5RrHl!mR5MvM96>0oNcjy8&VTH3;p*O$tvbgj|R4v6y`w!pfTvUQy^Mn%{uX zDHFn)8xTGf&nP(Dg3#qAgw7)ACWOrt3THz2Ty)Na(EB!o?G(BQ%Pj~lSr8&`LHI&! zp>U8wncEOTMfhz9gYQ5{qtIQrW* zoTlKP4WZsW2z^E3JqR}_WKoC|0of2{-N#9>AluZ!6eVs_X!ZaRA@>mxBWB-+@RY(U z3UQ+O0|+Z0LRj+v!T|A%LZ?R%x;%t1NF+VPfF3MNk3d62XVOryo-|BY9)pI9P|^so zg)~wWe*&_KaF96o6a}R{K|!O0>r)7WpFtS$6vAkcN}=j=2%gU%j1fbgK{!PrgTgrB z@f!cz*bC}^VjYX~dfKv?q{!VK{Y!i-KTy5J3bA(Gx8z`>*zo>5qBX_`Hwvx)&h zcRSpQOKaMLU`D#pPE*RQKTesnzTxI=L3Bw5=pDYMba7(kRP;GOeL)oH%Z@% z1`eS0Vm3%TwV>En4k&h`Xl{Z2gIG-3B%YCe6zvLtl0_0}voI9|Z4sSGKZ*6Et-?|W zv`vJPwu>#K9in()&`uE!5(kT*k~DVMZsA%4!r-D1MihatSENEPrHBeeLHooI(teRn zIv_lXAvmEpf+rS3aH=>B!E{J?7Y7{{iKHXqA}LJ-lmHzSQ%OIIo1|l+K}k@$m`yq^ zvPr*)=8m9W#bVM4@r?AFXy*hvDUwL1gsBwhwCGGaBi56C7ZzvGSrJOg5L-aP#TAV! zKnk;klS}w^%FH!T$55`2BnG)X!9_AubA09ZKz4Y5WZp-pvTEra#0H z@0TQYgs2XzC043KNd;sRQT*mSF7EfKlH=v}(XWeQ@ZH3hM*hoY9fo`RtS@q`=n@cfC2J&HuZD;}a zYZWy7=ijqYj?eB?lYgnX7)8wD-L@oGhoA0#Clu_*yC9kwsle~3A?&ketP^K zBGcJn&M^4=+C~}Z1lvVs{s9{P>yLH1N$p1&=M3Ee8hbKXYWzB%MaFHGnk%$BECkyY zKEBCxe)-Jy<&$mDn9d^=Blma*1e)$ZoYZ#8xH8ZNNR4;5X_f^BNo|kR%0U|^A^6K3C1mRROcioG#-$pxH@{&s)>PDXQd?+AmV$;Ozl$`&DWj zynO11+X+6t2VEb%AA&h*Z4YdF!L~hl$A`9AHb-Mzdr0zksd+=&FOU0K zsrf*&Jq?-x4gZx|9Dj3U;CU&sAq%B;QD&|UZIRS2NzE79WT{=28lPR7Dzz(8^M_Um z8VB-KsRh6;1kK8)@UBUb4>}cr$Wd#1>YUFsWh0%VmQS8@kLv;tVRPft=hX1G2oz`7 z9LSkc<8x2vU~?ebo=&e1o2M@~JgIIxJ{Sy#69-2(PYo8>09XW@qnp2}pwCafm?sqAY*!y#Q^s|J&0jBbkaj7;uDrpL?CABcv z{K_837=QVsKR(w(D>?s{#OB%Yw&pt{bc6eu-OnE8h@#U!w!~OfYkcH=R?@+ z@;XxM3p-6}fnc;%A1~IQrN}QUuty?+V^Rx}T0dy%QmYSU<|u#`d4^ zgbk$@1N#PS9>PXai-mm&HV&axX)MJ!h+A1QwkA^R54#t%Zs4X;8vy%L8TXOY214V& zdnfs;R< zEgb`l1^7TJA5P_yvEe`;G4+XBsc?6gJ%BKvr`Y{OEiJx&q89Vu9qI-ES3r$`#y}I` zBcK_;MNkW%rEq$xwrpM(W<4MXs1NX)n_GeHz)oNnuop-H_5u5W13)Tp2si?y0Y}CB zr>b+c6EIH#rvN?O85`aX2ORot4f5{gI3;+fKgMh)n5FiGK0-}L#W<^YXrj~K- z1E((#0Yn1*01nh3vG180*tIpxHb7gT9nc=&l))*2Q$jVMI`9F&GujE@B+3br6Cx+T z{D1{819pHtz)R3ARWy9AHtfg=@+;tLU<@!87zd08CIAxwu7M^4T=z@^G=S@!I~Xu` zfo$MD@BnxSJi=`NwZ1o3D?GnBCvytsdCe)7(nfVRP%D{wi^vyh zE!-?x`~vIu+DPyP_*C>nFdxs31o{C{Kr|2o!~$_Ze}I2wcmXP34BTcrfzyB^_M6|p zeFnUTnD>E-0KYa+7T{{A2*4E(e?{fM$N$YR_)Umcz#d>PkOJ%j_5-s4u5ji8^MC~a zS2v4*CBRZ(7BC5z3`_+yU?MOa7y*m~tUx?44Cn%M1wQ8by)KNJfH&X;jKyBKgUbN# z0q+CFfqVcL5?nZN0l@1$Rv=1gU^(`91+WrG0=Ty0TJ8eC)e1jcOb2EF8ZZ%<4B%re zuEoG;;A>zEP#Ne3bjJ-F)wy0xm|S?&0zL$41HOPi5CGH#>H$Fj*CJed2w)a4Pb@|e z)?P3_208#8flq)=z-NFPP!Zs=gI{uw0NMa$0REMOT^JP0faSm{U^TD?SPOg)%m)?% zi-5(zbYKQB6A-{;RH00PF$stVz5+PWa=Fn@w0o^qvR;IF3`hqK11Z2hfC~tJfL{l{ z2V?_PF<$s}kxD>0fZti+7g|aJc7O`J#-Vxx90CplslZtv1K>6Pdw|#Z&A?VO2H`3g ztAXXfx4V&+<5EwD}%L*J+ctQ%ne0Bi#InJ^jPXTEQMFM)pm4S_~LV;~r40Prs!bOAhp z8UU}DyzsS$-3(|Bd<%aiuoM^r3;{+0SAeU)C0x z0*?TGIj20pZx6kXhFnI+>;Sd_p#awouc5yIDgpdB$18Lg5Ds(%J^^?&Zo-w!M=+WJ zEr6ClD0_Fe$ zmGa;q!BcUKzL&z|Ru=?7)u-qtNI;0IPSa{=F5hg18(n0h~r- z4glwX^T2vwBd`)!0SI6^z{2z5m23#LgRuG0K5sg0E`UD=_;)5Y0~<{&NW`e-PpwVh zb2Z1f55nMvDGTNJfAAt$*uLHJ_9f;IDA`Um@hAOqP)JqX8>bu zWt@b4FFywtwnA?D($UI~h8zm_0bYK%xX1#|0~{9VzY;rq0^Pqy(-(jBt zP61p2oC9{CVE)yQj6A+gzXa_fZ~>6FC*OPkn`mYXz!S_CdksF~ zt$GGqp$vGA1TH_G0#AS};1>Kc#lgjZqMWmez$gsxbhiNc0eip<6aoqWoDT8um;Z3N!)OEA_#)We6@$R{c7~>s2ri zhr)RYX#ksSvp<3#1$+YVa?k1GEM_k+&tdIl#0KpcTNf2x=b#tfw8of$m|# zpJPP zX}thWJEyqrw};UaLSb+pnaDFG9CmMj_GqLYH%z|b&anI3g=Y0%`KLbaBG~ftu7&ru^ z0tbNuKn8FY_+7XD>&(anU{u$#%zc6#W zT492?7iM;Gx4oHWVEdU9eCuCH)61yM$R~;7y`E+tcll15_*+4{7FHR# zb8iOaMs(1`F_vz7pH2n4qKnRVSbr%u!uE!oI|&JA9X^WrD!0w6G*PxM^4i|9^L%`R zN+W-nJs>yYH%vp0X!EF_)hZv4R!8KfWNIP;dEM=%>%XUncQ5^H{MA}Tf6I+pCImpza-Gj5ma#MU~h&z382>*Pq{qx@a z^CKdUnYwJ6I2D1z|Lt5+C=z^st_Y6AP-{F-d`5rwJTVErukD>ao8H{MU%>6q`*x-e zjZv-G-ulyS&Ye5Ki_%}`rZ~?RXOY+4_M)JT@7r~$d1Fk;+=veIMX`P;-S%ps@snT8 zORiKrGdIHa@}X-D-gkJ^sBube%EtMkEA!f3S5#(b8`IU{C*pG>Y%eq_dMVxegO`V2 z=BC&$5UcxP?=CM97g)yprTV%le&E^)13vq)=26vzIfiGRvT&JxRgS;-C{-CfweMrq zBq-BV=bsvlprSr=6Ol^th0y^!=};tC^duOUiF`1R;#50AO# zPCzr4i&oL-Dcg&SuC~6DbbP@@1ra>SKEy|=BwG~JbM-i!(OcFQfS56YXG3KLq zS5b1lShH$UZ+|BW#9`N-eJ6tA5LkG%=tRHNYB7QSd#lCE0brliVi)yBtHr}O6yU#B zr1UorR=2JdK?BU?)nn_#myA1+B!&(!_jI?tROnhIzjI5s{k)WOjWI|5u|yZ)HPGBz z&0Hr&4n)t|-d)t;+o5aA4zIcl**-KTUEAA?Y7}+oR&MIXOr+FAe_>zE>%|e|b+^6G z$RV*&{_{(h)I)^q{O;>#4#LS}d;gGYi4qYVDi!?|0kwD;!DpNeqV*ti;{r!F@TL;p z0=7ZS8)U9#@3RqCK+#};*~!6{BqoKJ-Q1IrfYn$0f>qOQm2x^h21WTByO*+2lpSn# z`7i(%&p%#yagp%m)Zgm|eYG9`^!c#fdpb>DuA16HaYkvP-w?B#;{TNs(M~nZ`9T~(3a-RigUz+n+M7h(Avij(FfunHL$9~C;u)Uhe^?S!P zDH~p6%C(h#M|ci5*K${P;$9c_tfj@iUXfnS2IY89Rl~2=g3MrbruM zc6C3r+cEi&`EmXEgs3XmDRLfqmU?eInu~(G0njOWFk>>W&@lh}A z6&QPpO7r8Q?pQRS+i~L|kEoJD^HEDzfrWfnN*QVGh<;REq5_QI17j zqS~(asF|CR^qVL&4!yh|xADs2SWZ~7sZPxLF>bjLS491BW>;U^i>PY+7~(y2!4|CO4(YNihk9``X@aRjqk**OGTvugs0Gz5lAWW9ml7bKjoHO<8bK zoJC%B$4T*c90rV%A*v;Si)DzeV0YVlsa)3mR4k-!?>h2W;>7KnAyy}#vu$sxDt+*q zVKtvu+^&xc{UR|XLtH?Fuk9UIR~`l=j2)CUSeAj4&v*@%694AGwQmw0HOkUc^r3wy zLwF^k!)!0kT9Mk?Z@w~fV{ZLfA~ezLil^9zC7MJ2teqIXvLx{`(fonKM;DEizBpFN z?53W(C|Z4kov(FC^!x@_MD?=qfn@uHhfN*t4%x4osu}B7yiZFU{RZ9fFCz*B^L{C| z;Hje1c=H9-{U=dpg1Kyq%+KI6=<~Z}Q1n!z+_z$=r3UC_mX;+Fk!V=GDK~&|YEvJ;U&RHqX$w@X&sGMo}Tz>@G$G z+YJ`+7tMAa|CHRjhG&hp>{csKMS@y HUE}`%mwtTa delta 28594 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 { console.log(actualOutput); }); +test("console.table ansi colors", () => { + const obj = { + [ansify("hello")]: ansify("this is a long string with ansi color codes"), + [ansify("world")]: ansify("this is another long string with ansi color"), + [ansify("foo")]: ansify("bar"), + }; + + function ansify(str: string) { + return `\u001b[31m${str}\u001b[39m`; + } + + const { stdout } = spawnSync({ + cmd: [bunExe(), `${import.meta.dir}/console-table-run.ts`, `(() => [${JSON.stringify(obj, null, 2)}])`], + stdout: "pipe", + stderr: "inherit", + env: bunEnv, + }); + + const actualOutput = stdout + .toString() + // todo: fix bug causing this to be necessary: + .replaceAll("`", "'"); + expect(actualOutput).toMatchSnapshot(); + console.log(actualOutput); +}); + test.skip("console.table character widths", () => { // note: this test cannot be automated because cannot test printed witdhs consistently. // so this test is just meant to be run manually diff --git a/test/js/bun/util/stringWidth.test.ts b/test/js/bun/util/stringWidth.test.ts new file mode 100644 index 0000000000..a58fa7b694 --- /dev/null +++ b/test/js/bun/util/stringWidth.test.ts @@ -0,0 +1,85 @@ +import { test, expect, describe } from "bun:test"; + +import npmStringWidth from "string-width"; +import { stringWidth } from "bun"; + +expect.extend({ + toMatchNPMStringWidth(received: string) { + const width = npmStringWidth(received); + const bunWidth = stringWidth(received); + const pass = width === bunWidth; + const message = () => `expected ${received} to have npm string width ${width} but got ${bunWidth}`; + return { pass, message }; + }, + toMatchNPMStringWidthExcludeANSI(received: string) { + const width = npmStringWidth(received, { countAnsiEscapeCodes: false }); + const bunWidth = stringWidth(received, { countAnsiEscapeCodes: false }); + const pass = width === bunWidth; + const message = () => `expected ${received} to have npm string width ${width} but got ${bunWidth}`; + return { pass, message }; + }, +}); + +test("stringWidth", () => { + expect(undefined).toMatchNPMStringWidth(); + expect("").toMatchNPMStringWidth(); + expect("a").toMatchNPMStringWidth(); + expect("ab").toMatchNPMStringWidth(); + expect("abc").toMatchNPMStringWidth(); + expect("😀").toMatchNPMStringWidth(); + expect("😀😀").toMatchNPMStringWidth(); + expect("😀😀😀").toMatchNPMStringWidth(); + expect("😀😀😀😀").toMatchNPMStringWidth(); + expect("😀😀😀😀😀").toMatchNPMStringWidth(); + expect("😀😀😀😀😀😀").toMatchNPMStringWidth(); + expect("😀😀😀😀😀😀😀").toMatchNPMStringWidth(); + expect("😀😀😀😀😀😀😀😀").toMatchNPMStringWidth(); + expect("😀😀😀😀😀😀😀😀😀").toMatchNPMStringWidth(); + expect("😀😀😀😀😀😀😀😀😀😀").toMatchNPMStringWidth(); +}); + +for (let matcher of ["toMatchNPMStringWidth", "toMatchNPMStringWidthExcludeANSI"]) { + describe(matcher, () => { + test("ansi colors", () => { + expect("\u001b[31m")[matcher](); + expect("\u001b[31ma")[matcher](); + expect("\u001b[31mab")[matcher](); + expect("\u001b[31mabc")[matcher](); + expect("\u001b[31m😀")[matcher](); + expect("\u001b[31m😀😀")[matcher](); + expect("\u001b[31m😀😀😀")[matcher](); + expect("\u001b[31m😀😀😀😀")[matcher](); + expect("\u001b[31m😀😀😀😀😀")[matcher](); + expect("\u001b[31m😀😀😀😀😀😀")[matcher](); + expect("\u001b[31m😀😀😀😀😀😀😀")[matcher](); + expect("\u001b[31m😀😀😀😀😀😀😀😀")[matcher](); + expect("\u001b[31m😀😀😀😀😀😀😀😀😀")[matcher](); + expect("\u001b[31m😀😀😀😀😀😀😀😀😀😀")[matcher](); + + expect("a\u001b[31m")[matcher](); + expect("ab\u001b[31m")[matcher](); + expect("abc\u001b[31m")[matcher](); + expect("😀\u001b[31m")[matcher](); + expect("😀😀\u001b[31m")[matcher](); + expect("😀😀😀\u001b[31m")[matcher](); + expect("😀😀😀😀\u001b[31m")[matcher](); + expect("😀😀😀😀😀\u001b[31m")[matcher](); + expect("😀😀😀😀😀😀\u001b[31m")[matcher](); + expect("😀😀😀😀😀😀😀\u001b[31m")[matcher](); + expect("😀😀😀😀😀😀😀😀\u001b[31m")[matcher](); + expect("😀😀😀😀😀😀😀😀😀\u001b[31m")[matcher](); + + expect("a\u001b[31mb")[matcher](); + expect("ab\u001b[31mc")[matcher](); + expect("abc\u001b[31m😀")[matcher](); + expect("😀\u001b[31m😀😀")[matcher](); + expect("😀😀\u001b[31m😀😀😀")[matcher](); + expect("😀😀😀\u001b[31m😀😀😀😀")[matcher](); + expect("😀😀😀😀\u001b[31m😀😀😀😀😀")[matcher](); + expect("😀😀😀😀😀\u001b[31m😀😀😀😀😀😀")[matcher](); + expect("😀😀😀😀😀😀\u001b[31m😀😀😀😀😀😀😀")[matcher](); + expect("😀😀😀😀😀😀😀\u001b[31m😀😀😀😀😀😀😀😀")[matcher](); + expect("😀😀😀😀😀😀😀😀\u001b[31m😀😀😀😀😀😀😀😀😀")[matcher](); + }); + }); +} diff --git a/test/package.json b/test/package.json index 8e2f01345c..7f2c75454a 100644 --- a/test/package.json +++ b/test/package.json @@ -41,6 +41,7 @@ "sinon": "6.0.0", "socket.io": "4.7.1", "socket.io-client": "4.7.1", + "string-width": "7.0.0", "supertest": "6.3.3", "svelte": "3.55.1", "typescript": "5.0.2",