import { describe, expect, test } from "bun:test"; import npmStringWidth from "string-width"; expect.extend({ toMatchNPMStringWidth(received: string) { const width = npmStringWidth(received, { countAnsiEscapeCodes: true }); const bunWidth = Bun.stringWidth(received, { countAnsiEscapeCodes: true }); 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 = Bun.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](); }); }); } for (let matcher of ["toMatchNPMStringWidth", "toMatchNPMStringWidthExcludeANSI"]) { test("leading non-ansi characters in UTF-16 string seems to fail", () => { expect("\x1b[31mhshh🌎")[matcher](); expect("a\x1b[31mhshh🌎")[matcher](); expect("a\x1b[31mhshh🌎a")[matcher](); }); } for (let matcher of ["toMatchNPMStringWidth", "toMatchNPMStringWidthExcludeANSI"]) { test("upstream", () => { expect("abcde")[matcher](); expect("叀池や")[matcher](); expect("あいうabc")[matcher](); expect("γ‚γ„γ†β˜…")[matcher](); expect("Β±")[matcher](); expect("γƒŽγƒΌγƒ‰.js")[matcher](); expect("δ½ ε₯½")[matcher](); expect("μ•ˆλ…•ν•˜μ„Έμš”")[matcher](); expect("A\uD83C\uDE00BC")[matcher](); expect("\u001B[31m\u001B[39m")[matcher](); // expect("\u001B]8;;https://github.com\u0007Click\u001B]8;;\u0007")[matcher](); expect("\u{231A}")[matcher](); expect("\u{2194}\u{FE0F}")[matcher](); expect("\u{1F469}")[matcher](); expect("\u{1F469}\u{1F3FF}")[matcher](); expect("\u{845B}\u{E0100}")[matcher](); expect("ปฏัก")[matcher](); expect("_\u0E34")[matcher](); expect("\u001B[31m\u001B[39m")[matcher](); }); } test("ambiguousIsNarrow=false", () => { for (let countAnsiEscapeCodes of [false, true]) { for (let string of ["⛣", "γ‚γ„γ†β˜…", "β€œ"]) { const actual = Bun.stringWidth(string, { countAnsiEscapeCodes, ambiguousIsNarrow: false }); expect(actual).toBe(npmStringWidth(string, { countAnsiEscapeCodes, ambiguousIsNarrow: false })); } } }); for (let matcher of ["toMatchNPMStringWidth", "toMatchNPMStringWidthExcludeANSI"]) { test("ignores control characters", () => { expect(String.fromCodePoint(0))[matcher](); expect(String.fromCodePoint(31))[matcher](); expect(String.fromCodePoint(127))[matcher](); expect(String.fromCodePoint(134))[matcher](); expect(String.fromCodePoint(159))[matcher](); expect("\u001B")[matcher](); }); } for (let matcher of ["toMatchNPMStringWidth", "toMatchNPMStringWidthExcludeANSI"]) { test("handles combining characters", () => { expect("x\u0300")[matcher](); }); } for (let matcher of ["toMatchNPMStringWidth", "toMatchNPMStringWidthExcludeANSI"]) { test("handles ZWJ characters", () => { expect("πŸ‘Ά")[matcher](); expect("πŸ‘ΆπŸ½")[matcher](); expect("aaπŸ‘ΆπŸ½aa")[matcher](); expect("πŸ‘©β€πŸ‘©β€πŸ‘¦β€πŸ‘¦")[matcher](); expect("πŸ‘¨β€β€οΈβ€πŸ’‹β€πŸ‘¨")[matcher](); }); }