// @vitest-environment jsdom import { describe, it, expect, vi } from "vitest"; import React from "react"; import { render, fireEvent } from "@testing-library/react"; import "@testing-library/jest-dom"; import { ProgressBadge } from "./ProgressBadge"; import type { ParsedMessage } from "../lib/types"; // Mock the markdown module to avoid pulling in marked/hljs in jsdom vi.mock("../lib/markdown", () => ({ renderMarkdown: (text: string) => `

${text}

`, })); function makeEvent( overrides: Partial = {} ): ParsedMessage { return { uuid: crypto.randomUUID(), category: "hook_progress", content: "Running pre-commit hook", rawIndex: 0, timestamp: "2025-01-15T10:30:00Z", progressSubtype: "hook", ...overrides, }; } describe("ProgressBadge", () => { describe("collapsed state", () => { it("shows pill counts but hides event content", () => { const events = [ makeEvent({ progressSubtype: "hook" }), makeEvent({ progressSubtype: "hook" }), makeEvent({ progressSubtype: "bash" }), ]; const { container, queryByText } = render( ); // Pill counts visible expect(container.textContent).toContain("hook: 2"); expect(container.textContent).toContain("bash: 1"); // Event content should NOT be visible when collapsed expect(queryByText("Running pre-commit hook")).toBeNull(); }); }); describe("expanded state", () => { it("shows all event content when clicked", () => { const events = [ makeEvent({ content: "First event" }), makeEvent({ content: "Second event" }), ]; const { container } = render(); // Click to expand fireEvent.click(container.querySelector("button")!); // Content should be visible (rendered as markdown via mock) expect(container.innerHTML).toContain("First event"); expect(container.innerHTML).toContain("Second event"); }); it("renders content through markdown into prose-message-progress container", () => { const events = [ makeEvent({ content: "**bold text**" }), ]; const { container } = render(); // Expand fireEvent.click(container.querySelector("button")!); // Our mock wraps in

, so the prose container should have rendered HTML const proseEl = container.querySelector(".prose-message-progress"); expect(proseEl).toBeInTheDocument(); // Content is from local JSONL session files owned by the user, // same trust model as MessageBubble's markdown rendering expect(proseEl?.innerHTML).toContain("

**bold text**

"); }); it("does not have max-h-48 or overflow-y-auto on expanded container", () => { const events = [makeEvent()]; const { container } = render(); fireEvent.click(container.querySelector("button")!); const expandedDiv = container.querySelector("[data-testid='progress-expanded']"); expect(expandedDiv).toBeInTheDocument(); expect(expandedDiv?.className).not.toContain("max-h-48"); expect(expandedDiv?.className).not.toContain("overflow-y-auto"); }); it("does not have truncate class on content", () => { const events = [makeEvent()]; const { container } = render(); fireEvent.click(container.querySelector("button")!); // Generic (non-agent) expanded view should not truncate const proseElements = container.querySelectorAll(".prose-message-progress"); for (const el of proseElements) { expect(el.className).not.toContain("truncate"); } }); it("displays timestamps and subtype badges per event", () => { const events = [ makeEvent({ timestamp: "2025-01-15T10:30:00Z", progressSubtype: "bash", content: "npm test", }), ]; const { container } = render(); fireEvent.click(container.querySelector("button")!); // Subtype badge visible expect(container.textContent).toContain("bash"); // Timestamp visible (formatted by toLocaleTimeString) const expandedDiv = container.querySelector("[data-testid='progress-expanded']"); expect(expandedDiv).toBeInTheDocument(); // The timestamp text should exist somewhere in the expanded area expect(expandedDiv?.textContent).toMatch(/\d{1,2}:\d{2}:\d{2}/); }); }); describe("edge cases", () => { it("handles empty events array", () => { const { container } = render(); // Should render without crashing, no pills expect(container.querySelector("button")).toBeInTheDocument(); }); it("handles missing timestamps", () => { const events = [makeEvent({ timestamp: undefined })]; const { container } = render(); fireEvent.click(container.querySelector("button")!); expect(container.textContent).toContain("--:--:--"); }); it("defaults undefined subtype to hook", () => { const events = [makeEvent({ progressSubtype: undefined })]; const { container } = render(); // In pill counts expect(container.textContent).toContain("hook: 1"); }); }); describe("agent subtype delegation", () => { function makeAgentEvent( overrides: Partial = {} ): ParsedMessage { const data = { message: { type: "assistant", message: { role: "assistant", content: [ { type: "tool_use", id: "toolu_abc", name: "Read", input: { file_path: "/src/foo.ts" }, }, ], }, timestamp: "2026-01-30T16:22:21.000Z", }, type: "agent_progress", prompt: "Explore the codebase", agentId: "a6945d4", }; return { uuid: crypto.randomUUID(), category: "hook_progress", content: JSON.stringify(data), rawIndex: 0, timestamp: "2026-01-30T16:22:21.000Z", progressSubtype: "agent", ...overrides, }; } it("renders AgentProgressView when all events are agent subtype", () => { const events = [makeAgentEvent(), makeAgentEvent()]; const { container } = render(); // Expand fireEvent.click(container.querySelector("button")!); // Should render AgentProgressView expect( container.querySelector("[data-testid='agent-progress-view']") ).toBeInTheDocument(); // Should NOT render generic prose-message-progress expect( container.querySelector(".prose-message-progress") ).toBeNull(); }); it("renders generic list when events are mixed subtypes", () => { const events = [ makeAgentEvent(), makeEvent({ progressSubtype: "hook" }), ]; const { container } = render(); // Expand fireEvent.click(container.querySelector("button")!); // Should NOT render AgentProgressView expect( container.querySelector("[data-testid='agent-progress-view']") ).toBeNull(); // Should render generic view expect( container.querySelector(".prose-message-progress") ).toBeInTheDocument(); }); it("pills and collapsed state unchanged regardless of subtype", () => { const events = [makeAgentEvent(), makeAgentEvent(), makeAgentEvent()]; const { container } = render(); // Pills show agent count expect(container.textContent).toContain("agent: 3"); // No expanded content initially expect( container.querySelector("[data-testid='progress-expanded']") ).toBeNull(); }); }); });