


















@@ -0,0 +1,101 @@
1+/* @vitest-environment jsdom */
2+3+import { render } from "lit";
4+import { describe, expect, it, vi } from "vitest";
5+import { renderChatAvatar } from "./chat-avatar.ts";
6+7+vi.mock("../views/agents-utils.ts", () => {
8+const isRenderableControlUiAvatarUrl = (value: string) =>
9+/^data:image\//i.test(value) || (value.startsWith("/") && !value.startsWith("//"));
10+11+return {
12+assistantAvatarFallbackUrl: () => "/openclaw-molty.png",
13+ isRenderableControlUiAvatarUrl,
14+resolveAssistantTextAvatar: (value: string | null | undefined) => {
15+const trimmed = value?.trim();
16+if (!trimmed || trimmed === "A") {
17+return null;
18+}
19+if (trimmed.startsWith("blob:") || isRenderableControlUiAvatarUrl(trimmed)) {
20+return null;
21+}
22+if (
23+trimmed.length > 8 ||
24+/\s/.test(trimmed) ||
25+/[\\/.:]/.test(trimmed) ||
26+/[\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFEFF]/u.test(trimmed)
27+) {
28+return null;
29+}
30+return trimmed;
31+},
32+resolveChatAvatarRenderUrl: (
33+candidate: string | null | undefined,
34+agent: { identity?: { avatar?: string; avatarUrl?: string } },
35+) => {
36+if (typeof candidate === "string" && candidate.startsWith("blob:")) {
37+return candidate;
38+}
39+for (const value of [candidate, agent.identity?.avatarUrl, agent.identity?.avatar]) {
40+if (typeof value === "string" && isRenderableControlUiAvatarUrl(value)) {
41+return value;
42+}
43+}
44+return null;
45+},
46+};
47+});
48+49+function renderAvatar(params: Parameters<typeof renderChatAvatar>) {
50+const container = document.createElement("div");
51+render(renderChatAvatar(...params), container);
52+return container.querySelector<HTMLElement>(".chat-avatar");
53+}
54+55+describe("renderChatAvatar", () => {
56+it("uses the assistant fallback when no assistant avatar is configured", () => {
57+const avatar = renderAvatar(["assistant"]);
58+59+expect(avatar).not.toBeNull();
60+expect(avatar?.getAttribute("src")).toBe("/openclaw-molty.png");
61+});
62+63+it("renders assistant fallback, blob image, and text avatars", () => {
64+const remoteAvatar = renderAvatar([
65+"assistant",
66+{ avatar: "https://example.com/avatar.png", name: "Val" },
67+]);
68+expect(remoteAvatar?.getAttribute("src")).toBe("/openclaw-molty.png");
69+70+const blobAvatar = renderAvatar(["assistant", { avatar: "blob:managed-image", name: "Val" }]);
71+expect(blobAvatar?.tagName).toBe("IMG");
72+expect(blobAvatar?.getAttribute("src")).toBe("blob:managed-image");
73+74+const textAvatar = renderAvatar(["assistant", { avatar: "VC", name: "Val" }]);
75+expect(textAvatar?.tagName).toBe("DIV");
76+expect(textAvatar?.textContent).toContain("VC");
77+expect(textAvatar?.getAttribute("aria-label")).toBe("Val");
78+});
79+80+it("uses the assistant fallback while authenticated avatar routes are loading", () => {
81+const avatar = renderAvatar([
82+"assistant",
83+{ avatar: "/avatar/main", name: "OpenClaw" },
84+undefined,
85+"",
86+"session-token",
87+]);
88+89+expect(avatar?.getAttribute("src")).toBe("/openclaw-molty.png");
90+});
91+92+it("renders local user image and text avatars", () => {
93+const imageAvatar = renderAvatar(["user", undefined, { name: "Buns", avatar: "/avatar/user" }]);
94+expect(imageAvatar?.getAttribute("src")).toBe("/avatar/user");
95+expect(imageAvatar?.getAttribute("alt")).toBe("Buns");
96+97+const textAvatar = renderAvatar(["user", undefined, { name: "Buns", avatar: "AB" }]);
98+expect(textAvatar?.tagName).toBe("DIV");
99+expect(textAvatar?.textContent).toContain("AB");
100+});
101+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。