- Create ChatMessageIdentityBar with agent icon, name, timestamp, and streaming dot - Create ChatStreamingCursor with animate-cursor-blink and aria-hidden - Extend ChatMessage with agentName, agentIcon, agentRole, timestamp, isStreaming props - Wrap assistant messages in group div for hover actions (Plan 03) - Create agent-role-colors.ts with 11 distinct role colors (light + dark variants) - Create ChatMarkdownMessage prerequisite from phase-21 base - All 4 ChatMessageIdentityBar tests pass
66 lines
2 KiB
TypeScript
66 lines
2 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { act } from "react";
|
|
import { createRoot } from "react-dom/client";
|
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
import { ChatMessageIdentityBar } from "./ChatMessageIdentityBar";
|
|
|
|
// Tell React this environment uses act() for event flushing.
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
|
|
|
|
describe("ChatMessageIdentityBar", () => {
|
|
let container: HTMLDivElement;
|
|
let root: ReturnType<typeof createRoot> | null = null;
|
|
|
|
beforeEach(() => {
|
|
container = document.createElement("div");
|
|
document.body.appendChild(container);
|
|
root = null;
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (root) {
|
|
act(() => {
|
|
root!.unmount();
|
|
});
|
|
root = null;
|
|
}
|
|
if (container.parentNode) {
|
|
container.remove();
|
|
}
|
|
});
|
|
|
|
function render(props: Parameters<typeof ChatMessageIdentityBar>[0]) {
|
|
root = createRoot(container);
|
|
act(() => {
|
|
root!.render(<ChatMessageIdentityBar {...props} />);
|
|
});
|
|
return container;
|
|
}
|
|
|
|
it("renders agent name in semibold text", () => {
|
|
render({ agentName: "PM Agent" });
|
|
const nameEl = container.querySelector(".font-semibold");
|
|
expect(nameEl).toBeDefined();
|
|
expect(nameEl?.textContent).toBe("PM Agent");
|
|
});
|
|
|
|
it("renders timestamp when provided", () => {
|
|
render({ agentName: "Test", timestamp: "2026-01-01T12:30:00Z" });
|
|
const allText = container.textContent ?? "";
|
|
expect(allText).toContain("12:30");
|
|
});
|
|
|
|
it("applies role-specific color class", () => {
|
|
render({ agentName: "PM", agentRole: "pm" });
|
|
const nameEl = container.querySelector(".font-semibold");
|
|
expect(nameEl?.className).toContain("text-blue-600");
|
|
expect(nameEl?.className).toContain("dark:text-blue-400");
|
|
});
|
|
|
|
it("shows streaming indicator dot when isStreaming", () => {
|
|
render({ agentName: "Test", isStreaming: true });
|
|
const dot = container.querySelector(".animate-pulse");
|
|
expect(dot).not.toBeNull();
|
|
});
|
|
});
|