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>
This commit is contained in:
240
src/client/components/ProgressBadge.test.tsx
Normal file
240
src/client/components/ProgressBadge.test.tsx
Normal file
@@ -0,0 +1,240 @@
|
||||
// @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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user