import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; import { setAgentGuiI18nTestLocale } from "../../../i18n/testUtils"; import type { WorkspaceAgentSessionDetailToolCall } from "../../workspaceAgentSessionDetailViewModel"; import type { AgentToolCallVM } from "../contracts/agentToolCallVM"; import type { AgentToolGroupRowVM } from "../contracts/agentToolGroupRowVM"; import { projectAgentToolCall } from "../projection/agentToolProjection"; import { AgentToolGroupRow } from "./AgentToolGroupRow"; describe("AgentToolGroupRow", () => { afterEach(async () => { setAgentGuiI18nTestLocale("zh-CN"); }); it("renders approval timeline calls as collapsible tool cards", async () => { setAgentGuiI18nTestLocale("en"); render( `Tool calls (${count})`} thinkingLabel="Thinking" /> ); expect(screen.getByRole("button", { expanded: false })).toHaveAttribute( "aria-expanded", "false" ); }); it("renders the task glyph before grouped tool call labels", async () => { render( `Tool calls (${count})`} thinkingLabel="Thought process" /> ); const button = screen.getByRole("button", { name: "Tool calls (2)" }); const icon = button.querySelector( ".workspace-agents-status-panel__detail-tool-count-icon" ); const chevron = button.querySelector( ".workspace-agents-status-panel__detail-tool-count-chevron" ); expect(icon).toBeInTheDocument(); expect(icon).toHaveAttribute("width", "16"); expect(icon).toHaveAttribute("height", "16"); expect(icon?.querySelector("path")?.getAttribute("d")).toContain( "M8.29297 14.793" ); expect(chevron).toBeInTheDocument(); fireEvent.click(button); await flushCollapsibleRevealFrames(); const list = document.querySelector( ".workspace-agents-status-panel__detail-tool-list" ); await waitFor(() => expect(list?.closest(".agent-collapsible-reveal")).toHaveAttribute( "data-expanded", "true" ) ); expect( button.closest(".workspace-agents-status-panel__detail-tool-section") ).toHaveAttribute("data-tool-group-expanded", "true"); expect( document.querySelectorAll( ".workspace-agents-status-panel__detail-tool-list-item" ) ).toHaveLength(2); }); it("keeps inner tool cards collapsed until their own buttons are clicked", async () => { const calls = [ toolCall({ id: "call:1", output: { text: "first output" } }), toolCall({ id: "call:2", output: { text: "second output" } }) ]; render( ({ kind: "tool-call", call })) }} label={(count) => `Tool calls (${count})`} thinkingLabel="Thought process" /> ); fireEvent.click(screen.getByRole("button", { name: "Tool calls (2)" })); await flushCollapsibleRevealFrames(); const innerToggles = document.querySelectorAll( "button.workspace-agents-status-panel__detail-tool-row-head--button" ); expect(innerToggles).toHaveLength(2); expect( [...innerToggles].every( (toggle) => toggle.getAttribute("aria-expanded") === "false" ) ).toBe(true); }); it("does not render grouped file-change summaries under the tool-call count", () => { render( `Tool calls (${count})`} thinkingLabel="Thought process" /> ); expect(screen.queryByText("Changed App.tsx and 1 more files")).toBeNull(); }); it("renders raw timeline JSON disclosure for debug session views", () => { render( `Tool calls (${count})`} thinkingLabel="Thought process" showRawTimelineJson rawTimelineJsonLabel="Raw JSON" /> ); fireEvent.click(screen.getByRole("button", { name: "Raw JSON" })); expect(screen.getByText(/"eventId": "tool-started"/)).toBeInTheDocument(); }); it("keeps collapsed grouped tool details unmounted while idle", () => { let idleCallback: IdleRequestCallback | null = null; vi.stubGlobal("requestIdleCallback", (callback: IdleRequestCallback) => { idleCallback = callback; return 1; }); vi.stubGlobal("cancelIdleCallback", () => undefined); render( `Tool calls (${count})`} thinkingLabel="Thought process" /> ); expect( document.querySelector(".workspace-agents-status-panel__detail-tool-list") ).toBeNull(); act(() => { idleCallback?.({ didTimeout: false, timeRemaining: () => 50 }); }); expect( document.querySelector(".workspace-agents-status-panel__detail-tool-list") ).toBeNull(); }); }); async function flushCollapsibleRevealFrames(): Promise { await flushAnimationFrame(); await flushAnimationFrame(); } async function flushAnimationFrame(): Promise { await act(async () => { await new Promise((resolve) => { requestAnimationFrame(() => resolve()); }); }); } function toolGroupRow(): AgentToolGroupRowVM { const calls = [ toolCall({ id: "call:1", name: "Read file", summary: "README.md" }), toolCall({ id: "call:2", name: "Edit file", summary: "src/App.tsx" }) ]; return { kind: "tool-group", id: "group:1", turnId: "turn:1", grouped: true, calls, summary: null, entries: calls.map((call) => ({ kind: "tool-call", call })), occurredAtUnixMs: null }; } function toolCall(overrides: Partial = {}): AgentToolCallVM { return { kind: "tool-call", id: "call:1", turnId: "turn:1", name: "Tool", toolName: "tool", callType: "tool", status: "Completed", statusKind: "completed", summary: "", compactSummary: null, payload: null, input: null, output: null, error: null, metadata: null, locations: null, rendererKind: "default", approval: null, planMode: null, askUserQuestion: null, task: null, occurredAtUnixMs: null, ...overrides }; } function detailToolCall( overrides: Partial = {} ): WorkspaceAgentSessionDetailToolCall { return { id: "call:1", name: "Tool", toolName: null, callType: "tool", status: "Completed", statusKind: "completed", summary: "", payload: null, ...overrides }; }