test(ui): split chat avatar render coverage

This commit is contained in:
Peter Steinberger
2026-04-25 23:07:35 +01:00
parent cf303b3101
commit 309f7f1873
2 changed files with 87 additions and 55 deletions

View File

@@ -0,0 +1,87 @@
/* @vitest-environment jsdom */
import { render } from "lit";
import { describe, expect, it, vi } from "vitest";
import { renderChatAvatar } from "./chat-avatar.ts";
vi.mock("../views/agents-utils.ts", () => ({
isRenderableControlUiAvatarUrl: (value: string) =>
/^data:image\//i.test(value) || (value.startsWith("/") && !value.startsWith("//")),
assistantAvatarFallbackUrl: () => "apple-touch-icon.png",
resolveAssistantTextAvatar: (value: string | null | undefined) => {
if (!value) {
return null;
}
return value.length <= 3 ? value : null;
},
resolveChatAvatarRenderUrl: (
candidate: string | null | undefined,
agent: { identity?: { avatar?: string; avatarUrl?: string } },
) => {
const isRenderableControlUiAvatarUrl = (value: string) =>
/^data:image\//i.test(value) || (value.startsWith("/") && !value.startsWith("//"));
if (typeof candidate === "string" && candidate.startsWith("blob:")) {
return candidate;
}
for (const value of [candidate, agent.identity?.avatarUrl, agent.identity?.avatar]) {
if (typeof value === "string" && isRenderableControlUiAvatarUrl(value)) {
return value;
}
}
return null;
},
}));
function renderAvatar(params: Parameters<typeof renderChatAvatar>) {
const container = document.createElement("div");
render(renderChatAvatar(...params), container);
return container.querySelector<HTMLElement>(".chat-avatar");
}
describe("renderChatAvatar", () => {
it("uses the assistant fallback when no assistant avatar is configured", () => {
const avatar = renderAvatar(["assistant"]);
expect(avatar).not.toBeNull();
expect(avatar?.getAttribute("src")).toBe("apple-touch-icon.png");
});
it("renders assistant fallback, blob image, and text avatars", () => {
const remoteAvatar = renderAvatar([
"assistant",
{ avatar: "https://example.com/avatar.png", name: "Val" },
]);
expect(remoteAvatar?.getAttribute("src")).toBe("apple-touch-icon.png");
const blobAvatar = renderAvatar(["assistant", { avatar: "blob:managed-image", name: "Val" }]);
expect(blobAvatar?.tagName).toBe("IMG");
expect(blobAvatar?.getAttribute("src")).toBe("blob:managed-image");
const textAvatar = renderAvatar(["assistant", { avatar: "VC", name: "Val" }]);
expect(textAvatar?.tagName).toBe("DIV");
expect(textAvatar?.textContent).toContain("VC");
expect(textAvatar?.getAttribute("aria-label")).toBe("Val");
});
it("uses the assistant fallback while authenticated avatar routes are loading", () => {
const avatar = renderAvatar([
"assistant",
{ avatar: "/avatar/main", name: "OpenClaw" },
undefined,
"",
"session-token",
]);
expect(avatar?.getAttribute("src")).toBe("apple-touch-icon.png");
});
it("renders local user image and text avatars", () => {
const imageAvatar = renderAvatar(["user", undefined, { name: "Buns", avatar: "/avatar/user" }]);
expect(imageAvatar?.getAttribute("src")).toBe("/avatar/user");
expect(imageAvatar?.getAttribute("alt")).toBe("Buns");
const textAvatar = renderAvatar(["user", undefined, { name: "Buns", avatar: "AB" }]);
expect(textAvatar?.tagName).toBe("DIV");
expect(textAvatar?.textContent).toContain("AB");
});
});

View File

@@ -8,7 +8,6 @@ import {
createSessionsListResult,
DEFAULT_CHAT_MODEL_CATALOG,
} from "../chat-model.test-helpers.ts";
import { renderChatAvatar } from "../chat/chat-avatar.ts";
import { renderChatQueue } from "../chat/chat-queue.ts";
import { buildRawSidebarContent } from "../chat/chat-sidebar-raw.ts";
import { renderWelcomeState } from "../chat/chat-welcome.ts";
@@ -80,12 +79,6 @@ vi.mock("./agents-utils.ts", () => ({
},
}));
function renderAvatar(params: Parameters<typeof renderChatAvatar>) {
const container = document.createElement("div");
render(renderChatAvatar(...params), container);
return container.querySelector<HTMLElement>(".chat-avatar");
}
function renderQueue(params: {
queue: ChatQueueItem[];
canAbort?: boolean;
@@ -264,54 +257,6 @@ describe("chat queue", () => {
});
});
describe("renderChatAvatar", () => {
it("uses the assistant fallback when no assistant avatar is configured", () => {
const avatar = renderAvatar(["assistant"]);
expect(avatar).not.toBeNull();
expect(avatar?.getAttribute("src")).toBe("apple-touch-icon.png");
});
it("renders assistant fallback, blob image, and text avatars", () => {
const remoteAvatar = renderAvatar([
"assistant",
{ avatar: "https://example.com/avatar.png", name: "Val" },
]);
expect(remoteAvatar?.getAttribute("src")).toBe("apple-touch-icon.png");
const blobAvatar = renderAvatar(["assistant", { avatar: "blob:managed-image", name: "Val" }]);
expect(blobAvatar?.tagName).toBe("IMG");
expect(blobAvatar?.getAttribute("src")).toBe("blob:managed-image");
const textAvatar = renderAvatar(["assistant", { avatar: "VC", name: "Val" }]);
expect(textAvatar?.tagName).toBe("DIV");
expect(textAvatar?.textContent).toContain("VC");
expect(textAvatar?.getAttribute("aria-label")).toBe("Val");
});
it("uses the assistant fallback while authenticated avatar routes are loading", () => {
const avatar = renderAvatar([
"assistant",
{ avatar: "/avatar/main", name: "OpenClaw" },
undefined,
"",
"session-token",
]);
expect(avatar?.getAttribute("src")).toBe("apple-touch-icon.png");
});
it("renders local user image and text avatars", () => {
const imageAvatar = renderAvatar(["user", undefined, { name: "Buns", avatar: "/avatar/user" }]);
expect(imageAvatar?.getAttribute("src")).toBe("/avatar/user");
expect(imageAvatar?.getAttribute("alt")).toBe("Buns");
const textAvatar = renderAvatar(["user", undefined, { name: "Buns", avatar: "AB" }]);
expect(textAvatar?.tagName).toBe("DIV");
expect(textAvatar?.textContent).toContain("AB");
});
});
describe("chat sidebar raw content", () => {
it("keeps markdown raw text toggles idempotent", () => {
const rawMarkdown = "```ts\nconst value = 1;\n```";