import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import { MessageBubble } from "@/components/MessageBubble"; import { fmtDateTime, formatMessageEndTime } from "@/lib/format"; import type { CliAppInfo, McpPresetInfo, SlashCommand, UIMessage, } from "@/lib/types"; const CLI_APPS: CliAppInfo[] = [ { name: "zoom", display_name: "Zoom", category: "productivity", description: "Meetings", requires: "", source: "harness", entry_point: "cli-anything-zoom", install_supported: true, installed: true, available: true, status: "installed", logo_url: "https://example.invalid/zoom.svg", brand_color: "#0B5CFF", skill_installed: true, }, { name: "krita", display_name: "Krita", category: "image", description: "Painting", requires: "", source: "harness", entry_point: "cli-anything-krita", install_supported: true, installed: false, available: false, status: "not_installed", logo_url: null, brand_color: "#3BABFF", skill_installed: false, }, ]; const MCP_PRESETS: McpPresetInfo[] = [ { name: "browserbase", display_name: "Browserbase", category: "browser", description: "Cloud browser automation", docs_url: "https://docs.browserbase.com", transport: "streamableHttp", requires: "Browserbase API key", note: "", install_supported: true, installed: true, configured: true, available: true, status: "configured", logo_url: "https://example.invalid/browserbase.svg", brand_color: "#111827", required_fields: [], connection_summary: "https://mcp.browserbase.com/mcp", }, ]; const SLASH_COMMANDS: SlashCommand[] = [ { command: "/model", title: "Show or switch model", description: "Show the active model or switch to another configuration.", icon: "brain", lifecycle: "agent_turn_with_args", acceptsArgs: true, }, { command: "/goal", title: "Start a goal", description: "Start a sustained goal.", icon: "activity", lifecycle: "agent_turn_with_args", acceptsArgs: true, }, { command: "/new", title: "New chat", description: "Start a new chat.", icon: "square-pen", lifecycle: "finalize_active_turn", acceptsArgs: false, }, ]; describe("MessageBubble", () => { it("renders user messages as right-aligned pills", () => { const message: UIMessage = { id: "u1", role: "user", content: "hello", createdAt: Date.now(), }; const { container } = render(); const row = container.firstElementChild; const pill = screen.getByText("hello"); expect(row).toHaveClass("ml-auto", "flex"); expect(pill).toHaveClass("ml-auto", "w-fit", "rounded-[18px]"); expect(screen.getByRole("button", { name: "Copy" })).toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Fork" })).not.toBeInTheDocument(); }); it("renders failed delivery details on focus without persistent accepted chrome", async () => { const message: UIMessage = { id: "u-delivery", role: "user", content: "hello", createdAt: Date.now(), deliveryStatus: "sending", }; const { rerender } = render(); expect(screen.getByRole("status")).toHaveTextContent("Sending…"); rerender(); expect(screen.queryByRole("status")).not.toBeInTheDocument(); rerender( , ); expect(screen.queryByRole("status")).not.toBeInTheDocument(); const failedStatus = screen.getByRole("button", { name: "Not sent: Message too large", }); expect(failedStatus).toHaveClass( "text-destructive/80", "dark:text-red-400/80", ); expect(screen.getByText("hello")).not.toHaveClass("ring-1"); expect(screen.getByText("hello")).not.toHaveClass("ring-destructive/30"); expect(screen.queryByRole("tooltip")).not.toBeInTheDocument(); fireEvent.focus(failedStatus); const tooltip = await screen.findByRole("tooltip"); expect(tooltip).toHaveTextContent("Message too large"); expect(tooltip).toHaveTextContent( "The server rejected your last message because it exceeded the size limit.", ); expect(screen.getByRole("alert")).toHaveClass("sr-only"); }); it("styles only generated quoted context in user messages", () => { const message: UIMessage = { id: "u-quote", role: "user", content: "> [!QUOTE]\n> selected assistant excerpt\n\nWhat about this?", createdAt: Date.now(), }; const { rerender } = render(); const quote = screen.getByLabelText("Quoted context"); expect(quote).toHaveTextContent("selected assistant excerpt"); expect(screen.queryByText("Quoted context")).not.toBeInTheDocument(); expect(screen.getByText("What about this?")).toBeInTheDocument(); rerender( manually typed quote\n\nWhat about this?", }} />, ); expect(screen.queryByLabelText("Quoted context")).not.toBeInTheDocument(); }); it("copies user messages from the shared message action", async () => { const writeText = vi.fn().mockResolvedValue(undefined); Object.defineProperty(navigator, "clipboard", { configurable: true, value: { writeText }, }); const message: UIMessage = { id: "u-copy", role: "user", content: "Copy this user prompt.", createdAt: Date.now(), }; try { render(); fireEvent.click(screen.getByRole("button", { name: "Copy" })); expect(writeText).toHaveBeenCalledWith("Copy this user prompt."); await waitFor(() => expect(screen.getByRole("button", { name: "Copied" })).toBeInTheDocument(), ); } finally { Reflect.deleteProperty(navigator, "clipboard"); } }); it("highlights recognized slash command names without adding container chrome", () => { const message: UIMessage = { id: "u-command", role: "user", content: "/model gpt-5", createdAt: Date.now(), }; render(); const command = screen.getByTestId("message-slash-command"); expect(command).toHaveTextContent("/model"); expect(command).toHaveClass( "font-[550]", "transition-colors", "duration-150", ); expect(command).not.toHaveClass("font-mono"); expect(command.getAttribute("style")).not.toContain("text-shadow"); expect(command.getAttribute("style")).toContain("var(--inline-token-highlight)"); expect(command.className).not.toMatch(/(?:^|\s)(?:bg-|border|ring|rounded)/); expect(command.parentElement).toHaveTextContent("/model gpt-5"); expect(command.parentElement).toHaveClass("rounded-[18px]", "bg-secondary/70"); }); it("keeps unknown and invalid slash commands as plain message text", () => { const unknown: UIMessage = { id: "u-unknown-command", role: "user", content: "/unknown value", createdAt: Date.now(), }; const invalidExactCommand: UIMessage = { id: "u-invalid-command", role: "user", content: "/new with-arguments", createdAt: Date.now(), }; const { rerender } = render( , ); expect(screen.queryByTestId("message-slash-command")).not.toBeInTheDocument(); expect(screen.getByText("/unknown value")).toBeInTheDocument(); rerender(); expect(screen.queryByTestId("message-slash-command")).not.toBeInTheDocument(); expect(screen.getByText("/new with-arguments")).toBeInTheDocument(); }); it("preserves installed capability mentions in slash command arguments", () => { const message: UIMessage = { id: "u-command-mention", role: "user", content: "/goal ask @zoom to schedule the review", createdAt: Date.now(), }; render( , ); expect(screen.getByTestId("message-slash-command")).toHaveTextContent("/goal"); expect(screen.getByTestId("message-cli-mention-zoom")).toHaveTextContent("@zoom"); }); it("highlights skill references without a live skill catalog", () => { const message: UIMessage = { id: "u-skill-reference", role: "user", content: "Ask $github to review this with @zoom", createdAt: Date.now(), }; render( , ); const skill = screen.getByTestId("message-skill-reference-github"); expect(skill).toHaveTextContent(/^github$/); expect(skill).toHaveClass( "font-[550]", "transition-colors", "duration-150", ); expect(skill.getAttribute("style")).not.toContain("text-shadow"); expect(skill.getAttribute("style")).toContain("var(--inline-token-highlight)"); expect(skill.className).not.toMatch(/(?:^|\s)(?:bg-|border|ring|rounded)/); expect(screen.getByTestId("message-cli-mention-zoom")).toHaveTextContent("@zoom"); expect(skill.parentElement).toHaveTextContent("Ask github to review this with @zoom"); }); it("highlights well-formed skill references and leaves a bare marker plain", () => { const message: UIMessage = { id: "u-plain-skill-reference", role: "user", content: "Try $unknown or $blocked-skill and $", createdAt: Date.now(), }; render(); expect(screen.getByTestId("message-skill-reference-unknown")).toHaveTextContent(/^unknown$/); expect(screen.getByTestId("message-skill-reference-blocked-skill")) .toHaveTextContent(/^blocked-skill$/); const references = screen.getAllByTestId(/^message-skill-reference-/); expect(references).toHaveLength(2); expect(references[0].parentElement) .toHaveTextContent("Try unknown or blocked-skill and $"); }); it("renders fork control in completed assistant action rows", () => { const onForkFromHere = vi.fn(); const message: UIMessage = { id: "a-fork", role: "assistant", content: "branch after this answer", latencyMs: 1_200, createdAt: Date.now(), }; render(); fireEvent.click(screen.getByRole("button", { name: "Fork" })); expect(onForkFromHere).toHaveBeenCalledTimes(1); }); it("shows the assistant completion time in the former latency slot", async () => { const completedAt = Date.UTC(2026, 6, 25, 12, 34, 56); const { container } = render( , ); expect(screen.getByRole("button", { name: "Copy" })).toBeInTheDocument(); const time = container.querySelector("[data-assistant-completed-at]"); expect(time).toHaveTextContent(formatMessageEndTime(completedAt)); expect(time).toHaveAttribute("dateTime", new Date(completedAt).toISOString()); expect(time).not.toHaveAttribute("title"); expect(time).toHaveAttribute("tabIndex", "0"); expect(time).toHaveClass( "cursor-help", "text-[11px]", "leading-none", "text-muted-foreground/70", "tabular-nums", ); fireEvent.pointerMove(time!); expect(await screen.findByRole("tooltip")).toHaveTextContent(fmtDateTime(completedAt)); }); it("falls back to the assistant creation time when replay has no completion time", () => { const createdAt = Date.UTC(2026, 6, 25, 12, 34, 56); const { container } = render( , ); const time = container.querySelector("[data-message-timestamp]"); expect(time).toHaveTextContent(formatMessageEndTime(createdAt)); expect(time).toHaveAttribute("dateTime", new Date(createdAt).toISOString()); expect(time).not.toHaveAttribute("title"); expect(time).not.toHaveAttribute("data-assistant-completed-at"); }); it("renders the creation time for user messages", async () => { const createdAt = Date.UTC(2026, 6, 25, 12, 34, 56); const { container } = render( , ); const time = container.querySelector("[data-message-created-at]"); expect(time).toHaveTextContent(formatMessageEndTime(createdAt)); expect(time).toHaveAttribute("dateTime", new Date(createdAt).toISOString()); expect(time).not.toHaveAttribute("title"); expect(time).toHaveAttribute("tabIndex", "0"); fireEvent.pointerMove(time!); expect(await screen.findByRole("tooltip")).toHaveTextContent(fmtDateTime(createdAt)); }); it("does not infer completion time from the assistant creation timestamp", () => { const createdAt = Date.UTC(2026, 6, 25, 12, 34, 0); const latencyMs = 13_000; const { container } = render( , ); expect(container.querySelector("[data-assistant-completed-at]")).not.toBeInTheDocument(); }); it("renders installed CLI app mentions inside sent user messages", () => { const message: UIMessage = { id: "u-cli", role: "user", content: "Hi nano, please use @zoom to book a meeting, not @krita", createdAt: Date.now(), }; render(); const token = screen.getByTestId("message-cli-mention-zoom"); expect(token).toHaveTextContent("@zoom"); expect(token).toHaveAttribute("title", "CLI app: Zoom"); expect(token).toHaveClass("font-[550]"); expect(token.className).not.toContain("rounded"); expect(token.className).not.toContain("px-"); expect(token.getAttribute("style")).toContain("color: #0B5CFF"); expect(token.getAttribute("style")).not.toContain("text-shadow"); expect(screen.getByTestId("message-cli-mention-logo-zoom")).toBeInTheDocument(); expect(screen.queryByTestId("message-cli-mention-krita")).not.toBeInTheDocument(); expect(screen.getByText(/not @krita/)).toBeInTheDocument(); }); it("places automation metadata after the timestamp and reveals its source on hover", async () => { const completedAt = Date.UTC(2026, 6, 25, 12, 34, 56); const message: UIMessage = { id: "a-cron", role: "assistant", content: "Time to drink water.", source: { kind: "cron", label: "drink water" }, completedAt, createdAt: completedAt - 1_000, }; const { container } = render(); const footer = container.querySelector("[data-assistant-footer]")!; const timestamp = footer.querySelector("[data-message-timestamp]")!; const trigger = footer.querySelector("[data-automation-trigger]")!; expect(timestamp).toHaveTextContent(formatMessageEndTime(completedAt)); expect(trigger).toHaveTextContent("Triggered automatically"); expect(trigger.previousElementSibling).toBe(timestamp); expect(trigger).toHaveClass( "text-[11px]", "leading-none", "text-muted-foreground/70", "tabular-nums", ); expect(trigger.className).not.toMatch(/(?:^|\s)(?:border|bg-)/); expect(trigger.querySelector("svg")).not.toBeInTheDocument(); expect(screen.queryByText("drink water")).not.toBeInTheDocument(); expect(screen.getByText("Time to drink water.")).toBeInTheDocument(); fireEvent.pointerMove(trigger); expect(await screen.findByRole("tooltip")).toHaveTextContent("drink water"); }); it("renders structured CLI app attachments even without the installed catalog", () => { const message: UIMessage = { id: "u-cli-attached", role: "user", content: "Please use @drawio for the diagram", createdAt: Date.now(), cliApps: [{ name: "drawio", display_name: "Draw.io", category: "diagram", entry_point: "cli-anything-drawio", logo_url: "https://example.invalid/drawio.svg", brand_color: "#F08705", }], }; render(); const token = screen.getByTestId("message-cli-mention-drawio"); expect(token).toHaveTextContent("@drawio"); expect(token.className).not.toContain("rounded"); expect(token.className).not.toContain("px-"); expect(token.getAttribute("style")).toContain("color: #F08705"); expect(screen.getByTestId("message-cli-mention-logo-drawio")).toBeInTheDocument(); }); it("renders MCP preset mentions inside sent user messages", () => { const message: UIMessage = { id: "u-mcp", role: "user", content: "Use @browserbase to inspect the checkout flow", createdAt: Date.now(), }; render(); const token = screen.getByTestId("message-mcp-mention-browserbase"); expect(token).toHaveTextContent("@browserbase"); expect(token).toHaveAttribute("title", "MCP server: Browserbase"); expect(token.getAttribute("style")).toContain("color: #111827"); expect(screen.getByTestId("message-mcp-mention-logo-browserbase")).toBeInTheDocument(); }); it("renders persisted session mentions inside sent user messages", () => { const message: UIMessage = { id: "u-session", role: "user", content: "Use @收费设计 as context", createdAt: Date.now(), sessionMentions: [{ name: "收费设计", session_key: "websocket:pricing", title: "收费设计", }], }; render(); const token = screen.getByTestId("message-session-mention-收费设计"); expect(token).toHaveTextContent("@收费设计"); expect(token).toHaveAttribute("title", "Session: 收费设计"); expect(token.closest("a")).toHaveAttribute("href", "#/chat/websocket%3Apricing"); expect(token.closest("a")?.getAttribute("style")).toContain( "text-decoration-color: var(--inline-token-highlight)", ); }); it("copies completed assistant replies from the action row", async () => { const writeText = vi.fn().mockResolvedValue(undefined); Object.defineProperty(navigator, "clipboard", { configurable: true, value: { writeText }, }); const message: UIMessage = { id: "a-copy", role: "assistant", content: "I can help with the next step.", createdAt: Date.now(), }; render(); fireEvent.click(screen.getByRole("button", { name: "Copy" })); expect(writeText).toHaveBeenCalledWith("I can help with the next step."); await waitFor(() => expect(screen.getByRole("button", { name: "Copied" })).toBeInTheDocument(), ); }); it("copies completed assistant replies with the textarea fallback", async () => { Object.defineProperty(navigator, "clipboard", { configurable: true, value: undefined, }); const execCommand = vi.fn().mockReturnValue(true); Object.defineProperty(document, "execCommand", { configurable: true, value: execCommand, }); const message: UIMessage = { id: "a-copy-fallback", role: "assistant", content: "Fallback copy reply.", createdAt: Date.now(), }; try { render(); fireEvent.click(screen.getByRole("button", { name: "Copy" })); await waitFor(() => expect(execCommand).toHaveBeenCalledWith("copy")); await waitFor(() => expect(screen.getByRole("button", { name: "Copied" })).toBeInTheDocument(), ); } finally { Reflect.deleteProperty(navigator, "clipboard"); Reflect.deleteProperty(document, "execCommand"); } }); it("falls back when the Clipboard API rejects assistant reply copy", async () => { const writeText = vi.fn().mockRejectedValue(new Error("not allowed")); Object.defineProperty(navigator, "clipboard", { configurable: true, value: { writeText }, }); const execCommand = vi.fn().mockReturnValue(true); Object.defineProperty(document, "execCommand", { configurable: true, value: execCommand, }); const message: UIMessage = { id: "a-copy-reject", role: "assistant", content: "Rejected clipboard copy.", createdAt: Date.now(), }; try { render(); fireEvent.click(screen.getByRole("button", { name: "Copy" })); expect(writeText).toHaveBeenCalledWith("Rejected clipboard copy."); await waitFor(() => expect(execCommand).toHaveBeenCalledWith("copy")); await waitFor(() => expect(screen.getByRole("button", { name: "Copied" })).toBeInTheDocument(), ); } finally { Reflect.deleteProperty(navigator, "clipboard"); Reflect.deleteProperty(document, "execCommand"); } }); it("does not show copy actions for streaming placeholders", () => { const message: UIMessage = { id: "a-streaming", role: "assistant", content: "", isStreaming: true, createdAt: Date.now(), }; render(); expect(screen.queryByRole("button", { name: "Copy" })).not.toBeInTheDocument(); }); it("keeps assistant footer geometry mounted across stream completion", () => { const streaming: UIMessage = { id: "a-footer-stable", role: "assistant", content: "Stable answer", isStreaming: true, createdAt: Date.now(), }; const { container, rerender } = render(); const reservedFooter = container.querySelector("[data-assistant-footer]"); expect(reservedFooter).not.toBeNull(); expect(reservedFooter).toHaveAttribute("data-state", "reserved"); expect(reservedFooter).toHaveClass("mt-2", "min-h-8", "opacity-0"); rerender( , ); const visibleFooter = container.querySelector("[data-assistant-footer]"); expect(visibleFooter).toBe(reservedFooter); expect(visibleFooter).toHaveAttribute("data-state", "visible"); expect(visibleFooter).toHaveClass("mt-2", "min-h-8", "opacity-100"); }); it("does not show copy when showCopyAction is false", () => { const message: UIMessage = { id: "a-mid", role: "assistant", content: "Mid-turn snippet.", createdAt: Date.now(), }; render(); expect(screen.queryByRole("button", { name: "Copy" })).not.toBeInTheDocument(); }); it("renders trace messages as collapsible tool groups", () => { const message: UIMessage = { id: "t1", role: "tool", kind: "trace", content: 'search "hk weather"', traces: ['weather("get")', 'search "hk weather"'], createdAt: Date.now(), }; render(); const toggle = screen.getByRole("button", { name: /used 2 tools/i }); expect(screen.queryByText('weather("get")')).not.toBeInTheDocument(); expect(screen.queryByText('search "hk weather"')).not.toBeInTheDocument(); fireEvent.click(toggle); expect(screen.getByText('weather("get")')).toBeInTheDocument(); expect(screen.getByText('search "hk weather"')).toBeInTheDocument(); }); it("renders video media as an inline player", () => { const message: UIMessage = { id: "a1", role: "assistant", content: "here is the clip", createdAt: Date.now(), media: [ { kind: "video", url: "/api/media/sig/payload", name: "demo.mp4", }, ], }; const { container } = render(); expect(screen.getByText("here is the clip")).toBeInTheDocument(); const video = screen.getByLabelText(/video attachment/i); expect(video.tagName).toBe("VIDEO"); expect(video).toHaveAttribute("src", "/api/media/sig/payload"); expect(video).toHaveAttribute("preload", "metadata"); expect(container.querySelector("video[controls]")).toBeInTheDocument(); expect(screen.queryByText("Preview")).not.toBeInTheDocument(); expect(screen.queryByText("Code")).not.toBeInTheDocument(); }); it("renders streaming reasoning as one compact activity line", () => { const message: UIMessage = { id: "a-reasoning-streaming", role: "assistant", content: "", createdAt: Date.now(), reasoning: "Step 1: parse intent. Step 2: compute.", reasoningStreaming: true, }; const { container } = render(); const preview = screen.getByText("Step 1: parse intent. Step 2: compute."); expect(preview).toBeInTheDocument(); expect(container.querySelector(".reasoning-sheen-stripe")).not.toBeInTheDocument(); expect(preview).toHaveClass("streaming-text-sheen"); expect(preview).toHaveAttribute( "data-sheen-text", "Step 1: parse intent. Step 2: compute.", ); expect(screen.queryByText("Thinking…")).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: /thinking/i })).not.toBeInTheDocument(); }); it("keeps completed reasoning on one line above the answer", () => { const message: UIMessage = { id: "a-reasoning-done", role: "assistant", content: "The answer is 42.", createdAt: Date.now(), reasoning: "hidden until expanded", reasoningStreaming: false, }; render(); const preview = screen.getByText("hidden until expanded"); expect(preview).toBeInTheDocument(); expect(screen.getByText("The answer is 42.")).toBeInTheDocument(); expect(preview.closest('[data-testid="activity-step"]')).toHaveClass("mb-2"); expect(screen.queryByText("Thinking")).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: /thinking/i })).not.toBeInTheDocument(); }); it("compacts reasoning markdown into plain single-line text", () => { const message: UIMessage = { id: "a-reasoning-md", role: "assistant", content: "", createdAt: Date.now(), reasoning: "### Section title\n\nBody line.", reasoningStreaming: false, }; const { container } = render(); expect(screen.getByText("Section title Body line.")).toBeInTheDocument(); expect(container.textContent).not.toContain("###"); expect(container.querySelector("h3")).not.toBeInTheDocument(); }); it("renders inline file paths as compact file references", async () => { await import("@/components/MarkdownTextRenderer"); const message: UIMessage = { id: "a-file-path", role: "assistant", content: "改动在 `webui/src/components/MarkdownTextRenderer.tsx` 和 `/Users/renxubin/.nanobot/workspace/minecraft-fps/index.html`。", createdAt: Date.now(), }; try { render(); const references = await screen.findAllByTestId("inline-file-path"); expect(references).toHaveLength(2); expect(references[0].parentElement).not.toHaveClass("translate-y-[0.08em]"); expect(references[0].parentElement).toHaveClass("align-baseline"); expect(references[0].parentElement).toHaveClass("leading-[inherit]"); expect(references[0]).toHaveClass("items-baseline"); expect(references[0]).toHaveTextContent("MarkdownTextRenderer.tsx"); expect(references[0]).not.toHaveTextContent("webui/src/components"); expect(screen.getByText("index.html")).toBeInTheDocument(); expect(references[1]).not.toHaveTextContent("/Users/renxubin"); expect(references[1]).not.toHaveAttribute("title"); expect(references[1]).toHaveAttribute( "aria-label", "/Users/renxubin/.nanobot/workspace/minecraft-fps/index.html", ); vi.useFakeTimers(); fireEvent.pointerMove(references[1].parentElement!); await act(async () => { vi.advanceTimersByTime(500); }); const tooltip = screen.getByRole("tooltip"); expect(tooltip).toHaveTextContent( "/Users/renxubin/.nanobot/workspace/minecraft-fps/index.html", ); } finally { vi.useRealTimers(); } }); it("renders assistant image media as a larger generated result", () => { const message: UIMessage = { id: "a-image", role: "assistant", content: "done", createdAt: Date.now(), media: [ { kind: "image", url: "/api/media/sig/image", name: "generated.png", }, ], }; const { container } = render(); const imageButton = screen.getByRole("button", { name: /view image/i }); expect(imageButton).toHaveClass("w-[min(100%,34rem)]", "rounded-[20px]"); expect(imageButton).not.toHaveAttribute("title"); expect(container.querySelector("img")).toHaveClass("h-auto", "w-full", "object-contain"); }); it("renders mislabeled html assistant media as a file attachment", () => { const message: UIMessage = { id: "a-html", role: "assistant", content: "file ready", createdAt: Date.now(), media: [ { kind: "image", url: "/api/media/sig/html", name: "index.html", }, ], }; const { container } = render(); expect(screen.getByLabelText("File attachment")).toHaveTextContent("index.html"); expect(container.querySelector("img")).not.toBeInTheDocument(); }); it("renders assistant svg media as an image preview", () => { const message: UIMessage = { id: "a-svg", role: "assistant", content: "chart ready", createdAt: Date.now(), media: [ { kind: "file", url: "/api/media/sig/svg", name: "growth.svg", }, ], }; const { container } = render(); expect(screen.getByRole("button", { name: /view image: growth.svg/i })).toBeInTheDocument(); expect(container.querySelector('img[src="/api/media/sig/svg"]')).toBeInTheDocument(); expect(screen.queryByLabelText("File attachment")).not.toBeInTheDocument(); }); });