test(ui): split tool card render coverage

This commit is contained in:
Peter Steinberger
2026-04-25 22:49:15 +01:00
parent f1b1c3dc99
commit ce364121aa
2 changed files with 188 additions and 180 deletions

View File

@@ -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> = {}): 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<HTMLButtonElement>(".chat-tool-card__raw-toggle");
const rawBody = container.querySelector<HTMLElement>(".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<HTMLButtonElement>(".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",
}),
);
});
});

View File

@@ -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<HTMLButtonElement>(".chat-tool-card__raw-toggle");
const rawBody = container.querySelector<HTMLElement>(".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<HTMLButtonElement>(".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",
}),
);
});
});