| import { describe, expect, it } from "vitest"; |
|
|
| import { visibleWidth } from "./ansi.js"; |
| import { renderTable } from "./table.js"; |
|
|
| describe("renderTable", () => { |
| it("prefers shrinking flex columns to avoid wrapping non-flex labels", () => { |
| const out = renderTable({ |
| width: 40, |
| columns: [ |
| { key: "Item", header: "Item", minWidth: 10 }, |
| { key: "Value", header: "Value", flex: true, minWidth: 24 }, |
| ], |
| rows: [{ Item: "Dashboard", Value: "http://127.0.0.1:7860/" }], |
| }); |
|
|
| expect(out).toContain("Dashboard"); |
| expect(out).toMatch(/│ Dashboard\s+│/); |
| }); |
|
|
| it("expands flex columns to fill available width", () => { |
| const width = 60; |
| const out = renderTable({ |
| width, |
| columns: [ |
| { key: "Item", header: "Item", minWidth: 10 }, |
| { key: "Value", header: "Value", flex: true, minWidth: 24 }, |
| ], |
| rows: [{ Item: "OS", Value: "macos 26.2 (arm64)" }], |
| }); |
|
|
| const firstLine = out.trimEnd().split("\n")[0] ?? ""; |
| expect(visibleWidth(firstLine)).toBe(width); |
| }); |
|
|
| it("wraps ANSI-colored cells without corrupting escape sequences", () => { |
| const out = renderTable({ |
| width: 36, |
| columns: [ |
| { key: "K", header: "K", minWidth: 3 }, |
| { key: "V", header: "V", flex: true, minWidth: 10 }, |
| ], |
| rows: [ |
| { |
| K: "X", |
| V: `\x1b[33m${"a".repeat(120)}\x1b[0m`, |
| }, |
| ], |
| }); |
|
|
| const ESC = "\u001b"; |
| for (let i = 0; i < out.length; i += 1) { |
| if (out[i] !== ESC) { |
| continue; |
| } |
|
|
| |
| if (out[i + 1] === "[") { |
| let j = i + 2; |
| while (j < out.length) { |
| const ch = out[j]; |
| if (ch === "m") { |
| break; |
| } |
| if (ch && ch >= "0" && ch <= "9") { |
| j += 1; |
| continue; |
| } |
| if (ch === ";") { |
| j += 1; |
| continue; |
| } |
| break; |
| } |
| expect(out[j]).toBe("m"); |
| i = j; |
| continue; |
| } |
|
|
| |
| if (out[i + 1] === "]" && out.slice(i + 2, i + 5) === "8;;") { |
| const st = out.indexOf(`${ESC}\\`, i + 5); |
| expect(st).toBeGreaterThanOrEqual(0); |
| i = st + 1; |
| continue; |
| } |
|
|
| throw new Error(`Unexpected escape sequence at index ${i}`); |
| } |
| }); |
|
|
| it("resets ANSI styling on wrapped lines", () => { |
| const reset = "\x1b[0m"; |
| const out = renderTable({ |
| width: 24, |
| columns: [ |
| { key: "K", header: "K", minWidth: 3 }, |
| { key: "V", header: "V", flex: true, minWidth: 10 }, |
| ], |
| rows: [ |
| { |
| K: "X", |
| V: `\x1b[31m${"a".repeat(80)}${reset}`, |
| }, |
| ], |
| }); |
|
|
| const lines = out.split("\n").filter((line) => line.includes("a")); |
| for (const line of lines) { |
| const resetIndex = line.lastIndexOf(reset); |
| const lastSep = line.lastIndexOf("│"); |
| expect(resetIndex).toBeGreaterThan(-1); |
| expect(lastSep).toBeGreaterThan(resetIndex); |
| } |
| }); |
|
|
| it("respects explicit newlines in cell values", () => { |
| const out = renderTable({ |
| width: 48, |
| columns: [ |
| { key: "A", header: "A", minWidth: 6 }, |
| { key: "B", header: "B", minWidth: 10, flex: true }, |
| ], |
| rows: [{ A: "row", B: "line1\nline2" }], |
| }); |
|
|
| const lines = out.trimEnd().split("\n"); |
| const line1Index = lines.findIndex((line) => line.includes("line1")); |
| const line2Index = lines.findIndex((line) => line.includes("line2")); |
| expect(line1Index).toBeGreaterThan(-1); |
| expect(line2Index).toBe(line1Index + 1); |
| }); |
| }); |
|
|