/** * KHAELOR * File: tests/tui/v2-widgets.test.ts * Description: TUI v2 widget tests — gradients, sparkline, verify strip, design panel, splash, motion, doctor (TUI v2). * * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai */ import { describe, expect, it } from "vitest"; import { renderDesignPanel } from "../../src/tui/components/design-panel.js"; import { brailleSparkline } from "../../src/tui/components/sparkline.js"; import { renderVerifyStrip } from "../../src/tui/components/verify-strip.js"; import { doctorReport } from "../../src/tui/doctor.js"; import { MotionController } from "../../src/tui/motion.js"; import { renderSplashFrame, SPLASH_FRAMES } from "../../src/tui/splash.js"; import { gradientText, mixOklch } from "../../src/tui/theme/gradient.js"; import { resolveTheme } from "../../src/tui/theme.js"; const mono = resolveTheme({ colorDepth: "mono" }); const truecolor = resolveTheme({ colorDepth: "truecolor" }); describe("OKLCH gradient", () => { it("interpolates endpoints exactly and midpoints smoothly", () => { expect(mixOklch([255, 107, 53], [255, 184, 107], 0)).toEqual([255, 107, 53]); expect(mixOklch([255, 107, 53], [255, 184, 107], 1)).toEqual([255, 184, 107]); const mid = mixOklch([255, 107, 53], [255, 184, 107], 0.5); expect(mid[0]).toBeGreaterThan(200); // stays warm — no muddy midpoint }); it("emits one truecolor SGR per character and closes the color", () => { const painted = gradientText("ABC", [255, 107, 53], [255, 184, 107]); expect(painted.match(/\x1b\[38;2;/g)).toHaveLength(3); expect(painted.endsWith("\x1b[39m")).toBe(true); }); it("theme.paintGradient('ember', …) is identity in mono", () => { expect(mono.paintGradient("ember", "KHAELOR")).toBe("KHAELOR"); expect(truecolor.paintGradient("ember", "KHAELOR")).toContain("\x1b[38;2;"); }); }); describe("brailleSparkline", () => { it("renders two values per braille cell", () => { expect(brailleSparkline([])).toBe(""); expect(brailleSparkline([1, 2, 3, 4])).toHaveLength(2); const flat = brailleSparkline([5, 5, 5, 5]); expect(flat).toHaveLength(2); expect(flat[0]).toBe(flat[1]); }); it("caps to maxCells keeping the most recent samples", () => { const line = brailleSparkline([1, 2, 3, 4, 5, 6, 7, 8], 2); expect(line).toHaveLength(2); }); }); describe("verify strip", () => { it("shows live check states with a spinner on running checks", () => { const lines = renderVerifyStrip( [ { check: "typecheck", state: "ok", durationMs: 1200 }, { check: "tests", state: "running" }, ], 100, mono, ); expect(lines[0]).toContain("verify"); expect(lines[0]).toContain("typecheck ✓"); expect(lines[0]).toContain("tests ⠧"); }); it("contracts to a single ✓ verified line when everything passes (TUI v2 §5.4)", () => { const lines = renderVerifyStrip( [ { check: "typecheck", state: "ok", durationMs: 1200 }, { check: "tests", state: "ok", durationMs: 4200 }, ], 100, mono, ); expect(lines).toHaveLength(1); expect(lines[0]).toContain("✓ verified 4.2s"); }); it("pins failures with their first error lines", () => { const lines = renderVerifyStrip( [{ check: "tests", state: "failed", errorHead: ["FAIL src/a.test.ts", "expected 2 to be 3"] }], 100, mono, ); expect(lines[0]).toContain("tests ✗"); expect(lines[1]).toContain("FAIL src/a.test.ts"); }); }); describe("design panel", () => { const artifact = { goal: "Fix the renderer memory leak", filesTouched: ["src/tui/render.ts", "src/tui/buffer.ts"], approach: "Replace the growing buffer with a ring buffer.", risks: ["scrollback regression (medium)"], verification: "vitest run tests/tui + manual demo", outOfScope: ["theming"], }; it("renders the full pending panel with approve/reject hints", () => { const lines = renderDesignPanel({ ...artifact, decision: "pending" }, 100, mono); const text = lines.join("\n"); expect(text).toContain("DESIGN — approval required"); expect(text).toContain("Fix the renderer memory leak"); expect(text).toContain("[a]"); expect(text).toContain("[r]"); expect(lines[0]).toContain("╭"); expect(lines[lines.length - 1]).toContain("╰"); }); it("collapses to one line once decided (TUI v2 §5.2)", () => { const approved = renderDesignPanel({ ...artifact, decision: "auto-approved" }, 100, mono); expect(approved).toHaveLength(1); expect(approved[0]).toContain("✓ design auto-approved · 2 files"); const rejected = renderDesignPanel({ ...artifact, decision: "rejected" }, 100, mono); expect(rejected[0]).toContain("✗ design rejected"); }); }); describe("splash", () => { it("renders the wordmark and settles on the final gradient frame", () => { const first = renderSplashFrame(0, true); const last = renderSplashFrame(SPLASH_FRAMES - 1, true); expect(first.length).toBe(last.length); // The tagline is gradient-painted per character — strip SGR before matching. const plain = last.join("\n").replace(/\x1b\[[0-9;]*m/g, ""); expect(plain).toContain("understand · design · implement"); expect(last.join("\n")).toContain("\x1b[38;2;"); const monoFrame = renderSplashFrame(0, false); expect(monoFrame.join("\n")).not.toContain("\x1b["); }); }); describe("motion controller", () => { it("advances effects only on tick and drops finished transitions", () => { const motion = new MotionController({ enabled: true }); motion.start("phase", "phase-flash"); expect(motion.frame("phase")).toBe(0); motion.tick(); expect(motion.frame("phase")).toBe(1); motion.tick(); motion.tick(); expect(motion.frame("phase")).toBeNull(); // 3 frames max }); it("--motion off reduces everything to static instantly", () => { const motion = new MotionController({ enabled: false }); motion.start("panel", "panel-breath"); expect(motion.frame("panel")).toBeNull(); expect(motion.breathIntensity("panel")).toBe(0); }); }); describe("doctor-tui", () => { it("reports detected capabilities with reasons", () => { const report = doctorReport( { TERM: "xterm-256color", COLORTERM: "truecolor", TERM_PROGRAM: "iTerm.app", LANG: "en_US.UTF-8" }, true, 120, ); const text = report.join("\n"); expect(text).toContain("truecolor"); expect(text).toContain("utf-8"); expect(text).toContain("osc8 links"); expect(text).toContain("khaelis"); }); });