mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:00:43 +00:00
test(ui): split tool card render coverage
This commit is contained in:
@@ -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",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
188
ui/src/ui/chat/tool-cards.test.ts
Normal file
188
ui/src/ui/chat/tool-cards.test.ts
Normal 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",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user