import { afterEach, beforeEach, describe, expect, it, vi } from "bun:test"; import { makeMockTUI, makeTheme } from "../components/components.fixtures"; import type { ToolStat } from "../types"; import { Usage } from "./Usage"; describe("Usage", () => { const mockTui = makeMockTUI(); const tokenUsage = { total: 10000, input: 5000, output: 4000, cacheRead: 500, cacheWrite: 500, }; const tools: ToolStat[] = [ { name: "bash", count: 150 }, { name: "read", count: 120 }, { name: "edit", count: 45 }, ]; it("renders token section with StatCards", () => { const tab = new Usage(tools, tokenUsage, makeTheme(), mockTui, 10); const text = tab.render(80).join("\n"); expect(text).toContain("Tokens"); expect(text).toContain("10k"); expect(text).toContain("Input"); expect(text).toContain("5k"); expect(text).toContain("Output"); expect(text).toContain("4k"); expect(text).toContain("Cache Read"); expect(text).toContain("500"); expect(text).toContain("Cache Write"); }); it("renders tool table with formatted data", () => { const tab = new Usage(tools, tokenUsage, makeTheme(), mockTui, 10); const lines = tab.render(80).slice(4); const text = lines.join("\n"); expect(lines[0]).toContain("Tools"); // Headers expect(text).toContain("Command"); expect(text).toContain("Calls"); expect(text).toContain("Share %"); // Tool names expect(text).toContain("bash"); expect(text).toContain("read"); expect(text).toContain("edit"); // Call counts expect(text).toContain("150"); expect(text).toContain("120"); expect(text).toContain("45"); }); it("shows empty state when tools is empty", () => { const tab = new Usage([], tokenUsage, makeTheme(), mockTui, 10); const lines = tab.render(80).slice(4); const text = lines.join("\n"); expect(lines[0]).toContain("Tools"); expect(text).toContain("No tools data for this time range"); }); it("renders tool names with control characters as single lines", () => { const dirtyTools: ToolStat[] = [ { name: "ls -la agent/\n { const tab = new Usage(tools, tokenUsage, makeTheme(), mockTui, 10); const lines = tab.render(50); for (const line of lines) { const visLen = line.replace(/\x1b\[[0-9;]*m/g, "").length; expect(visLen).toBeLessThanOrEqual(50); } }); it("shows sort indicator on Calls column", () => { const tab = new Usage(tools, tokenUsage, makeTheme(), mockTui, 10); const lines = tab.render(80); const text = lines.join("\n"); expect(text).toContain("Calls ▼"); }); it("invalidates render cache", () => { const tab = new Usage(tools, tokenUsage, makeTheme(), mockTui, 10); tab.render(80); tab.invalidate(); const lines = tab.render(60); for (const line of lines) { const visLen = line.replace(/\x1b\[[0-9;]*m/g, "").length; expect(visLen).toBeLessThanOrEqual(60); } }); it("supports re-render after invalidation (lifecycle path)", () => { const tab = new Usage(tools, tokenUsage, makeTheme(), mockTui, 10); const lines1 = tab.render(80); expect(lines1.join("\n")).toContain("bash"); tab.invalidate(); const lines2 = tab.render(80); const text = lines2.join("\n"); expect(text).toContain("bash"); expect(text).toContain("Command"); expect(text).toContain("Calls ▼"); // Token section still intact expect(text).toContain("5k"); expect(text).toContain("4k"); for (const line of lines2) { const visLen = line.replace(/\x1b\[[0-9;]*m/g, "").length; expect(visLen).toBeLessThanOrEqual(80); } }); describe("marquee lifecycle", () => { beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); }); it("clears marquee timers on invalidate", () => { const longTool: ToolStat[] = [{ name: "a-very-long-tool-name-x", count: 150 }]; const tab = new Usage(longTool, tokenUsage, makeTheme(), mockTui, 10); tab.render(30); expect(vi.getTimerCount()).toBe(1); tab.invalidate(); expect(vi.getTimerCount()).toBe(0); const lines = tab.render(80); const text = lines.join("\n"); expect(text).toContain("a-very-long-tool"); }); }); });