import { describe, it, expect } from "vitest"; import { render, screen, fireEvent } from "@testing-library/react"; import { AgentLogViewer } from "../AgentLogViewer"; import type { AgentLogEntry } from "@kb/core"; function makeEntry(overrides: Partial = {}): AgentLogEntry { return { timestamp: "2026-01-01T00:00:00Z", taskId: "KB-001", text: "Hello world", type: "text", ...overrides, }; } describe("AgentLogViewer", () => { it("shows loading message when loading with no entries", () => { render(); expect(screen.getByText("Loading agent logs…")).toBeTruthy(); }); it("shows empty message when no entries and not loading", () => { render(); expect(screen.getByText("No agent output yet.")).toBeTruthy(); }); it("renders text entries as spans", () => { const entries = [ makeEntry({ text: "first chunk" }), makeEntry({ text: "second chunk" }), ]; const { container } = render(); const textSpans = container.querySelectorAll(".agent-log-text"); expect(textSpans).toHaveLength(2); expect(textSpans[0].textContent).toBe("first chunk"); expect(textSpans[1].textContent).toBe("second chunk"); }); it("renders tool entries with distinct styling", () => { const entries = [ makeEntry({ text: "Read", type: "tool" }), ]; const { container } = render(); const toolDiv = container.querySelector(".agent-log-tool"); expect(toolDiv).toBeTruthy(); expect(toolDiv!.textContent).toContain("Read"); }); it("renders a mix of text and tool entries", () => { const entries = [ makeEntry({ text: "Starting...", type: "text" }), makeEntry({ text: "Bash", type: "tool" }), makeEntry({ text: "Done!", type: "text" }), ]; const { container } = render(); expect(container.querySelectorAll(".agent-log-text")).toHaveLength(2); expect(container.querySelectorAll(".agent-log-tool")).toHaveLength(1); }); it("renders tool entry detail when present", () => { const entries = [ makeEntry({ text: "Bash", type: "tool", detail: "ls -la packages/" }), ]; const { container } = render(); const detail = container.querySelector(".agent-log-tool-detail"); expect(detail).toBeTruthy(); expect(detail!.textContent).toContain("ls -la packages/"); }); it("does not render detail span when detail is absent", () => { const entries = [ makeEntry({ text: "Bash", type: "tool" }), ]; const { container } = render(); const detail = container.querySelector(".agent-log-tool-detail"); expect(detail).toBeNull(); }); it("renders long detail text without breaking layout", () => { const longDetail = "a/very/long/path/".repeat(10) + "file.ts"; const entries = [ makeEntry({ text: "Read", type: "tool", detail: longDetail }), ]; const { container } = render(); const detail = container.querySelector(".agent-log-tool-detail"); expect(detail).toBeTruthy(); expect(detail!.textContent).toContain(longDetail); // Verify the tool div still renders correctly const toolDiv = container.querySelector(".agent-log-tool"); expect(toolDiv).toBeTruthy(); }); it("has a monospace font family", () => { const entries = [makeEntry()]; const { container } = render(); const viewer = container.querySelector("[data-testid='agent-log-viewer']") as HTMLElement; expect(viewer.style.fontFamily).toBe("monospace"); }); it("auto-scrolls to the bottom by default when entries are present", () => { const entries = [makeEntry({ text: "line 1" }), makeEntry({ text: "line 2" })]; const { container } = render(); const viewer = container.querySelector("[data-testid='agent-log-viewer']") as HTMLElement; // After render with autoScroll=true, scrollTop should be set to scrollHeight expect(viewer.scrollTop).toBe(viewer.scrollHeight); }); it("disables auto-scroll when user scrolls up", () => { const entries = [makeEntry({ text: "line 1" })]; const { container, rerender } = render( , ); const viewer = container.querySelector("[data-testid='agent-log-viewer']") as HTMLElement; // Simulate an initial scroll position then scroll up Object.defineProperty(viewer, "scrollTop", { value: 100, writable: true, configurable: true }); fireEvent.scroll(viewer); // Now scroll up (scrollTop decreases) Object.defineProperty(viewer, "scrollTop", { value: 50, writable: true, configurable: true }); fireEvent.scroll(viewer); // Re-render with new entries — auto-scroll should be disabled, so scrollTop stays const newEntries = [...entries, makeEntry({ text: "line 2" })]; rerender(); // scrollTop should remain at 50, not jump to scrollHeight expect(viewer.scrollTop).toBe(50); }); it("re-enables auto-scroll when user scrolls down", () => { const entries = [makeEntry({ text: "line 1" })]; const { container, rerender } = render( , ); const viewer = container.querySelector("[data-testid='agent-log-viewer']") as HTMLElement; // Scroll up first to disable auto-scroll Object.defineProperty(viewer, "scrollTop", { value: 100, writable: true, configurable: true }); fireEvent.scroll(viewer); Object.defineProperty(viewer, "scrollTop", { value: 50, writable: true, configurable: true }); fireEvent.scroll(viewer); // Now scroll down (any amount) to re-enable Object.defineProperty(viewer, "scrollTop", { value: 70, writable: true, configurable: true }); fireEvent.scroll(viewer); // Re-render with new entries — auto-scroll should be re-enabled const newEntries = [...entries, makeEntry({ text: "line 2" })]; rerender(); // scrollTop should jump to scrollHeight since auto-scroll is re-enabled expect(viewer.scrollTop).toBe(viewer.scrollHeight); }); });