/** * KHAELOR * File: tests/tui/theme.test.ts * Description: Theme tests — capability ladder, NO_COLOR monochrome purity, light/dark variants. * * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai */ import { describe, expect, it } from "vitest"; import { detectColorDepth } from "../../src/tui/renderer/capabilities.js"; import { resolveTheme } from "../../src/tui/theme.js"; import type { StyleRole } from "../../src/tui/theme.js"; const ALL_ROLES: StyleRole[] = [ "text", "dim", "accent", "accentDim", "success", "warning", "error", "code", "violet", "cyan", "magenta", "teal", "orange", "bold", "italic", "strike", "underline", "invert", "synKeyword", "synString", "synComment", "synNumber", "synFunction", "synType", ]; describe("resolveTheme", () => { it("mono emits no SGR codes for any role (NO_COLOR honored absolutely)", () => { const mono = resolveTheme({ colorDepth: "mono" }); for (const role of ALL_ROLES) { expect(mono.paint(role, "state")).toBe("state"); } }); it("truecolor emits 24-bit foreground and closes with default-fg", () => { const dark = resolveTheme({ colorDepth: "truecolor" }); const painted = dark.paint("accent", "x"); expect(painted).toMatch(/^\x1b\[38;2;\d+;\d+;\d+mx\x1b\[39m$/); }); it("ansi256 uses indexed colors, ansi16 uses basic codes", () => { expect(resolveTheme({ colorDepth: "ansi256" }).paint("error", "x")).toContain("38;5;"); expect(resolveTheme({ colorDepth: "ansi16" }).paint("error", "x")).toContain("\x1b[31m"); }); it("text role in ansi16 stays the terminal default (no SGR)", () => { expect(resolveTheme({ colorDepth: "ansi16" }).paint("text", "x")).toBe("x"); }); it("format roles use targeted close codes, never a full reset", () => { const dark = resolveTheme({ colorDepth: "truecolor" }); expect(dark.paint("bold", "x")).toBe("\x1b[1mx\x1b[22m"); expect(dark.paint("underline", "x")).toBe("\x1b[4mx\x1b[24m"); expect(dark.paint("bold", "x")).not.toContain("\x1b[0m"); }); it("selects the light variant for light backgrounds", () => { const light = resolveTheme({ colorDepth: "truecolor", background: "light" }); expect(light.name).toBe("khaelor-light"); expect(light.paint("text", "x")).not.toBe( resolveTheme({ colorDepth: "truecolor" }).paint("text", "x"), ); }); it("painting an empty string emits nothing", () => { const dark = resolveTheme({ colorDepth: "truecolor" }); expect(dark.paint("accent", "")).toBe(""); }); }); describe("gradients and tints", () => { it("brand gradient interpolates per character in truecolor and strips clean", () => { const dark = resolveTheme({ colorDepth: "truecolor" }); const painted = dark.paintGradient("brand", "KHAELOR"); const codes = painted.match(/\x1b\[38;2;\d+;\d+;\d+m/g) ?? []; expect(codes.length).toBe(7); // one 24-bit color per character expect(new Set(codes).size).toBeGreaterThan(1); // actually a gradient expect(painted.replace(/\x1b\[[0-9;]*m/g, "")).toBe("KHAELOR"); }); it("gradient degrades to a solid brand color on ansi256 and to identity on mono", () => { const c256 = resolveTheme({ colorDepth: "ansi256" }).paintGradient("brand", "KHAELOR"); expect(c256.match(/38;5;/g)?.length).toBe(1); expect(resolveTheme({ colorDepth: "mono" }).paintGradient("brand", "KHAELOR")).toBe("KHAELOR"); }); it("line tints are truecolor-only backgrounds that close with bg-reset", () => { const dark = resolveTheme({ colorDepth: "truecolor" }); expect(dark.tintLine("add", "x")).toMatch(/^\x1b\[48;2;\d+;\d+;\d+mx\x1b\[49m$/); expect(resolveTheme({ colorDepth: "ansi256" }).tintLine("add", "x")).toBe("x"); expect(resolveTheme({ colorDepth: "mono" }).tintLine("del", "x")).toBe("x"); }); }); describe("detectColorDepth", () => { it("NO_COLOR and TERM=dumb force mono; non-TTY is mono", () => { expect(detectColorDepth({ NO_COLOR: "1", COLORTERM: "truecolor" }, true)).toBe("mono"); expect(detectColorDepth({ TERM: "dumb" }, true)).toBe("mono"); expect(detectColorDepth({ COLORTERM: "truecolor", TERM: "xterm" }, false)).toBe("mono"); }); it("walks the ladder: truecolor → 256 → 16", () => { expect(detectColorDepth({ COLORTERM: "truecolor", TERM: "xterm-256color" }, true)).toBe( "truecolor", ); expect(detectColorDepth({ TERM: "xterm-256color" }, true)).toBe("ansi256"); expect(detectColorDepth({ TERM: "xterm" }, true)).toBe("ansi16"); }); });