Files
session-viewer/src/client/components/ProgressBadge.test.tsx
teernisse 3fe8d7d3b5 Add comprehensive test suite for progress tracking system
Test fixture updates:
- Add toolUseId fields (toolu_read1, toolu_edit1) to tool_use blocks
- Add parentToolUseID-linked progress events for read and edit tools
- Add orphaned SessionStart progress event (no parent)
- Update tool_result references to match new toolUseId values
- Add bash_progress and mcp_progress subtypes for subtype derivation

session-parser tests (7 new):
- toolUseId extraction from tool_use blocks with and without id field
- parentToolUseId and progressSubtype extraction from hook_progress
- Subtype derivation for bash_progress, mcp_progress, agent_progress
- Fallback to "hook" for unknown data types
- Undefined parentToolUseId when field is absent

progress-grouper tests (7 new):
- Partition parented progress into toolProgress map
- Remove parented progress from filtered messages array
- Keep orphaned progress (no parentToolUseId) in main stream
- Keep progress with invalid parentToolUseId (no matching tool_call)
- Empty input handling
- Sort each group by rawIndex
- Multiple tool_call parents tracked independently

agent-progress-parser tests (full suite):
- Parse user text events with prompt/agentId metadata extraction
- Parse tool_use blocks into AgentToolCall events
- Parse tool_result blocks with content extraction
- Parse text content as text_response with line counting
- Handle multiple content blocks in single turn
- Post-pass tool_result→tool_call linking (sourceTool, language)
- Empty input and malformed JSON → raw_content fallback
- stripLineNumbers for cat-n prefixed output
- summarizeToolCall for Read, Grep, Glob, Bash, Task, WarpGrep, etc.

ProgressBadge component tests:
- Collapsed state shows pill counts, hides content
- Expanded state shows all event content via markdown
- Subtype counting accuracy
- Agent-only events route to AgentProgressView

AgentProgressView component tests:
- Prompt banner rendering with truncation
- Agent ID and turn count display
- Summary rows with timestamps and tool names
- Click-to-expand drill-down content

html-exporter tests (8 new):
- Collapsible rendering for thinking, tool_call, tool_result
- Toggle button and JavaScript inclusion
- Non-collapsible messages lack collapse attributes
- Diff content detection and highlighting
- Progress badge rendering with toolProgress data

filters tests (2 new):
- hook_progress included/excluded by category toggle

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 23:05:01 -05:00

241 lines
7.9 KiB
TypeScript

// @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) => `<p>${text}</p>`,
}));
function makeEvent(
overrides: Partial<ParsedMessage> = {}
): 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(
<ProgressBadge events={events} />
);
// 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(<ProgressBadge events={events} />);
// 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(<ProgressBadge events={events} />);
// Expand
fireEvent.click(container.querySelector("button")!);
// Our mock wraps in <p>, 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("<p>**bold text**</p>");
});
it("does not have max-h-48 or overflow-y-auto on expanded container", () => {
const events = [makeEvent()];
const { container } = render(<ProgressBadge events={events} />);
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(<ProgressBadge events={events} />);
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(<ProgressBadge events={events} />);
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(<ProgressBadge events={[]} />);
// Should render without crashing, no pills
expect(container.querySelector("button")).toBeInTheDocument();
});
it("handles missing timestamps", () => {
const events = [makeEvent({ timestamp: undefined })];
const { container } = render(<ProgressBadge events={events} />);
fireEvent.click(container.querySelector("button")!);
expect(container.textContent).toContain("--:--:--");
});
it("defaults undefined subtype to hook", () => {
const events = [makeEvent({ progressSubtype: undefined })];
const { container } = render(<ProgressBadge events={events} />);
// In pill counts
expect(container.textContent).toContain("hook: 1");
});
});
describe("agent subtype delegation", () => {
function makeAgentEvent(
overrides: Partial<ParsedMessage> = {}
): 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(<ProgressBadge events={events} />);
// 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(<ProgressBadge events={events} />);
// 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(<ProgressBadge events={events} />);
// Pills show agent count
expect(container.textContent).toContain("agent: 3");
// No expanded content initially
expect(
container.querySelector("[data-testid='progress-expanded']")
).toBeNull();
});
});
});