import { autoLayout } from '../layout/auto-layout'; import { graphToFlow } from '../serialization'; import type { WorkflowGraph } from '../types'; function noOverlap( nodes: { id: string; position: { x: number; y: number }; width?: number; height?: number }[] ): boolean { for (let i = 0; i < nodes.length; i++) { for (let j = i + 1; j < nodes.length; j++) { const a = nodes[i]; const b = nodes[j]; const aw = a.width ?? 180; const ah = a.height ?? 60; const bw = b.width ?? 180; const bh = b.height ?? 60; const overlapX = a.position.x < b.position.x + bw && a.position.x + aw > b.position.x; const overlapY = a.position.y < b.position.y + bh && a.position.y + ah > b.position.y; if (overlapX && overlapY) return false; } } return true; } const chainGraph: WorkflowGraph = { nodes: [ { id: 'a', name: 'A', type: 't' }, { id: 'b', name: 'B', type: 't' }, { id: 'c', name: 'C', type: 't' }, ], edges: [ { source: 'a', target: 'b' }, { source: 'b', target: 'c' }, ], }; describe('auto-layout', () => { it('assigns non-overlapping positions (LR)', () => { const { nodes, edges } = graphToFlow(chainGraph); const laid = autoLayout(nodes, edges, { direction: 'LR' }); expect(laid.nodes).toHaveLength(3); expect(noOverlap(laid.nodes)).toBe(true); }); it('LR lays out left-to-right (x increases along the chain)', () => { const { nodes, edges } = graphToFlow(chainGraph); const laid = autoLayout(nodes, edges, { direction: 'LR' }); const byId = Object.fromEntries(laid.nodes.map((n) => [n.id, n])); expect(byId.b.position.x).toBeGreaterThan(byId.a.position.x); expect(byId.c.position.x).toBeGreaterThan(byId.b.position.x); }); it('TB lays out top-to-bottom (y increases along the chain)', () => { const { nodes, edges } = graphToFlow(chainGraph); const laid = autoLayout(nodes, edges, { direction: 'TB' }); const byId = Object.fromEntries(laid.nodes.map((n) => [n.id, n])); expect(byId.b.position.y).toBeGreaterThan(byId.a.position.y); expect(byId.c.position.y).toBeGreaterThan(byId.b.position.y); }); it('preserves explicit positions when preservePositions is set', () => { const graph: WorkflowGraph = { nodes: [ { id: 'a', name: 'A', type: 't', position: { x: 500, y: 500 } }, { id: 'b', name: 'B', type: 't' }, ], edges: [{ source: 'a', target: 'b' }], }; const { nodes, edges } = graphToFlow(graph); const laid = autoLayout(nodes, edges, { direction: 'LR', preservePositions: true }); const byId = Object.fromEntries(laid.nodes.map((n) => [n.id, n])); expect(byId.a.position).toEqual({ x: 500, y: 500 }); // b still gets a computed position expect(byId.b.position).toBeDefined(); }); it('places an edgeless / orphan node (still gets a position, no overlap)', () => { const graph: WorkflowGraph = { nodes: [ { id: 'a', name: 'A', type: 't' }, { id: 'b', name: 'B', type: 't' }, { id: 'orphan', name: 'O', type: 't' }, ], edges: [{ source: 'a', target: 'b' }], }; const { nodes, edges } = graphToFlow(graph); const laid = autoLayout(nodes, edges, { direction: 'LR' }); const orphan = laid.nodes.find((n) => n.id === 'orphan'); expect(orphan).toBeDefined(); expect(orphan!.position).toBeDefined(); expect(Number.isFinite(orphan!.position.x)).toBe(true); expect(Number.isFinite(orphan!.position.y)).toBe(true); expect(noOverlap(laid.nodes)).toBe(true); }); it('handles an empty node set', () => { const laid = autoLayout([], [], { direction: 'TB' }); expect(laid.nodes).toEqual([]); expect(laid.edges).toEqual([]); }); it('defaults to TB when no direction is given', () => { const { nodes, edges } = graphToFlow(chainGraph); const laid = autoLayout(nodes, edges); const byId = Object.fromEntries(laid.nodes.map((n) => [n.id, n])); expect(byId.c.position.y).toBeGreaterThan(byId.a.position.y); }); });