@pyreon/flow 0.11.4 → 0.11.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
- import { effect } from "@pyreon/reactivity"
2
- import { describe, expect, it, vi } from "vitest"
1
+ import { effect } from '@pyreon/reactivity'
2
+ import { describe, expect, it, vi } from 'vitest'
3
3
  import {
4
4
  getBezierPath,
5
5
  getEdgePath,
@@ -8,15 +8,15 @@ import {
8
8
  getStepPath,
9
9
  getStraightPath,
10
10
  getWaypointPath,
11
- } from "../edges"
12
- import { createFlow } from "../flow"
13
- import { Position } from "../types"
11
+ } from '../edges'
12
+ import { createFlow } from '../flow'
13
+ import { Position } from '../types'
14
14
 
15
15
  // ─── Edge path math ──────────────────────────────────────────────────────────
16
16
 
17
- describe("edge paths", () => {
18
- describe("getBezierPath", () => {
19
- it("generates a valid SVG path", () => {
17
+ describe('edge paths', () => {
18
+ describe('getBezierPath', () => {
19
+ it('generates a valid SVG path', () => {
20
20
  const result = getBezierPath({
21
21
  sourceX: 0,
22
22
  sourceY: 0,
@@ -24,12 +24,12 @@ describe("edge paths", () => {
24
24
  targetY: 100,
25
25
  })
26
26
  expect(result.path).toMatch(/^M/)
27
- expect(result.path).toContain("C")
27
+ expect(result.path).toContain('C')
28
28
  expect(result.labelX).toBe(100)
29
29
  expect(result.labelY).toBe(50)
30
30
  })
31
31
 
32
- it("respects source/target positions", () => {
32
+ it('respects source/target positions', () => {
33
33
  const right = getBezierPath({
34
34
  sourceX: 0,
35
35
  sourceY: 0,
@@ -50,7 +50,7 @@ describe("edge paths", () => {
50
50
  expect(right.path).not.toBe(bottom.path)
51
51
  })
52
52
 
53
- it("handles custom curvature", () => {
53
+ it('handles custom curvature', () => {
54
54
  const low = getBezierPath({
55
55
  sourceX: 0,
56
56
  sourceY: 0,
@@ -69,22 +69,22 @@ describe("edge paths", () => {
69
69
  })
70
70
  })
71
71
 
72
- describe("getStraightPath", () => {
73
- it("generates a straight line", () => {
72
+ describe('getStraightPath', () => {
73
+ it('generates a straight line', () => {
74
74
  const result = getStraightPath({
75
75
  sourceX: 0,
76
76
  sourceY: 0,
77
77
  targetX: 100,
78
78
  targetY: 100,
79
79
  })
80
- expect(result.path).toBe("M0,0 L100,100")
80
+ expect(result.path).toBe('M0,0 L100,100')
81
81
  expect(result.labelX).toBe(50)
82
82
  expect(result.labelY).toBe(50)
83
83
  })
84
84
  })
85
85
 
86
- describe("getSmoothStepPath", () => {
87
- it("generates a valid SVG path", () => {
86
+ describe('getSmoothStepPath', () => {
87
+ it('generates a valid SVG path', () => {
88
88
  const result = getSmoothStepPath({
89
89
  sourceX: 0,
90
90
  sourceY: 0,
@@ -98,7 +98,7 @@ describe("edge paths", () => {
98
98
  expect(result.labelY).toBe(50)
99
99
  })
100
100
 
101
- it("handles all position combinations", () => {
101
+ it('handles all position combinations', () => {
102
102
  const combos = [
103
103
  { sourcePosition: Position.Right, targetPosition: Position.Top },
104
104
  { sourcePosition: Position.Bottom, targetPosition: Position.Left },
@@ -118,8 +118,8 @@ describe("edge paths", () => {
118
118
  })
119
119
  })
120
120
 
121
- describe("getStepPath", () => {
122
- it("is smoothstep with borderRadius 0", () => {
121
+ describe('getStepPath', () => {
122
+ it('is smoothstep with borderRadius 0', () => {
123
123
  const step = getStepPath({
124
124
  sourceX: 0,
125
125
  sourceY: 0,
@@ -130,27 +130,27 @@ describe("edge paths", () => {
130
130
  })
131
131
  })
132
132
 
133
- describe("getEdgePath", () => {
134
- it("routes to correct path generator", () => {
135
- const bezier = getEdgePath("bezier", 0, 0, Position.Right, 200, 100, Position.Left)
136
- const straight = getEdgePath("straight", 0, 0, Position.Right, 200, 100, Position.Left)
137
- const smooth = getEdgePath("smoothstep", 0, 0, Position.Right, 200, 100, Position.Left)
138
- const step = getEdgePath("step", 0, 0, Position.Right, 200, 100, Position.Left)
133
+ describe('getEdgePath', () => {
134
+ it('routes to correct path generator', () => {
135
+ const bezier = getEdgePath('bezier', 0, 0, Position.Right, 200, 100, Position.Left)
136
+ const straight = getEdgePath('straight', 0, 0, Position.Right, 200, 100, Position.Left)
137
+ const smooth = getEdgePath('smoothstep', 0, 0, Position.Right, 200, 100, Position.Left)
138
+ const step = getEdgePath('step', 0, 0, Position.Right, 200, 100, Position.Left)
139
139
 
140
- expect(bezier.path).toContain("C") // bezier has control points
141
- expect(straight.path).toContain("L") // straight is a line
140
+ expect(bezier.path).toContain('C') // bezier has control points
141
+ expect(straight.path).toContain('L') // straight is a line
142
142
  expect(smooth.path).toMatch(/^M/) // smoothstep is valid
143
143
  expect(step.path).toMatch(/^M/) // step is valid
144
144
  })
145
145
 
146
- it("defaults to bezier for unknown type", () => {
147
- const result = getEdgePath("unknown", 0, 0, Position.Right, 200, 100, Position.Left)
148
- expect(result.path).toContain("C")
146
+ it('defaults to bezier for unknown type', () => {
147
+ const result = getEdgePath('unknown', 0, 0, Position.Right, 200, 100, Position.Left)
148
+ expect(result.path).toContain('C')
149
149
  })
150
150
  })
151
151
 
152
- describe("getHandlePosition", () => {
153
- it("returns correct positions", () => {
152
+ describe('getHandlePosition', () => {
153
+ it('returns correct positions', () => {
154
154
  const top = getHandlePosition(Position.Top, 0, 0, 100, 50)
155
155
  expect(top).toEqual({ x: 50, y: 0 })
156
156
 
@@ -168,249 +168,249 @@ describe("edge paths", () => {
168
168
 
169
169
  // ─── createFlow ──────────────────────────────────────────────────────────────
170
170
 
171
- describe("createFlow", () => {
172
- describe("initialization", () => {
173
- it("creates with default empty state", () => {
171
+ describe('createFlow', () => {
172
+ describe('initialization', () => {
173
+ it('creates with default empty state', () => {
174
174
  const flow = createFlow()
175
175
  expect(flow.nodes()).toEqual([])
176
176
  expect(flow.edges()).toEqual([])
177
177
  expect(flow.viewport()).toEqual({ x: 0, y: 0, zoom: 1 })
178
178
  })
179
179
 
180
- it("creates with initial nodes and edges", () => {
180
+ it('creates with initial nodes and edges', () => {
181
181
  const flow = createFlow({
182
182
  nodes: [
183
- { id: "1", position: { x: 0, y: 0 }, data: { label: "A" } },
184
- { id: "2", position: { x: 200, y: 0 }, data: { label: "B" } },
183
+ { id: '1', position: { x: 0, y: 0 }, data: { label: 'A' } },
184
+ { id: '2', position: { x: 200, y: 0 }, data: { label: 'B' } },
185
185
  ],
186
- edges: [{ source: "1", target: "2" }],
186
+ edges: [{ source: '1', target: '2' }],
187
187
  })
188
188
  expect(flow.nodes()).toHaveLength(2)
189
189
  expect(flow.edges()).toHaveLength(1)
190
190
  expect(flow.edges()[0]!.id).toBeDefined()
191
191
  })
192
192
 
193
- it("auto-generates edge ids", () => {
193
+ it('auto-generates edge ids', () => {
194
194
  const flow = createFlow({
195
195
  nodes: [
196
- { id: "1", position: { x: 0, y: 0 }, data: {} },
197
- { id: "2", position: { x: 100, y: 0 }, data: {} },
196
+ { id: '1', position: { x: 0, y: 0 }, data: {} },
197
+ { id: '2', position: { x: 100, y: 0 }, data: {} },
198
198
  ],
199
- edges: [{ source: "1", target: "2" }],
199
+ edges: [{ source: '1', target: '2' }],
200
200
  })
201
- expect(flow.edges()[0]!.id).toBe("e-1-2")
201
+ expect(flow.edges()[0]!.id).toBe('e-1-2')
202
202
  })
203
203
  })
204
204
 
205
- describe("node operations", () => {
206
- it("addNode adds a node", () => {
205
+ describe('node operations', () => {
206
+ it('addNode adds a node', () => {
207
207
  const flow = createFlow()
208
208
  flow.addNode({
209
- id: "1",
209
+ id: '1',
210
210
  position: { x: 0, y: 0 },
211
- data: { label: "New" },
211
+ data: { label: 'New' },
212
212
  })
213
213
  expect(flow.nodes()).toHaveLength(1)
214
- expect(flow.nodes()[0]!.id).toBe("1")
214
+ expect(flow.nodes()[0]!.id).toBe('1')
215
215
  })
216
216
 
217
- it("removeNode removes node and connected edges", () => {
217
+ it('removeNode removes node and connected edges', () => {
218
218
  const flow = createFlow({
219
219
  nodes: [
220
- { id: "1", position: { x: 0, y: 0 }, data: {} },
221
- { id: "2", position: { x: 100, y: 0 }, data: {} },
222
- { id: "3", position: { x: 200, y: 0 }, data: {} },
220
+ { id: '1', position: { x: 0, y: 0 }, data: {} },
221
+ { id: '2', position: { x: 100, y: 0 }, data: {} },
222
+ { id: '3', position: { x: 200, y: 0 }, data: {} },
223
223
  ],
224
224
  edges: [
225
- { source: "1", target: "2" },
226
- { source: "2", target: "3" },
225
+ { source: '1', target: '2' },
226
+ { source: '2', target: '3' },
227
227
  ],
228
228
  })
229
229
 
230
- flow.removeNode("2")
230
+ flow.removeNode('2')
231
231
  expect(flow.nodes()).toHaveLength(2)
232
232
  expect(flow.edges()).toHaveLength(0) // both edges connected to '2'
233
233
  })
234
234
 
235
- it("updateNode updates node properties", () => {
235
+ it('updateNode updates node properties', () => {
236
236
  const flow = createFlow({
237
- nodes: [{ id: "1", position: { x: 0, y: 0 }, data: { label: "Old" } }],
237
+ nodes: [{ id: '1', position: { x: 0, y: 0 }, data: { label: 'Old' } }],
238
238
  })
239
- flow.updateNode("1", { data: { label: "New" } })
240
- expect(flow.getNode("1")!.data.label).toBe("New")
239
+ flow.updateNode('1', { data: { label: 'New' } })
240
+ expect(flow.getNode('1')!.data.label).toBe('New')
241
241
  })
242
242
 
243
- it("updateNodePosition updates position", () => {
243
+ it('updateNodePosition updates position', () => {
244
244
  const flow = createFlow({
245
- nodes: [{ id: "1", position: { x: 0, y: 0 }, data: {} }],
245
+ nodes: [{ id: '1', position: { x: 0, y: 0 }, data: {} }],
246
246
  })
247
- flow.updateNodePosition("1", { x: 100, y: 200 })
248
- expect(flow.getNode("1")!.position).toEqual({ x: 100, y: 200 })
247
+ flow.updateNodePosition('1', { x: 100, y: 200 })
248
+ expect(flow.getNode('1')!.position).toEqual({ x: 100, y: 200 })
249
249
  })
250
250
 
251
- it("updateNodePosition snaps to grid when enabled", () => {
251
+ it('updateNodePosition snaps to grid when enabled', () => {
252
252
  const flow = createFlow({
253
- nodes: [{ id: "1", position: { x: 0, y: 0 }, data: {} }],
253
+ nodes: [{ id: '1', position: { x: 0, y: 0 }, data: {} }],
254
254
  snapToGrid: true,
255
255
  snapGrid: 10,
256
256
  })
257
- flow.updateNodePosition("1", { x: 13, y: 27 })
258
- expect(flow.getNode("1")!.position).toEqual({ x: 10, y: 30 })
257
+ flow.updateNodePosition('1', { x: 13, y: 27 })
258
+ expect(flow.getNode('1')!.position).toEqual({ x: 10, y: 30 })
259
259
  })
260
260
 
261
- it("getNode returns undefined for missing id", () => {
261
+ it('getNode returns undefined for missing id', () => {
262
262
  const flow = createFlow()
263
- expect(flow.getNode("missing")).toBeUndefined()
263
+ expect(flow.getNode('missing')).toBeUndefined()
264
264
  })
265
265
  })
266
266
 
267
- describe("edge operations", () => {
268
- it("addEdge adds an edge", () => {
267
+ describe('edge operations', () => {
268
+ it('addEdge adds an edge', () => {
269
269
  const flow = createFlow({
270
270
  nodes: [
271
- { id: "1", position: { x: 0, y: 0 }, data: {} },
272
- { id: "2", position: { x: 100, y: 0 }, data: {} },
271
+ { id: '1', position: { x: 0, y: 0 }, data: {} },
272
+ { id: '2', position: { x: 100, y: 0 }, data: {} },
273
273
  ],
274
274
  })
275
- flow.addEdge({ source: "1", target: "2" })
275
+ flow.addEdge({ source: '1', target: '2' })
276
276
  expect(flow.edges()).toHaveLength(1)
277
277
  })
278
278
 
279
- it("addEdge prevents duplicates", () => {
279
+ it('addEdge prevents duplicates', () => {
280
280
  const flow = createFlow({
281
281
  nodes: [
282
- { id: "1", position: { x: 0, y: 0 }, data: {} },
283
- { id: "2", position: { x: 100, y: 0 }, data: {} },
282
+ { id: '1', position: { x: 0, y: 0 }, data: {} },
283
+ { id: '2', position: { x: 100, y: 0 }, data: {} },
284
284
  ],
285
- edges: [{ source: "1", target: "2" }],
285
+ edges: [{ source: '1', target: '2' }],
286
286
  })
287
- flow.addEdge({ source: "1", target: "2" })
287
+ flow.addEdge({ source: '1', target: '2' })
288
288
  expect(flow.edges()).toHaveLength(1) // not duplicated
289
289
  })
290
290
 
291
- it("removeEdge removes an edge", () => {
291
+ it('removeEdge removes an edge', () => {
292
292
  const flow = createFlow({
293
293
  nodes: [
294
- { id: "1", position: { x: 0, y: 0 }, data: {} },
295
- { id: "2", position: { x: 100, y: 0 }, data: {} },
294
+ { id: '1', position: { x: 0, y: 0 }, data: {} },
295
+ { id: '2', position: { x: 100, y: 0 }, data: {} },
296
296
  ],
297
- edges: [{ source: "1", target: "2" }],
297
+ edges: [{ source: '1', target: '2' }],
298
298
  })
299
- flow.removeEdge("e-1-2")
299
+ flow.removeEdge('e-1-2')
300
300
  expect(flow.edges()).toHaveLength(0)
301
301
  })
302
302
 
303
- it("getEdge returns edge by id", () => {
303
+ it('getEdge returns edge by id', () => {
304
304
  const flow = createFlow({
305
305
  nodes: [
306
- { id: "1", position: { x: 0, y: 0 }, data: {} },
307
- { id: "2", position: { x: 100, y: 0 }, data: {} },
306
+ { id: '1', position: { x: 0, y: 0 }, data: {} },
307
+ { id: '2', position: { x: 100, y: 0 }, data: {} },
308
308
  ],
309
- edges: [{ id: "my-edge", source: "1", target: "2" }],
309
+ edges: [{ id: 'my-edge', source: '1', target: '2' }],
310
310
  })
311
- expect(flow.getEdge("my-edge")).toBeDefined()
312
- expect(flow.getEdge("missing")).toBeUndefined()
311
+ expect(flow.getEdge('my-edge')).toBeDefined()
312
+ expect(flow.getEdge('missing')).toBeUndefined()
313
313
  })
314
314
  })
315
315
 
316
- describe("connection rules", () => {
317
- it("validates connections based on rules", () => {
316
+ describe('connection rules', () => {
317
+ it('validates connections based on rules', () => {
318
318
  const flow = createFlow({
319
319
  nodes: [
320
- { id: "1", type: "input", position: { x: 0, y: 0 }, data: {} },
321
- { id: "2", type: "process", position: { x: 100, y: 0 }, data: {} },
322
- { id: "3", type: "output", position: { x: 200, y: 0 }, data: {} },
320
+ { id: '1', type: 'input', position: { x: 0, y: 0 }, data: {} },
321
+ { id: '2', type: 'process', position: { x: 100, y: 0 }, data: {} },
322
+ { id: '3', type: 'output', position: { x: 200, y: 0 }, data: {} },
323
323
  ],
324
324
  connectionRules: {
325
- input: { outputs: ["process"] },
326
- process: { outputs: ["process", "output"] },
325
+ input: { outputs: ['process'] },
326
+ process: { outputs: ['process', 'output'] },
327
327
  output: { outputs: [] },
328
328
  },
329
329
  })
330
330
 
331
- expect(flow.isValidConnection({ source: "1", target: "2" })).toBe(true) // input → process
332
- expect(flow.isValidConnection({ source: "2", target: "3" })).toBe(true) // process → output
333
- expect(flow.isValidConnection({ source: "1", target: "3" })).toBe(false) // input → output (blocked)
334
- expect(flow.isValidConnection({ source: "3", target: "1" })).toBe(false) // output → (no outputs)
331
+ expect(flow.isValidConnection({ source: '1', target: '2' })).toBe(true) // input → process
332
+ expect(flow.isValidConnection({ source: '2', target: '3' })).toBe(true) // process → output
333
+ expect(flow.isValidConnection({ source: '1', target: '3' })).toBe(false) // input → output (blocked)
334
+ expect(flow.isValidConnection({ source: '3', target: '1' })).toBe(false) // output → (no outputs)
335
335
  })
336
336
 
337
- it("allows all connections without rules", () => {
337
+ it('allows all connections without rules', () => {
338
338
  const flow = createFlow({
339
339
  nodes: [
340
- { id: "1", position: { x: 0, y: 0 }, data: {} },
341
- { id: "2", position: { x: 100, y: 0 }, data: {} },
340
+ { id: '1', position: { x: 0, y: 0 }, data: {} },
341
+ { id: '2', position: { x: 100, y: 0 }, data: {} },
342
342
  ],
343
343
  })
344
- expect(flow.isValidConnection({ source: "1", target: "2" })).toBe(true)
344
+ expect(flow.isValidConnection({ source: '1', target: '2' })).toBe(true)
345
345
  })
346
346
  })
347
347
 
348
- describe("selection", () => {
349
- it("selectNode selects a node", () => {
348
+ describe('selection', () => {
349
+ it('selectNode selects a node', () => {
350
350
  const flow = createFlow({
351
- nodes: [{ id: "1", position: { x: 0, y: 0 }, data: {} }],
351
+ nodes: [{ id: '1', position: { x: 0, y: 0 }, data: {} }],
352
352
  })
353
- flow.selectNode("1")
354
- expect(flow.selectedNodes()).toEqual(["1"])
353
+ flow.selectNode('1')
354
+ expect(flow.selectedNodes()).toEqual(['1'])
355
355
  })
356
356
 
357
- it("selectNode replaces selection by default", () => {
357
+ it('selectNode replaces selection by default', () => {
358
358
  const flow = createFlow({
359
359
  nodes: [
360
- { id: "1", position: { x: 0, y: 0 }, data: {} },
361
- { id: "2", position: { x: 100, y: 0 }, data: {} },
360
+ { id: '1', position: { x: 0, y: 0 }, data: {} },
361
+ { id: '2', position: { x: 100, y: 0 }, data: {} },
362
362
  ],
363
363
  })
364
- flow.selectNode("1")
365
- flow.selectNode("2")
366
- expect(flow.selectedNodes()).toEqual(["2"])
364
+ flow.selectNode('1')
365
+ flow.selectNode('2')
366
+ expect(flow.selectedNodes()).toEqual(['2'])
367
367
  })
368
368
 
369
- it("selectNode with additive adds to selection", () => {
369
+ it('selectNode with additive adds to selection', () => {
370
370
  const flow = createFlow({
371
371
  nodes: [
372
- { id: "1", position: { x: 0, y: 0 }, data: {} },
373
- { id: "2", position: { x: 100, y: 0 }, data: {} },
372
+ { id: '1', position: { x: 0, y: 0 }, data: {} },
373
+ { id: '2', position: { x: 100, y: 0 }, data: {} },
374
374
  ],
375
375
  })
376
- flow.selectNode("1")
377
- flow.selectNode("2", true)
378
- expect(flow.selectedNodes()).toEqual(expect.arrayContaining(["1", "2"]))
376
+ flow.selectNode('1')
377
+ flow.selectNode('2', true)
378
+ expect(flow.selectedNodes()).toEqual(expect.arrayContaining(['1', '2']))
379
379
  })
380
380
 
381
- it("clearSelection clears all", () => {
381
+ it('clearSelection clears all', () => {
382
382
  const flow = createFlow({
383
- nodes: [{ id: "1", position: { x: 0, y: 0 }, data: {} }],
383
+ nodes: [{ id: '1', position: { x: 0, y: 0 }, data: {} }],
384
384
  })
385
- flow.selectNode("1")
385
+ flow.selectNode('1')
386
386
  flow.clearSelection()
387
387
  expect(flow.selectedNodes()).toEqual([])
388
388
  })
389
389
 
390
- it("selectAll selects all nodes", () => {
390
+ it('selectAll selects all nodes', () => {
391
391
  const flow = createFlow({
392
392
  nodes: [
393
- { id: "1", position: { x: 0, y: 0 }, data: {} },
394
- { id: "2", position: { x: 100, y: 0 }, data: {} },
393
+ { id: '1', position: { x: 0, y: 0 }, data: {} },
394
+ { id: '2', position: { x: 100, y: 0 }, data: {} },
395
395
  ],
396
396
  })
397
397
  flow.selectAll()
398
398
  expect(flow.selectedNodes()).toHaveLength(2)
399
399
  })
400
400
 
401
- it("deleteSelected removes selected nodes and edges", () => {
401
+ it('deleteSelected removes selected nodes and edges', () => {
402
402
  const flow = createFlow({
403
403
  nodes: [
404
- { id: "1", position: { x: 0, y: 0 }, data: {} },
405
- { id: "2", position: { x: 100, y: 0 }, data: {} },
406
- { id: "3", position: { x: 200, y: 0 }, data: {} },
404
+ { id: '1', position: { x: 0, y: 0 }, data: {} },
405
+ { id: '2', position: { x: 100, y: 0 }, data: {} },
406
+ { id: '3', position: { x: 200, y: 0 }, data: {} },
407
407
  ],
408
408
  edges: [
409
- { source: "1", target: "2" },
410
- { source: "2", target: "3" },
409
+ { source: '1', target: '2' },
410
+ { source: '2', target: '3' },
411
411
  ],
412
412
  })
413
- flow.selectNode("2")
413
+ flow.selectNode('2')
414
414
  flow.deleteSelected()
415
415
  expect(flow.nodes()).toHaveLength(2)
416
416
  expect(flow.edges()).toHaveLength(0)
@@ -418,22 +418,22 @@ describe("createFlow", () => {
418
418
  })
419
419
  })
420
420
 
421
- describe("viewport", () => {
422
- it("zoomIn increases zoom", () => {
421
+ describe('viewport', () => {
422
+ it('zoomIn increases zoom', () => {
423
423
  const flow = createFlow()
424
424
  const initial = flow.zoom()
425
425
  flow.zoomIn()
426
426
  expect(flow.zoom()).toBeGreaterThan(initial)
427
427
  })
428
428
 
429
- it("zoomOut decreases zoom", () => {
429
+ it('zoomOut decreases zoom', () => {
430
430
  const flow = createFlow()
431
431
  const initial = flow.zoom()
432
432
  flow.zoomOut()
433
433
  expect(flow.zoom()).toBeLessThan(initial)
434
434
  })
435
435
 
436
- it("zoomTo clamps to min/max", () => {
436
+ it('zoomTo clamps to min/max', () => {
437
437
  const flow = createFlow({ minZoom: 0.5, maxZoom: 2 })
438
438
  flow.zoomTo(0.1)
439
439
  expect(flow.zoom()).toBe(0.5)
@@ -441,11 +441,11 @@ describe("createFlow", () => {
441
441
  expect(flow.zoom()).toBe(2)
442
442
  })
443
443
 
444
- it("fitView adjusts viewport to show all nodes", () => {
444
+ it('fitView adjusts viewport to show all nodes', () => {
445
445
  const flow = createFlow({
446
446
  nodes: [
447
- { id: "1", position: { x: 0, y: 0 }, data: {} },
448
- { id: "2", position: { x: 500, y: 300 }, data: {} },
447
+ { id: '1', position: { x: 0, y: 0 }, data: {} },
448
+ { id: '2', position: { x: 500, y: 300 }, data: {} },
449
449
  ],
450
450
  })
451
451
  flow.fitView()
@@ -453,22 +453,22 @@ describe("createFlow", () => {
453
453
  expect(flow.viewport().zoom).toBeGreaterThan(0)
454
454
  })
455
455
 
456
- it("fitView with specific nodes", () => {
456
+ it('fitView with specific nodes', () => {
457
457
  const flow = createFlow({
458
458
  nodes: [
459
- { id: "1", position: { x: 0, y: 0 }, data: {} },
460
- { id: "2", position: { x: 1000, y: 1000 }, data: {} },
461
- { id: "3", position: { x: 50, y: 50 }, data: {} },
459
+ { id: '1', position: { x: 0, y: 0 }, data: {} },
460
+ { id: '2', position: { x: 1000, y: 1000 }, data: {} },
461
+ { id: '3', position: { x: 50, y: 50 }, data: {} },
462
462
  ],
463
463
  })
464
- flow.fitView(["1", "3"])
464
+ flow.fitView(['1', '3'])
465
465
  // Should zoom in more since only close nodes are targeted
466
466
  expect(flow.viewport().zoom).toBeGreaterThan(0)
467
467
  })
468
468
  })
469
469
 
470
- describe("viewport — panTo and isNodeVisible", () => {
471
- it("panTo updates viewport position", () => {
470
+ describe('viewport — panTo and isNodeVisible', () => {
471
+ it('panTo updates viewport position', () => {
472
472
  const flow = createFlow()
473
473
  flow.panTo({ x: 100, y: 200 })
474
474
  const vp = flow.viewport()
@@ -476,19 +476,19 @@ describe("createFlow", () => {
476
476
  expect(vp.y).toBe(-200)
477
477
  })
478
478
 
479
- it("isNodeVisible checks viewport bounds", () => {
479
+ it('isNodeVisible checks viewport bounds', () => {
480
480
  const flow = createFlow({
481
481
  nodes: [
482
- { id: "1", position: { x: 0, y: 0 }, data: {} },
483
- { id: "2", position: { x: 5000, y: 5000 }, data: {} },
482
+ { id: '1', position: { x: 0, y: 0 }, data: {} },
483
+ { id: '2', position: { x: 5000, y: 5000 }, data: {} },
484
484
  ],
485
485
  })
486
- expect(flow.isNodeVisible("1")).toBe(true)
487
- expect(flow.isNodeVisible("2")).toBe(false)
488
- expect(flow.isNodeVisible("missing")).toBe(false)
486
+ expect(flow.isNodeVisible('1')).toBe(true)
487
+ expect(flow.isNodeVisible('2')).toBe(false)
488
+ expect(flow.isNodeVisible('missing')).toBe(false)
489
489
  })
490
490
 
491
- it("fitView with no nodes does nothing", () => {
491
+ it('fitView with no nodes does nothing', () => {
492
492
  const flow = createFlow()
493
493
  const before = flow.viewport()
494
494
  flow.fitView()
@@ -496,160 +496,160 @@ describe("createFlow", () => {
496
496
  })
497
497
  })
498
498
 
499
- describe("edge selection", () => {
500
- it("selectEdge selects an edge", () => {
499
+ describe('edge selection', () => {
500
+ it('selectEdge selects an edge', () => {
501
501
  const flow = createFlow({
502
502
  nodes: [
503
- { id: "1", position: { x: 0, y: 0 }, data: {} },
504
- { id: "2", position: { x: 100, y: 0 }, data: {} },
503
+ { id: '1', position: { x: 0, y: 0 }, data: {} },
504
+ { id: '2', position: { x: 100, y: 0 }, data: {} },
505
505
  ],
506
- edges: [{ id: "e1", source: "1", target: "2" }],
506
+ edges: [{ id: 'e1', source: '1', target: '2' }],
507
507
  })
508
- flow.selectEdge("e1")
509
- expect(flow.selectedEdges()).toEqual(["e1"])
508
+ flow.selectEdge('e1')
509
+ expect(flow.selectedEdges()).toEqual(['e1'])
510
510
  })
511
511
 
512
- it("selectEdge clears node selection by default", () => {
512
+ it('selectEdge clears node selection by default', () => {
513
513
  const flow = createFlow({
514
514
  nodes: [
515
- { id: "1", position: { x: 0, y: 0 }, data: {} },
516
- { id: "2", position: { x: 100, y: 0 }, data: {} },
515
+ { id: '1', position: { x: 0, y: 0 }, data: {} },
516
+ { id: '2', position: { x: 100, y: 0 }, data: {} },
517
517
  ],
518
- edges: [{ id: "e1", source: "1", target: "2" }],
518
+ edges: [{ id: 'e1', source: '1', target: '2' }],
519
519
  })
520
- flow.selectNode("1")
521
- flow.selectEdge("e1")
520
+ flow.selectNode('1')
521
+ flow.selectEdge('e1')
522
522
  expect(flow.selectedNodes()).toEqual([])
523
- expect(flow.selectedEdges()).toEqual(["e1"])
523
+ expect(flow.selectedEdges()).toEqual(['e1'])
524
524
  })
525
525
 
526
- it("selectEdge additive mode", () => {
526
+ it('selectEdge additive mode', () => {
527
527
  const flow = createFlow({
528
528
  nodes: [
529
- { id: "1", position: { x: 0, y: 0 }, data: {} },
530
- { id: "2", position: { x: 100, y: 0 }, data: {} },
531
- { id: "3", position: { x: 200, y: 0 }, data: {} },
529
+ { id: '1', position: { x: 0, y: 0 }, data: {} },
530
+ { id: '2', position: { x: 100, y: 0 }, data: {} },
531
+ { id: '3', position: { x: 200, y: 0 }, data: {} },
532
532
  ],
533
533
  edges: [
534
- { id: "e1", source: "1", target: "2" },
535
- { id: "e2", source: "2", target: "3" },
534
+ { id: 'e1', source: '1', target: '2' },
535
+ { id: 'e2', source: '2', target: '3' },
536
536
  ],
537
537
  })
538
- flow.selectEdge("e1")
539
- flow.selectEdge("e2", true)
540
- expect(flow.selectedEdges()).toEqual(expect.arrayContaining(["e1", "e2"]))
538
+ flow.selectEdge('e1')
539
+ flow.selectEdge('e2', true)
540
+ expect(flow.selectedEdges()).toEqual(expect.arrayContaining(['e1', 'e2']))
541
541
  })
542
542
 
543
- it("deselectNode removes from selection", () => {
543
+ it('deselectNode removes from selection', () => {
544
544
  const flow = createFlow({
545
545
  nodes: [
546
- { id: "1", position: { x: 0, y: 0 }, data: {} },
547
- { id: "2", position: { x: 100, y: 0 }, data: {} },
546
+ { id: '1', position: { x: 0, y: 0 }, data: {} },
547
+ { id: '2', position: { x: 100, y: 0 }, data: {} },
548
548
  ],
549
549
  })
550
- flow.selectNode("1")
551
- flow.selectNode("2", true)
552
- flow.deselectNode("1")
553
- expect(flow.selectedNodes()).toEqual(["2"])
550
+ flow.selectNode('1')
551
+ flow.selectNode('2', true)
552
+ flow.deselectNode('1')
553
+ expect(flow.selectedNodes()).toEqual(['2'])
554
554
  })
555
555
 
556
- it("deleteSelected with selected edges only", () => {
556
+ it('deleteSelected with selected edges only', () => {
557
557
  const flow = createFlow({
558
558
  nodes: [
559
- { id: "1", position: { x: 0, y: 0 }, data: {} },
560
- { id: "2", position: { x: 100, y: 0 }, data: {} },
559
+ { id: '1', position: { x: 0, y: 0 }, data: {} },
560
+ { id: '2', position: { x: 100, y: 0 }, data: {} },
561
561
  ],
562
- edges: [{ id: "e1", source: "1", target: "2" }],
562
+ edges: [{ id: 'e1', source: '1', target: '2' }],
563
563
  })
564
- flow.selectEdge("e1")
564
+ flow.selectEdge('e1')
565
565
  flow.deleteSelected()
566
566
  expect(flow.edges()).toHaveLength(0)
567
567
  expect(flow.nodes()).toHaveLength(2) // nodes untouched
568
568
  })
569
569
  })
570
570
 
571
- describe("connection rules — edge cases", () => {
572
- it("returns false for missing source node", () => {
571
+ describe('connection rules — edge cases', () => {
572
+ it('returns false for missing source node', () => {
573
573
  const flow = createFlow({
574
- nodes: [{ id: "1", position: { x: 0, y: 0 }, data: {} }],
575
- connectionRules: { default: { outputs: ["default"] } },
574
+ nodes: [{ id: '1', position: { x: 0, y: 0 }, data: {} }],
575
+ connectionRules: { default: { outputs: ['default'] } },
576
576
  })
577
- expect(flow.isValidConnection({ source: "missing", target: "1" })).toBe(false)
577
+ expect(flow.isValidConnection({ source: 'missing', target: '1' })).toBe(false)
578
578
  })
579
579
 
580
- it("returns false for missing target node", () => {
580
+ it('returns false for missing target node', () => {
581
581
  const flow = createFlow({
582
- nodes: [{ id: "1", position: { x: 0, y: 0 }, data: {} }],
583
- connectionRules: { default: { outputs: ["default"] } },
582
+ nodes: [{ id: '1', position: { x: 0, y: 0 }, data: {} }],
583
+ connectionRules: { default: { outputs: ['default'] } },
584
584
  })
585
- expect(flow.isValidConnection({ source: "1", target: "missing" })).toBe(false)
585
+ expect(flow.isValidConnection({ source: '1', target: 'missing' })).toBe(false)
586
586
  })
587
587
 
588
- it("allows connection when no rule for source type", () => {
588
+ it('allows connection when no rule for source type', () => {
589
589
  const flow = createFlow({
590
590
  nodes: [
591
- { id: "1", type: "custom", position: { x: 0, y: 0 }, data: {} },
592
- { id: "2", position: { x: 100, y: 0 }, data: {} },
591
+ { id: '1', type: 'custom', position: { x: 0, y: 0 }, data: {} },
592
+ { id: '2', position: { x: 100, y: 0 }, data: {} },
593
593
  ],
594
594
  connectionRules: { default: { outputs: [] } },
595
595
  })
596
- expect(flow.isValidConnection({ source: "1", target: "2" })).toBe(true) // no rule for 'custom'
596
+ expect(flow.isValidConnection({ source: '1', target: '2' })).toBe(true) // no rule for 'custom'
597
597
  })
598
598
  })
599
599
 
600
- describe("graph queries", () => {
601
- it("getConnectedEdges returns edges for a node", () => {
600
+ describe('graph queries', () => {
601
+ it('getConnectedEdges returns edges for a node', () => {
602
602
  const flow = createFlow({
603
603
  nodes: [
604
- { id: "1", position: { x: 0, y: 0 }, data: {} },
605
- { id: "2", position: { x: 100, y: 0 }, data: {} },
606
- { id: "3", position: { x: 200, y: 0 }, data: {} },
604
+ { id: '1', position: { x: 0, y: 0 }, data: {} },
605
+ { id: '2', position: { x: 100, y: 0 }, data: {} },
606
+ { id: '3', position: { x: 200, y: 0 }, data: {} },
607
607
  ],
608
608
  edges: [
609
- { source: "1", target: "2" },
610
- { source: "2", target: "3" },
609
+ { source: '1', target: '2' },
610
+ { source: '2', target: '3' },
611
611
  ],
612
612
  })
613
- expect(flow.getConnectedEdges("2")).toHaveLength(2)
614
- expect(flow.getConnectedEdges("1")).toHaveLength(1)
613
+ expect(flow.getConnectedEdges('2')).toHaveLength(2)
614
+ expect(flow.getConnectedEdges('1')).toHaveLength(1)
615
615
  })
616
616
 
617
- it("getIncomers returns upstream nodes", () => {
617
+ it('getIncomers returns upstream nodes', () => {
618
618
  const flow = createFlow({
619
619
  nodes: [
620
- { id: "1", position: { x: 0, y: 0 }, data: {} },
621
- { id: "2", position: { x: 100, y: 0 }, data: {} },
622
- { id: "3", position: { x: 200, y: 0 }, data: {} },
620
+ { id: '1', position: { x: 0, y: 0 }, data: {} },
621
+ { id: '2', position: { x: 100, y: 0 }, data: {} },
622
+ { id: '3', position: { x: 200, y: 0 }, data: {} },
623
623
  ],
624
624
  edges: [
625
- { source: "1", target: "3" },
626
- { source: "2", target: "3" },
625
+ { source: '1', target: '3' },
626
+ { source: '2', target: '3' },
627
627
  ],
628
628
  })
629
- const incomers = flow.getIncomers("3")
629
+ const incomers = flow.getIncomers('3')
630
630
  expect(incomers).toHaveLength(2)
631
- expect(incomers.map((n) => n.id)).toEqual(expect.arrayContaining(["1", "2"]))
631
+ expect(incomers.map((n) => n.id)).toEqual(expect.arrayContaining(['1', '2']))
632
632
  })
633
633
 
634
- it("getOutgoers returns downstream nodes", () => {
634
+ it('getOutgoers returns downstream nodes', () => {
635
635
  const flow = createFlow({
636
636
  nodes: [
637
- { id: "1", position: { x: 0, y: 0 }, data: {} },
638
- { id: "2", position: { x: 100, y: 0 }, data: {} },
639
- { id: "3", position: { x: 200, y: 0 }, data: {} },
637
+ { id: '1', position: { x: 0, y: 0 }, data: {} },
638
+ { id: '2', position: { x: 100, y: 0 }, data: {} },
639
+ { id: '3', position: { x: 200, y: 0 }, data: {} },
640
640
  ],
641
641
  edges: [
642
- { source: "1", target: "2" },
643
- { source: "1", target: "3" },
642
+ { source: '1', target: '2' },
643
+ { source: '1', target: '3' },
644
644
  ],
645
645
  })
646
- const outgoers = flow.getOutgoers("1")
646
+ const outgoers = flow.getOutgoers('1')
647
647
  expect(outgoers).toHaveLength(2)
648
648
  })
649
649
  })
650
650
 
651
- describe("reactivity", () => {
652
- it("nodes signal is reactive in effects", () => {
651
+ describe('reactivity', () => {
652
+ it('nodes signal is reactive in effects', () => {
653
653
  const flow = createFlow()
654
654
  const counts: number[] = []
655
655
 
@@ -657,13 +657,13 @@ describe("createFlow", () => {
657
657
  counts.push(flow.nodes().length)
658
658
  })
659
659
 
660
- flow.addNode({ id: "1", position: { x: 0, y: 0 }, data: {} })
661
- flow.addNode({ id: "2", position: { x: 100, y: 0 }, data: {} })
660
+ flow.addNode({ id: '1', position: { x: 0, y: 0 }, data: {} })
661
+ flow.addNode({ id: '2', position: { x: 100, y: 0 }, data: {} })
662
662
 
663
663
  expect(counts).toEqual([0, 1, 2])
664
664
  })
665
665
 
666
- it("zoom is a reactive computed", () => {
666
+ it('zoom is a reactive computed', () => {
667
667
  const flow = createFlow()
668
668
  const zooms: number[] = []
669
669
 
@@ -677,11 +677,11 @@ describe("createFlow", () => {
677
677
  expect(zooms).toHaveLength(3)
678
678
  })
679
679
 
680
- it("selectedNodes is reactive", () => {
680
+ it('selectedNodes is reactive', () => {
681
681
  const flow = createFlow({
682
682
  nodes: [
683
- { id: "1", position: { x: 0, y: 0 }, data: {} },
684
- { id: "2", position: { x: 100, y: 0 }, data: {} },
683
+ { id: '1', position: { x: 0, y: 0 }, data: {} },
684
+ { id: '2', position: { x: 100, y: 0 }, data: {} },
685
685
  ],
686
686
  })
687
687
  const selections: string[][] = []
@@ -690,71 +690,71 @@ describe("createFlow", () => {
690
690
  selections.push([...flow.selectedNodes()])
691
691
  })
692
692
 
693
- flow.selectNode("1")
694
- flow.selectNode("2", true)
693
+ flow.selectNode('1')
694
+ flow.selectNode('2', true)
695
695
 
696
696
  expect(selections).toHaveLength(3)
697
- expect(selections[2]).toEqual(expect.arrayContaining(["1", "2"]))
697
+ expect(selections[2]).toEqual(expect.arrayContaining(['1', '2']))
698
698
  })
699
699
  })
700
700
 
701
- describe("listeners", () => {
702
- it("onConnect fires when edge is added", () => {
701
+ describe('listeners', () => {
702
+ it('onConnect fires when edge is added', () => {
703
703
  const flow = createFlow({
704
704
  nodes: [
705
- { id: "1", position: { x: 0, y: 0 }, data: {} },
706
- { id: "2", position: { x: 100, y: 0 }, data: {} },
705
+ { id: '1', position: { x: 0, y: 0 }, data: {} },
706
+ { id: '2', position: { x: 100, y: 0 }, data: {} },
707
707
  ],
708
708
  })
709
709
  const fn = vi.fn()
710
710
  flow.onConnect(fn)
711
711
 
712
- flow.addEdge({ source: "1", target: "2" })
713
- expect(fn).toHaveBeenCalledWith(expect.objectContaining({ source: "1", target: "2" }))
712
+ flow.addEdge({ source: '1', target: '2' })
713
+ expect(fn).toHaveBeenCalledWith(expect.objectContaining({ source: '1', target: '2' }))
714
714
  })
715
715
 
716
- it("onNodesChange fires on position update", () => {
716
+ it('onNodesChange fires on position update', () => {
717
717
  const flow = createFlow({
718
- nodes: [{ id: "1", position: { x: 0, y: 0 }, data: {} }],
718
+ nodes: [{ id: '1', position: { x: 0, y: 0 }, data: {} }],
719
719
  })
720
720
  const fn = vi.fn()
721
721
  flow.onNodesChange(fn)
722
722
 
723
- flow.updateNodePosition("1", { x: 100, y: 200 })
724
- expect(fn).toHaveBeenCalledWith([{ type: "position", id: "1", position: { x: 100, y: 200 } }])
723
+ flow.updateNodePosition('1', { x: 100, y: 200 })
724
+ expect(fn).toHaveBeenCalledWith([{ type: 'position', id: '1', position: { x: 100, y: 200 } }])
725
725
  })
726
726
 
727
- it("onNodesChange fires on remove", () => {
727
+ it('onNodesChange fires on remove', () => {
728
728
  const flow = createFlow({
729
- nodes: [{ id: "1", position: { x: 0, y: 0 }, data: {} }],
729
+ nodes: [{ id: '1', position: { x: 0, y: 0 }, data: {} }],
730
730
  })
731
731
  const fn = vi.fn()
732
732
  flow.onNodesChange(fn)
733
733
 
734
- flow.removeNode("1")
735
- expect(fn).toHaveBeenCalledWith([{ type: "remove", id: "1" }])
734
+ flow.removeNode('1')
735
+ expect(fn).toHaveBeenCalledWith([{ type: 'remove', id: '1' }])
736
736
  })
737
737
 
738
- it("listeners can be unsubscribed", () => {
738
+ it('listeners can be unsubscribed', () => {
739
739
  const flow = createFlow({
740
740
  nodes: [
741
- { id: "1", position: { x: 0, y: 0 }, data: {} },
742
- { id: "2", position: { x: 100, y: 0 }, data: {} },
741
+ { id: '1', position: { x: 0, y: 0 }, data: {} },
742
+ { id: '2', position: { x: 100, y: 0 }, data: {} },
743
743
  ],
744
744
  })
745
745
  const fn = vi.fn()
746
746
  const unsub = flow.onConnect(fn)
747
747
 
748
- flow.addEdge({ source: "1", target: "2" })
748
+ flow.addEdge({ source: '1', target: '2' })
749
749
  expect(fn).toHaveBeenCalledOnce()
750
750
 
751
751
  unsub()
752
- flow.removeEdge("e-1-2")
753
- flow.addEdge({ source: "1", target: "2" })
752
+ flow.removeEdge('e-1-2')
753
+ flow.addEdge({ source: '1', target: '2' })
754
754
  expect(fn).toHaveBeenCalledOnce() // not called again
755
755
  })
756
756
 
757
- it("dispose clears all listeners", () => {
757
+ it('dispose clears all listeners', () => {
758
758
  const flow = createFlow()
759
759
  const fn1 = vi.fn()
760
760
  const fn2 = vi.fn()
@@ -763,15 +763,15 @@ describe("createFlow", () => {
763
763
 
764
764
  flow.dispose()
765
765
 
766
- flow.addNode({ id: "1", position: { x: 0, y: 0 }, data: {} })
767
- flow.addEdge({ source: "1", target: "1" })
766
+ flow.addNode({ id: '1', position: { x: 0, y: 0 }, data: {} })
767
+ flow.addEdge({ source: '1', target: '1' })
768
768
  expect(fn1).not.toHaveBeenCalled()
769
769
  expect(fn2).not.toHaveBeenCalled()
770
770
  })
771
771
  })
772
772
 
773
- describe("batch", () => {
774
- it("batches multiple operations", () => {
773
+ describe('batch', () => {
774
+ it('batches multiple operations', () => {
775
775
  const flow = createFlow()
776
776
  const counts: number[] = []
777
777
 
@@ -780,9 +780,9 @@ describe("createFlow", () => {
780
780
  })
781
781
 
782
782
  flow.batch(() => {
783
- flow.addNode({ id: "1", position: { x: 0, y: 0 }, data: {} })
784
- flow.addNode({ id: "2", position: { x: 100, y: 0 }, data: {} })
785
- flow.addNode({ id: "3", position: { x: 200, y: 0 }, data: {} })
783
+ flow.addNode({ id: '1', position: { x: 0, y: 0 }, data: {} })
784
+ flow.addNode({ id: '2', position: { x: 100, y: 0 }, data: {} })
785
+ flow.addNode({ id: '3', position: { x: 200, y: 0 }, data: {} })
786
786
  })
787
787
 
788
788
  // Should batch into fewer updates (initial + batch result)
@@ -790,73 +790,73 @@ describe("createFlow", () => {
790
790
  })
791
791
  })
792
792
 
793
- describe("real-world patterns", () => {
794
- it("pipeline workflow", () => {
793
+ describe('real-world patterns', () => {
794
+ it('pipeline workflow', () => {
795
795
  const flow = createFlow({
796
796
  nodes: [
797
797
  {
798
- id: "fetch",
799
- type: "input",
798
+ id: 'fetch',
799
+ type: 'input',
800
800
  position: { x: 0, y: 0 },
801
- data: { label: "Fetch Data" },
801
+ data: { label: 'Fetch Data' },
802
802
  },
803
803
  {
804
- id: "transform",
805
- type: "process",
804
+ id: 'transform',
805
+ type: 'process',
806
806
  position: { x: 200, y: 0 },
807
- data: { label: "Transform" },
807
+ data: { label: 'Transform' },
808
808
  },
809
809
  {
810
- id: "validate",
811
- type: "process",
810
+ id: 'validate',
811
+ type: 'process',
812
812
  position: { x: 400, y: 0 },
813
- data: { label: "Validate" },
813
+ data: { label: 'Validate' },
814
814
  },
815
815
  {
816
- id: "store",
817
- type: "output",
816
+ id: 'store',
817
+ type: 'output',
818
818
  position: { x: 600, y: 0 },
819
- data: { label: "Store" },
819
+ data: { label: 'Store' },
820
820
  },
821
821
  ],
822
822
  edges: [
823
- { source: "fetch", target: "transform" },
824
- { source: "transform", target: "validate" },
825
- { source: "validate", target: "store" },
823
+ { source: 'fetch', target: 'transform' },
824
+ { source: 'transform', target: 'validate' },
825
+ { source: 'validate', target: 'store' },
826
826
  ],
827
827
  })
828
828
 
829
829
  expect(flow.nodes()).toHaveLength(4)
830
830
  expect(flow.edges()).toHaveLength(3)
831
- expect(flow.getOutgoers("fetch").map((n) => n.id)).toEqual(["transform"])
832
- expect(flow.getIncomers("store").map((n) => n.id)).toEqual(["validate"])
831
+ expect(flow.getOutgoers('fetch').map((n) => n.id)).toEqual(['transform'])
832
+ expect(flow.getIncomers('store').map((n) => n.id)).toEqual(['validate'])
833
833
  })
834
834
 
835
- it("branching workflow", () => {
835
+ it('branching workflow', () => {
836
836
  const flow = createFlow({
837
837
  nodes: [
838
- { id: "start", position: { x: 0, y: 100 }, data: {} },
839
- { id: "branch-a", position: { x: 200, y: 0 }, data: {} },
840
- { id: "branch-b", position: { x: 200, y: 200 }, data: {} },
841
- { id: "merge", position: { x: 400, y: 100 }, data: {} },
838
+ { id: 'start', position: { x: 0, y: 100 }, data: {} },
839
+ { id: 'branch-a', position: { x: 200, y: 0 }, data: {} },
840
+ { id: 'branch-b', position: { x: 200, y: 200 }, data: {} },
841
+ { id: 'merge', position: { x: 400, y: 100 }, data: {} },
842
842
  ],
843
843
  edges: [
844
- { source: "start", target: "branch-a" },
845
- { source: "start", target: "branch-b" },
846
- { source: "branch-a", target: "merge" },
847
- { source: "branch-b", target: "merge" },
844
+ { source: 'start', target: 'branch-a' },
845
+ { source: 'start', target: 'branch-b' },
846
+ { source: 'branch-a', target: 'merge' },
847
+ { source: 'branch-b', target: 'merge' },
848
848
  ],
849
849
  })
850
850
 
851
- expect(flow.getOutgoers("start")).toHaveLength(2)
852
- expect(flow.getIncomers("merge")).toHaveLength(2)
851
+ expect(flow.getOutgoers('start')).toHaveLength(2)
852
+ expect(flow.getIncomers('merge')).toHaveLength(2)
853
853
  })
854
854
  })
855
855
 
856
856
  // ─── Waypoints ─────────────────────────────────────────────────────────
857
857
 
858
- describe("edge waypoints", () => {
859
- it("getWaypointPath generates path through waypoints", () => {
858
+ describe('edge waypoints', () => {
859
+ it('getWaypointPath generates path through waypoints', () => {
860
860
  const result = getWaypointPath({
861
861
  sourceX: 0,
862
862
  sourceY: 0,
@@ -867,13 +867,13 @@ describe("createFlow", () => {
867
867
  { x: 200, y: -50 },
868
868
  ],
869
869
  })
870
- expect(result.path).toBe("M0,0 L100,50 L200,-50 L300,0")
870
+ expect(result.path).toBe('M0,0 L100,50 L200,-50 L300,0')
871
871
  // Label at middle waypoint (index 1 of 2)
872
872
  expect(result.labelX).toBe(200)
873
873
  expect(result.labelY).toBe(-50)
874
874
  })
875
875
 
876
- it("getWaypointPath with empty waypoints falls back to straight", () => {
876
+ it('getWaypointPath with empty waypoints falls back to straight', () => {
877
877
  const result = getWaypointPath({
878
878
  sourceX: 0,
879
879
  sourceY: 0,
@@ -881,33 +881,33 @@ describe("createFlow", () => {
881
881
  targetY: 100,
882
882
  waypoints: [],
883
883
  })
884
- expect(result.path).toBe("M0,0 L100,100")
884
+ expect(result.path).toBe('M0,0 L100,100')
885
885
  })
886
886
 
887
- it("addEdgeWaypoint adds a bend point", () => {
887
+ it('addEdgeWaypoint adds a bend point', () => {
888
888
  const flow = createFlow({
889
889
  nodes: [
890
- { id: "1", position: { x: 0, y: 0 }, data: {} },
891
- { id: "2", position: { x: 200, y: 0 }, data: {} },
890
+ { id: '1', position: { x: 0, y: 0 }, data: {} },
891
+ { id: '2', position: { x: 200, y: 0 }, data: {} },
892
892
  ],
893
- edges: [{ id: "e1", source: "1", target: "2" }],
893
+ edges: [{ id: 'e1', source: '1', target: '2' }],
894
894
  })
895
895
 
896
- flow.addEdgeWaypoint("e1", { x: 100, y: 50 })
897
- expect(flow.getEdge("e1")!.waypoints).toEqual([{ x: 100, y: 50 }])
896
+ flow.addEdgeWaypoint('e1', { x: 100, y: 50 })
897
+ expect(flow.getEdge('e1')!.waypoints).toEqual([{ x: 100, y: 50 }])
898
898
  })
899
899
 
900
- it("addEdgeWaypoint at specific index", () => {
900
+ it('addEdgeWaypoint at specific index', () => {
901
901
  const flow = createFlow({
902
902
  nodes: [
903
- { id: "1", position: { x: 0, y: 0 }, data: {} },
904
- { id: "2", position: { x: 200, y: 0 }, data: {} },
903
+ { id: '1', position: { x: 0, y: 0 }, data: {} },
904
+ { id: '2', position: { x: 200, y: 0 }, data: {} },
905
905
  ],
906
906
  edges: [
907
907
  {
908
- id: "e1",
909
- source: "1",
910
- target: "2",
908
+ id: 'e1',
909
+ source: '1',
910
+ target: '2',
911
911
  waypoints: [
912
912
  { x: 50, y: 0 },
913
913
  { x: 150, y: 0 },
@@ -916,108 +916,108 @@ describe("createFlow", () => {
916
916
  ],
917
917
  })
918
918
 
919
- flow.addEdgeWaypoint("e1", { x: 100, y: 50 }, 1)
920
- expect(flow.getEdge("e1")!.waypoints).toHaveLength(3)
921
- expect(flow.getEdge("e1")!.waypoints![1]).toEqual({ x: 100, y: 50 })
919
+ flow.addEdgeWaypoint('e1', { x: 100, y: 50 }, 1)
920
+ expect(flow.getEdge('e1')!.waypoints).toHaveLength(3)
921
+ expect(flow.getEdge('e1')!.waypoints![1]).toEqual({ x: 100, y: 50 })
922
922
  })
923
923
 
924
- it("removeEdgeWaypoint removes a bend point", () => {
924
+ it('removeEdgeWaypoint removes a bend point', () => {
925
925
  const flow = createFlow({
926
926
  nodes: [
927
- { id: "1", position: { x: 0, y: 0 }, data: {} },
928
- { id: "2", position: { x: 200, y: 0 }, data: {} },
927
+ { id: '1', position: { x: 0, y: 0 }, data: {} },
928
+ { id: '2', position: { x: 200, y: 0 }, data: {} },
929
929
  ],
930
930
  edges: [
931
931
  {
932
- id: "e1",
933
- source: "1",
934
- target: "2",
932
+ id: 'e1',
933
+ source: '1',
934
+ target: '2',
935
935
  waypoints: [{ x: 100, y: 50 }],
936
936
  },
937
937
  ],
938
938
  })
939
939
 
940
- flow.removeEdgeWaypoint("e1", 0)
941
- expect(flow.getEdge("e1")!.waypoints).toBeUndefined()
940
+ flow.removeEdgeWaypoint('e1', 0)
941
+ expect(flow.getEdge('e1')!.waypoints).toBeUndefined()
942
942
  })
943
943
 
944
- it("updateEdgeWaypoint moves a bend point", () => {
944
+ it('updateEdgeWaypoint moves a bend point', () => {
945
945
  const flow = createFlow({
946
946
  nodes: [
947
- { id: "1", position: { x: 0, y: 0 }, data: {} },
948
- { id: "2", position: { x: 200, y: 0 }, data: {} },
947
+ { id: '1', position: { x: 0, y: 0 }, data: {} },
948
+ { id: '2', position: { x: 200, y: 0 }, data: {} },
949
949
  ],
950
950
  edges: [
951
951
  {
952
- id: "e1",
953
- source: "1",
954
- target: "2",
952
+ id: 'e1',
953
+ source: '1',
954
+ target: '2',
955
955
  waypoints: [{ x: 100, y: 50 }],
956
956
  },
957
957
  ],
958
958
  })
959
959
 
960
- flow.updateEdgeWaypoint("e1", 0, { x: 100, y: -50 })
961
- expect(flow.getEdge("e1")!.waypoints![0]).toEqual({ x: 100, y: -50 })
960
+ flow.updateEdgeWaypoint('e1', 0, { x: 100, y: -50 })
961
+ expect(flow.getEdge('e1')!.waypoints![0]).toEqual({ x: 100, y: -50 })
962
962
  })
963
963
  })
964
964
 
965
965
  // ─── Search / Filter ───────────────────────────────────────────────────
966
966
 
967
- describe("search and filter", () => {
968
- it("findNodes with predicate", () => {
967
+ describe('search and filter', () => {
968
+ it('findNodes with predicate', () => {
969
969
  const flow = createFlow({
970
970
  nodes: [
971
- { id: "1", type: "input", position: { x: 0, y: 0 }, data: {} },
972
- { id: "2", type: "process", position: { x: 100, y: 0 }, data: {} },
973
- { id: "3", type: "process", position: { x: 200, y: 0 }, data: {} },
974
- { id: "4", type: "output", position: { x: 300, y: 0 }, data: {} },
971
+ { id: '1', type: 'input', position: { x: 0, y: 0 }, data: {} },
972
+ { id: '2', type: 'process', position: { x: 100, y: 0 }, data: {} },
973
+ { id: '3', type: 'process', position: { x: 200, y: 0 }, data: {} },
974
+ { id: '4', type: 'output', position: { x: 300, y: 0 }, data: {} },
975
975
  ],
976
976
  })
977
977
 
978
- expect(flow.findNodes((n) => n.type === "process")).toHaveLength(2)
979
- expect(flow.findNodes((n) => n.type === "input")).toHaveLength(1)
980
- expect(flow.findNodes((n) => n.type === "missing")).toHaveLength(0)
978
+ expect(flow.findNodes((n) => n.type === 'process')).toHaveLength(2)
979
+ expect(flow.findNodes((n) => n.type === 'input')).toHaveLength(1)
980
+ expect(flow.findNodes((n) => n.type === 'missing')).toHaveLength(0)
981
981
  })
982
982
 
983
- it("searchNodes by label", () => {
983
+ it('searchNodes by label', () => {
984
984
  const flow = createFlow({
985
985
  nodes: [
986
- { id: "1", position: { x: 0, y: 0 }, data: { label: "Fetch Data" } },
987
- { id: "2", position: { x: 100, y: 0 }, data: { label: "Transform" } },
986
+ { id: '1', position: { x: 0, y: 0 }, data: { label: 'Fetch Data' } },
987
+ { id: '2', position: { x: 100, y: 0 }, data: { label: 'Transform' } },
988
988
  {
989
- id: "3",
989
+ id: '3',
990
990
  position: { x: 200, y: 0 },
991
- data: { label: "Fetch Users" },
991
+ data: { label: 'Fetch Users' },
992
992
  },
993
993
  ],
994
994
  })
995
995
 
996
- expect(flow.searchNodes("fetch")).toHaveLength(2)
997
- expect(flow.searchNodes("transform")).toHaveLength(1)
998
- expect(flow.searchNodes("FETCH")).toHaveLength(2) // case-insensitive
999
- expect(flow.searchNodes("missing")).toHaveLength(0)
996
+ expect(flow.searchNodes('fetch')).toHaveLength(2)
997
+ expect(flow.searchNodes('transform')).toHaveLength(1)
998
+ expect(flow.searchNodes('FETCH')).toHaveLength(2) // case-insensitive
999
+ expect(flow.searchNodes('missing')).toHaveLength(0)
1000
1000
  })
1001
1001
 
1002
- it("searchNodes falls back to node id", () => {
1002
+ it('searchNodes falls back to node id', () => {
1003
1003
  const flow = createFlow({
1004
- nodes: [{ id: "api-gateway", position: { x: 0, y: 0 }, data: {} }],
1004
+ nodes: [{ id: 'api-gateway', position: { x: 0, y: 0 }, data: {} }],
1005
1005
  })
1006
1006
 
1007
- expect(flow.searchNodes("gateway")).toHaveLength(1)
1007
+ expect(flow.searchNodes('gateway')).toHaveLength(1)
1008
1008
  })
1009
1009
  })
1010
1010
 
1011
1011
  // ─── Export / Import ───────────────────────────────────────────────────
1012
1012
 
1013
- describe("toJSON / fromJSON", () => {
1014
- it("exports and imports flow state", () => {
1013
+ describe('toJSON / fromJSON', () => {
1014
+ it('exports and imports flow state', () => {
1015
1015
  const flow = createFlow({
1016
1016
  nodes: [
1017
- { id: "1", position: { x: 0, y: 0 }, data: { label: "A" } },
1018
- { id: "2", position: { x: 200, y: 0 }, data: { label: "B" } },
1017
+ { id: '1', position: { x: 0, y: 0 }, data: { label: 'A' } },
1018
+ { id: '2', position: { x: 200, y: 0 }, data: { label: 'B' } },
1019
1019
  ],
1020
- edges: [{ source: "1", target: "2" }],
1020
+ edges: [{ source: '1', target: '2' }],
1021
1021
  })
1022
1022
 
1023
1023
  flow.zoomTo(1.5)
@@ -1036,12 +1036,12 @@ describe("createFlow", () => {
1036
1036
  expect(flow2.zoom()).toBe(1.5)
1037
1037
  })
1038
1038
 
1039
- it("fromJSON without viewport keeps current", () => {
1039
+ it('fromJSON without viewport keeps current', () => {
1040
1040
  const flow = createFlow()
1041
1041
  flow.zoomTo(2)
1042
1042
 
1043
1043
  flow.fromJSON({
1044
- nodes: [{ id: "1", position: { x: 0, y: 0 }, data: {} }],
1044
+ nodes: [{ id: '1', position: { x: 0, y: 0 }, data: {} }],
1045
1045
  edges: [],
1046
1046
  })
1047
1047
 
@@ -1052,26 +1052,26 @@ describe("createFlow", () => {
1052
1052
 
1053
1053
  // ─── Collision detection ───────────────────────────────────────────────
1054
1054
 
1055
- describe("collision detection", () => {
1056
- it("getOverlappingNodes detects overlap", () => {
1055
+ describe('collision detection', () => {
1056
+ it('getOverlappingNodes detects overlap', () => {
1057
1057
  const flow = createFlow({
1058
1058
  nodes: [
1059
1059
  {
1060
- id: "1",
1060
+ id: '1',
1061
1061
  position: { x: 0, y: 0 },
1062
1062
  width: 100,
1063
1063
  height: 50,
1064
1064
  data: {},
1065
1065
  },
1066
1066
  {
1067
- id: "2",
1067
+ id: '2',
1068
1068
  position: { x: 50, y: 25 },
1069
1069
  width: 100,
1070
1070
  height: 50,
1071
1071
  data: {},
1072
1072
  },
1073
1073
  {
1074
- id: "3",
1074
+ id: '3',
1075
1075
  position: { x: 500, y: 500 },
1076
1076
  width: 100,
1077
1077
  height: 50,
@@ -1080,116 +1080,116 @@ describe("createFlow", () => {
1080
1080
  ],
1081
1081
  })
1082
1082
 
1083
- expect(flow.getOverlappingNodes("1")).toHaveLength(1)
1084
- expect(flow.getOverlappingNodes("1")[0]!.id).toBe("2")
1085
- expect(flow.getOverlappingNodes("3")).toHaveLength(0)
1083
+ expect(flow.getOverlappingNodes('1')).toHaveLength(1)
1084
+ expect(flow.getOverlappingNodes('1')[0]!.id).toBe('2')
1085
+ expect(flow.getOverlappingNodes('3')).toHaveLength(0)
1086
1086
  })
1087
1087
  })
1088
1088
 
1089
1089
  // ─── Proximity connect ─────────────────────────────────────────────────
1090
1090
 
1091
- describe("proximity connect", () => {
1092
- it("finds nearest unconnected node", () => {
1091
+ describe('proximity connect', () => {
1092
+ it('finds nearest unconnected node', () => {
1093
1093
  const flow = createFlow({
1094
1094
  nodes: [
1095
- { id: "1", position: { x: 0, y: 0 }, data: {} },
1096
- { id: "2", position: { x: 100, y: 0 }, data: {} },
1097
- { id: "3", position: { x: 500, y: 500 }, data: {} },
1095
+ { id: '1', position: { x: 0, y: 0 }, data: {} },
1096
+ { id: '2', position: { x: 100, y: 0 }, data: {} },
1097
+ { id: '3', position: { x: 500, y: 500 }, data: {} },
1098
1098
  ],
1099
1099
  })
1100
1100
 
1101
- const conn = flow.getProximityConnection("1", 200)
1101
+ const conn = flow.getProximityConnection('1', 200)
1102
1102
  expect(conn).not.toBeNull()
1103
- expect(conn!.target).toBe("2")
1103
+ expect(conn!.target).toBe('2')
1104
1104
  })
1105
1105
 
1106
- it("returns null when no node is close", () => {
1106
+ it('returns null when no node is close', () => {
1107
1107
  const flow = createFlow({
1108
1108
  nodes: [
1109
- { id: "1", position: { x: 0, y: 0 }, data: {} },
1110
- { id: "2", position: { x: 500, y: 500 }, data: {} },
1109
+ { id: '1', position: { x: 0, y: 0 }, data: {} },
1110
+ { id: '2', position: { x: 500, y: 500 }, data: {} },
1111
1111
  ],
1112
1112
  })
1113
1113
 
1114
- expect(flow.getProximityConnection("1", 50)).toBeNull()
1114
+ expect(flow.getProximityConnection('1', 50)).toBeNull()
1115
1115
  })
1116
1116
 
1117
- it("skips already connected nodes", () => {
1117
+ it('skips already connected nodes', () => {
1118
1118
  const flow = createFlow({
1119
1119
  nodes: [
1120
- { id: "1", position: { x: 0, y: 0 }, data: {} },
1121
- { id: "2", position: { x: 100, y: 0 }, data: {} },
1120
+ { id: '1', position: { x: 0, y: 0 }, data: {} },
1121
+ { id: '2', position: { x: 100, y: 0 }, data: {} },
1122
1122
  ],
1123
- edges: [{ source: "1", target: "2" }],
1123
+ edges: [{ source: '1', target: '2' }],
1124
1124
  })
1125
1125
 
1126
- expect(flow.getProximityConnection("1", 200)).toBeNull()
1126
+ expect(flow.getProximityConnection('1', 200)).toBeNull()
1127
1127
  })
1128
1128
  })
1129
1129
 
1130
1130
  // ─── Node extent ───────────────────────────────────────────────────────
1131
1131
 
1132
- describe("node extent", () => {
1133
- it("clamps node position to extent", () => {
1132
+ describe('node extent', () => {
1133
+ it('clamps node position to extent', () => {
1134
1134
  const flow = createFlow({
1135
- nodes: [{ id: "1", position: { x: 0, y: 0 }, data: {} }],
1135
+ nodes: [{ id: '1', position: { x: 0, y: 0 }, data: {} }],
1136
1136
  nodeExtent: [
1137
1137
  [0, 0],
1138
1138
  [500, 500],
1139
1139
  ],
1140
1140
  })
1141
1141
 
1142
- flow.updateNodePosition("1", { x: -100, y: -100 })
1143
- expect(flow.getNode("1")!.position.x).toBe(0)
1144
- expect(flow.getNode("1")!.position.y).toBe(0)
1142
+ flow.updateNodePosition('1', { x: -100, y: -100 })
1143
+ expect(flow.getNode('1')!.position.x).toBe(0)
1144
+ expect(flow.getNode('1')!.position.y).toBe(0)
1145
1145
 
1146
- flow.updateNodePosition("1", { x: 600, y: 600 })
1147
- expect(flow.getNode("1")!.position.x).toBeLessThanOrEqual(500)
1148
- expect(flow.getNode("1")!.position.y).toBeLessThanOrEqual(500)
1146
+ flow.updateNodePosition('1', { x: 600, y: 600 })
1147
+ expect(flow.getNode('1')!.position.x).toBeLessThanOrEqual(500)
1148
+ expect(flow.getNode('1')!.position.y).toBeLessThanOrEqual(500)
1149
1149
  })
1150
1150
 
1151
- it("setNodeExtent changes boundaries dynamically", () => {
1151
+ it('setNodeExtent changes boundaries dynamically', () => {
1152
1152
  const flow = createFlow({
1153
- nodes: [{ id: "1", position: { x: 0, y: 0 }, data: {} }],
1153
+ nodes: [{ id: '1', position: { x: 0, y: 0 }, data: {} }],
1154
1154
  })
1155
1155
 
1156
1156
  // No extent — no clamping
1157
- flow.updateNodePosition("1", { x: -999, y: -999 })
1158
- expect(flow.getNode("1")!.position.x).toBe(-999)
1157
+ flow.updateNodePosition('1', { x: -999, y: -999 })
1158
+ expect(flow.getNode('1')!.position.x).toBe(-999)
1159
1159
 
1160
1160
  // Set extent — large enough for default node size (150x40)
1161
1161
  flow.setNodeExtent([
1162
1162
  [0, 0],
1163
1163
  [500, 500],
1164
1164
  ])
1165
- flow.updateNodePosition("1", { x: -999, y: -999 })
1166
- expect(flow.getNode("1")!.position.x).toBe(0)
1165
+ flow.updateNodePosition('1', { x: -999, y: -999 })
1166
+ expect(flow.getNode('1')!.position.x).toBe(0)
1167
1167
  })
1168
1168
  })
1169
1169
 
1170
1170
  // ─── Undo / Redo ───────────────────────────────────────────────────────
1171
1171
 
1172
- describe("undo / redo", () => {
1173
- it("undo restores previous state", () => {
1172
+ describe('undo / redo', () => {
1173
+ it('undo restores previous state', () => {
1174
1174
  const flow = createFlow({
1175
- nodes: [{ id: "1", position: { x: 0, y: 0 }, data: {} }],
1175
+ nodes: [{ id: '1', position: { x: 0, y: 0 }, data: {} }],
1176
1176
  })
1177
1177
 
1178
1178
  flow.pushHistory()
1179
- flow.addNode({ id: "2", position: { x: 100, y: 0 }, data: {} })
1179
+ flow.addNode({ id: '2', position: { x: 100, y: 0 }, data: {} })
1180
1180
  expect(flow.nodes()).toHaveLength(2)
1181
1181
 
1182
1182
  flow.undo()
1183
1183
  expect(flow.nodes()).toHaveLength(1)
1184
1184
  })
1185
1185
 
1186
- it("redo restores undone state", () => {
1186
+ it('redo restores undone state', () => {
1187
1187
  const flow = createFlow({
1188
- nodes: [{ id: "1", position: { x: 0, y: 0 }, data: {} }],
1188
+ nodes: [{ id: '1', position: { x: 0, y: 0 }, data: {} }],
1189
1189
  })
1190
1190
 
1191
1191
  flow.pushHistory()
1192
- flow.addNode({ id: "2", position: { x: 100, y: 0 }, data: {} })
1192
+ flow.addNode({ id: '2', position: { x: 100, y: 0 }, data: {} })
1193
1193
 
1194
1194
  flow.undo()
1195
1195
  expect(flow.nodes()).toHaveLength(1)
@@ -1201,18 +1201,18 @@ describe("createFlow", () => {
1201
1201
 
1202
1202
  // ─── Copy / Paste ──────────────────────────────────────────────────────
1203
1203
 
1204
- describe("copy / paste", () => {
1205
- it("copies and pastes selected nodes", () => {
1204
+ describe('copy / paste', () => {
1205
+ it('copies and pastes selected nodes', () => {
1206
1206
  const flow = createFlow({
1207
1207
  nodes: [
1208
- { id: "1", position: { x: 0, y: 0 }, data: { label: "A" } },
1209
- { id: "2", position: { x: 200, y: 0 }, data: { label: "B" } },
1208
+ { id: '1', position: { x: 0, y: 0 }, data: { label: 'A' } },
1209
+ { id: '2', position: { x: 200, y: 0 }, data: { label: 'B' } },
1210
1210
  ],
1211
- edges: [{ source: "1", target: "2" }],
1211
+ edges: [{ source: '1', target: '2' }],
1212
1212
  })
1213
1213
 
1214
- flow.selectNode("1")
1215
- flow.selectNode("2", true)
1214
+ flow.selectNode('1')
1215
+ flow.selectNode('2', true)
1216
1216
  flow.copySelected()
1217
1217
  flow.paste()
1218
1218
 
@@ -1222,9 +1222,9 @@ describe("createFlow", () => {
1222
1222
  expect(new Set(ids).size).toBe(4)
1223
1223
  })
1224
1224
 
1225
- it("paste without copy does nothing", () => {
1225
+ it('paste without copy does nothing', () => {
1226
1226
  const flow = createFlow({
1227
- nodes: [{ id: "1", position: { x: 0, y: 0 }, data: {} }],
1227
+ nodes: [{ id: '1', position: { x: 0, y: 0 }, data: {} }],
1228
1228
  })
1229
1229
 
1230
1230
  flow.paste()