Files
fusion/plugins/fusion-plugin-dependency-graph/src/__tests__/layout.test.ts
Fusion a466416bd0 feat(FN-3082): restructure dependency graph plugin with modular architectur
Implements a modular dependency graph feature for the Fusion dashboard plugin, replacing the monolithic `DependencyGraphView` component with a factored architecture: graph types and filtering (Step 1), a data hook (Step 2), auto-layout engine (Step 3), SVG edge rendering (Step 4), an interaction hoo

Fusion-Task-Id: FN-3082
2026-05-07 01:42:01 -07:00

71 lines
2.4 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { GraphData } from "../types";
import { computeAutoLayout } from "../layout";
function graph(nodeIds: string[], edges: Array<{ source: string; target: string }> = []): GraphData {
return {
nodes: nodeIds.map((id) => ({ task: { id } as never })),
edges,
};
}
describe("computeAutoLayout", () => {
it("returns empty map for empty graph", () => {
expect(computeAutoLayout({ nodes: [], edges: [] }).size).toBe(0);
});
it("positions single node", () => {
const positions = computeAutoLayout(graph(["A"]));
expect(positions.has("A")).toBe(true);
});
it("places linear chain in increasing depth", () => {
const positions = computeAutoLayout(graph(["A", "B", "C"], [
{ source: "A", target: "B" },
{ source: "B", target: "C" },
]));
expect((positions.get("C")?.y ?? 0)).toBeLessThan(positions.get("B")?.y ?? 0);
expect((positions.get("B")?.y ?? 0)).toBeLessThan(positions.get("A")?.y ?? 0);
});
it("spreads wide layer horizontally", () => {
const positions = computeAutoLayout(graph(["A", "B", "C"]));
const xs = [positions.get("A")?.x, positions.get("B")?.x, positions.get("C")?.x].filter((x): x is number => x !== undefined);
expect(new Set(xs).size).toBe(3);
});
it("handles diamond dependencies", () => {
const positions = computeAutoLayout(graph(["A", "B", "C", "D"], [
{ source: "A", target: "B" },
{ source: "A", target: "C" },
{ source: "B", target: "D" },
{ source: "C", target: "D" },
]));
expect((positions.get("D")?.y ?? 0)).toBeLessThan(positions.get("B")?.y ?? 0);
expect((positions.get("D")?.y ?? 0)).toBeLessThan(positions.get("C")?.y ?? 0);
expect((positions.get("B")?.y ?? 0)).toBeLessThan(positions.get("A")?.y ?? 0);
expect((positions.get("C")?.y ?? 0)).toBeLessThan(positions.get("A")?.y ?? 0);
});
it("handles cycles without crashing", () => {
const positions = computeAutoLayout(graph(["A", "B"], [
{ source: "A", target: "B" },
{ source: "B", target: "A" },
]));
expect(positions.size).toBe(2);
});
it("respects custom spacing options", () => {
const positions = computeAutoLayout(graph(["A", "B"]), {
nodeWidth: 200,
nodeHeight: 120,
horizontalGap: 100,
verticalGap: 20,
});
expect(Math.abs((positions.get("A")?.x ?? 0) - (positions.get("B")?.x ?? 0))).toBe(300);
});
});