Ilya Ashikhmin e70ec822c9 test: cover domain, engine and controller scenarios; add headless mode
Add Vitest suites for relations, properties, catalogue, symbols, edge
styles, flow solver, settings, JointJS shapes/serialization and the
controller lifecycle (add/connect/edit/delete, undo-redo, copy-paste,
flow, save-load). Introduce headless DiagramController (Paper-less) so the
controller is testable outside a browser, and fix the flow direction sign.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 23:31:12 +02:00

83 lines
2.7 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import { solveFlow, balanceSupplies, type FlowNodeInput, type FlowEdgeInput } from '@diagram/flow'
describe('balanceSupplies', () => {
it('scales demand to match supply', () => {
const b = balanceSupplies([
{ id: 's', supply: 10 },
{ id: 'a', supply: -5 },
{ id: 'b', supply: -5 }
])
expect(b.get('s')).toBe(10)
expect((b.get('a') ?? 0) + (b.get('b') ?? 0)).toBeCloseTo(-10)
})
it('scales up when demand exceeds supply', () => {
const b = balanceSupplies([
{ id: 's', supply: 4 },
{ id: 'a', supply: -8 }
])
expect(b.get('a')).toBeCloseTo(-4)
})
})
describe('solveFlow (tree)', () => {
// source(10) -> j -> two outlets(5 each)
const nodes: FlowNodeInput[] = [
{ id: 'src', supply: 10 },
{ id: 'j', supply: 0 },
{ id: 'o1', supply: -5 },
{ id: 'o2', supply: -5 }
]
const edges: FlowEdgeInput[] = [
{ id: 'e1', source: 'src', target: 'j' },
{ id: 'e2', source: 'j', target: 'o1' },
{ id: 'e3', source: 'j', target: 'o2' }
]
it('conserves mass (zero residual on a tree)', () => {
expect(solveFlow(nodes, edges).residual).toBeCloseTo(0, 6)
})
it('carries full supply on the trunk and splits on branches', () => {
const sol = solveFlow(nodes, edges)
expect(sol.edges.get('e1')!.magnitude).toBeCloseTo(10)
expect(sol.edges.get('e2')!.magnitude).toBeCloseTo(5)
expect(sol.edges.get('e3')!.magnitude).toBeCloseTo(5)
expect(sol.maxMagnitude).toBeCloseTo(10)
})
it('orients flow from source toward sinks', () => {
const sol = solveFlow(nodes, edges)
// e1 is src->j, flow should go forward (source to target)
expect(sol.edges.get('e1')!.forward).toBe(true)
// e2 is j->o1, forward means j -> outlet
expect(sol.edges.get('e2')!.forward).toBe(true)
})
it('reverses direction when an edge is drawn against the flow', () => {
const reversed: FlowEdgeInput[] = [{ id: 'e1', source: 'j', target: 'src' }, edges[1], edges[2]]
const sol = solveFlow(nodes, reversed)
expect(sol.edges.get('e1')!.forward).toBe(false)
expect(sol.edges.get('e1')!.magnitude).toBeCloseTo(10)
})
})
describe('solveFlow (loop)', () => {
it('still conserves mass with a cycle present', () => {
const nodes: FlowNodeInput[] = [
{ id: 'src', supply: 6 },
{ id: 'a', supply: 0 },
{ id: 'b', supply: 0 },
{ id: 'sink', supply: -6 }
]
const edges: FlowEdgeInput[] = [
{ id: 'e1', source: 'src', target: 'a' },
{ id: 'e2', source: 'a', target: 'b' },
{ id: 'e3', source: 'b', target: 'sink' },
{ id: 'e4', source: 'a', target: 'sink' } // extra loop edge
]
expect(solveFlow(nodes, edges).residual).toBeCloseTo(0, 6)
})
})