import { describe, expect, test } from "bun:test"; import { escapeRegExp, capitalize, plural, firstLineOf, countCharInString, normalizeFullWidthDigits, normalizeFullWidthSpace, safeJoinLines, EndTruncatingAccumulator, truncateToLines, } from "../stringUtils"; describe("escapeRegExp", () => { test("escapes special regex chars", () => { expect(escapeRegExp("a.b*c?d")).toBe("a\\.b\\*c\\?d"); }); test("escapes brackets and parens", () => { expect(escapeRegExp("[foo](bar)")).toBe("\\[foo\\]\\(bar\\)"); }); test("escapes all special chars", () => { expect(escapeRegExp("^${}()|[]\\.*+?")).toBe( "\\^\\$\\{\\}\\(\\)\\|\\[\\]\\\\\\.\\*\\+\\?" ); }); test("returns normal string unchanged", () => { expect(escapeRegExp("hello")).toBe("hello"); }); }); describe("capitalize", () => { test("uppercases first char", () => { expect(capitalize("hello")).toBe("Hello"); }); test("does NOT lowercase rest", () => { expect(capitalize("fooBar")).toBe("FooBar"); }); test("handles single char", () => { expect(capitalize("a")).toBe("A"); }); test("handles empty string", () => { expect(capitalize("")).toBe(""); }); }); describe("plural", () => { test("returns singular for 1", () => { expect(plural(1, "file")).toBe("file"); }); test("returns plural for 0", () => { expect(plural(0, "file")).toBe("files"); }); test("returns plural for many", () => { expect(plural(3, "file")).toBe("files"); }); test("uses custom plural form", () => { expect(plural(2, "entry", "entries")).toBe("entries"); }); }); describe("firstLineOf", () => { test("returns first line of multiline string", () => { expect(firstLineOf("line1\nline2\nline3")).toBe("line1"); }); test("returns whole string if no newline", () => { expect(firstLineOf("single line")).toBe("single line"); }); test("returns empty string for leading newline", () => { expect(firstLineOf("\nline2")).toBe(""); }); }); describe("countCharInString", () => { test("counts occurrences of a character", () => { expect(countCharInString("hello world", "l")).toBe(3); }); test("returns 0 for no match", () => { expect(countCharInString("hello", "z")).toBe(0); }); test("counts from start offset", () => { expect(countCharInString("aabaa", "a", 2)).toBe(2); }); test("returns 0 for empty string", () => { expect(countCharInString("", "a")).toBe(0); }); }); describe("normalizeFullWidthDigits", () => { test("converts full-width digits to half-width", () => { expect(normalizeFullWidthDigits("0123456789")).toBe("0123456789"); }); test("leaves half-width digits unchanged", () => { expect(normalizeFullWidthDigits("0123")).toBe("0123"); }); test("handles mixed content", () => { expect(normalizeFullWidthDigits("test123")).toBe("test123"); }); }); describe("normalizeFullWidthSpace", () => { test("converts full-width space to half-width", () => { expect(normalizeFullWidthSpace("a\u3000b")).toBe("a b"); }); test("leaves normal spaces unchanged", () => { expect(normalizeFullWidthSpace("a b")).toBe("a b"); }); }); describe("safeJoinLines", () => { test("joins lines with delimiter", () => { expect(safeJoinLines(["a", "b", "c"], ",")).toBe("a,b,c"); }); test("truncates when exceeding maxSize", () => { const result = safeJoinLines(["hello", "world", "foo"], ",", 12); expect(result.length).toBeLessThanOrEqual(12 + "...[truncated]".length); expect(result).toContain("...[truncated]"); }); test("returns empty string for empty input", () => { expect(safeJoinLines([])).toBe(""); }); }); describe("EndTruncatingAccumulator", () => { test("accumulates text", () => { const acc = new EndTruncatingAccumulator(100); acc.append("hello "); acc.append("world"); expect(acc.toString()).toBe("hello world"); }); test("truncates when exceeding maxSize", () => { const acc = new EndTruncatingAccumulator(10); acc.append("12345678901234567890"); expect(acc.truncated).toBe(true); expect(acc.length).toBe(10); }); test("reports total bytes received", () => { const acc = new EndTruncatingAccumulator(5); acc.append("1234567890"); expect(acc.totalBytes).toBe(10); }); test("clear resets state", () => { const acc = new EndTruncatingAccumulator(100); acc.append("hello"); acc.clear(); expect(acc.toString()).toBe(""); expect(acc.length).toBe(0); expect(acc.truncated).toBe(false); }); test("stops accepting data once truncated and full", () => { const acc = new EndTruncatingAccumulator(5); acc.append("12345"); acc.append("67890"); expect(acc.length).toBe(5); acc.append("more"); expect(acc.length).toBe(5); }); }); describe("truncateToLines", () => { test("returns text unchanged if within limit", () => { expect(truncateToLines("a\nb\nc", 5)).toBe("a\nb\nc"); }); test("truncates text exceeding limit", () => { expect(truncateToLines("a\nb\nc\nd\ne", 3)).toBe("a\nb\nc…"); }); test("handles single line", () => { expect(truncateToLines("hello", 1)).toBe("hello"); }); });