// @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();
});
});
});