|
@@ -0,0 +1,195 @@
|
|
|
|
|
+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");
|
|
|
|
|
+ });
|
|
|
|
|
+});
|