evOo-local/packages/ui-web/src/bot-avatar.test.tsx
cynarAI 9f4991abe0
feat(avatar): draw every bot as a living Oo on web (#136)
Bots now render as an Oo rig built once in core: nine species, styled
Oo eyes, eyewear, headwear, neckwear, arms and moment props, with a
level of detail per size. A shared animation frame loop moves only
visible avatars and holds still under reduced motion.


Claude-Session: https://claude.ai/code/session_01EKuME5CgL1ddGGN7LGvtBj

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-04 22:40:12 +02:00

119 lines
4.7 KiB
TypeScript

import { ACTIVE_RUN_STATUSES, OO_DEFAULT_COLOR } from "@rakazo/core";
import { renderToString } from "react-dom/server";
import { describe, expect, it } from "vitest";
import { BotAvatar, OoShapePreview, ooMomentForPose, parseBotAvatar } from "./bot-avatar.js";
describe("BotAvatar", () => {
it("renders distinct shape gradient IDs for concurrent working avatars", () => {
const html = renderToString(
<div>
<BotAvatar color="#8B5CF6" status="running" />
<BotAvatar color="#10B981" status="running" />
</div>,
);
const bodyGradIds = [...html.matchAll(/id="(oo[^"]+-body)"/g)].map((m) => m[1]);
expect(bodyGradIds).toHaveLength(2);
expect(new Set(bodyGradIds).size).toBe(2);
for (const id of bodyGradIds) {
expect(html).toContain(`url(#${id})`);
}
});
it.each([...ACTIVE_RUN_STATUSES])("marks active run status %s as working", (status) => {
const html = renderToString(<BotAvatar color="#3B82F6" status={status} />);
expect(html).toContain("<svg");
expect(html).toContain('data-working="true"');
});
it("keeps working attribute false when idle", () => {
const html = renderToString(<BotAvatar color="#F59E0B" status="idle" />);
expect(html).toContain('data-working="false"');
});
it("draws the Oo eyes for plain color values", () => {
const html = renderToString(
<BotAvatar color="#D9508A" identity="maya" size={28} status="running" />,
);
expect(html).toContain("<svg");
expect(html).toContain("<path");
expect(html).toContain('data-part="eye-big"');
expect(html).toContain('data-part="eye-small"');
expect(html).toContain('data-oo-moment="working"');
expect(html).toContain('data-working="true"');
});
it("draws the stored look", () => {
const html = renderToString(
<BotAvatar color="oo1:c=FF781C;sp=cat;ew=round;hd=chef" size={72} />,
);
expect(html).toContain('data-oo-species="cat"');
expect(html).toContain('data-part="ear-left"');
expect(html).toContain("#FFFFFF");
});
it("keeps small avatars free of arms and props", () => {
const small = renderToString(<BotAvatar color="#8B5CF6" status="running" size={24} />);
const large = renderToString(<BotAvatar color="#8B5CF6" status="running" size={72} />);
expect(small).not.toContain('data-part="arm-left"');
expect(large).toContain('data-part="arm-left"');
});
it("maps mascot poses to moments", () => {
expect(ooMomentForPose("delighted")).toBe("success");
expect(ooMomentForPose("typing")).toBe("working");
expect(ooMomentForPose("error")).toBe("concerned");
expect(ooMomentForPose("listening")).toBe("listening");
const html = renderToString(<BotAvatar color="#8B5CF6" pose="thinking" size={72} />);
expect(html).toContain('data-oo-moment="thinking"');
});
it("renders distinct shapes for distinct bot identities", () => {
const maya = renderToString(<BotAvatar color="#D9508A" identity="maya" />);
const github = renderToString(<BotAvatar color="#D9508A" identity="github" />);
expect(maya).not.toEqual(github);
});
it("parses shape indexes from encoded color values", () => {
const parsed = parseBotAvatar(`${OO_DEFAULT_COLOR}::shape_3`);
expect(parsed.color).toBe(OO_DEFAULT_COLOR);
expect(parsed.shapeIndex).toBe(3);
expect(parsed.isImage).toBe(false);
});
it("normalizes malformed shape suffixes to shape 0", () => {
expect(parseBotAvatar(`${OO_DEFAULT_COLOR}::shape_-1`).shapeIndex).toBe(0);
expect(parseBotAvatar(`${OO_DEFAULT_COLOR}::shape_3junk`).shapeIndex).toBe(0);
expect(parseBotAvatar(`${OO_DEFAULT_COLOR}::shape_`).shapeIndex).toBe(0);
});
it("renders uploaded images without the geometric svg", () => {
const html = renderToString(
<BotAvatar color="data:image/png;base64,abc" identity="maya" size={32} />,
);
expect(html).toContain("<img");
expect(html).not.toContain("<path");
expect(html).not.toContain("data-part");
});
it("does not treat arbitrary http(s) color values as remote images", () => {
const parsed = parseBotAvatar("https://evil.example/track.png");
expect(parsed.isImage).toBe(false);
expect(parsed.imageUrl).toBeUndefined();
const html = renderToString(
<BotAvatar color="https://evil.example/track.png" identity="maya" size={32} />,
);
expect(html).not.toContain("<img");
expect(html).not.toContain("evil.example");
});
it("exposes shape picker name and pressed state", () => {
const html = renderToString(
<OoShapePreview shapeIndex={0} color="#8B5CF6" selected onClick={() => undefined} />,
);
expect(html).toContain('aria-label="EvOo"');
expect(html).toContain('aria-pressed="true"');
expect(html).toContain("focus-visible:ring-2");
});
});