// @vitest-environment happy-dom import { describe, expect, it, vi } from "vitest"; import { fireEvent, waitFor, within } from "@testing-library/react"; import { buildToolGroup, buildTranscriptRow, renderInlineMarkdown, renderMarkdown, } from "./structuredRender.js"; import type { TranscriptRow } from "./types.js"; function row( kind: K, data: unknown, id = "r1", ): TranscriptRow & { kind: K } { return { id, kind, data }; } describe("buildTranscriptRow: user/agent/thought", () => { it("renders a user row with the text content and no markdown", () => { const el = buildTranscriptRow( row("user", { content: { type: "text", text: "hi there" } }), ); expect(el.className).toContain("st-row-user"); expect(within(el).getByText("hi there")).toBeTruthy(); // Neither element exists at all — no accessible text/role to query by, so // this stays a direct DOM check for "never actual markup from user input". expect(el.querySelector(".st-who")).toBeNull(); expect(el.querySelector("b")).toBeNull(); }); it("renders agent prose as paragraphs with inline markdown", () => { const el = buildTranscriptRow( row("agent_text", { content: { type: "text", text: "one **two** `three`\n\nfour" } }), ); expect(el.className).toContain("st-row-agent"); const ps = within(el).getAllByRole("paragraph"); expect(ps).toHaveLength(2); expect(ps[0].innerHTML).toContain("two"); expect(ps[0].innerHTML).toContain("three"); }); it("trims ACP framing whitespace and renders thought-row inline markdown", () => { const el = buildTranscriptRow( row("agent_thought", { content: { type: "text", text: "\n\n**Planning comply tool commentary update**\n" }, }), ); expect(el.className).toContain("st-row-thought"); expect(el.textContent).toBe("Planning comply tool commentary update"); expect( within(el).getByText("Planning comply tool commentary update", { selector: "strong" }), ).toBeTruthy(); }); it("falls back to a labelled placeholder for non-text content, never crashing", () => { const el = buildTranscriptRow(row("user", { content: { type: "image" } })); expect(within(el).getByText("[image]")).toBeTruthy(); }); it("never clips text: a very long user message is not truncated", () => { const long = "x".repeat(5000); const el = buildTranscriptRow(row("user", { content: { type: "text", text: long } })); expect(within(el).getByText(long)).toBeTruthy(); }); it("copies message blocks on click and supports keyboard activation", async () => { const writeText = vi.fn().mockResolvedValue(undefined); Object.defineProperty(navigator, "clipboard", { configurable: true, value: { writeText } }); const el = buildTranscriptRow( row("agent_text", { content: { type: "text", text: "copy **this** exactly" } }), ); expect(el.getAttribute("role")).toBe("button"); fireEvent.click(el); await waitFor(() => expect(writeText).toHaveBeenCalledWith("copy **this** exactly")); fireEvent.keyDown(el, { key: "Enter" }); await waitFor(() => expect(writeText).toHaveBeenCalledTimes(2)); }); }); describe("renderInlineMarkdown", () => { it("escapes raw HTML before applying markdown", () => { expect(renderInlineMarkdown("")).not.toContain("[bad](javascript:alert(3))', ); expect(html).not.toContain(" { it("shows glyph, name, argument and no meta for a completed non-diff call", () => { const el = buildTranscriptRow( row("tool_call", { toolCallId: "t1", title: "Read(src/lib.rs)", kind: "read", status: "completed", }), ); const head = within(el).getByRole("button"); expect(within(head).getByText("Read")).toBeTruthy(); expect(within(head).getByText("src/lib.rs")).toBeTruthy(); // No badge text/role exists at all in this case — nothing accessible to // query for its absence, so this stays a direct DOM check. expect(head.querySelector(".st-tool-meta")).toBeNull(); }); it("falls back to the kind label and full title when the title has no Name(arg) shape", () => { const el = buildTranscriptRow(row("tool_call", { title: "does something odd", kind: "execute" })); const head = within(el).getByRole("button"); expect(within(head).getByText("Execute")).toBeTruthy(); expect(within(head).getByText("does something odd")).toBeTruthy(); }); it("shows a running badge for an in-progress call", () => { const el = buildTranscriptRow(row("tool_call", { title: "Bash(sleep 1)", status: "in_progress" })); const meta = within(el).getByText("running"); expect(meta.className).toContain("st-tool-meta-running"); }); it("shows a failed badge for a failed call", () => { const el = buildTranscriptRow(row("tool_call", { title: "Bash(false)", status: "failed" })); expect(within(el).getByText("failed")).toBeTruthy(); }); it("starts collapsed and expands its text output on click", () => { const el = buildTranscriptRow( row("tool_call", { title: "Grep(x)", status: "completed", content: [{ type: "content", content: { type: "text", text: "match found" } }], }), ); const head = within(el).getByRole("button"); // The disclosure wrapper has no accessible role or text of its own before // it is expanded, so it's located and inspected via raw DOM throughout. const out = el.querySelector(".st-tool-out") as HTMLElement; expect(out.hidden).toBe(true); expect(head.getAttribute("aria-expanded")).toBe("false"); fireEvent.click(head); expect(out.hidden).toBe(false); expect(head.getAttribute("aria-expanded")).toBe("true"); expect(within(out).getByText("match found")).toBeTruthy(); fireEvent.click(head); expect(out.hidden).toBe(false); expect(out.getAttribute("aria-hidden")).toBe("true"); expect(out.inert).toBe(true); expect(out.classList.contains("is-expanded")).toBe(false); }); it("does not expand when there is nothing to show", () => { const el = buildTranscriptRow(row("tool_call", { title: "Read(x)", status: "completed" })); const head = within(el).getByRole("button"); fireEvent.click(head); expect(head.getAttribute("aria-expanded")).toBe("false"); }); it("renders command fields and keeps the exact JSON available", () => { const el = buildTranscriptRow( row("tool_call", { title: "Bash(test)", rawInput: { command: "printf ok", cwd: "/workspace", timeout_ms: 1000 }, rawOutput: { formatted_output: "ok", exit_code: 0 }, }), ); fireEvent.click(within(el).getByRole("button")); expect(within(el).getByText("printf ok")).toBeTruthy(); // "Exit code" and its value are separate elements with no shared text // node, so getByText can't match "Exit code0" — locate via the label. const fact = within(el).getByText("Exit code").closest(".st-tool-fact"); expect(fact?.textContent).toBe("Exit code0"); const raw = within(el).getByText("Raw JSON").closest("details") as HTMLDetailsElement; expect(raw.open).toBe(false); expect(raw.textContent).toContain('"timeout_ms": 1000'); expect(raw.textContent).toContain('"exit_code": 0'); }); it("renders MCP arguments and results without hiding the source payload", () => { const el = buildTranscriptRow( row("tool_call", { title: "mcp.github.search", rawInput: { server: "github", tool: "search", arguments: { query: "ACP" } }, rawOutput: { result: { count: 2 }, error: null }, }), ); fireEvent.click(within(el).getByRole("button")); const labels = within(el) .getAllByText(/^(Arguments|Result)$/) .map((node) => node.textContent); expect(labels).toEqual(["Arguments", "Result"]); const raw = within(el).getByText("Raw JSON").closest("details"); expect(raw?.textContent).toContain('"server": "github"'); }); it("renders a recognized web search query", () => { const el = buildTranscriptRow( row("tool_call", { title: "Search the web", rawInput: { type: "webSearch", query: "Agent Client Protocol", action: { type: "search" } }, }), ); fireEvent.click(within(el).getByRole("button")); expect(within(el).getByText("Query")).toBeTruthy(); expect(within(el).getByText("Agent Client Protocol")).toBeTruthy(); }); it("falls back to raw JSON for an unknown provider shape", () => { const el = buildTranscriptRow( row("tool_call", { title: "Custom", rawInput: { peculiar: [1, 2] }, rawOutput: "done" }), ); fireEvent.click(within(el).getByRole("button")); // No provider-specific detail block has any known label to query by when // none matched — asserting the absence of the whole class stays raw DOM. expect( el.querySelectorAll( ".st-tool-out > .st-disclosure-clip > .st-disclosure-inner > .st-tool-detail", ), ).toHaveLength(0); const raw = within(el).getByText("Raw JSON").closest("details"); expect(raw?.textContent).toContain('"peculiar"'); expect(raw?.textContent).toContain('"done"'); }); it("renders standardized content before provider-specific and raw data", () => { const el = buildTranscriptRow( row("tool_call", { title: "Bash(test)", content: [ { type: "content", content: { type: "text", text: "typed result" } }, { type: "content", content: { type: "resource_link", name: "report", uri: "file:///tmp/report" } }, { type: "terminal", terminalId: "term-1" }, ], rawInput: { command: "test" }, rawOutput: { formatted_output: "typed result", exit_code: 0 }, }), ); fireEvent.click(within(el).getByRole("button")); // Ordering across block kinds has no accessible query of its own — this // stays a direct DOM structure check. const inner = el.querySelector(".st-tool-out > .st-disclosure-clip > .st-disclosure-inner")!; expect([...inner.children].map((node) => node.className)).toEqual([ "st-tool-text st-copyable", "st-tool-resource", "st-tool-terminal", "st-tool-detail", "st-tool-fact", "st-tool-raw", ]); expect(within(el).getByText("file:///tmp/report")).toBeTruthy(); expect(within(el).getByText("Terminal · term-1")).toBeTruthy(); }); it("renders safe typed image and audio blocks as media controls", () => { const el = buildTranscriptRow( row("tool_call", { title: "Media", content: [ { type: "content", content: { type: "image", mimeType: "image/png", data: "aW1hZ2U=" } }, { type: "content", content: { type: "audio", mimeType: "audio/mpeg", data: "YXVkaW8=" } }, ], }), ); fireEvent.click(within(el).getByRole("button")); expect((within(el).getByRole("img") as HTMLImageElement).src).toBe( "data:image/png;base64,aW1hZ2U=", ); //