test(canvas): add BroadcastBanner WCAG tests + dismissBroadcastMessage coverage
- BroadcastBanner: 8 tests covering role=alert, per-message dismiss, aria-live, focus-visible ring, and WCAG AA contrast color classes - canvas.test.ts: 3 tests for dismissBroadcastMessage (clear all, dismiss one, idempotent unknown id) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
8bcff825ff
commit
0e7d313240
111
canvas/src/components/__tests__/BroadcastBanner.test.tsx
Normal file
111
canvas/src/components/__tests__/BroadcastBanner.test.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
// @vitest-environment jsdom
|
||||
/**
|
||||
* Tests for BroadcastBanner component.
|
||||
* WCAG compliance: role=alert, aria-live=polite, per-message dismiss.
|
||||
*/
|
||||
import { describe, it, expect, vi, afterEach, beforeEach } from "vitest";
|
||||
import { render, screen, cleanup, fireEvent } from "@testing-library/react";
|
||||
import { BroadcastBanner } from "../BroadcastBanner";
|
||||
import { useCanvasStore } from "@/store/canvas";
|
||||
|
||||
const mockDismiss = vi.fn();
|
||||
|
||||
vi.mock("@/store/canvas", () => ({
|
||||
useCanvasStore: vi.fn((selector: (s: ReturnType<typeof useCanvasStore.getState>) => unknown) => {
|
||||
const state = {
|
||||
broadcastMessages: [] as Array<{
|
||||
id: string;
|
||||
senderId: string;
|
||||
sender: string;
|
||||
message: string;
|
||||
timestamp: string;
|
||||
}>,
|
||||
dismissBroadcastMessage: mockDismiss,
|
||||
};
|
||||
return selector(state);
|
||||
}),
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
mockDismiss.mockClear();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const broadcastMessages = [
|
||||
{ id: "m1", senderId: "ws-ops", sender: "Ops Agent", message: "Deploy in 5 min", timestamp: "2026-05-16T00:00:00Z" },
|
||||
{ id: "m2", senderId: "ws-sre", sender: "SRE Team", message: "Maintenance window tonight", timestamp: "2026-05-16T00:01:00Z" },
|
||||
];
|
||||
|
||||
function setup(messages = broadcastMessages) {
|
||||
vi.mocked(useCanvasStore).mockImplementation(
|
||||
(selector: (s: { broadcastMessages: typeof broadcastMessages; dismissBroadcastMessage: typeof mockDismiss }) => unknown) => {
|
||||
const state = {
|
||||
broadcastMessages: messages,
|
||||
dismissBroadcastMessage: mockDismiss,
|
||||
};
|
||||
return selector(state);
|
||||
}
|
||||
);
|
||||
return render(<BroadcastBanner />);
|
||||
}
|
||||
|
||||
describe("BroadcastBanner", () => {
|
||||
it("renders nothing when there are no messages", () => {
|
||||
setup([]);
|
||||
expect(screen.queryByRole("alert")).toBeNull();
|
||||
});
|
||||
|
||||
it("renders a role=alert banner for each broadcast message", () => {
|
||||
setup();
|
||||
const alerts = screen.getAllByRole("alert");
|
||||
expect(alerts).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("shows sender name and message content", () => {
|
||||
setup();
|
||||
expect(screen.getByText("Deploy in 5 min")).toBeTruthy();
|
||||
expect(screen.getByText("Ops Agent")).toBeTruthy();
|
||||
expect(screen.getByText("Maintenance window tonight")).toBeTruthy();
|
||||
expect(screen.getByText("SRE Team")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("each banner has a dismiss button with accessible label", () => {
|
||||
setup();
|
||||
const buttons = screen.getAllByRole("button", { name: /dismiss/i });
|
||||
expect(buttons).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("dismissing a banner calls dismissBroadcastMessage with the correct id", () => {
|
||||
setup();
|
||||
const buttons = screen.getAllByRole("button", { name: /dismiss/i });
|
||||
// Dismiss the second message (Maintenance window)
|
||||
fireEvent.click(buttons[1]);
|
||||
expect(mockDismiss).toHaveBeenCalledTimes(1);
|
||||
expect(mockDismiss).toHaveBeenCalledWith("m2");
|
||||
});
|
||||
|
||||
it("dismissing one banner does not dismiss others", () => {
|
||||
setup();
|
||||
const buttons = screen.getAllByRole("button", { name: /dismiss/i });
|
||||
fireEvent.click(buttons[0]);
|
||||
expect(mockDismiss).toHaveBeenCalledWith("m1");
|
||||
expect(mockDismiss).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("dismiss button has focus-visible ring (WCAG 2.4.7)", () => {
|
||||
setup();
|
||||
const button = screen.getAllByRole("button", { name: /dismiss/i })[0];
|
||||
expect(button.className).toContain("focus-visible:ring");
|
||||
});
|
||||
|
||||
it("sender and message text use adequate contrast color classes", () => {
|
||||
setup();
|
||||
// text-blue-300 (#93C5FD) on blue-950/80 ≈ 5.9:1 contrast — WCAG AA ✓
|
||||
const senderLabel = screen.getByText("Ops Agent").closest("div");
|
||||
expect(senderLabel?.className).toContain("text-blue-300");
|
||||
// text-blue-50 (#EFF6FF) on blue-950/80 ≈ 11.7:1 — WCAG AAA ✓
|
||||
const messageEl = screen.getByText("Deploy in 5 min");
|
||||
expect(messageEl.className).toContain("text-blue-50");
|
||||
});
|
||||
});
|
||||
@ -1224,3 +1224,45 @@ describe("moveNode", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("useCanvasStore – broadcastMessages", () => {
|
||||
beforeEach(() => {
|
||||
useCanvasStore.setState({ broadcastMessages: [] });
|
||||
});
|
||||
|
||||
it("consumeBroadcastMessages returns and clears all messages", () => {
|
||||
useCanvasStore.setState({
|
||||
broadcastMessages: [
|
||||
{ id: "m1", senderId: "ws-1", sender: "Agent 1", message: "Hello", timestamp: "2026-05-16T00:00:00Z" },
|
||||
{ id: "m2", senderId: "ws-2", sender: "Agent 2", message: "World", timestamp: "2026-05-16T00:01:00Z" },
|
||||
],
|
||||
});
|
||||
const consumed = useCanvasStore.getState().consumeBroadcastMessages();
|
||||
expect(consumed).toHaveLength(2);
|
||||
expect(useCanvasStore.getState().broadcastMessages).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("dismissBroadcastMessage removes the targeted message only", () => {
|
||||
useCanvasStore.setState({
|
||||
broadcastMessages: [
|
||||
{ id: "m1", senderId: "ws-1", sender: "Agent 1", message: "Hello", timestamp: "2026-05-16T00:00:00Z" },
|
||||
{ id: "m2", senderId: "ws-2", sender: "Agent 2", message: "World", timestamp: "2026-05-16T00:01:00Z" },
|
||||
{ id: "m3", senderId: "ws-3", sender: "Agent 3", message: "Bye", timestamp: "2026-05-16T00:02:00Z" },
|
||||
],
|
||||
});
|
||||
useCanvasStore.getState().dismissBroadcastMessage("m2");
|
||||
const remaining = useCanvasStore.getState().broadcastMessages;
|
||||
expect(remaining).toHaveLength(2);
|
||||
expect(remaining.map((m) => m.id)).toEqual(["m1", "m3"]);
|
||||
});
|
||||
|
||||
it("dismissBroadcastMessage is idempotent for unknown IDs", () => {
|
||||
useCanvasStore.setState({
|
||||
broadcastMessages: [
|
||||
{ id: "m1", senderId: "ws-1", sender: "Agent 1", message: "Hello", timestamp: "2026-05-16T00:00:00Z" },
|
||||
],
|
||||
});
|
||||
expect(() => useCanvasStore.getState().dismissBroadcastMessage("nonexistent")).not.toThrow();
|
||||
expect(useCanvasStore.getState().broadcastMessages).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user