/** * Terminal Component Tests * * Tests the pure helper functions extracted from the Terminal component: * - getLineClass: maps line types to CSS class names * - getLinePrefix: maps line types to display prefixes * - formatTime: formats a Date as "HH:MM AM/PM" * - isToolContentLong: checks if tool content exceeds collapse threshold * - truncateToolContent: truncates long tool content with ellipsis * * Manual testing checklist: * - [ ] rate-limit lines appear in amber * - [ ] error lines appear in red * - [ ] tool lines appear in purple * - [ ] system lines appear in grey italic * - [ ] user lines appear in cyan * - [ ] assistant lines appear in primary text colour * - [ ] long tool content is collapsed by default with a toggle button */ import { describe, it, expect } from "vitest"; // Mirror functions from Terminal.svelte for isolated testing function getLineClass(type: string): string { switch (type) { case "user": return "terminal-user"; case "assistant": return "terminal-assistant"; case "system": return "terminal-system italic"; case "tool": return "terminal-tool"; case "error": return "terminal-error"; case "thinking": return "terminal-thinking"; case "rate-limit": return "terminal-rate-limit"; case "compact-prompt": return "terminal-compact-prompt"; case "worktree": return "terminal-worktree"; case "config-change": return "terminal-config-change"; default: return "terminal-default"; } } function getLinePrefix(type: string): string { switch (type) { case "user": return ">"; case "assistant": return ""; case "system": return "[system]"; case "tool": return "[tool]"; case "error": return "[error]"; case "rate-limit": return "[rate-limit]"; case "worktree": return "[worktree]"; case "config-change": return "[config]"; default: return ""; } } function formatTime(date: Date): string { return date.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", }); } const TOOL_COLLAPSE_THRESHOLD = 60; function isToolContentLong(content: string): boolean { return content.length > TOOL_COLLAPSE_THRESHOLD; } function truncateToolContent(content: string): string { return content.slice(0, TOOL_COLLAPSE_THRESHOLD) + "…"; } // Virtual windowing helpers — mirror the logic in Terminal.svelte const WINDOW_SIZE = 150; const LOAD_CHUNK = 50; const AVG_LINE_HEIGHT = 60; /** Returns the windowStart index when auto-scrolling to the bottom. */ function autoScrollWindowStart(linesLength: number, windowSize: number): number { return Math.max(0, linesLength - windowSize); } /** Returns the new windowStart after loading LOAD_CHUNK older messages. */ function olderWindowStart(currentStart: number, chunkSize: number): number { return Math.max(0, currentStart - chunkSize); } /** Returns the height in pixels of the invisible top spacer. */ function topSpacerHeight(windowStart: number, avgLineHeight: number): number { return windowStart * avgLineHeight; } // --- describe("getLineClass", () => { it("returns terminal-user for user lines", () => { expect(getLineClass("user")).toBe("terminal-user"); }); it("returns terminal-assistant for assistant lines", () => { expect(getLineClass("assistant")).toBe("terminal-assistant"); }); it("returns terminal-system italic for system lines", () => { expect(getLineClass("system")).toBe("terminal-system italic"); }); it("returns terminal-tool for tool lines", () => { expect(getLineClass("tool")).toBe("terminal-tool"); }); it("returns terminal-error for error lines", () => { expect(getLineClass("error")).toBe("terminal-error"); }); it("returns terminal-thinking for thinking lines", () => { expect(getLineClass("thinking")).toBe("terminal-thinking"); }); it("returns terminal-rate-limit for rate-limit lines", () => { expect(getLineClass("rate-limit")).toBe("terminal-rate-limit"); }); it("returns terminal-compact-prompt for compact-prompt lines", () => { expect(getLineClass("compact-prompt")).toBe("terminal-compact-prompt"); }); it("returns terminal-worktree for worktree lines", () => { expect(getLineClass("worktree")).toBe("terminal-worktree"); }); it("returns terminal-config-change for config-change lines", () => { expect(getLineClass("config-change")).toBe("terminal-config-change"); }); it("returns terminal-default for unknown line types", () => { expect(getLineClass("unknown")).toBe("terminal-default"); expect(getLineClass("")).toBe("terminal-default"); expect(getLineClass("random-future-type")).toBe("terminal-default"); }); }); describe("getLinePrefix", () => { it("returns > for user lines", () => { expect(getLinePrefix("user")).toBe(">"); }); it("returns empty string for assistant lines", () => { expect(getLinePrefix("assistant")).toBe(""); }); it("returns [system] for system lines", () => { expect(getLinePrefix("system")).toBe("[system]"); }); it("returns [tool] for tool lines", () => { expect(getLinePrefix("tool")).toBe("[tool]"); }); it("returns [error] for error lines", () => { expect(getLinePrefix("error")).toBe("[error]"); }); it("returns [rate-limit] for rate-limit lines", () => { expect(getLinePrefix("rate-limit")).toBe("[rate-limit]"); }); it("returns empty string for compact-prompt lines (button renders instead)", () => { expect(getLinePrefix("compact-prompt")).toBe(""); }); it("returns [worktree] for worktree lines", () => { expect(getLinePrefix("worktree")).toBe("[worktree]"); }); it("returns [config] for config-change lines", () => { expect(getLinePrefix("config-change")).toBe("[config]"); }); it("returns empty string for thinking lines (no prefix)", () => { expect(getLinePrefix("thinking")).toBe(""); }); it("returns empty string for unknown line types", () => { expect(getLinePrefix("unknown")).toBe(""); expect(getLinePrefix("")).toBe(""); }); }); describe("formatTime", () => { it("formats time in 12-hour format with AM/PM", () => { const date = new Date(2026, 1, 7, 14, 35); const formatted = formatTime(date); expect(formatted).toMatch(/\d{2}:\d{2}\s?(AM|PM)/i); }); it("formats afternoon times correctly", () => { const date = new Date(2026, 1, 7, 14, 35); const formatted = formatTime(date); expect(formatted).toContain("02:35"); expect(formatted.toUpperCase()).toContain("PM"); }); it("formats morning times correctly", () => { const date = new Date(2026, 1, 7, 9, 5); const formatted = formatTime(date); expect(formatted).toContain("09:05"); expect(formatted.toUpperCase()).toContain("AM"); }); it("formats midnight correctly", () => { const date = new Date(2026, 1, 7, 0, 0); const formatted = formatTime(date); expect(formatted).toContain("12:00"); expect(formatted.toUpperCase()).toContain("AM"); }); it("formats noon correctly", () => { const date = new Date(2026, 1, 7, 12, 0); const formatted = formatTime(date); expect(formatted).toContain("12:00"); expect(formatted.toUpperCase()).toContain("PM"); }); }); describe("isToolContentLong", () => { it("returns false for content at or below the threshold", () => { const exactThreshold = "x".repeat(TOOL_COLLAPSE_THRESHOLD); expect(isToolContentLong(exactThreshold)).toBe(false); }); it("returns true for content exceeding the threshold", () => { const overThreshold = "x".repeat(TOOL_COLLAPSE_THRESHOLD + 1); expect(isToolContentLong(overThreshold)).toBe(true); }); it("returns false for short content", () => { expect(isToolContentLong("short")).toBe(false); }); it("returns false for empty content", () => { expect(isToolContentLong("")).toBe(false); }); }); describe("truncateToolContent", () => { it("truncates content to the threshold length with an ellipsis", () => { const long = "x".repeat(100); const result = truncateToolContent(long); expect(result).toBe("x".repeat(TOOL_COLLAPSE_THRESHOLD) + "…"); }); it("keeps content shorter than threshold unchanged (plus ellipsis)", () => { const short = "hello"; const result = truncateToolContent(short); expect(result).toBe("hello…"); }); it("uses the unicode ellipsis character (not three dots)", () => { const long = "x".repeat(100); const result = truncateToolContent(long); expect(result.endsWith("…")).toBe(true); expect(result.endsWith("...")).toBe(false); }); }); describe("autoScrollWindowStart", () => { it("returns 0 when lines fit within the window", () => { expect(autoScrollWindowStart(50, WINDOW_SIZE)).toBe(0); }); it("returns 0 when lines exactly fill the window", () => { expect(autoScrollWindowStart(WINDOW_SIZE, WINDOW_SIZE)).toBe(0); }); it("advances when lines exceed the window size", () => { expect(autoScrollWindowStart(200, WINDOW_SIZE)).toBe(50); }); it("never returns a negative value", () => { expect(autoScrollWindowStart(0, WINDOW_SIZE)).toBe(0); }); it("keeps last WINDOW_SIZE lines visible for large collections", () => { expect(autoScrollWindowStart(500, WINDOW_SIZE)).toBe(350); }); }); describe("olderWindowStart", () => { it("subtracts the chunk size from the current start", () => { expect(olderWindowStart(100, LOAD_CHUNK)).toBe(50); }); it("never returns a negative value when chunk is larger than start", () => { expect(olderWindowStart(20, LOAD_CHUNK)).toBe(0); }); it("returns 0 when current start is 0", () => { expect(olderWindowStart(0, LOAD_CHUNK)).toBe(0); }); it("returns 0 when current start exactly equals the chunk size", () => { expect(olderWindowStart(LOAD_CHUNK, LOAD_CHUNK)).toBe(0); }); it("correctly loads a partial chunk near the beginning", () => { expect(olderWindowStart(30, LOAD_CHUNK)).toBe(0); }); }); describe("topSpacerHeight", () => { it("returns 0 when windowStart is 0", () => { expect(topSpacerHeight(0, AVG_LINE_HEIGHT)).toBe(0); }); it("multiplies windowStart by avgLineHeight", () => { expect(topSpacerHeight(10, AVG_LINE_HEIGHT)).toBe(600); }); it("scales linearly with windowStart", () => { expect(topSpacerHeight(50, AVG_LINE_HEIGHT)).toBe(3000); expect(topSpacerHeight(100, AVG_LINE_HEIGHT)).toBe(6000); expect(topSpacerHeight(150, AVG_LINE_HEIGHT)).toBe(9000); }); it("uses the provided avgLineHeight rather than a hard-coded value", () => { expect(topSpacerHeight(5, 100)).toBe(500); expect(topSpacerHeight(5, 80)).toBe(400); }); }); describe("virtual windowing constants", () => { it("WINDOW_SIZE is 150", () => { expect(WINDOW_SIZE).toBe(150); }); it("LOAD_CHUNK is 50", () => { expect(LOAD_CHUNK).toBe(50); }); it("LOAD_CHUNK is smaller than WINDOW_SIZE", () => { expect(LOAD_CHUNK).toBeLessThan(WINDOW_SIZE); }); it("AVG_LINE_HEIGHT is a positive number", () => { expect(AVG_LINE_HEIGHT).toBeGreaterThan(0); }); });