import { describe, it, expect } from 'vitest'; import { PipelineGraph, parseDocument, serializeDocument } from '../src/model/graph'; function sampleGraph(): PipelineGraph { const g = new PipelineGraph(); g.addNode({ id: 'src', typeId: 'waterSource', x: 0, y: 0, angle: 0 }); g.addNode({ id: 'pump', typeId: 'pump', x: 200, y: 0, angle: 0 }); g.addNode({ id: 'home', typeId: 'consumer', x: 400, y: 0, angle: 0 }); g.addEdge({ id: 'e1', sourceNodeId: 'src', sourcePortId: 'out', targetNodeId: 'pump', targetPortId: 'in', }); g.addEdge({ id: 'e2', sourceNodeId: 'pump', sourcePortId: 'out', targetNodeId: 'home', targetPortId: 'waterIn', }); return g; } describe('PipelineGraph', () => { it('adds nodes with type defaults merged into props', () => { const g = sampleGraph(); const src = g.nodes.get('src')!; expect(src.props.supply).toBe(50); // waterSource default expect(src.props.title).toBe('Water source'); }); it('rejects unknown node types, ports and duplicate ids', () => { const g = sampleGraph(); expect(() => g.addNode({ id: 'x', typeId: 'nope', x: 0, y: 0, angle: 0 })).toThrow(/Unknown node type/); expect(() => g.addNode({ id: 'src', typeId: 'pump', x: 0, y: 0, angle: 0 })).toThrow(/Duplicate/); expect(() => g.addEdge({ id: 'e3', sourceNodeId: 'src', sourcePortId: 'nope', targetNodeId: 'pump', targetPortId: 'in' }), ).toThrow(/Unknown source port/); }); it('rejects edges between incompatible ports', () => { const g = sampleGraph(); g.addNode({ id: 'ps', typeId: 'powerSupply', x: 0, y: 200, angle: 0 }); expect(() => g.addEdge({ id: 'bad', sourceNodeId: 'ps', sourcePortId: 'out', targetNodeId: 'home', targetPortId: 'waterIn' }), ).toThrow(/Incompatible relations/); // power → consumer power inlet is fine expect(() => g.addEdge({ id: 'ok', sourceNodeId: 'ps', sourcePortId: 'out', targetNodeId: 'home', targetPortId: 'powerIn' }), ).not.toThrow(); }); it('derives edge relation from the source port', () => { const g = sampleGraph(); expect(g.edges.get('e1')!.relation).toBe('water'); }); it('removing a node cascades to connected edges', () => { const g = sampleGraph(); g.removeNode('pump'); expect(g.nodes.has('pump')).toBe(false); expect(g.edges.size).toBe(0); }); it('edgesOf returns incident edges', () => { const g = sampleGraph(); expect(g.edgesOf('pump').map((e) => e.id).sort()).toEqual(['e1', 'e2']); expect(g.edgesOf('home').map((e) => e.id)).toEqual(['e2']); }); it('survives a serialization round-trip', () => { const g = sampleGraph(); g.edges.get('e1')!.vertices = [{ x: 100, y: 50 }]; g.nodes.get('pump')!.props.head = 45; const json = serializeDocument(g.toDocument()); const restored = PipelineGraph.fromDocument(parseDocument(json)); expect(restored.nodes.size).toBe(3); expect(restored.edges.size).toBe(2); expect(restored.nodes.get('pump')!.props.head).toBe(45); expect(restored.edges.get('e1')!.vertices).toEqual([{ x: 100, y: 50 }]); expect(restored.edges.get('e2')!.relation).toBe('water'); }); it('parseDocument rejects malformed payloads', () => { expect(() => parseDocument('{"foo": 1}')).toThrow(/Invalid diagram document/); expect(() => parseDocument('not json')).toThrow(); }); });