745 lines
21 KiB
TypeScript
745 lines
21 KiB
TypeScript
/**
|
|
* Tests for src/adapter.ts — the bridge between ComfyUI graph objects
|
|
* and the pure layout framework types.
|
|
*/
|
|
|
|
import { describe, it, expect } from "vitest";
|
|
import {
|
|
extractLayoutInput,
|
|
extractGroupLayoutInput,
|
|
applyLayoutOutput,
|
|
anchorSelectedGroupLayoutResult,
|
|
} from "../../src/adapter";
|
|
import type { GraphLike } from "../../src/adapter";
|
|
import type {
|
|
Position,
|
|
GroupBounds,
|
|
LayoutGroup,
|
|
FrameworkResult,
|
|
} from "../../src/layout/types";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helper: create a minimal graph-like object
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function makeGraph(opts: {
|
|
nodes?: Array<{
|
|
id: number;
|
|
type: string;
|
|
title: string;
|
|
pos: number[];
|
|
size: number[];
|
|
}>;
|
|
inputNode?: {
|
|
id: number;
|
|
pos: number[];
|
|
size: number[];
|
|
};
|
|
outputNode?: {
|
|
id: number;
|
|
pos: number[];
|
|
size: number[];
|
|
};
|
|
groups?: Array<{
|
|
id: number;
|
|
title: string;
|
|
pos: number[];
|
|
size: number[];
|
|
childrenIds?: number[];
|
|
includeChildrenSet?: boolean;
|
|
}>;
|
|
links?: Array<{
|
|
id: number;
|
|
origin_id: number;
|
|
target_id: number;
|
|
}>;
|
|
}): GraphLike {
|
|
const nodes = (opts.nodes ?? []).map((n) => ({
|
|
id: n.id,
|
|
type: n.type,
|
|
title: n.title,
|
|
pos: [...n.pos],
|
|
size: [...n.size],
|
|
inputs: [],
|
|
outputs: [],
|
|
}));
|
|
|
|
const groups = (opts.groups ?? []).map((g) => {
|
|
const childrenIdSet = new Set(g.childrenIds ?? []);
|
|
// _children includes all nodes/groups spatially inside
|
|
const children =
|
|
g.includeChildrenSet === false
|
|
? undefined
|
|
: new Set(
|
|
nodes
|
|
.filter((n) => isInsideGroup(n, g))
|
|
.map((n) => ({ id: n.id })),
|
|
);
|
|
if (children) {
|
|
for (const cid of childrenIdSet) {
|
|
children.add({ id: cid });
|
|
}
|
|
}
|
|
return {
|
|
id: g.id,
|
|
title: g.title,
|
|
pos: [...g.pos],
|
|
size: [...g.size],
|
|
_children: children as ReadonlySet<{ readonly id: number }> | undefined,
|
|
};
|
|
});
|
|
|
|
const linksMap = new Map<
|
|
number,
|
|
{ id: number; origin_id: number; target_id: number }
|
|
>();
|
|
for (const link of opts.links ?? []) {
|
|
linksMap.set(link.id, link);
|
|
}
|
|
|
|
const graph = {
|
|
_nodes: nodes,
|
|
_groups: groups,
|
|
links: linksMap,
|
|
setDirtyCanvas: undefined,
|
|
};
|
|
|
|
if (opts.inputNode) {
|
|
Object.assign(graph, {
|
|
inputNode: {
|
|
id: opts.inputNode.id,
|
|
pos: [...opts.inputNode.pos],
|
|
size: [...opts.inputNode.size],
|
|
},
|
|
});
|
|
}
|
|
|
|
if (opts.outputNode) {
|
|
Object.assign(graph, {
|
|
outputNode: {
|
|
id: opts.outputNode.id,
|
|
pos: [...opts.outputNode.pos],
|
|
size: [...opts.outputNode.size],
|
|
},
|
|
});
|
|
}
|
|
|
|
return graph as GraphLike;
|
|
}
|
|
|
|
function isInsideGroup(
|
|
node: { pos: number[]; size: number[] },
|
|
group: { pos: number[]; size: number[] },
|
|
): boolean {
|
|
return (
|
|
node.pos[0] >= group.pos[0] &&
|
|
node.pos[1] >= group.pos[1] &&
|
|
node.pos[0] + node.size[0] <= group.pos[0] + group.size[0] &&
|
|
node.pos[1] + node.size[1] <= group.pos[1] + group.size[1]
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// extractLayoutInput
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("extractLayoutInput", () => {
|
|
it("extracts nodes with correct ids and dimensions", () => {
|
|
const graph = makeGraph({
|
|
nodes: [
|
|
{ id: 1, type: "KSampler", title: "Sampler", pos: [0, 0], size: [200, 100] },
|
|
{ id: 2, type: "VAEDecode", title: "Decode", pos: [300, 0], size: [150, 80] },
|
|
],
|
|
});
|
|
|
|
const { nodes } = extractLayoutInput(graph);
|
|
|
|
expect(nodes).toHaveLength(2);
|
|
expect(nodes[0]).toEqual({ id: "1", width: 200, height: 100 });
|
|
expect(nodes[1]).toEqual({ id: "2", width: 150, height: 80 });
|
|
});
|
|
|
|
it("extracts edges from links map", () => {
|
|
const graph = makeGraph({
|
|
nodes: [
|
|
{ id: 1, type: "A", title: "A", pos: [0, 0], size: [100, 50] },
|
|
{ id: 2, type: "B", title: "B", pos: [200, 0], size: [100, 50] },
|
|
],
|
|
links: [{ id: 1, origin_id: 1, target_id: 2 }],
|
|
});
|
|
|
|
const { edges } = extractLayoutInput(graph);
|
|
|
|
expect(edges).toHaveLength(1);
|
|
expect(edges[0]).toEqual({ source: "1", target: "2" });
|
|
});
|
|
|
|
it("returns empty arrays for an empty graph", () => {
|
|
const graph = makeGraph({});
|
|
const { nodes, edges, groups } = extractLayoutInput(graph);
|
|
|
|
expect(nodes).toHaveLength(0);
|
|
expect(edges).toHaveLength(0);
|
|
expect(groups).toHaveLength(0);
|
|
});
|
|
|
|
it("builds group hierarchy from spatial containment", () => {
|
|
const graph = makeGraph({
|
|
nodes: [
|
|
{ id: 1, type: "A", title: "A", pos: [120, 120], size: [100, 50] },
|
|
{ id: 2, type: "B", title: "B", pos: [520, 120], size: [100, 50] },
|
|
],
|
|
groups: [
|
|
{ id: 100, title: "Outer", pos: [0, 0], size: [1000, 500] },
|
|
{ id: 101, title: "Inner", pos: [100, 100], size: [300, 200] },
|
|
],
|
|
});
|
|
|
|
const { groups } = extractLayoutInput(graph);
|
|
|
|
// Outer group should list Inner as child
|
|
const outer = groups.find((g) => g.id === "group:100");
|
|
expect(outer).toBeDefined();
|
|
expect(outer!.childGroupIds).toContain("group:101");
|
|
|
|
// Inner group should contain node 1, outer should contain node 2 (not in inner)
|
|
const inner = groups.find((g) => g.id === "group:101");
|
|
expect(inner).toBeDefined();
|
|
expect(inner!.memberIds).toContain("1");
|
|
expect(inner!.childGroupIds).toHaveLength(0);
|
|
|
|
// Node 2 is in outer but not in inner
|
|
expect(outer!.memberIds).toContain("2");
|
|
expect(outer!.memberIds).not.toContain("1"); // 1 is in inner, not direct member of outer
|
|
});
|
|
|
|
it("infers group members from spatial containment when _children is missing", () => {
|
|
const graph = makeGraph({
|
|
nodes: [
|
|
{ id: 1, type: "A", title: "A", pos: [120, 120], size: [100, 50] },
|
|
{ id: 2, type: "B", title: "B", pos: [520, 120], size: [100, 50] },
|
|
],
|
|
groups: [
|
|
{
|
|
id: 100,
|
|
title: "Outer",
|
|
pos: [0, 0],
|
|
size: [1000, 500],
|
|
includeChildrenSet: false,
|
|
},
|
|
{
|
|
id: 101,
|
|
title: "Inner",
|
|
pos: [100, 100],
|
|
size: [300, 200],
|
|
includeChildrenSet: false,
|
|
},
|
|
],
|
|
});
|
|
|
|
const { groups } = extractLayoutInput(graph);
|
|
const outer = groups.find((group) => group.id === "group:100");
|
|
const inner = groups.find((group) => group.id === "group:101");
|
|
|
|
expect(inner?.memberIds).toEqual(["1"]);
|
|
expect(outer?.memberIds).toEqual(["2"]);
|
|
expect(outer?.childGroupIds).toEqual(["group:101"]);
|
|
});
|
|
|
|
it("treats a node whose center is inside a group as a member", () => {
|
|
const graph = makeGraph({
|
|
nodes: [
|
|
{
|
|
id: 1,
|
|
type: "PreviewImage",
|
|
title: "Preview",
|
|
pos: [120, 50],
|
|
size: [120, 120],
|
|
},
|
|
],
|
|
groups: [
|
|
{
|
|
id: 100,
|
|
title: "Wrapper",
|
|
pos: [100, 100],
|
|
size: [100, 100],
|
|
includeChildrenSet: false,
|
|
},
|
|
],
|
|
});
|
|
|
|
const { groups } = extractLayoutInput(graph);
|
|
|
|
expect(groups[0]?.memberIds).toEqual(["1"]);
|
|
});
|
|
|
|
it("excludes a node whose center is outside the group", () => {
|
|
const graph = makeGraph({
|
|
nodes: [
|
|
{
|
|
id: 1,
|
|
type: "PreviewImage",
|
|
title: "Preview",
|
|
pos: [10, 50],
|
|
size: [120, 120],
|
|
},
|
|
],
|
|
groups: [
|
|
{
|
|
id: 100,
|
|
title: "Wrapper",
|
|
pos: [100, 100],
|
|
size: [100, 100],
|
|
includeChildrenSet: false,
|
|
},
|
|
],
|
|
});
|
|
|
|
const { groups } = extractLayoutInput(graph);
|
|
|
|
expect(groups[0]?.memberIds).toEqual([]);
|
|
});
|
|
|
|
it("assigns the nearest containing group as parent", () => {
|
|
const graph = makeGraph({
|
|
nodes: [
|
|
{ id: 1, type: "A", title: "A", pos: [140, 140], size: [80, 40] },
|
|
],
|
|
groups: [
|
|
{
|
|
id: 100,
|
|
title: "Outer",
|
|
pos: [0, 0],
|
|
size: [1000, 1000],
|
|
includeChildrenSet: false,
|
|
},
|
|
{
|
|
id: 101,
|
|
title: "Middle",
|
|
pos: [100, 100],
|
|
size: [400, 400],
|
|
includeChildrenSet: false,
|
|
},
|
|
{
|
|
id: 102,
|
|
title: "Inner",
|
|
pos: [120, 120],
|
|
size: [200, 200],
|
|
includeChildrenSet: false,
|
|
},
|
|
],
|
|
});
|
|
|
|
const { groups } = extractLayoutInput(graph);
|
|
const outer = groups.find((group) => group.id === "group:100");
|
|
const middle = groups.find((group) => group.id === "group:101");
|
|
const inner = groups.find((group) => group.id === "group:102");
|
|
|
|
expect(outer?.childGroupIds).toEqual(["group:101"]);
|
|
expect(middle?.childGroupIds).toEqual(["group:102"]);
|
|
expect(inner?.memberIds).toEqual(["1"]);
|
|
});
|
|
|
|
it("namespaces group ids to avoid collisions with node ids", () => {
|
|
const graph = makeGraph({
|
|
nodes: [
|
|
{ id: 3, type: "KSampler", title: "KSampler", pos: [500, 100], size: [100, 50] },
|
|
],
|
|
groups: [
|
|
{
|
|
id: 3,
|
|
title: "Prompt Group",
|
|
pos: [0, 0],
|
|
size: [1000, 500],
|
|
includeChildrenSet: false,
|
|
},
|
|
],
|
|
});
|
|
|
|
const { nodes, groups } = extractLayoutInput(graph);
|
|
|
|
expect(nodes[0].id).toBe("3");
|
|
expect(groups[0].id).toBe("group:3");
|
|
});
|
|
|
|
it("parses layout tokens from group titles", () => {
|
|
const graph = makeGraph({
|
|
nodes: [
|
|
{ id: 1, type: "A", title: "A", pos: [20, 20], size: [100, 50] },
|
|
],
|
|
groups: [
|
|
{
|
|
id: 100,
|
|
title: "My Group [HORIZONTAL]",
|
|
pos: [0, 0],
|
|
size: [500, 300],
|
|
},
|
|
],
|
|
});
|
|
|
|
const { groups } = extractLayoutInput(graph);
|
|
|
|
expect(groups[0].token).toBeDefined();
|
|
expect(groups[0].id).toBe("group:100");
|
|
expect(groups[0].token!.mode).toBe("horizontal");
|
|
});
|
|
|
|
it("handles groups with no token in title", () => {
|
|
const graph = makeGraph({
|
|
nodes: [
|
|
{ id: 1, type: "A", title: "A", pos: [20, 20], size: [100, 50] },
|
|
],
|
|
groups: [
|
|
{ id: 100, title: "Plain Group", pos: [0, 0], size: [500, 300] },
|
|
],
|
|
});
|
|
|
|
const { groups } = extractLayoutInput(graph);
|
|
|
|
expect(groups[0].token).toBeUndefined();
|
|
});
|
|
|
|
it("handles links stored as Record (old ComfyUI format)", () => {
|
|
const nodes = [
|
|
{
|
|
id: 1,
|
|
type: "A",
|
|
title: "A",
|
|
pos: [0, 0],
|
|
size: [100, 50],
|
|
inputs: [] as Array<{ link: number | null }>,
|
|
outputs: [] as Array<{ links: number[] | null }>,
|
|
},
|
|
{
|
|
id: 2,
|
|
type: "B",
|
|
title: "B",
|
|
pos: [200, 0],
|
|
size: [100, 50],
|
|
inputs: [] as Array<{ link: number | null }>,
|
|
outputs: [] as Array<{ links: number[] | null }>,
|
|
},
|
|
];
|
|
// Use Record instead of Map for links
|
|
const linksRecord: Record<number, { id: number; origin_id: number; target_id: number }> = {
|
|
1: { id: 1, origin_id: 1, target_id: 2 },
|
|
};
|
|
const graph: GraphLike = {
|
|
_nodes: nodes,
|
|
_groups: [],
|
|
links: linksRecord as GraphLike["links"],
|
|
};
|
|
|
|
const { edges } = extractLayoutInput(graph);
|
|
|
|
expect(edges).toHaveLength(1);
|
|
expect(edges[0]).toEqual({ source: "1", target: "2" });
|
|
});
|
|
|
|
it("includes subgraph boundary nodes in extracted layout input", () => {
|
|
const graph = makeGraph({
|
|
nodes: [
|
|
{ id: 1, type: "A", title: "A", pos: [0, 0], size: [100, 50] },
|
|
],
|
|
inputNode: { id: -10, pos: [-300, 0], size: [120, 220] },
|
|
outputNode: { id: -20, pos: [300, 0], size: [120, 60] },
|
|
links: [
|
|
{ id: 1, origin_id: -10, target_id: 1 },
|
|
{ id: 2, origin_id: 1, target_id: -20 },
|
|
],
|
|
});
|
|
|
|
const { nodes, edges } = extractLayoutInput(graph);
|
|
|
|
expect(nodes).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ id: "-10", width: 120, height: 220 }),
|
|
expect.objectContaining({ id: "1", width: 100, height: 50 }),
|
|
expect.objectContaining({ id: "-20", width: 120, height: 60 }),
|
|
]),
|
|
);
|
|
expect(edges).toEqual(
|
|
expect.arrayContaining([
|
|
{ source: "-10", target: "1" },
|
|
{ source: "1", target: "-20" },
|
|
]),
|
|
);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// applyLayoutOutput
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("applyLayoutOutput", () => {
|
|
it("applies node positions from layout result", () => {
|
|
const graph = makeGraph({
|
|
nodes: [
|
|
{ id: 1, type: "A", title: "A", pos: [0, 0], size: [100, 50] },
|
|
{ id: 2, type: "B", title: "B", pos: [0, 0], size: [100, 50] },
|
|
],
|
|
});
|
|
|
|
const positions = new Map<string, Position>([
|
|
["1", { x: 100, y: 200 }],
|
|
["2", { x: 300, y: 400 }],
|
|
]);
|
|
const groupBounds = new Map<string, GroupBounds>();
|
|
|
|
applyLayoutOutput(graph, { positions, groupBounds });
|
|
|
|
expect(graph._nodes[0].pos[0]).toBe(100);
|
|
expect(graph._nodes[0].pos[1]).toBe(200);
|
|
expect(graph._nodes[1].pos[0]).toBe(300);
|
|
expect(graph._nodes[1].pos[1]).toBe(400);
|
|
});
|
|
|
|
it("applies group bounds from layout result", () => {
|
|
const graph = makeGraph({
|
|
groups: [
|
|
{ id: 100, title: "Group", pos: [0, 0], size: [100, 100] },
|
|
],
|
|
});
|
|
|
|
const positions = new Map<string, Position>();
|
|
const groupBounds = new Map<string, GroupBounds>([
|
|
["group:100", { x: 50, y: 60, width: 500, height: 300 }],
|
|
]);
|
|
|
|
applyLayoutOutput(graph, { positions, groupBounds });
|
|
|
|
expect(graph._groups[0].pos[0]).toBe(50);
|
|
expect(graph._groups[0].pos[1]).toBe(60);
|
|
expect(graph._groups[0].size[0]).toBe(500);
|
|
expect(graph._groups[0].size[1]).toBe(300);
|
|
});
|
|
|
|
it("mutates Float64Array in-place", () => {
|
|
const node = {
|
|
id: 1,
|
|
type: "A",
|
|
title: "A",
|
|
pos: new Float64Array([0, 0]),
|
|
size: new Float64Array([100, 50]),
|
|
inputs: [] as Array<{ link: number | null }>,
|
|
outputs: [] as Array<{ links: number[] | null }>,
|
|
};
|
|
const graph: GraphLike = {
|
|
_nodes: [node],
|
|
_groups: [],
|
|
links: new Map(),
|
|
};
|
|
|
|
const positions = new Map<string, Position>([
|
|
["1", { x: 42, y: 84 }],
|
|
]);
|
|
applyLayoutOutput(graph, { positions, groupBounds: new Map() });
|
|
|
|
// Same Float64Array instance, mutated in-place
|
|
expect(node.pos).toBeInstanceOf(Float64Array);
|
|
expect(node.pos[0]).toBe(42);
|
|
expect(node.pos[1]).toBe(84);
|
|
});
|
|
|
|
it("calls setDirtyCanvas when available", () => {
|
|
let dirtyCalled = false;
|
|
const graph = makeGraph({
|
|
nodes: [
|
|
{ id: 1, type: "A", title: "A", pos: [0, 0], size: [100, 50] },
|
|
],
|
|
});
|
|
(graph as { setDirtyCanvas: (fg: boolean, bg: boolean) => void }).setDirtyCanvas = () => {
|
|
dirtyCalled = true;
|
|
};
|
|
|
|
applyLayoutOutput(graph, {
|
|
positions: new Map([["1", { x: 0, y: 0 }]]),
|
|
groupBounds: new Map(),
|
|
});
|
|
|
|
expect(dirtyCalled).toBe(true);
|
|
});
|
|
|
|
it("skips nodes without matching position data", () => {
|
|
const graph = makeGraph({
|
|
nodes: [
|
|
{ id: 1, type: "A", title: "A", pos: [10, 20], size: [100, 50] },
|
|
],
|
|
});
|
|
|
|
// Empty positions map — node should not be modified
|
|
applyLayoutOutput(graph, {
|
|
positions: new Map(),
|
|
groupBounds: new Map(),
|
|
});
|
|
|
|
expect(graph._nodes[0].pos[0]).toBe(10);
|
|
expect(graph._nodes[0].pos[1]).toBe(20);
|
|
});
|
|
|
|
it("applies positions to subgraph boundary nodes", () => {
|
|
const graph = makeGraph({
|
|
nodes: [
|
|
{ id: 1, type: "A", title: "A", pos: [10, 20], size: [100, 50] },
|
|
],
|
|
inputNode: { id: -10, pos: [-300, 0], size: [120, 220] },
|
|
outputNode: { id: -20, pos: [300, 0], size: [120, 60] },
|
|
}) as GraphLike & {
|
|
inputNode: { pos: number[] };
|
|
outputNode: { pos: number[] };
|
|
};
|
|
|
|
applyLayoutOutput(graph, {
|
|
positions: new Map([
|
|
["-10", { x: -100, y: 40 }],
|
|
["1", { x: 100, y: 200 }],
|
|
["-20", { x: 400, y: 60 }],
|
|
]),
|
|
groupBounds: new Map(),
|
|
});
|
|
|
|
expect(graph.inputNode.pos).toEqual([-100, 40]);
|
|
expect(graph._nodes[0].pos).toEqual([100, 200]);
|
|
expect(graph.outputNode.pos).toEqual([400, 60]);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// extractGroupLayoutInput
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("extractGroupLayoutInput", () => {
|
|
it("filters to only selected groups and their members", () => {
|
|
const graph = makeGraph({
|
|
nodes: [
|
|
{ id: 1, type: "A", title: "A", pos: [20, 20], size: [100, 50] },
|
|
{ id: 2, type: "B", title: "B", pos: [520, 20], size: [100, 50] },
|
|
{ id: 3, type: "C", title: "C", pos: [1020, 20], size: [100, 50] },
|
|
],
|
|
groups: [
|
|
{ id: 100, title: "Group 1", pos: [0, 0], size: [500, 300] },
|
|
{ id: 101, title: "Group 2", pos: [500, 0], size: [500, 300] },
|
|
],
|
|
links: [
|
|
{ id: 1, origin_id: 1, target_id: 2 },
|
|
{ id: 2, origin_id: 2, target_id: 3 },
|
|
],
|
|
});
|
|
|
|
// Select only group 100
|
|
const { nodes, edges, groups } = extractGroupLayoutInput(
|
|
graph,
|
|
new Set([100]),
|
|
);
|
|
|
|
expect(groups).toHaveLength(1);
|
|
expect(groups[0].id).toBe("group:100");
|
|
expect(nodes.some((n) => n.id === "1")).toBe(true);
|
|
// Node 2 is in group 101, not selected — should not appear
|
|
expect(nodes.some((n) => n.id === "2")).toBe(false);
|
|
expect(nodes.some((n) => n.id === "3")).toBe(false);
|
|
// Edge from 1->2 crosses group boundaries, target not included
|
|
expect(edges).toHaveLength(0);
|
|
});
|
|
|
|
it("includes nested child groups when parent is selected", () => {
|
|
const graph = makeGraph({
|
|
nodes: [
|
|
{ id: 1, type: "A", title: "A", pos: [120, 120], size: [100, 50] },
|
|
{ id: 2, type: "B", title: "B", pos: [520, 120], size: [100, 50] },
|
|
],
|
|
groups: [
|
|
{ id: 100, title: "Outer", pos: [0, 0], size: [1000, 500] },
|
|
{ id: 101, title: "Inner", pos: [100, 100], size: [300, 200] },
|
|
],
|
|
});
|
|
|
|
const { groups } = extractGroupLayoutInput(graph, new Set([100]));
|
|
|
|
// Both outer and inner should be included
|
|
expect(groups).toHaveLength(2);
|
|
const ids = groups.map((g) => g.id);
|
|
expect(ids).toContain("group:100");
|
|
expect(ids).toContain("group:101");
|
|
});
|
|
|
|
it("keeps edges between members of selected groups", () => {
|
|
const graph = makeGraph({
|
|
nodes: [
|
|
{ id: 1, type: "A", title: "A", pos: [20, 20], size: [100, 50] },
|
|
{ id: 2, type: "B", title: "B", pos: [200, 20], size: [100, 50] },
|
|
],
|
|
groups: [
|
|
{ id: 100, title: "Group", pos: [0, 0], size: [500, 300] },
|
|
],
|
|
links: [{ id: 1, origin_id: 1, target_id: 2 }],
|
|
});
|
|
|
|
const { edges } = extractGroupLayoutInput(graph, new Set([100]));
|
|
|
|
expect(edges).toHaveLength(1);
|
|
expect(edges[0]).toEqual({ source: "1", target: "2" });
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// anchorSelectedGroupLayoutResult
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("anchorSelectedGroupLayoutResult", () => {
|
|
it("reanchors a selected nested group subtree to its original position", () => {
|
|
const graph = makeGraph({
|
|
groups: [
|
|
{ id: 100, title: "Outer", pos: [100, 100], size: [600, 400] },
|
|
{ id: 101, title: "Inner", pos: [250, 220], size: [250, 180] },
|
|
],
|
|
});
|
|
const groups: LayoutGroup[] = [
|
|
{
|
|
id: "group:101",
|
|
title: "Inner",
|
|
memberIds: ["1"],
|
|
childGroupIds: ["group:102"],
|
|
},
|
|
{
|
|
id: "group:102",
|
|
title: "Child",
|
|
memberIds: ["2"],
|
|
childGroupIds: [],
|
|
},
|
|
];
|
|
const result: FrameworkResult = {
|
|
positions: new Map<string, Position>([
|
|
["1", { x: 30, y: 60 }],
|
|
["2", { x: 80, y: 120 }],
|
|
]),
|
|
groupBounds: new Map<string, GroupBounds>([
|
|
["group:101", { x: 0, y: 0, width: 300, height: 220 }],
|
|
["group:102", { x: 40, y: 90, width: 120, height: 100 }],
|
|
]),
|
|
};
|
|
|
|
const anchored = anchorSelectedGroupLayoutResult(
|
|
graph,
|
|
groups,
|
|
new Set([101]),
|
|
result,
|
|
);
|
|
|
|
expect(anchored.groupBounds.get("group:101")).toEqual({
|
|
x: 250,
|
|
y: 220,
|
|
width: 300,
|
|
height: 220,
|
|
});
|
|
expect(anchored.groupBounds.get("group:102")).toEqual({
|
|
x: 290,
|
|
y: 310,
|
|
width: 120,
|
|
height: 100,
|
|
});
|
|
expect(anchored.positions.get("1")).toEqual({ x: 280, y: 280 });
|
|
expect(anchored.positions.get("2")).toEqual({ x: 330, y: 340 });
|
|
});
|
|
});
|