721 lines
20 KiB
TypeScript
721 lines
20 KiB
TypeScript
import { describe, it, beforeEach, expect } from "vitest";
|
|
import { loadFixture, capturePositions } from "../helpers";
|
|
import { layoutSelectedGroups } from "../../src/layout/selected-groups";
|
|
import { parseLayoutToken } from "../../src/layout/title-tokens";
|
|
import {
|
|
assertNodesInsideGroups,
|
|
assertFiniteCoordinates,
|
|
} from "../invariants";
|
|
import type { LGraph } from "../../src/layout/types";
|
|
|
|
/**
|
|
* Tests for layoutSelectedGroups function
|
|
* Note: External overlaps are acceptable per requirements
|
|
*/
|
|
describe("layoutSelectedGroups", () => {
|
|
describe("single group with internal connections", () => {
|
|
let graph: LGraph;
|
|
|
|
beforeEach(() => {
|
|
// nested-groups.json has groups with internal connections
|
|
// Group 1 "Step 1 - Load model" contains node 4 (CheckpointLoaderSimple)
|
|
graph = loadFixture("nested-groups.json");
|
|
});
|
|
|
|
it("organizes nodes within selected group", () => {
|
|
// Select group 1 which contains node 4
|
|
const result = layoutSelectedGroups(graph, new Set([1]));
|
|
|
|
expect(result.nodeCount).toBeGreaterThanOrEqual(0);
|
|
expect(result.groupCount).toBe(1);
|
|
assertFiniteCoordinates(graph);
|
|
});
|
|
|
|
it("resizes group to fit contents", () => {
|
|
layoutSelectedGroups(graph, new Set([1]));
|
|
|
|
const groupAfter = graph._groups.find((g) => g.id === 1);
|
|
// Group size should be finite and positive
|
|
expect(Number.isFinite(groupAfter!.size[0])).toBe(true);
|
|
expect(Number.isFinite(groupAfter!.size[1])).toBe(true);
|
|
expect(groupAfter!.size[0]).toBeGreaterThan(0);
|
|
expect(groupAfter!.size[1]).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("preserves external node positions", () => {
|
|
// Capture positions of nodes NOT in group 1
|
|
const nodesBefore = new Map<number, [number, number]>();
|
|
for (const node of graph._nodes) {
|
|
// Node 4 is in group 1, all others should be unchanged
|
|
if (node.id !== 4) {
|
|
nodesBefore.set(node.id, [...node.pos]);
|
|
}
|
|
}
|
|
|
|
layoutSelectedGroups(graph, new Set([1]));
|
|
|
|
// Verify external nodes haven't moved
|
|
for (const [nodeId, posBefore] of nodesBefore) {
|
|
const node = graph._nodes.find((n) => n.id === nodeId);
|
|
expect(node).toBeDefined();
|
|
expect(node!.pos[0]).toBe(posBefore[0]);
|
|
expect(node!.pos[1]).toBe(posBefore[1]);
|
|
}
|
|
});
|
|
|
|
it("is idempotent", () => {
|
|
layoutSelectedGroups(graph, new Set([1]));
|
|
const positions1 = capturePositions(graph);
|
|
|
|
layoutSelectedGroups(graph, new Set([1]));
|
|
const positions2 = capturePositions(graph);
|
|
|
|
// Compare positions with small tolerance
|
|
for (const [id, pos1] of positions1) {
|
|
const pos2 = positions2.get(id);
|
|
if (pos2) {
|
|
expect(Math.abs(pos1.x - pos2.x)).toBeLessThan(5);
|
|
expect(Math.abs(pos1.y - pos2.y)).toBeLessThan(5);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("single group with disconnected nodes", () => {
|
|
let graph: LGraph;
|
|
|
|
beforeEach(() => {
|
|
// Create a graph with a group containing disconnected nodes
|
|
graph = {
|
|
_nodes: [
|
|
{
|
|
id: 1,
|
|
type: "Note",
|
|
title: "Note 1",
|
|
pos: [100, 100],
|
|
size: [200, 100],
|
|
inputs: [],
|
|
outputs: [],
|
|
},
|
|
{
|
|
id: 2,
|
|
type: "Note",
|
|
title: "Note 2",
|
|
pos: [150, 250],
|
|
size: [200, 100],
|
|
inputs: [],
|
|
outputs: [],
|
|
},
|
|
{
|
|
id: 3,
|
|
type: "Note",
|
|
title: "External Node",
|
|
pos: [500, 500],
|
|
size: [200, 100],
|
|
inputs: [],
|
|
outputs: [],
|
|
},
|
|
],
|
|
_groups: [
|
|
{
|
|
id: 1,
|
|
title: "Test Group",
|
|
pos: [50, 50],
|
|
size: [350, 350],
|
|
},
|
|
],
|
|
links: new Map(),
|
|
};
|
|
});
|
|
|
|
it("stacks disconnected nodes vertically", () => {
|
|
const result = layoutSelectedGroups(graph, new Set([1]));
|
|
|
|
expect(result.nodeCount).toBe(2);
|
|
expect(result.groupCount).toBe(1);
|
|
|
|
// Nodes should have same X (stacked vertically)
|
|
const node1 = graph._nodes.find((n) => n.id === 1)!;
|
|
const node2 = graph._nodes.find((n) => n.id === 2)!;
|
|
expect(node1.pos[0]).toBe(node2.pos[0]);
|
|
|
|
assertFiniteCoordinates(graph);
|
|
});
|
|
|
|
it("resizes group to fit stacked nodes", () => {
|
|
layoutSelectedGroups(graph, new Set([1]));
|
|
|
|
const group = graph._groups.find((g) => g.id === 1)!;
|
|
expect(Number.isFinite(group.size[0])).toBe(true);
|
|
expect(Number.isFinite(group.size[1])).toBe(true);
|
|
expect(group.size[0]).toBeGreaterThan(0);
|
|
expect(group.size[1]).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe("multiple groups", () => {
|
|
let graph: LGraph;
|
|
|
|
beforeEach(() => {
|
|
graph = loadFixture("nested-groups.json");
|
|
});
|
|
|
|
it("organizes each group independently", () => {
|
|
// Select groups 1 and 2 (not nested)
|
|
const result = layoutSelectedGroups(graph, new Set([1, 2]));
|
|
|
|
expect(result.groupCount).toBeGreaterThanOrEqual(2);
|
|
assertFiniteCoordinates(graph);
|
|
});
|
|
|
|
it("handles non-overlapping groups", () => {
|
|
layoutSelectedGroups(graph, new Set([1, 2]));
|
|
|
|
// Both groups should have valid coordinates
|
|
const group1 = graph._groups.find((g) => g.id === 1)!;
|
|
const group2 = graph._groups.find((g) => g.id === 2)!;
|
|
|
|
expect(Number.isFinite(group1.pos[0])).toBe(true);
|
|
expect(Number.isFinite(group2.pos[0])).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("nested groups", () => {
|
|
let graph: LGraph;
|
|
|
|
beforeEach(() => {
|
|
// nested-groups.json has Group 3 containing Group 4 (nested)
|
|
graph = loadFixture("nested-groups.json");
|
|
});
|
|
|
|
it("auto-includes nested groups when parent selected", () => {
|
|
// Select group 3 which contains group 4
|
|
const result = layoutSelectedGroups(graph, new Set([3]));
|
|
|
|
// Should include the nested group
|
|
expect(result.groupCount).toBeGreaterThanOrEqual(1);
|
|
assertFiniteCoordinates(graph);
|
|
});
|
|
|
|
it("positions nested groups correctly", () => {
|
|
layoutSelectedGroups(graph, new Set([3]));
|
|
|
|
// Group 4 should still be inside Group 3
|
|
const group3 = graph._groups.find((g) => g.id === 3)!;
|
|
const group4 = graph._groups.find((g) => g.id === 4)!;
|
|
|
|
// Check group 4 is within group 3 bounds
|
|
expect(group4.pos[0]).toBeGreaterThanOrEqual(group3.pos[0]);
|
|
expect(group4.pos[1]).toBeGreaterThanOrEqual(group3.pos[1]);
|
|
expect(group4.pos[0] + group4.size[0]).toBeLessThanOrEqual(
|
|
group3.pos[0] + group3.size[0]
|
|
);
|
|
expect(group4.pos[1] + group4.size[1]).toBeLessThanOrEqual(
|
|
group3.pos[1] + group3.size[1]
|
|
);
|
|
});
|
|
|
|
it("keeps nodes inside selected groups", () => {
|
|
layoutSelectedGroups(graph, new Set([3]));
|
|
|
|
// Nodes inside group 3 should remain inside it
|
|
assertNodesInsideGroups(graph);
|
|
});
|
|
});
|
|
|
|
describe("edge cases", () => {
|
|
it("handles empty graph", () => {
|
|
const graph: LGraph = {
|
|
_nodes: [],
|
|
_groups: [],
|
|
links: new Map(),
|
|
};
|
|
|
|
const result = layoutSelectedGroups(graph, new Set([1]));
|
|
|
|
expect(result.nodeCount).toBe(0);
|
|
expect(result.groupCount).toBe(0);
|
|
});
|
|
|
|
it("handles non-existent group IDs", () => {
|
|
const graph = loadFixture("nested-groups.json");
|
|
|
|
const result = layoutSelectedGroups(graph, new Set([999]));
|
|
|
|
expect(result.groupCount).toBe(0);
|
|
});
|
|
|
|
it("handles empty groups", () => {
|
|
const graph: LGraph = {
|
|
_nodes: [
|
|
{
|
|
id: 1,
|
|
type: "Note",
|
|
title: "External",
|
|
pos: [500, 500],
|
|
size: [200, 100],
|
|
inputs: [],
|
|
outputs: [],
|
|
},
|
|
],
|
|
_groups: [
|
|
{
|
|
id: 1,
|
|
title: "Empty Group",
|
|
pos: [50, 50],
|
|
size: [200, 200],
|
|
},
|
|
],
|
|
links: new Map(),
|
|
};
|
|
|
|
// Should not crash
|
|
const result = layoutSelectedGroups(graph, new Set([1]));
|
|
|
|
// Empty group is still processed, just has no content
|
|
expect(result.nodeCount).toBe(0);
|
|
assertFiniteCoordinates(graph);
|
|
});
|
|
|
|
it("handles locked nodes", () => {
|
|
const graph: LGraph = {
|
|
_nodes: [
|
|
{
|
|
id: 1,
|
|
type: "Note",
|
|
title: "Locked Node",
|
|
pos: [100, 100],
|
|
size: [200, 100],
|
|
inputs: [],
|
|
outputs: [],
|
|
locked: true,
|
|
},
|
|
{
|
|
id: 2,
|
|
type: "Note",
|
|
title: "Normal Node",
|
|
pos: [100, 250],
|
|
size: [200, 100],
|
|
inputs: [],
|
|
outputs: [],
|
|
},
|
|
],
|
|
_groups: [
|
|
{
|
|
id: 1,
|
|
title: "Test Group",
|
|
pos: [50, 50],
|
|
size: [350, 400],
|
|
},
|
|
],
|
|
links: new Map(),
|
|
};
|
|
|
|
const lockedPosBefore = [...graph._nodes[0].pos];
|
|
|
|
layoutSelectedGroups(graph, new Set([1]));
|
|
|
|
// Locked node should not move
|
|
expect(graph._nodes[0].pos[0]).toBe(lockedPosBefore[0]);
|
|
expect(graph._nodes[0].pos[1]).toBe(lockedPosBefore[1]);
|
|
});
|
|
|
|
it("handles group with only locked nodes", () => {
|
|
const graph: LGraph = {
|
|
_nodes: [
|
|
{
|
|
id: 1,
|
|
type: "Note",
|
|
title: "Locked Node",
|
|
pos: [100, 100],
|
|
size: [200, 100],
|
|
inputs: [],
|
|
outputs: [],
|
|
locked: true,
|
|
},
|
|
],
|
|
_groups: [
|
|
{
|
|
id: 1,
|
|
title: "Test Group",
|
|
pos: [50, 50],
|
|
size: [350, 250],
|
|
},
|
|
],
|
|
links: new Map(),
|
|
};
|
|
|
|
// Should not crash
|
|
const result = layoutSelectedGroups(graph, new Set([1]));
|
|
expect(result).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe("groups with internal connections", () => {
|
|
it("uses layer-based layout for connected nodes", () => {
|
|
const graph: LGraph = {
|
|
_nodes: [
|
|
{
|
|
id: 1,
|
|
type: "NodeA",
|
|
title: "Node A",
|
|
pos: [100, 100],
|
|
size: [200, 100],
|
|
inputs: [],
|
|
outputs: [{ name: "out", type: "FLOAT", links: [1] }],
|
|
},
|
|
{
|
|
id: 2,
|
|
type: "NodeB",
|
|
title: "Node B",
|
|
pos: [100, 300],
|
|
size: [200, 100],
|
|
inputs: [{ name: "in", type: "FLOAT", link: 1 }],
|
|
outputs: [],
|
|
},
|
|
],
|
|
_groups: [
|
|
{
|
|
id: 1,
|
|
title: "Test Group",
|
|
pos: [50, 50],
|
|
size: [400, 400],
|
|
},
|
|
],
|
|
links: new Map([
|
|
[
|
|
1,
|
|
{
|
|
id: 1,
|
|
origin_id: 1,
|
|
origin_slot: 0,
|
|
target_id: 2,
|
|
target_slot: 0,
|
|
type: "FLOAT",
|
|
},
|
|
],
|
|
]),
|
|
};
|
|
|
|
layoutSelectedGroups(graph, new Set([1]));
|
|
|
|
// With layer-based layout, source (node 1) should be left of target (node 2)
|
|
const node1 = graph._nodes.find((n) => n.id === 1)!;
|
|
const node2 = graph._nodes.find((n) => n.id === 2)!;
|
|
expect(node1.pos[0]).toBeLessThan(node2.pos[0]);
|
|
|
|
assertFiniteCoordinates(graph);
|
|
});
|
|
});
|
|
|
|
describe("parseLayoutToken", () => {
|
|
it("parses [HORIZONTAL] token", () => {
|
|
expect(parseLayoutToken("My Group [HORIZONTAL]")).toEqual({
|
|
type: "horizontal",
|
|
});
|
|
});
|
|
|
|
it("parses [VERTICAL] token", () => {
|
|
expect(parseLayoutToken("[VERTICAL] Group")).toEqual({ type: "vertical" });
|
|
});
|
|
|
|
it("parses [xROW] tokens", () => {
|
|
expect(parseLayoutToken("Group [2ROW]")).toEqual({ type: "rows", count: 2 });
|
|
expect(parseLayoutToken("[3ROW] Test")).toEqual({ type: "rows", count: 3 });
|
|
});
|
|
|
|
it("parses [xCOL] tokens", () => {
|
|
expect(parseLayoutToken("Group [2COL]")).toEqual({
|
|
type: "columns",
|
|
count: 2,
|
|
});
|
|
expect(parseLayoutToken("[4COL] Test")).toEqual({
|
|
type: "columns",
|
|
count: 4,
|
|
});
|
|
});
|
|
|
|
it("treats [1ROW] as horizontal", () => {
|
|
expect(parseLayoutToken("Group [1ROW]")).toEqual({ type: "horizontal" });
|
|
});
|
|
|
|
it("treats [1COL] as vertical", () => {
|
|
expect(parseLayoutToken("Group [1COL]")).toEqual({ type: "vertical" });
|
|
});
|
|
|
|
it("is case-insensitive", () => {
|
|
expect(parseLayoutToken("[horizontal]")).toEqual({ type: "horizontal" });
|
|
expect(parseLayoutToken("[Vertical]")).toEqual({ type: "vertical" });
|
|
expect(parseLayoutToken("[2row]")).toEqual({ type: "rows", count: 2 });
|
|
expect(parseLayoutToken("[3Col]")).toEqual({ type: "columns", count: 3 });
|
|
});
|
|
|
|
it("returns default for no token", () => {
|
|
expect(parseLayoutToken("Normal Group")).toEqual({ type: "default" });
|
|
expect(parseLayoutToken("")).toEqual({ type: "default" });
|
|
});
|
|
|
|
it("returns first matching token", () => {
|
|
// HORIZONTAL comes before xROW in parsing order
|
|
expect(parseLayoutToken("[HORIZONTAL] [2ROW]")).toEqual({
|
|
type: "horizontal",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("groups with layout tokens", () => {
|
|
function createTestGraph(groupTitle: string): LGraph {
|
|
return {
|
|
_nodes: [
|
|
{
|
|
id: 1,
|
|
type: "Note",
|
|
title: "Node 1",
|
|
pos: [100, 100],
|
|
size: [200, 100],
|
|
inputs: [],
|
|
outputs: [],
|
|
},
|
|
{
|
|
id: 2,
|
|
type: "Note",
|
|
title: "Node 2",
|
|
pos: [100, 250],
|
|
size: [200, 100],
|
|
inputs: [],
|
|
outputs: [],
|
|
},
|
|
{
|
|
id: 3,
|
|
type: "Note",
|
|
title: "Node 3",
|
|
pos: [100, 400],
|
|
size: [200, 100],
|
|
inputs: [],
|
|
outputs: [],
|
|
},
|
|
{
|
|
id: 4,
|
|
type: "Note",
|
|
title: "Node 4",
|
|
pos: [100, 550],
|
|
size: [200, 100],
|
|
inputs: [],
|
|
outputs: [],
|
|
},
|
|
],
|
|
_groups: [
|
|
{
|
|
id: 1,
|
|
title: groupTitle,
|
|
pos: [50, 50],
|
|
size: [500, 700],
|
|
},
|
|
],
|
|
links: new Map(),
|
|
};
|
|
}
|
|
|
|
it("[HORIZONTAL] arranges nodes in single row", () => {
|
|
const graph = createTestGraph("Test [HORIZONTAL]");
|
|
layoutSelectedGroups(graph, new Set([1]));
|
|
|
|
// All nodes should have same Y (single row)
|
|
const ys = graph._nodes.map((n) => n.pos[1]);
|
|
expect(new Set(ys).size).toBe(1);
|
|
|
|
// Nodes should be spread horizontally
|
|
const xs = graph._nodes.map((n) => n.pos[0]).sort((a, b) => a - b);
|
|
expect(xs[3]).toBeGreaterThan(xs[0]);
|
|
|
|
assertFiniteCoordinates(graph);
|
|
});
|
|
|
|
it("[VERTICAL] arranges nodes in single column", () => {
|
|
const graph = createTestGraph("Test [VERTICAL]");
|
|
layoutSelectedGroups(graph, new Set([1]));
|
|
|
|
// All nodes should have same X (single column)
|
|
const xs = graph._nodes.map((n) => n.pos[0]);
|
|
expect(new Set(xs).size).toBe(1);
|
|
|
|
// Nodes should be spread vertically
|
|
const ys = graph._nodes.map((n) => n.pos[1]).sort((a, b) => a - b);
|
|
expect(ys[3]).toBeGreaterThan(ys[0]);
|
|
|
|
assertFiniteCoordinates(graph);
|
|
});
|
|
|
|
it("[2ROW] distributes nodes into 2 rows", () => {
|
|
const graph = createTestGraph("Test [2ROW]");
|
|
layoutSelectedGroups(graph, new Set([1]));
|
|
|
|
// Should have exactly 2 distinct Y values (2 rows)
|
|
const ys = new Set(graph._nodes.map((n) => n.pos[1]));
|
|
expect(ys.size).toBe(2);
|
|
|
|
assertFiniteCoordinates(graph);
|
|
});
|
|
|
|
it("[3COL] distributes nodes into 3 columns", () => {
|
|
const graph = createTestGraph("Test [3COL]");
|
|
layoutSelectedGroups(graph, new Set([1]));
|
|
|
|
// Should have 3 distinct X values (4 nodes into 3 cols = 2,1,1 distribution)
|
|
const xs = new Set(graph._nodes.map((n) => n.pos[0]));
|
|
expect(xs.size).toBe(3);
|
|
|
|
assertFiniteCoordinates(graph);
|
|
});
|
|
|
|
it("lowercase token works", () => {
|
|
const graph = createTestGraph("Test [horizontal]");
|
|
layoutSelectedGroups(graph, new Set([1]));
|
|
|
|
// All nodes should have same Y (single row)
|
|
const ys = graph._nodes.map((n) => n.pos[1]);
|
|
expect(new Set(ys).size).toBe(1);
|
|
|
|
assertFiniteCoordinates(graph);
|
|
});
|
|
|
|
it("ignores DAG structure with token", () => {
|
|
// Create graph with connected nodes
|
|
const graph: LGraph = {
|
|
_nodes: [
|
|
{
|
|
id: 1,
|
|
type: "NodeA",
|
|
title: "Node A",
|
|
pos: [100, 100],
|
|
size: [200, 100],
|
|
inputs: [],
|
|
outputs: [{ name: "out", type: "FLOAT", links: [1] }],
|
|
},
|
|
{
|
|
id: 2,
|
|
type: "NodeB",
|
|
title: "Node B",
|
|
pos: [100, 300],
|
|
size: [200, 100],
|
|
inputs: [{ name: "in", type: "FLOAT", link: 1 }],
|
|
outputs: [],
|
|
},
|
|
],
|
|
_groups: [
|
|
{
|
|
id: 1,
|
|
title: "Test [HORIZONTAL]",
|
|
pos: [50, 50],
|
|
size: [500, 400],
|
|
},
|
|
],
|
|
links: new Map([
|
|
[
|
|
1,
|
|
{
|
|
id: 1,
|
|
origin_id: 1,
|
|
origin_slot: 0,
|
|
target_id: 2,
|
|
target_slot: 0,
|
|
type: "FLOAT",
|
|
},
|
|
],
|
|
]),
|
|
};
|
|
|
|
layoutSelectedGroups(graph, new Set([1]));
|
|
|
|
// With [HORIZONTAL], both nodes should have same Y regardless of connection
|
|
const node1 = graph._nodes.find((n) => n.id === 1)!;
|
|
const node2 = graph._nodes.find((n) => n.id === 2)!;
|
|
expect(node1.pos[1]).toBe(node2.pos[1]);
|
|
|
|
assertFiniteCoordinates(graph);
|
|
});
|
|
|
|
it("nested groups respect their own tokens", () => {
|
|
const graph: LGraph = {
|
|
_nodes: [
|
|
// Nodes in outer group (not in inner)
|
|
{
|
|
id: 1,
|
|
type: "Note",
|
|
title: "Outer 1",
|
|
pos: [100, 100],
|
|
size: [100, 100],
|
|
inputs: [],
|
|
outputs: [],
|
|
},
|
|
{
|
|
id: 2,
|
|
type: "Note",
|
|
title: "Outer 2",
|
|
pos: [100, 250],
|
|
size: [100, 100],
|
|
inputs: [],
|
|
outputs: [],
|
|
},
|
|
// Nodes in inner group
|
|
{
|
|
id: 3,
|
|
type: "Note",
|
|
title: "Inner 1",
|
|
pos: [350, 150],
|
|
size: [100, 100],
|
|
inputs: [],
|
|
outputs: [],
|
|
},
|
|
{
|
|
id: 4,
|
|
type: "Note",
|
|
title: "Inner 2",
|
|
pos: [350, 300],
|
|
size: [100, 100],
|
|
inputs: [],
|
|
outputs: [],
|
|
},
|
|
],
|
|
_groups: [
|
|
{
|
|
id: 1,
|
|
title: "Outer [HORIZONTAL]",
|
|
pos: [50, 50],
|
|
size: [600, 500],
|
|
},
|
|
{
|
|
id: 2,
|
|
title: "Inner [VERTICAL]",
|
|
pos: [300, 100],
|
|
size: [200, 350],
|
|
},
|
|
],
|
|
links: new Map(),
|
|
};
|
|
|
|
layoutSelectedGroups(graph, new Set([1]));
|
|
|
|
// Inner group nodes should have same X (vertical)
|
|
const inner1 = graph._nodes.find((n) => n.id === 3)!;
|
|
const inner2 = graph._nodes.find((n) => n.id === 4)!;
|
|
expect(inner1.pos[0]).toBe(inner2.pos[0]);
|
|
|
|
assertFiniteCoordinates(graph);
|
|
});
|
|
|
|
it("resizes group to fit token-arranged contents", () => {
|
|
const graph = createTestGraph("Test [HORIZONTAL]");
|
|
layoutSelectedGroups(graph, new Set([1]));
|
|
|
|
const group = graph._groups.find((g) => g.id === 1)!;
|
|
|
|
// Group should be wider than tall (horizontal layout)
|
|
// With 4 nodes of 200px width + gaps, width should be significant
|
|
expect(group.size[0]).toBeGreaterThan(600);
|
|
|
|
assertFiniteCoordinates(graph);
|
|
});
|
|
});
|
|
});
|