Files
Auto-Trader/frontend/src/components/chat/__tests__/ToolProgressIndicator.test.tsx
T
Haozhe Wu 71933ecf31 feat(frontend): guided-minimalism WebUI overhaul — 12-way audit + fix fleet
Land the pre-talk WebUI campaign: first-frame stability (pre-paint theme
boot via CSP-safe public/theme-boot.js, self-hosted fonts + OFL license),
streaming performance (rAF token batching, throttled flush, streaming
markdown with deferred highlight, 160-row windowing, swarm state in its
own store slice with per-run coalescing), correctness (FIFO tool matching,
retry preserves request semantics, display/request separation, cache-hit
session refresh, multi-tab message.received), guided-minimalism welcome
(time-aware greeting + brand lockup + 4 quick actions + collapsible
library, fill-first affordance), SwarmDashboard rebuild with honest
indeterminate progress, five-locale copy de-jargoning with lazy locale
chunks, accessibility Level-A fixes (color-blind metric glyphs, aria-live
streaming, skip link, focus-visible affordances), robustness (markdown
link/table/KaTeX overflow, safe localStorage, portal modal), and '/' now
opens the product with marketing moved to /about.
2026-07-29 23:07:35 +08:00

135 lines
5.3 KiB
TypeScript

import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { ToolProgressIndicator } from "../ToolProgressIndicator";
import type { ToolCallEntry } from "@/types/agent";
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string, values?: Record<string, string | number>) => {
if (key === "toolProgress.step") return `Step ${values?.step} · ${values?.tool}`;
if (key === "toolProgress.toolsRunning") return `${values?.count} tools running`;
if (key === "toolProgress.toolsCompleted") return `${values?.count} tools completed`;
if (key === "toolProgress.earlier") return `+${values?.count} earlier`;
if (key === "toolProgress.etaSeconds") return `~${values?.seconds}s left`;
return key;
},
}),
}));
function makeTc(overrides: Partial<ToolCallEntry> = {}): ToolCallEntry {
return {
id: "tc-1",
tool: "backtest",
arguments: {},
status: "running",
timestamp: Date.now(),
...overrides,
};
}
describe("ToolProgressIndicator", () => {
it("keeps completed tools visible for the remainder of the attempt", () => {
const tcs = [makeTc({ status: "ok" }), makeTc({ id: "tc-2", status: "error" })];
render(<ToolProgressIndicator toolCalls={tcs} />);
expect(screen.getAllByText("2 tools completed")).toHaveLength(2);
expect(screen.getAllByText(/Run backtest/)).toHaveLength(2);
});
it("renders nothing for empty array", () => {
const { container } = render(<ToolProgressIndicator toolCalls={[]} />);
expect(container.innerHTML).toBe("");
});
it("renders single running tool", () => {
const tcs = [makeTc({ elapsed_s: 5 })];
render(<ToolProgressIndicator toolCalls={tcs} />);
expect(screen.getByRole("status")).toBeInTheDocument();
expect(screen.getByText(/Run backtest/)).toBeInTheDocument();
expect(screen.getByText("5s")).toBeInTheDocument();
});
it("renders multiple running tools with header", () => {
const tcs = [
makeTc({ id: "tc-1", tool: "bash" }),
makeTc({ id: "tc-2", tool: "write_file" }),
];
render(<ToolProgressIndicator toolCalls={tcs} />);
expect(screen.getAllByText("2 tools running")).toHaveLength(2);
expect(screen.getByText(/Run command/)).toBeInTheDocument();
expect(screen.getByText(/Generate code/)).toBeInTheDocument();
});
it("renders repeated tool names as distinct rows keyed by unique ids", () => {
const tcs = [
makeTc({ id: "backtest#1", status: "ok" }),
makeTc({ id: "backtest#2", status: "running" }),
];
render(<ToolProgressIndicator toolCalls={tcs} />);
expect(screen.getAllByText(/Run backtest/)).toHaveLength(2);
});
it("shows the most recent running tools behind a clickable earlier count", async () => {
const user = userEvent.setup();
const tcs = [
makeTc({ id: "tc-1", tool: "bash" }),
makeTc({ id: "tc-2", tool: "write_file" }),
makeTc({ id: "tc-3", tool: "backtest" }),
makeTc({ id: "tc-4", tool: "read_file" }),
];
render(<ToolProgressIndicator toolCalls={tcs} />);
expect(screen.queryByText(/Run command/)).not.toBeInTheDocument();
expect(screen.queryByText(/Generate code/)).not.toBeInTheDocument();
expect(screen.getByText(/Run backtest/)).toBeInTheDocument();
expect(screen.getByText(/Read file/)).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "+2 earlier" }));
expect(screen.getByText(/Run command/)).toBeInTheDocument();
expect(screen.getByText(/Generate code/)).toBeInTheDocument();
});
it("prioritizes running rows, then the most recent terminal row", async () => {
const user = userEvent.setup();
const tcs = [
makeTc({ id: "tc-1", tool: "bash", status: "ok" }),
makeTc({ id: "tc-2", tool: "write_file", status: "error" }),
makeTc({ id: "tc-3", tool: "backtest", status: "ok" }),
makeTc({ id: "tc-4", tool: "read_file", status: "running" }),
];
render(<ToolProgressIndicator toolCalls={tcs} />);
const visibleSteps = screen.getAllByText(/^Step /).map((node) => node.textContent);
expect(visibleSteps).toEqual([
"Step 4 · Read file",
"Step 3 · Run backtest",
]);
expect(screen.queryByText(/Generate code/)).not.toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "+2 earlier" }));
expect(screen.getByText(/Generate code/)).toBeInTheDocument();
expect(screen.getByText(/Run command/)).toBeInTheDocument();
});
it("shows determinate progress bar when progress data exists", () => {
const tcs = [
makeTc({
progress: { current: 5, total: 10, stage: "Processing" },
}),
];
render(<ToolProgressIndicator toolCalls={tcs} />);
expect(screen.getByText("Processing")).toBeInTheDocument();
expect(screen.getByText("5/10")).toBeInTheDocument();
// Should have a progressbar element
expect(screen.getByRole("progressbar")).toBeInTheDocument();
});
it("announces only coarse status and hides ticking elapsed numerals", () => {
render(<ToolProgressIndicator toolCalls={[makeTc({ elapsed_s: 5 })]} />);
const status = screen.getByRole("status");
expect(status).toHaveClass("sr-only");
expect(status).not.toHaveAttribute("aria-atomic");
expect(screen.getByText("5s")).toHaveAttribute("aria-hidden", "true");
});
});