Files
fusion/packages/dashboard/app/components/__tests__/AgentsView.orgchart.test.tsx
Fusion 7d61827cae test(FN-4551): complete Step 4 — org chart pan/zoom and connector coverage
Fusion-Task-Id: FN-4551
Fusion-Task-Lineage: 1e34e4ba-25a3-4ad8-b68c-3323ae458ba2
2026-05-14 22:16:44 -07:00

123 lines
7.4 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { AgentsView } from "../AgentsView";
import * as apiModule from "../../api";
const mockViewportMode = vi.fn<() => "mobile" | "tablet" | "desktop">(() => "desktop");
vi.mock("../../hooks/useViewportMode", () => ({ useViewportMode: () => mockViewportMode() }));
vi.mock("../../hooks/useConfirm", () => ({ useConfirm: () => ({ confirm: vi.fn().mockResolvedValue(true) }) }));
vi.mock("../AgentDetailView", () => ({ AgentDetailView: () => null, relativeTime: () => "now" }));
vi.mock("../../api", async (importOriginal) => {
const { createDashboardApiMock } = await import("../../test/mockApi");
return createDashboardApiMock(() => importOriginal<typeof import("../../api")>(), {
fetchAgents: vi.fn().mockResolvedValue([]),
fetchAgentStats: vi.fn().mockResolvedValue({ total: 0, byState: {}, byRole: {} }),
fetchOrgTree: vi.fn(),
fetchSettings: vi.fn().mockResolvedValue({ heartbeatMultiplier: 1 }),
updateSettings: vi.fn().mockResolvedValue({}),
});
});
const mockFetchOrgTree = vi.mocked((apiModule as any).fetchOrgTree);
const mockFetchAgents = vi.mocked((apiModule as any).fetchAgents);
const orgTree = [{ agent: { id: "ceo", name: "CEO", role: "scheduler", state: "active", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), metadata: {} }, children: [
{ agent: { id: "cto", name: "CTO", role: "engineer", state: "active", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), metadata: {} }, children: [
{ agent: { id: "eng-a", name: "Eng A", role: "executor", state: "idle", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), metadata: {} }, children: [] },
{ agent: { id: "eng-b", name: "Eng B", role: "executor", state: "idle", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), metadata: {} }, children: [{ agent: { id: "eng-c", name: "Eng C", role: "reviewer", state: "idle", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), metadata: {} }, children: [] }] },
] },
] }, { agent: { id: "cfo", name: "CFO", role: "triage", state: "idle", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), metadata: {} }, children: [] }];
function mockRects() {
vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockImplementation(function () {
const el = this as HTMLElement;
if (el.classList.contains("agent-org-chart-viewport")) return { left: 0, top: 0, width: 400, height: 280, right: 400, bottom: 280, x: 0, y: 0, toJSON: () => ({}) } as DOMRect;
if (el.classList.contains("agent-org-chart")) return { left: 0, top: 0, width: 700, height: 500, right: 700, bottom: 500, x: 0, y: 0, toJSON: () => ({}) } as DOMRect;
const id = el.getAttribute("data-agent-id");
const map: Record<string, DOMRect> = {
ceo: { left: 200, top: 20, width: 140, height: 80, right: 340, bottom: 100, x: 200, y: 20, toJSON: () => ({}) } as DOMRect,
cto: { left: 120, top: 180, width: 140, height: 80, right: 260, bottom: 260, x: 120, y: 180, toJSON: () => ({}) } as DOMRect,
cfo: { left: 360, top: 180, width: 140, height: 80, right: 500, bottom: 260, x: 360, y: 180, toJSON: () => ({}) } as DOMRect,
"eng-a": { left: 40, top: 340, width: 140, height: 80, right: 180, bottom: 420, x: 40, y: 340, toJSON: () => ({}) } as DOMRect,
"eng-b": { left: 200, top: 340, width: 140, height: 80, right: 340, bottom: 420, x: 200, y: 340, toJSON: () => ({}) } as DOMRect,
"eng-c": { left: 220, top: 500, width: 140, height: 80, right: 360, bottom: 580, x: 220, y: 500, toJSON: () => ({}) } as DOMRect,
};
return map[id ?? ""] ?? ({ left: 0, top: 0, width: 0, height: 0, right: 0, bottom: 0, x: 0, y: 0, toJSON: () => ({}) } as DOMRect);
});
}
describe("AgentsView org chart interactions", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.stubGlobal("ResizeObserver", class { observe() {} disconnect() {} });
mockFetchAgents.mockResolvedValue([]);
mockFetchOrgTree.mockResolvedValue(orgTree);
mockRects();
});
it("renders controls and supports transform interactions", async () => {
render(<AgentsView addToast={vi.fn()} />);
fireEvent.click(await screen.findByLabelText("Org Chart view"));
await screen.findByTestId("agent-org-chart-controls");
expect(screen.getByLabelText("Horizontal layout")).toBeInTheDocument();
expect(screen.getByLabelText("Fit org chart")).toBeInTheDocument();
const canvas = screen.getByTestId("agent-org-chart-viewport").querySelector(".agent-org-chart-canvas") as HTMLDivElement;
const zoomLabel = screen.getByText(/%/);
fireEvent.click(screen.getByLabelText("Zoom in org chart"));
expect(zoomLabel.textContent).not.toBe("100%");
expect(canvas.style.transform).toMatch(/scale\(/);
fireEvent.click(screen.getByLabelText("Fit org chart"));
expect(canvas.style.transform).not.toContain("translate(0px, 0px) scale(1)");
const viewport = screen.getByTestId("agent-org-chart-viewport");
fireEvent.pointerDown(viewport, { pointerId: 1, clientX: 30, clientY: 30 });
fireEvent.pointerMove(viewport, { pointerId: 1, clientX: 80, clientY: 90 });
fireEvent.pointerUp(viewport, { pointerId: 1, clientX: 80, clientY: 90 });
expect(canvas.style.transform).toContain("translate(");
const card = document.querySelector('.org-chart-node-card[data-agent-id="ceo"]') as HTMLElement;
const before = canvas.style.transform;
fireEvent.pointerDown(card, { pointerId: 2, clientX: 220, clientY: 40 });
fireEvent.pointerMove(viewport, { pointerId: 2, clientX: 280, clientY: 120 });
fireEvent.pointerUp(viewport, { pointerId: 2, clientX: 280, clientY: 120 });
expect(canvas.style.transform).toBe(before);
const scaleBefore = canvas.style.transform;
fireEvent.pointerDown(viewport, { pointerId: 3, clientX: 50, clientY: 50 });
fireEvent.pointerDown(viewport, { pointerId: 4, clientX: 120, clientY: 120 });
fireEvent.pointerMove(viewport, { pointerId: 4, clientX: 180, clientY: 180 });
fireEvent.pointerUp(viewport, { pointerId: 3, clientX: 50, clientY: 50 });
fireEvent.pointerUp(viewport, { pointerId: 4, clientX: 180, clientY: 180 });
expect(canvas.style.transform).not.toBe(scaleBefore);
viewport.focus();
fireEvent.keyDown(viewport, { key: "ArrowRight" });
fireEvent.keyDown(viewport, { key: "+" });
fireEvent.keyDown(viewport, { key: "-" });
fireEvent.keyDown(viewport, { key: "0" });
fireEvent.keyDown(viewport, { key: "Home" });
expect(canvas.style.transform).toContain("scale(1)");
await waitFor(() => {
const paths = document.querySelectorAll(".agent-org-chart-connectors path");
expect(paths.length).toBe(4);
expect(paths[0].getAttribute("d")).toContain("L");
});
fireEvent.click(screen.getByLabelText("Vertical layout"));
await waitFor(() => {
const firstPath = document.querySelector(".agent-org-chart-connectors path")?.getAttribute("d") ?? "";
expect(firstPath).toMatch(/^M\s\d+\s\d+\sL\s\d+\s\d+/);
});
});
it("renders mobile controls", async () => {
mockViewportMode.mockReturnValue("mobile");
render(<AgentsView addToast={vi.fn()} />);
fireEvent.click(await screen.findByLabelText("Org Chart view"));
expect(await screen.findByTestId("agent-org-chart-controls")).toBeInTheDocument();
expect(screen.getByLabelText("Center org chart")).toBeInTheDocument();
});
});