Files
OmniRoute/tests/unit/ui/orchestrationNodes.test.tsx
Diego Rodrigues de Sa e Souza d2a027a156 feat(dashboard): orchestration canvas fase 2 — quick wins + hardening (2.3/2.4/2.8/2.11/2.12, #12270, #12271) (#12393)
* feat(dashboard): orchestration pure snapshot filter (search + chips)

* feat(dashboard): orchestration collapse-by-source in flow projection

* refactor(dashboard): orchestration model polish (#12271 model items)

* feat(dashboard): theme-aware orchestration status tokens (2.12)

* feat(dashboard): particle StatusEdge for orchestration canvas (2.11)

* feat(dashboard): orchestration search/filter chips + collapse in URL (2.3+2.4)

* feat(dashboard): drawer trace copy + error/a11y hardening (2.8, #12270)

* chore(dashboard): orchestration UI hardening (#12270)

* fix(dashboard): type-safe source read in AgentsTab collapse handler

* fix(dashboard): pass staleSince into the sourceStale message

* test(dashboard): orchestration missing coverage + fase2 i18n (#12271)

* refactor(dashboard): split orchestration page/toolbar/drawer helpers under complexity ratchet

---------

Co-authored-by: Markus Hartung <diegosouzapw@users.noreply.github.com>
2026-09-01 23:13:47 -03:00

250 lines
9.3 KiB
TypeScript

// @vitest-environment jsdom
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { describe, it, expect, beforeAll, afterEach, vi } from "vitest";
beforeAll(() => {
globalThis.ResizeObserver = class {
observe() {}
unobserve() {}
disconnect() {}
} as unknown as typeof ResizeObserver;
});
vi.mock("@xyflow/react", async () => {
const actual = (await vi.importActual("@xyflow/react")) as Record<string, unknown>;
return { ...actual, Handle: () => null, Position: { Top: "top", Bottom: "bottom" } };
});
vi.mock("next-intl", () => ({
useTranslations: () => (k: string, v?: Record<string, unknown>) =>
v ? `${k}:${JSON.stringify(v)}` : k,
}));
import type { EdgeProps } from "@xyflow/react";
import { WorkNode } from "@/app/(dashboard)/dashboard/orchestration/nodes/WorkNode";
import { SourceNode } from "@/app/(dashboard)/dashboard/orchestration/nodes/SourceNode";
import { OrchestratorNode } from "@/app/(dashboard)/dashboard/orchestration/nodes/OrchestratorNode";
import { ActivityNode } from "@/app/(dashboard)/dashboard/orchestration/nodes/ActivityNode";
import { OverflowNode } from "@/app/(dashboard)/dashboard/orchestration/nodes/OverflowNode";
import { StatusEdge } from "@/app/(dashboard)/dashboard/orchestration/edges/StatusEdge";
function render(el: React.ReactElement) {
const c = document.createElement("div");
document.body.appendChild(c);
const root = createRoot(c);
act(() => root.render(el));
return {
c,
cleanup: () => {
act(() => root.unmount());
c.remove();
},
};
}
afterEach(() => {
document.body.innerHTML = "";
});
describe("orchestration nodes", () => {
it("WorkNode shows label, state text and an aria-label", () => {
const data = {
id: "cloud-agent:t1",
kind: "work",
source: "cloud-agent",
state: "waiting_approval",
label: "Fix CI",
sublabel: "devin",
};
const { c, cleanup } = render(<WorkNode data={data as never} />);
expect(c.textContent).toContain("Fix CI");
expect(c.textContent).toContain("stateWaitingApproval");
expect(c.querySelector("[aria-label]")).toBeTruthy();
cleanup();
});
it("SourceNode with sourceIssue=error shows the warning marker", () => {
const data = {
id: "source:a2a",
kind: "source",
source: "a2a",
label: "A2A",
sublabel: "error",
sourceIssue: "error",
};
const { c, cleanup } = render(<SourceNode data={data as never} />);
expect(c.textContent).toContain("⚠");
cleanup();
});
it("SourceNode with sourceIssue=error and a parseable staleSince renders the formatted time in sourceStale", () => {
const staleSince = "2026-09-01T12:34:56.000Z";
const data = {
id: "source:a2a",
kind: "source",
source: "a2a",
label: "A2A",
sublabel: "error",
sourceIssue: "error",
staleSince,
};
const { c, cleanup } = render(<SourceNode data={data as never} />);
// Mocked next-intl `t` returns `${key}:${JSON.stringify(values)}` when values are passed —
// asserts the component actually forwards `{ since }`, not just the raw key.
const since = new Date(staleSince).toLocaleTimeString();
expect(c.textContent).toContain(`sourceStale:${JSON.stringify({ since })}`);
cleanup();
});
it("SourceNode with sourceIssue=error and no staleSince falls back to an em dash", () => {
const data = {
id: "source:a2a",
kind: "source",
source: "a2a",
label: "A2A",
sublabel: "error",
sourceIssue: "error",
};
const { c, cleanup } = render(<SourceNode data={data as never} />);
expect(c.textContent).toContain(`sourceStale:${JSON.stringify({ since: "—" })}`);
cleanup();
});
it("SourceNode with sourceIssue=error and an unparseable staleSince also falls back to an em dash", () => {
const data = {
id: "source:a2a",
kind: "source",
source: "a2a",
label: "A2A",
sublabel: "error",
sourceIssue: "error",
staleSince: "not-a-date",
};
const { c, cleanup } = render(<SourceNode data={data as never} />);
expect(c.textContent).toContain(`sourceStale:${JSON.stringify({ since: "—" })}`);
cleanup();
});
it("all 5 memo'd orchestration node components have a displayName", () => {
expect(WorkNode.displayName).toBe("WorkNode");
expect(SourceNode.displayName).toBe("SourceNode");
expect(OrchestratorNode.displayName).toBe("OrchestratorNode");
expect(ActivityNode.displayName).toBe("ActivityNode");
expect(OverflowNode.displayName).toBe("OverflowNode");
});
it("OrchestratorNode renders the label as text content and as aria-label", () => {
const data = { id: "orchestrator", kind: "orchestrator", label: "OmniRoute" };
const { c, cleanup } = render(<OrchestratorNode data={data as never} />);
expect(c.textContent).toContain("OmniRoute");
expect(c.querySelector('[aria-label="OmniRoute"]')).toBeTruthy();
cleanup();
});
it("ActivityNode renders label + sublabel and exposes the label as aria-label", () => {
const data = {
id: "cloud-agent:t1:activity",
kind: "activity",
source: "cloud-agent",
label: "npm test",
sublabel: "command",
};
const { c, cleanup } = render(<ActivityNode data={data as never} />);
expect(c.textContent).toContain("npm test");
expect(c.textContent).toContain("command");
expect(c.querySelector('[aria-label="npm test"]')).toBeTruthy();
cleanup();
});
it("OverflowNode renders the overflowMore count in text + aria-label and a badge per non-zero state", () => {
const data = {
id: "overflow:cloud-agent",
kind: "overflow",
source: "cloud-agent",
label: "+7 more",
counts: { running: 5, failed: 2, queued: 0 },
};
const { c, cleanup } = render(<OverflowNode data={data as never} />);
// Mocked next-intl `t` returns `${key}:${JSON.stringify(values)}` — asserts the
// component forwards the summed total (5 + 2 = 7), not a raw/stale count.
const expectedText = `overflowMore:${JSON.stringify({ count: 7 })}`;
expect(c.textContent).toContain(expectedText);
const labelled = c.querySelector("[aria-label]");
expect(labelled?.getAttribute("aria-label")).toBe(expectedText);
// One badge span per state with a non-zero count (running, failed) — queued (0) omitted.
const badges = Array.from(c.querySelectorAll("div.flex.gap-1 span"));
expect(badges.length).toBe(2);
expect(badges.map((b) => b.textContent)).toEqual(["5", "2"]);
cleanup();
});
it("OverflowNode with no counts renders a zero total and no per-state badges", () => {
const data = { id: "overflow:a2a", kind: "overflow", source: "a2a", label: "+0 more" };
const { c, cleanup } = render(<OverflowNode data={data as never} />);
expect(c.textContent).toContain(`overflowMore:${JSON.stringify({ count: 0 })}`);
expect(c.querySelectorAll("div.flex.gap-1 span").length).toBe(0);
cleanup();
});
it("SourceNode shows a collapse caret + aria-expanded + title reflecting !data.collapsed", () => {
const expandedData = { id: "source:a2a", kind: "source", source: "a2a", label: "A2A" };
const r1 = render(<SourceNode data={expandedData as never} />);
const expandedEl = r1.c.querySelector("[aria-expanded]");
expect(expandedEl?.getAttribute("aria-expanded")).toBe("true");
expect(expandedEl?.getAttribute("title")).toBe("sourceCollapse");
expect(r1.c.textContent).toContain("▾");
r1.cleanup();
const collapsedData = { ...expandedData, collapsed: true };
const r2 = render(<SourceNode data={collapsedData as never} />);
const collapsedEl = r2.c.querySelector("[aria-expanded]");
expect(collapsedEl?.getAttribute("aria-expanded")).toBe("false");
expect(collapsedEl?.getAttribute("title")).toBe("sourceExpand");
expect(r2.c.textContent).toContain("▸");
r2.cleanup();
});
});
describe("StatusEdge", () => {
const baseProps = {
id: "e1",
source: "a",
target: "b",
sourceX: 0,
sourceY: 0,
targetX: 100,
targetY: 100,
sourcePosition: "bottom",
targetPosition: "top",
};
it("data.active renders PARTICLES ellipses, each starting opacity=0 with a paired <set> reveal", () => {
const props = { ...baseProps, data: { state: "running", active: true, mirror: false } };
const { c, cleanup } = render(
<svg>
<StatusEdge {...(props as unknown as EdgeProps)} />
</svg>
);
const ellipses = c.querySelectorAll("ellipse.orch-edge-particle");
expect(ellipses.length).toBe(3);
ellipses.forEach((ellipse) => {
expect(ellipse.getAttribute("opacity")).toBe("0");
const set = ellipse.querySelector("set");
expect(set).toBeTruthy();
expect(set?.getAttribute("attributeName")).toBe("opacity");
expect(set?.getAttribute("to")).toBe("1");
expect(set?.getAttribute("fill")).toBe("freeze");
expect(ellipse.querySelector("animateMotion")).toBeTruthy();
});
cleanup();
});
it("without data.active there is no particle ellipse", () => {
const props = { ...baseProps, data: { state: "succeeded", active: false, mirror: false } };
const { c, cleanup } = render(
<svg>
<StatusEdge {...(props as unknown as EdgeProps)} />
</svg>
);
expect(c.querySelectorAll("ellipse.orch-edge-particle").length).toBe(0);
cleanup();
});
});