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