// @vitest-environment node import { renderToStaticMarkup } from "react-dom/server"; import { describe, expect, it } from "vitest"; import { StatusGlyph } from "./StatusGlyph"; import { taskStatusIconVar } from "../lib/status-colors"; /** * The unified status glyph renders every status from ONE Lucide icon per status * (all on a shared `viewBox="0 0 24 24"`, at a `sm 14 / md 16 / lg 20` scale), * coloured from the AA-tuned `--status-task-icon-*` vars. These tests lock the * icon mapping, the size scale, the colour wiring and `in_queue`. */ /** Status → the Lucide class token its icon renders (`lucide-`). */ const STATUS_ICON_CLASS: Record = { backlog: "lucide-circle-dashed", todo: "lucide-circle", in_progress: "lucide-task-progress-spinner", in_review: "lucide-circle-dot", done: "lucide-circle-check", blocked: "lucide-circle-minus", cancelled: "lucide-ban", in_queue: "lucide-circle-minus", }; describe("StatusGlyph", () => { it("renders one 24-unit viewBox for every status (proportional scaling)", () => { for (const status of Object.keys(taskStatusIconVar)) { const html = renderToStaticMarkup(); expect(html).toContain('viewBox="0 0 24 24"'); expect(html).toContain('stroke-width="2"'); expect(html).toContain(" { for (const status of Object.keys(taskStatusIconVar)) { expect(renderToStaticMarkup()).toContain('width="14"'); expect(renderToStaticMarkup()).toContain('width="16"'); expect(renderToStaticMarkup()).toContain('width="20"'); } // Default size is md. expect(renderToStaticMarkup()).toContain('width="16"'); }); it("colours each status from its --status-task-icon-* var", () => { for (const [status, cssVar] of Object.entries(taskStatusIconVar)) { const html = renderToStaticMarkup(); expect(html).toContain(`var(${cssVar})`); } }); it("falls back to the backlog icon + var for unknown statuses", () => { const html = renderToStaticMarkup(); expect(html).toContain("var(--status-task-icon-backlog)"); expect(html).toContain("lucide-circle-dashed"); }); it("maps each status to its Lucide icon", () => { for (const [status, iconClass] of Object.entries(STATUS_ICON_CLASS)) { const html = renderToStaticMarkup(); expect(html).toContain(iconClass); } }); it("animates only in-progress task icons and respects reduced motion", () => { for (const status of Object.keys(taskStatusIconVar)) { const html = renderToStaticMarkup(); expect(html.includes("motion-safe:animate-spin")).toBe(status === "in_progress"); } }); it("uses the same circle radius and stroke for the spinner as other task icons", () => { for (const status of ["in_progress", "todo", "done", "blocked", "cancelled"]) { const html = renderToStaticMarkup(); expect(html).toContain('); expect(spinner).toContain('pathLength="100"'); expect(spinner).toContain('stroke-dasharray="80 20"'); }); it("gives todo the plain circle (not a compound circle icon)", () => { const html = renderToStaticMarkup(); expect(html).toContain("lucide-circle"); for (const compound of ["circle-dashed", "circle-dot", "circle-check", "circle-minus"]) { expect(html).not.toContain(compound); } }); it("renders in_queue as the blocked icon recoloured blue (in_queue var)", () => { const queue = renderToStaticMarkup(); const blocked = renderToStaticMarkup(); // Same icon as blocked (circle-minus)… expect(queue).toContain("lucide-circle-minus"); // …but coloured from the in_queue (blue) icon var, not blocked's red. expect(queue).toContain("var(--status-task-icon-in_queue)"); expect(queue).not.toContain("var(--status-task-icon-blocked)"); expect(blocked).toContain("var(--status-task-icon-blocked)"); }); it("is decorative by default and labelled when given a title", () => { expect(renderToStaticMarkup()).toContain('aria-hidden="true"'); const labelled = renderToStaticMarkup(); expect(labelled).toContain('role="img"'); expect(labelled).toContain('aria-label="Todo"'); expect(labelled).toContain("Todo"); }); });