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>
83 lines
2.7 KiB
TypeScript
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)
|
|
})
|
|
})
|