┌─ TS ───────────────────────────────────────────────────────────────────────┐
│ import { describe, expect, it } from "vitest"; │
│ import { buildAsciiTable, buildTransposedTable, type Cell } from "./ascii-table"; │
│ import { len } from "./ascii"; │
│ │
│ const cell = (text: string, align: Cell["align"] = "end"): Cell => ({ text, align │
│ }); │
│ │
│ const HEADER = [cell("NAME"), cell("VALUE")]; │
│ │
│ describe("buildAsciiTable", () => { │
│ it("emits every line at the same width, within the target", () => { │
│ const t = buildAsciiTable([HEADER, [cell("a"), cell("b")]], 1, 40); │
│ const widths = new Set(t.split("\n").map(len)); │
│ expect(widths.size).toBe(1); │
│ expect([...widths][0]).toBeLessThanOrEqual(40); │
│ }); │
│ │
│ it("keeps single-line tables compact (no row separators)", () => { │
│ const t = buildAsciiTable([HEADER, [cell("a"), cell("b")], [cell("c"), cell("d │
│ ")]], 1, 40); │
│ expect(t).not.toContain("╌"); │
│ // one solid separator under the header only │
│ expect(t.split("\n").filter((l) => l.startsWith("├")).length).toBe(1); │
│ }); │
│ │
│ it("adds dashed separators between body rows once any cell wraps", () => { │
│ const long = "a description long enough that it must wrap onto several lines"; │
│ const t = buildAsciiTable( │
│ [HEADER, [cell("frame"), cell(long)], [cell("select"), cell("short")]], │
│ 1, │
│ 30, │
│ ); │
│ expect(t).toContain("╌"); │
│ const dashed = t.split("\n").filter((l) => l.includes("╌")); │
│ expect(dashed.length).toBe(1); // between the two body rows │
│ }); │
│ │
│ it("right-aligns cells marked align start", () => { │
│ const t = buildAsciiTable([HEADER, [cell("x"), cell("42", "start")]], 1, 40); │
│ const row = t.split("\n").find((l) => l.includes("42"))!; │
│ expect(row).toMatch(/ 42 │$/); │
│ }); │
│ │
│ it("shrinks the widest column until the table fits", () => { │
│ const wide = "word ".repeat(30).trim(); │
│ const t = buildAsciiTable([HEADER, [cell("k"), cell(wide)]], 1, 48); │
│ for (const line of t.split("\n")) expect(len(line)).toBeLessThanOrEqual(48); │
│ }); │
│ │
│ it("pads ragged rows to the full column count", () => { │
│ const t = buildAsciiTable([HEADER, [cell("only-one")]], 1, 40); │
│ const widths = new Set(t.split("\n").map(len)); │
│ expect(widths.size).toBe(1); │
│ }); │
│ }); │
│ │
│ describe("buildTransposedTable", () => { │
│ const rows = [ │
│ [cell("host"), cell("spec"), cell("role"), cell("description"), cell("architec │
│ ture"), cell("status")], │
│ [cell("ooksdesk"), cell("7500F / RX5700XT / 32 GB DDR5"), cell("Workstation"), │
│ cell("Primary desktop workstation"), cell("x86_64"), cell("UP")], │
│ [cell("ooknode"), cell("Linode Nanode"), cell("Server"), cell("VPS for website │
│ "), cell("x86_64"), cell("UP")], │
│ ]; │
│ │
│ it("fits any column count at the narrow width", () => { │
│ const t = buildTransposedTable(rows, 1, 48); │
│ const widths = new Set(t.split("\n").map(len)); │
│ expect(widths.size).toBe(1); │
│ expect([...widths][0]).toBe(48); │
│ }); │
│ │
│ it("keys each row block by the header, separated by dashed rules", () => { │
│ const t = buildTransposedTable(rows, 1, 48); │
│ expect(t.match(/│ host /g)?.length).toBe(2); │
│ expect(t.match(/├╌+┤/g)?.length).toBe(1); │
│ expect(t).toContain("ooksdesk"); │
│ expect(t).toContain("Linode Nanode"); │
│ }); │
│ │
│ it("wraps long values within the value column", () => { │
│ const t = buildTransposedTable(rows, 1, 48); │
│ expect(t).toMatch(/│ spec\s+7500F \/ RX5700XT \//); │
│ }); │
│ │
│ it("hard-splits unbreakable cells instead of overflowing", () => { │
│ const long = [[cell("k")], [cell("x".repeat(120))]]; │
│ const t = buildTransposedTable(long, 1, 30); │
│ for (const line of t.split("\n")) expect(len(line)).toBe(30); │
│ }); │
│ }); │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ TS ─────────────────────────────────┐
│ import { describe, expect, it } from "vitest │
│ "; │
│ import { buildAsciiTable, buildTransposedTab │
│ le, type Cell } from "./ascii-table"; │
│ import { len } from "./ascii"; │
│ │
│ const cell = (text: string, align: Cell["ali │
│ gn"] = "end"): Cell => ({ text, align }); │
│ │
│ const HEADER = [cell("NAME"), cell("VALUE")] │
│ ; │
│ │
│ describe("buildAsciiTable", () => { │
│ it("emits every line at the same width, wi │
│ thin the target", () => { │
│ const t = buildAsciiTable([HEADER, [cell │
│ ("a"), cell("b")]], 1, 40); │
│ const widths = new Set(t.split("\n").map │
│ (len)); │
│ expect(widths.size).toBe(1); │
│ expect([...widths][0]).toBeLessThanOrEqu │
│ al(40); │
│ }); │
│ │
│ it("keeps single-line tables compact (no r │
│ ow separators)", () => { │
│ const t = buildAsciiTable([HEADER, [cell │
│ ("a"), cell("b")], [cell("c"), cell("d")]], │
│ 1, 40); │
│ expect(t).not.toContain("╌"); │
│ // one solid separator under the header │
│ only │
│ expect(t.split("\n").filter((l) => l.sta │
│ rtsWith("├")).length).toBe(1); │
│ }); │
│ │
│ it("adds dashed separators between body ro │
│ ws once any cell wraps", () => { │
│ const long = "a description long enough │
│ that it must wrap onto several lines"; │
│ const t = buildAsciiTable( │
│ [HEADER, [cell("frame"), cell(long)], │
│ [cell("select"), cell("short")]], │
│ 1, │
│ 30, │
│ ); │
│ expect(t).toContain("╌"); │
│ const dashed = t.split("\n").filter((l) │
│ => l.includes("╌")); │
│ expect(dashed.length).toBe(1); // betwee │
│ n the two body rows │
│ }); │
│ │
│ it("right-aligns cells marked align start" │
│ , () => { │
│ const t = buildAsciiTable([HEADER, [cell │
│ ("x"), cell("42", "start")]], 1, 40); │
│ const row = t.split("\n").find((l) => l. │
│ includes("42"))!; │
│ expect(row).toMatch(/ 42 │$/); │
│ }); │
│ │
│ it("shrinks the widest column until the ta │
│ ble fits", () => { │
│ const wide = "word ".repeat(30).trim(); │
│ const t = buildAsciiTable([HEADER, [cell │
│ ("k"), cell(wide)]], 1, 48); │
│ for (const line of t.split("\n")) expect │
│ (len(line)).toBeLessThanOrEqual(48); │
│ }); │
│ │
│ it("pads ragged rows to the full column co │
│ unt", () => { │
│ const t = buildAsciiTable([HEADER, [cell │
│ ("only-one")]], 1, 40); │
│ const widths = new Set(t.split("\n").map │
│ (len)); │
│ expect(widths.size).toBe(1); │
│ }); │
│ }); │
│ │
│ describe("buildTransposedTable", () => { │
│ const rows = [ │
│ [cell("host"), cell("spec"), cell("role" │
│ ), cell("description"), cell("architecture") │
│ , cell("status")], │
│ [cell("ooksdesk"), cell("7500F / RX5700X │
│ T / 32 GB DDR5"), cell("Workstation"), cell( │
│ "Primary desktop workstation"), cell("x86_64 │
│ "), cell("UP")], │
│ [cell("ooknode"), cell("Linode Nanode"), │
│ cell("Server"), cell("VPS for website"), ce │
│ ll("x86_64"), cell("UP")], │
│ ]; │
│ │
│ it("fits any column count at the narrow wi │
│ dth", () => { │
│ const t = buildTransposedTable(rows, 1, │
│ 48); │
│ const widths = new Set(t.split("\n").map │
│ (len)); │
│ expect(widths.size).toBe(1); │
│ expect([...widths][0]).toBe(48); │
│ }); │
│ │
│ it("keys each row block by the header, sep │
│ arated by dashed rules", () => { │
│ const t = buildTransposedTable(rows, 1, │
│ 48); │
│ expect(t.match(/│ host /g)?.length).toBe │
│ (2); │
│ expect(t.match(/├╌+┤/g)?.length).toBe(1) │
│ ; │
│ expect(t).toContain("ooksdesk"); │
│ expect(t).toContain("Linode Nanode"); │
│ }); │
│ │
│ it("wraps long values within the value col │
│ umn", () => { │
│ const t = buildTransposedTable(rows, 1, │
│ 48); │
│ expect(t).toMatch(/│ spec\s+7500F \/ RX5 │
│ 700XT \//); │
│ }); │
│ │
│ it("hard-splits unbreakable cells instead │
│ of overflowing", () => { │
│ const long = [[cell("k")], [cell("x".rep │
│ eat(120))]]; │
│ const t = buildTransposedTable(long, 1, │
│ 30); │
│ for (const line of t.split("\n")) expect │
│ (len(line)).toBe(30); │
│ }); │
│ }); │
└──────────────────────────────────────────────┘