diff --git a/ui/src/ui/chat/run-controls.test.ts b/ui/src/ui/chat/run-controls.test.ts index b5381fe8a3f..320b62d34e3 100644 --- a/ui/src/ui/chat/run-controls.test.ts +++ b/ui/src/ui/chat/run-controls.test.ts @@ -11,7 +11,6 @@ import { import { renderChatRunControls, type ChatRunControlsProps } from "./run-controls.ts"; import { renderSideResult } from "./side-result-render.ts"; import { renderCompactionIndicator, renderFallbackIndicator } from "./status-indicators.ts"; -import { renderToolCard } from "./tool-cards.ts"; vi.mock("../icons.ts", () => ({ icons: {}, @@ -21,18 +20,6 @@ vi.mock("../markdown.ts", () => ({ toSanitizedMarkdownHtml: (value: string) => value, })); -vi.mock("../tool-display.ts", () => ({ - formatToolDetail: () => undefined, - resolveToolDisplay: ({ name }: { name: string }) => ({ - name, - label: name - .split(/[._-]/g) - .map((part) => (part ? part[0].toUpperCase() + part.slice(1) : part)) - .join(" "), - icon: "zap", - }), -})); - function createProps(overrides: Partial = {}): ChatRunControlsProps { return { canAbort: false, @@ -380,170 +367,3 @@ describe("side result render", () => { expect(container.querySelector(".chat-side-result--error")).not.toBeNull(); }); }); - -describe("tool-cards", () => { - it("renders expanded cards with inline input and output sections", () => { - const container = document.createElement("div"); - const toggle = vi.fn(); - render( - renderToolCard( - { - id: "msg:4:call-4", - name: "browser.open", - args: { url: "https://example.com" }, - inputText: '{\n "url": "https://example.com"\n}', - outputText: "Opened page", - }, - { expanded: true, onToggleExpanded: toggle }, - ), - container, - ); - - expect(container.textContent).toContain("Tool input"); - expect(container.textContent).toContain("Tool output"); - expect(container.textContent).toContain("https://example.com"); - expect(container.textContent).toContain("Opened page"); - }); - - it("renders expanded tool calls without an inline output block when no output is present", () => { - const container = document.createElement("div"); - render( - renderToolCard( - { - id: "msg:4b:call-4b", - name: "sessions_spawn", - args: { mode: "session", thread: true }, - inputText: '{\n "mode": "session",\n "thread": true\n}', - }, - { expanded: true, onToggleExpanded: vi.fn() }, - ), - container, - ); - - expect(container.textContent).toContain("Tool input"); - expect(container.textContent).toContain('"thread": true'); - expect(container.textContent).not.toContain("Tool output"); - expect(container.textContent).not.toContain("No output"); - }); - - it("labels collapsed tool calls as tool call", () => { - const container = document.createElement("div"); - render( - renderToolCard( - { - id: "msg:5:call-5", - name: "sessions_spawn", - args: { mode: "run" }, - inputText: '{\n "mode": "run"\n}', - }, - { expanded: false, onToggleExpanded: vi.fn() }, - ), - container, - ); - - expect(container.textContent).toContain("Tool call"); - expect(container.textContent).not.toContain("Tool input"); - const summaryButton = container.querySelector("button.chat-tool-msg-summary"); - expect(summaryButton).not.toBeNull(); - expect(summaryButton?.getAttribute("aria-expanded")).toBe("false"); - }); - - it("keeps raw details for legacy canvas tool output without rendering tool-row previews", () => { - const container = document.createElement("div"); - render( - renderToolCard( - { - id: "msg:view:7", - name: "canvas_render", - outputText: JSON.stringify({ - kind: "canvas", - view: { - backend: "canvas", - id: "cv_counter", - url: "/__openclaw__/canvas/documents/cv_counter/index.html", - title: "Counter demo", - preferred_height: 480, - }, - presentation: { - target: "tool_card", - }, - }), - preview: { - kind: "canvas", - surface: "assistant_message", - render: "url", - viewId: "cv_counter", - title: "Counter demo", - url: "/__openclaw__/canvas/documents/cv_counter/index.html", - preferredHeight: 480, - }, - }, - { expanded: true, onToggleExpanded: vi.fn() }, - ), - container, - ); - - const rawToggle = container.querySelector(".chat-tool-card__raw-toggle"); - const rawBody = container.querySelector(".chat-tool-card__raw-body"); - - expect(container.textContent).toContain("Counter demo"); - expect(container.querySelector(".chat-tool-card__preview-frame")).toBeNull(); - expect(rawToggle?.getAttribute("aria-expanded")).toBe("false"); - expect(rawBody?.hidden).toBe(true); - - rawToggle?.dispatchEvent(new MouseEvent("click", { bubbles: true })); - - expect(rawToggle?.getAttribute("aria-expanded")).toBe("true"); - expect(rawBody?.hidden).toBe(false); - expect(rawBody?.textContent).toContain('"kind":"canvas"'); - }); - - it("opens assistant-surface canvas payloads in the sidebar when explicitly requested", () => { - const container = document.createElement("div"); - const onOpenSidebar = vi.fn(); - render( - renderToolCard( - { - id: "msg:view:8", - name: "canvas_render", - outputText: JSON.stringify({ - kind: "canvas", - view: { - backend: "canvas", - id: "cv_sidebar", - url: "/__openclaw__/canvas/documents/cv_sidebar/index.html", - title: "Player", - preferred_height: 360, - }, - presentation: { - target: "assistant_message", - }, - }), - preview: { - kind: "canvas", - surface: "assistant_message", - render: "url", - viewId: "cv_sidebar", - url: "/__openclaw__/canvas/documents/cv_sidebar/index.html", - title: "Player", - preferredHeight: 360, - }, - }, - { expanded: true, onToggleExpanded: vi.fn(), onOpenSidebar }, - ), - container, - ); - - const sidebarButton = container.querySelector(".chat-tool-card__action-btn"); - sidebarButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); - - expect(sidebarButton).not.toBeNull(); - expect(onOpenSidebar).toHaveBeenCalledWith( - expect.objectContaining({ - kind: "canvas", - docId: "cv_sidebar", - entryUrl: "/__openclaw__/canvas/documents/cv_sidebar/index.html", - }), - ); - }); -}); diff --git a/ui/src/ui/chat/tool-cards.test.ts b/ui/src/ui/chat/tool-cards.test.ts new file mode 100644 index 00000000000..1c58fa6b66d --- /dev/null +++ b/ui/src/ui/chat/tool-cards.test.ts @@ -0,0 +1,188 @@ +/* @vitest-environment jsdom */ + +import { render } from "lit"; +import { describe, expect, it, vi } from "vitest"; +import { renderToolCard } from "./tool-cards.ts"; + +vi.mock("../icons.ts", () => ({ + icons: {}, +})); + +vi.mock("../tool-display.ts", () => ({ + formatToolDetail: () => undefined, + resolveToolDisplay: ({ name }: { name: string }) => ({ + name, + label: name + .split(/[._-]/g) + .map((part) => (part ? part[0].toUpperCase() + part.slice(1) : part)) + .join(" "), + icon: "zap", + }), +})); + +describe("tool-cards", () => { + it("renders expanded cards with inline input and output sections", () => { + const container = document.createElement("div"); + const toggle = vi.fn(); + render( + renderToolCard( + { + id: "msg:4:call-4", + name: "browser.open", + args: { url: "https://example.com" }, + inputText: '{\n "url": "https://example.com"\n}', + outputText: "Opened page", + }, + { expanded: true, onToggleExpanded: toggle }, + ), + container, + ); + + expect(container.textContent).toContain("Tool input"); + expect(container.textContent).toContain("Tool output"); + expect(container.textContent).toContain("https://example.com"); + expect(container.textContent).toContain("Opened page"); + }); + + it("renders expanded tool calls without an inline output block when no output is present", () => { + const container = document.createElement("div"); + render( + renderToolCard( + { + id: "msg:4b:call-4b", + name: "sessions_spawn", + args: { mode: "session", thread: true }, + inputText: '{\n "mode": "session",\n "thread": true\n}', + }, + { expanded: true, onToggleExpanded: vi.fn() }, + ), + container, + ); + + expect(container.textContent).toContain("Tool input"); + expect(container.textContent).toContain('"thread": true'); + expect(container.textContent).not.toContain("Tool output"); + expect(container.textContent).not.toContain("No output"); + }); + + it("labels collapsed tool calls as tool call", () => { + const container = document.createElement("div"); + render( + renderToolCard( + { + id: "msg:5:call-5", + name: "sessions_spawn", + args: { mode: "run" }, + inputText: '{\n "mode": "run"\n}', + }, + { expanded: false, onToggleExpanded: vi.fn() }, + ), + container, + ); + + expect(container.textContent).toContain("Tool call"); + expect(container.textContent).not.toContain("Tool input"); + const summaryButton = container.querySelector("button.chat-tool-msg-summary"); + expect(summaryButton).not.toBeNull(); + expect(summaryButton?.getAttribute("aria-expanded")).toBe("false"); + }); + + it("keeps raw details for legacy canvas tool output without rendering tool-row previews", () => { + const container = document.createElement("div"); + render( + renderToolCard( + { + id: "msg:view:7", + name: "canvas_render", + outputText: JSON.stringify({ + kind: "canvas", + view: { + backend: "canvas", + id: "cv_counter", + url: "/__openclaw__/canvas/documents/cv_counter/index.html", + title: "Counter demo", + preferred_height: 480, + }, + presentation: { + target: "tool_card", + }, + }), + preview: { + kind: "canvas", + surface: "assistant_message", + render: "url", + viewId: "cv_counter", + title: "Counter demo", + url: "/__openclaw__/canvas/documents/cv_counter/index.html", + preferredHeight: 480, + }, + }, + { expanded: true, onToggleExpanded: vi.fn() }, + ), + container, + ); + + const rawToggle = container.querySelector(".chat-tool-card__raw-toggle"); + const rawBody = container.querySelector(".chat-tool-card__raw-body"); + + expect(container.textContent).toContain("Counter demo"); + expect(container.querySelector(".chat-tool-card__preview-frame")).toBeNull(); + expect(rawToggle?.getAttribute("aria-expanded")).toBe("false"); + expect(rawBody?.hidden).toBe(true); + + rawToggle?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + + expect(rawToggle?.getAttribute("aria-expanded")).toBe("true"); + expect(rawBody?.hidden).toBe(false); + expect(rawBody?.textContent).toContain('"kind":"canvas"'); + }); + + it("opens assistant-surface canvas payloads in the sidebar when explicitly requested", () => { + const container = document.createElement("div"); + const onOpenSidebar = vi.fn(); + render( + renderToolCard( + { + id: "msg:view:8", + name: "canvas_render", + outputText: JSON.stringify({ + kind: "canvas", + view: { + backend: "canvas", + id: "cv_sidebar", + url: "/__openclaw__/canvas/documents/cv_sidebar/index.html", + title: "Player", + preferred_height: 360, + }, + presentation: { + target: "assistant_message", + }, + }), + preview: { + kind: "canvas", + surface: "assistant_message", + render: "url", + viewId: "cv_sidebar", + url: "/__openclaw__/canvas/documents/cv_sidebar/index.html", + title: "Player", + preferredHeight: 360, + }, + }, + { expanded: true, onToggleExpanded: vi.fn(), onOpenSidebar }, + ), + container, + ); + + const sidebarButton = container.querySelector(".chat-tool-card__action-btn"); + sidebarButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + + expect(sidebarButton).not.toBeNull(); + expect(onOpenSidebar).toHaveBeenCalledWith( + expect.objectContaining({ + kind: "canvas", + docId: "cv_sidebar", + entryUrl: "/__openclaw__/canvas/documents/cv_sidebar/index.html", + }), + ); + }); +});