@pyreon/flow 0.11.5 → 0.11.7
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/README.md +10 -10
- package/lib/index.js.map +1 -1
- package/lib/types/index.d.ts +13 -13
- package/package.json +16 -16
- package/src/components/background.tsx +8 -8
- package/src/components/controls.tsx +9 -9
- package/src/components/flow-component.tsx +46 -46
- package/src/components/handle.tsx +8 -8
- package/src/components/minimap.tsx +5 -5
- package/src/components/node-resizer.tsx +25 -25
- package/src/components/node-toolbar.tsx +12 -12
- package/src/components/panel.tsx +9 -9
- package/src/edges.ts +6 -6
- package/src/flow.ts +11 -11
- package/src/index.ts +17 -17
- package/src/layout.ts +23 -23
- package/src/tests/flow-advanced.test.ts +190 -190
- package/src/tests/flow.test.ts +417 -417
- package/src/types.ts +24 -24
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import { describe, expect, it } from
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import {
|
|
3
3
|
getBezierPath,
|
|
4
4
|
getSmartHandlePositions,
|
|
5
5
|
getSmoothStepPath,
|
|
6
6
|
getWaypointPath,
|
|
7
|
-
} from
|
|
8
|
-
import { createFlow } from
|
|
9
|
-
import { Position } from
|
|
7
|
+
} from '../edges'
|
|
8
|
+
import { createFlow } from '../flow'
|
|
9
|
+
import { Position } from '../types'
|
|
10
10
|
|
|
11
11
|
// ─── Edge paths — additional coverage ────────────────────────────────────────
|
|
12
12
|
|
|
13
|
-
describe(
|
|
14
|
-
it(
|
|
13
|
+
describe('edge paths — additional branches', () => {
|
|
14
|
+
it('getBezierPath with Top/Left source positions', () => {
|
|
15
15
|
const top = getBezierPath({
|
|
16
16
|
sourceX: 100,
|
|
17
17
|
sourceY: 100,
|
|
@@ -33,7 +33,7 @@ describe("edge paths — additional branches", () => {
|
|
|
33
33
|
expect(left.path).toMatch(/^M/)
|
|
34
34
|
})
|
|
35
35
|
|
|
36
|
-
it(
|
|
36
|
+
it('getBezierPath with all target positions', () => {
|
|
37
37
|
for (const pos of [Position.Top, Position.Right, Position.Bottom, Position.Left]) {
|
|
38
38
|
const result = getBezierPath({
|
|
39
39
|
sourceX: 0,
|
|
@@ -47,7 +47,7 @@ describe("edge paths — additional branches", () => {
|
|
|
47
47
|
}
|
|
48
48
|
})
|
|
49
49
|
|
|
50
|
-
it(
|
|
50
|
+
it('getSmoothStepPath with horizontal→horizontal positions', () => {
|
|
51
51
|
const result = getSmoothStepPath({
|
|
52
52
|
sourceX: 0,
|
|
53
53
|
sourceY: 0,
|
|
@@ -59,7 +59,7 @@ describe("edge paths — additional branches", () => {
|
|
|
59
59
|
expect(result.path).toMatch(/^M/)
|
|
60
60
|
})
|
|
61
61
|
|
|
62
|
-
it(
|
|
62
|
+
it('getSmoothStepPath with vertical→vertical positions', () => {
|
|
63
63
|
const result = getSmoothStepPath({
|
|
64
64
|
sourceX: 0,
|
|
65
65
|
sourceY: 0,
|
|
@@ -71,7 +71,7 @@ describe("edge paths — additional branches", () => {
|
|
|
71
71
|
expect(result.path).toMatch(/^M/)
|
|
72
72
|
})
|
|
73
73
|
|
|
74
|
-
it(
|
|
74
|
+
it('getSmoothStepPath with vertical→horizontal', () => {
|
|
75
75
|
const result = getSmoothStepPath({
|
|
76
76
|
sourceX: 0,
|
|
77
77
|
sourceY: 0,
|
|
@@ -83,7 +83,7 @@ describe("edge paths — additional branches", () => {
|
|
|
83
83
|
expect(result.path).toMatch(/^M/)
|
|
84
84
|
})
|
|
85
85
|
|
|
86
|
-
it(
|
|
86
|
+
it('getSmoothStepPath with Left source', () => {
|
|
87
87
|
const result = getSmoothStepPath({
|
|
88
88
|
sourceX: 200,
|
|
89
89
|
sourceY: 0,
|
|
@@ -95,7 +95,7 @@ describe("edge paths — additional branches", () => {
|
|
|
95
95
|
expect(result.path).toMatch(/^M/)
|
|
96
96
|
})
|
|
97
97
|
|
|
98
|
-
it(
|
|
98
|
+
it('getSmoothStepPath with Bottom target', () => {
|
|
99
99
|
const result = getSmoothStepPath({
|
|
100
100
|
sourceX: 0,
|
|
101
101
|
sourceY: 0,
|
|
@@ -107,45 +107,45 @@ describe("edge paths — additional branches", () => {
|
|
|
107
107
|
expect(result.path).toMatch(/^M/)
|
|
108
108
|
})
|
|
109
109
|
|
|
110
|
-
it(
|
|
110
|
+
it('getSmartHandlePositions with nodes at various positions', () => {
|
|
111
111
|
// Target to the left
|
|
112
112
|
const leftward = getSmartHandlePositions(
|
|
113
|
-
{ id:
|
|
114
|
-
{ id:
|
|
113
|
+
{ id: '1', position: { x: 200, y: 0 }, data: {} },
|
|
114
|
+
{ id: '2', position: { x: 0, y: 0 }, data: {} },
|
|
115
115
|
)
|
|
116
116
|
expect(leftward.sourcePosition).toBe(Position.Left)
|
|
117
117
|
expect(leftward.targetPosition).toBe(Position.Right)
|
|
118
118
|
|
|
119
119
|
// Target below
|
|
120
120
|
const downward = getSmartHandlePositions(
|
|
121
|
-
{ id:
|
|
122
|
-
{ id:
|
|
121
|
+
{ id: '1', position: { x: 0, y: 0 }, data: {} },
|
|
122
|
+
{ id: '2', position: { x: 0, y: 300 }, data: {} },
|
|
123
123
|
)
|
|
124
124
|
expect(downward.sourcePosition).toBe(Position.Bottom)
|
|
125
125
|
expect(downward.targetPosition).toBe(Position.Top)
|
|
126
126
|
|
|
127
127
|
// Target above
|
|
128
128
|
const upward = getSmartHandlePositions(
|
|
129
|
-
{ id:
|
|
130
|
-
{ id:
|
|
129
|
+
{ id: '1', position: { x: 0, y: 300 }, data: {} },
|
|
130
|
+
{ id: '2', position: { x: 0, y: 0 }, data: {} },
|
|
131
131
|
)
|
|
132
132
|
expect(upward.sourcePosition).toBe(Position.Top)
|
|
133
133
|
expect(upward.targetPosition).toBe(Position.Bottom)
|
|
134
134
|
})
|
|
135
135
|
|
|
136
|
-
it(
|
|
136
|
+
it('getSmartHandlePositions with configured handles', () => {
|
|
137
137
|
const result = getSmartHandlePositions(
|
|
138
138
|
{
|
|
139
|
-
id:
|
|
139
|
+
id: '1',
|
|
140
140
|
position: { x: 0, y: 0 },
|
|
141
141
|
data: {},
|
|
142
|
-
sourceHandles: [{ type:
|
|
142
|
+
sourceHandles: [{ type: 'source', position: Position.Bottom, id: 'out' }],
|
|
143
143
|
},
|
|
144
144
|
{
|
|
145
|
-
id:
|
|
145
|
+
id: '2',
|
|
146
146
|
position: { x: 200, y: 200 },
|
|
147
147
|
data: {},
|
|
148
|
-
targetHandles: [{ type:
|
|
148
|
+
targetHandles: [{ type: 'target', position: Position.Top, id: 'in' }],
|
|
149
149
|
},
|
|
150
150
|
)
|
|
151
151
|
// Should use configured handle positions
|
|
@@ -153,7 +153,7 @@ describe("edge paths — additional branches", () => {
|
|
|
153
153
|
expect(result.targetPosition).toBe(Position.Top)
|
|
154
154
|
})
|
|
155
155
|
|
|
156
|
-
it(
|
|
156
|
+
it('getWaypointPath with single waypoint', () => {
|
|
157
157
|
const result = getWaypointPath({
|
|
158
158
|
sourceX: 0,
|
|
159
159
|
sourceY: 0,
|
|
@@ -161,7 +161,7 @@ describe("edge paths — additional branches", () => {
|
|
|
161
161
|
targetY: 0,
|
|
162
162
|
waypoints: [{ x: 100, y: 50 }],
|
|
163
163
|
})
|
|
164
|
-
expect(result.path).toBe(
|
|
164
|
+
expect(result.path).toBe('M0,0 L100,50 L200,0')
|
|
165
165
|
expect(result.labelX).toBe(100)
|
|
166
166
|
expect(result.labelY).toBe(50)
|
|
167
167
|
})
|
|
@@ -169,20 +169,20 @@ describe("edge paths — additional branches", () => {
|
|
|
169
169
|
|
|
170
170
|
// ─── Flow — advanced operations ──────────────────────────────────────────────
|
|
171
171
|
|
|
172
|
-
describe(
|
|
173
|
-
describe(
|
|
174
|
-
it(
|
|
172
|
+
describe('createFlow — advanced', () => {
|
|
173
|
+
describe('resolveCollisions', () => {
|
|
174
|
+
it('pushes overlapping nodes apart', () => {
|
|
175
175
|
const flow = createFlow({
|
|
176
176
|
nodes: [
|
|
177
177
|
{
|
|
178
|
-
id:
|
|
178
|
+
id: '1',
|
|
179
179
|
position: { x: 0, y: 0 },
|
|
180
180
|
width: 100,
|
|
181
181
|
height: 50,
|
|
182
182
|
data: {},
|
|
183
183
|
},
|
|
184
184
|
{
|
|
185
|
-
id:
|
|
185
|
+
id: '2',
|
|
186
186
|
position: { x: 50, y: 0 },
|
|
187
187
|
width: 100,
|
|
188
188
|
height: 50,
|
|
@@ -191,25 +191,25 @@ describe("createFlow — advanced", () => {
|
|
|
191
191
|
],
|
|
192
192
|
})
|
|
193
193
|
|
|
194
|
-
flow.resolveCollisions(
|
|
194
|
+
flow.resolveCollisions('1')
|
|
195
195
|
// Node 2 should have moved (either X or Y)
|
|
196
|
-
const node2 = flow.getNode(
|
|
196
|
+
const node2 = flow.getNode('2')!
|
|
197
197
|
const moved = node2.position.x !== 50 || node2.position.y !== 0
|
|
198
198
|
expect(moved).toBe(true)
|
|
199
199
|
})
|
|
200
200
|
|
|
201
|
-
it(
|
|
201
|
+
it('does nothing when no overlaps', () => {
|
|
202
202
|
const flow = createFlow({
|
|
203
203
|
nodes: [
|
|
204
204
|
{
|
|
205
|
-
id:
|
|
205
|
+
id: '1',
|
|
206
206
|
position: { x: 0, y: 0 },
|
|
207
207
|
width: 100,
|
|
208
208
|
height: 50,
|
|
209
209
|
data: {},
|
|
210
210
|
},
|
|
211
211
|
{
|
|
212
|
-
id:
|
|
212
|
+
id: '2',
|
|
213
213
|
position: { x: 500, y: 500 },
|
|
214
214
|
width: 100,
|
|
215
215
|
height: 50,
|
|
@@ -218,22 +218,22 @@ describe("createFlow — advanced", () => {
|
|
|
218
218
|
],
|
|
219
219
|
})
|
|
220
220
|
|
|
221
|
-
flow.resolveCollisions(
|
|
222
|
-
expect(flow.getNode(
|
|
221
|
+
flow.resolveCollisions('1')
|
|
222
|
+
expect(flow.getNode('2')!.position).toEqual({ x: 500, y: 500 })
|
|
223
223
|
})
|
|
224
224
|
|
|
225
|
-
it(
|
|
225
|
+
it('resolves vertical overlaps', () => {
|
|
226
226
|
const flow = createFlow({
|
|
227
227
|
nodes: [
|
|
228
228
|
{
|
|
229
|
-
id:
|
|
229
|
+
id: '1',
|
|
230
230
|
position: { x: 0, y: 0 },
|
|
231
231
|
width: 200,
|
|
232
232
|
height: 50,
|
|
233
233
|
data: {},
|
|
234
234
|
},
|
|
235
235
|
{
|
|
236
|
-
id:
|
|
236
|
+
id: '2',
|
|
237
237
|
position: { x: 0, y: 30 },
|
|
238
238
|
width: 200,
|
|
239
239
|
height: 50,
|
|
@@ -242,121 +242,121 @@ describe("createFlow — advanced", () => {
|
|
|
242
242
|
],
|
|
243
243
|
})
|
|
244
244
|
|
|
245
|
-
flow.resolveCollisions(
|
|
246
|
-
const node2 = flow.getNode(
|
|
245
|
+
flow.resolveCollisions('1')
|
|
246
|
+
const node2 = flow.getNode('2')!
|
|
247
247
|
// Should push vertically since horizontal overlap is larger
|
|
248
248
|
expect(node2.position.y).not.toBe(30)
|
|
249
249
|
})
|
|
250
250
|
})
|
|
251
251
|
|
|
252
|
-
describe(
|
|
253
|
-
it(
|
|
252
|
+
describe('getChildNodes / getAbsolutePosition', () => {
|
|
253
|
+
it('returns child nodes of a group', () => {
|
|
254
254
|
const flow = createFlow({
|
|
255
255
|
nodes: [
|
|
256
256
|
{
|
|
257
|
-
id:
|
|
257
|
+
id: 'group',
|
|
258
258
|
position: { x: 0, y: 0 },
|
|
259
259
|
group: true,
|
|
260
260
|
data: {},
|
|
261
261
|
},
|
|
262
262
|
{
|
|
263
|
-
id:
|
|
263
|
+
id: 'child1',
|
|
264
264
|
position: { x: 10, y: 10 },
|
|
265
|
-
parentId:
|
|
265
|
+
parentId: 'group',
|
|
266
266
|
data: {},
|
|
267
267
|
},
|
|
268
268
|
{
|
|
269
|
-
id:
|
|
269
|
+
id: 'child2',
|
|
270
270
|
position: { x: 20, y: 20 },
|
|
271
|
-
parentId:
|
|
271
|
+
parentId: 'group',
|
|
272
272
|
data: {},
|
|
273
273
|
},
|
|
274
|
-
{ id:
|
|
274
|
+
{ id: 'other', position: { x: 100, y: 100 }, data: {} },
|
|
275
275
|
],
|
|
276
276
|
})
|
|
277
277
|
|
|
278
|
-
const children = flow.getChildNodes(
|
|
278
|
+
const children = flow.getChildNodes('group')
|
|
279
279
|
expect(children).toHaveLength(2)
|
|
280
|
-
expect(children.map((n) => n.id)).toEqual(expect.arrayContaining([
|
|
280
|
+
expect(children.map((n) => n.id)).toEqual(expect.arrayContaining(['child1', 'child2']))
|
|
281
281
|
})
|
|
282
282
|
|
|
283
|
-
it(
|
|
283
|
+
it('getAbsolutePosition accounts for parent offset', () => {
|
|
284
284
|
const flow = createFlow({
|
|
285
285
|
nodes: [
|
|
286
286
|
{
|
|
287
|
-
id:
|
|
287
|
+
id: 'parent',
|
|
288
288
|
position: { x: 100, y: 200 },
|
|
289
289
|
data: {},
|
|
290
290
|
},
|
|
291
291
|
{
|
|
292
|
-
id:
|
|
292
|
+
id: 'child',
|
|
293
293
|
position: { x: 10, y: 20 },
|
|
294
|
-
parentId:
|
|
294
|
+
parentId: 'parent',
|
|
295
295
|
data: {},
|
|
296
296
|
},
|
|
297
297
|
],
|
|
298
298
|
})
|
|
299
299
|
|
|
300
|
-
const abs = flow.getAbsolutePosition(
|
|
300
|
+
const abs = flow.getAbsolutePosition('child')
|
|
301
301
|
expect(abs).toEqual({ x: 110, y: 220 })
|
|
302
302
|
})
|
|
303
303
|
|
|
304
|
-
it(
|
|
304
|
+
it('getAbsolutePosition for root node returns position', () => {
|
|
305
305
|
const flow = createFlow({
|
|
306
|
-
nodes: [{ id:
|
|
306
|
+
nodes: [{ id: '1', position: { x: 50, y: 75 }, data: {} }],
|
|
307
307
|
})
|
|
308
308
|
|
|
309
|
-
expect(flow.getAbsolutePosition(
|
|
309
|
+
expect(flow.getAbsolutePosition('1')).toEqual({ x: 50, y: 75 })
|
|
310
310
|
})
|
|
311
311
|
|
|
312
|
-
it(
|
|
312
|
+
it('getAbsolutePosition for missing node returns 0,0', () => {
|
|
313
313
|
const flow = createFlow()
|
|
314
|
-
expect(flow.getAbsolutePosition(
|
|
314
|
+
expect(flow.getAbsolutePosition('missing')).toEqual({ x: 0, y: 0 })
|
|
315
315
|
})
|
|
316
316
|
})
|
|
317
317
|
|
|
318
|
-
describe(
|
|
319
|
-
it(
|
|
318
|
+
describe('moveSelectedNodes', () => {
|
|
319
|
+
it('moves all selected nodes by delta', () => {
|
|
320
320
|
const flow = createFlow({
|
|
321
321
|
nodes: [
|
|
322
|
-
{ id:
|
|
323
|
-
{ id:
|
|
324
|
-
{ id:
|
|
322
|
+
{ id: '1', position: { x: 0, y: 0 }, data: {} },
|
|
323
|
+
{ id: '2', position: { x: 100, y: 0 }, data: {} },
|
|
324
|
+
{ id: '3', position: { x: 200, y: 0 }, data: {} },
|
|
325
325
|
],
|
|
326
326
|
})
|
|
327
327
|
|
|
328
|
-
flow.selectNode(
|
|
329
|
-
flow.selectNode(
|
|
328
|
+
flow.selectNode('1')
|
|
329
|
+
flow.selectNode('2', true)
|
|
330
330
|
flow.moveSelectedNodes(50, 25)
|
|
331
331
|
|
|
332
|
-
expect(flow.getNode(
|
|
333
|
-
expect(flow.getNode(
|
|
334
|
-
expect(flow.getNode(
|
|
332
|
+
expect(flow.getNode('1')!.position).toEqual({ x: 50, y: 25 })
|
|
333
|
+
expect(flow.getNode('2')!.position).toEqual({ x: 150, y: 25 })
|
|
334
|
+
expect(flow.getNode('3')!.position).toEqual({ x: 200, y: 0 }) // not selected
|
|
335
335
|
})
|
|
336
336
|
|
|
337
|
-
it(
|
|
337
|
+
it('does nothing with no selection', () => {
|
|
338
338
|
const flow = createFlow({
|
|
339
|
-
nodes: [{ id:
|
|
339
|
+
nodes: [{ id: '1', position: { x: 0, y: 0 }, data: {} }],
|
|
340
340
|
})
|
|
341
341
|
|
|
342
342
|
flow.moveSelectedNodes(100, 100)
|
|
343
|
-
expect(flow.getNode(
|
|
343
|
+
expect(flow.getNode('1')!.position).toEqual({ x: 0, y: 0 })
|
|
344
344
|
})
|
|
345
345
|
})
|
|
346
346
|
|
|
347
|
-
describe(
|
|
348
|
-
it(
|
|
347
|
+
describe('getSnapLines', () => {
|
|
348
|
+
it('snaps to aligned nodes', () => {
|
|
349
349
|
const flow = createFlow({
|
|
350
350
|
nodes: [
|
|
351
351
|
{
|
|
352
|
-
id:
|
|
352
|
+
id: '1',
|
|
353
353
|
position: { x: 100, y: 100 },
|
|
354
354
|
width: 100,
|
|
355
355
|
height: 50,
|
|
356
356
|
data: {},
|
|
357
357
|
},
|
|
358
358
|
{
|
|
359
|
-
id:
|
|
359
|
+
id: '2',
|
|
360
360
|
position: { x: 300, y: 200 },
|
|
361
361
|
width: 100,
|
|
362
362
|
height: 50,
|
|
@@ -366,23 +366,23 @@ describe("createFlow — advanced", () => {
|
|
|
366
366
|
})
|
|
367
367
|
|
|
368
368
|
// Move node 2 close to node 1's center X
|
|
369
|
-
const snap = flow.getSnapLines(
|
|
369
|
+
const snap = flow.getSnapLines('2', { x: 98, y: 200 })
|
|
370
370
|
expect(snap.x).not.toBeNull()
|
|
371
371
|
expect(snap.snappedPosition.x).not.toBe(98)
|
|
372
372
|
})
|
|
373
373
|
|
|
374
|
-
it(
|
|
374
|
+
it('returns null lines when no alignment', () => {
|
|
375
375
|
const flow = createFlow({
|
|
376
376
|
nodes: [
|
|
377
377
|
{
|
|
378
|
-
id:
|
|
378
|
+
id: '1',
|
|
379
379
|
position: { x: 0, y: 0 },
|
|
380
380
|
width: 100,
|
|
381
381
|
height: 50,
|
|
382
382
|
data: {},
|
|
383
383
|
},
|
|
384
384
|
{
|
|
385
|
-
id:
|
|
385
|
+
id: '2',
|
|
386
386
|
position: { x: 500, y: 500 },
|
|
387
387
|
width: 100,
|
|
388
388
|
height: 50,
|
|
@@ -391,23 +391,23 @@ describe("createFlow — advanced", () => {
|
|
|
391
391
|
],
|
|
392
392
|
})
|
|
393
393
|
|
|
394
|
-
const snap = flow.getSnapLines(
|
|
394
|
+
const snap = flow.getSnapLines('2', { x: 500, y: 500 })
|
|
395
395
|
expect(snap.x).toBeNull()
|
|
396
396
|
expect(snap.y).toBeNull()
|
|
397
397
|
})
|
|
398
398
|
|
|
399
|
-
it(
|
|
399
|
+
it('snaps to left edge alignment', () => {
|
|
400
400
|
const flow = createFlow({
|
|
401
401
|
nodes: [
|
|
402
402
|
{
|
|
403
|
-
id:
|
|
403
|
+
id: '1',
|
|
404
404
|
position: { x: 100, y: 0 },
|
|
405
405
|
width: 100,
|
|
406
406
|
height: 50,
|
|
407
407
|
data: {},
|
|
408
408
|
},
|
|
409
409
|
{
|
|
410
|
-
id:
|
|
410
|
+
id: '2',
|
|
411
411
|
position: { x: 100, y: 100 },
|
|
412
412
|
width: 100,
|
|
413
413
|
height: 50,
|
|
@@ -417,22 +417,22 @@ describe("createFlow — advanced", () => {
|
|
|
417
417
|
})
|
|
418
418
|
|
|
419
419
|
// Node 2 is already aligned on left edge
|
|
420
|
-
const snap = flow.getSnapLines(
|
|
420
|
+
const snap = flow.getSnapLines('2', { x: 102, y: 100 })
|
|
421
421
|
expect(snap.snappedPosition.x).toBe(100) // snapped to left edge
|
|
422
422
|
})
|
|
423
423
|
|
|
424
|
-
it(
|
|
424
|
+
it('snaps to right edge alignment', () => {
|
|
425
425
|
const flow = createFlow({
|
|
426
426
|
nodes: [
|
|
427
427
|
{
|
|
428
|
-
id:
|
|
428
|
+
id: '1',
|
|
429
429
|
position: { x: 100, y: 0 },
|
|
430
430
|
width: 100,
|
|
431
431
|
height: 50,
|
|
432
432
|
data: {},
|
|
433
433
|
},
|
|
434
434
|
{
|
|
435
|
-
id:
|
|
435
|
+
id: '2',
|
|
436
436
|
position: { x: 100, y: 100 },
|
|
437
437
|
width: 100,
|
|
438
438
|
height: 50,
|
|
@@ -442,24 +442,24 @@ describe("createFlow — advanced", () => {
|
|
|
442
442
|
})
|
|
443
443
|
|
|
444
444
|
// Move node 2 so right edges almost align
|
|
445
|
-
const snap = flow.getSnapLines(
|
|
445
|
+
const snap = flow.getSnapLines('2', { x: 98, y: 100 })
|
|
446
446
|
// Right edge of node1: 200, right edge of moved node2: 98+100=198
|
|
447
447
|
// Diff = 2, within threshold 5
|
|
448
448
|
expect(snap.snappedPosition.x).toBe(100) // snapped
|
|
449
449
|
})
|
|
450
450
|
|
|
451
|
-
it(
|
|
451
|
+
it('snaps to center Y alignment', () => {
|
|
452
452
|
const flow = createFlow({
|
|
453
453
|
nodes: [
|
|
454
454
|
{
|
|
455
|
-
id:
|
|
455
|
+
id: '1',
|
|
456
456
|
position: { x: 0, y: 100 },
|
|
457
457
|
width: 100,
|
|
458
458
|
height: 50,
|
|
459
459
|
data: {},
|
|
460
460
|
},
|
|
461
461
|
{
|
|
462
|
-
id:
|
|
462
|
+
id: '2',
|
|
463
463
|
position: { x: 200, y: 0 },
|
|
464
464
|
width: 100,
|
|
465
465
|
height: 50,
|
|
@@ -470,22 +470,22 @@ describe("createFlow — advanced", () => {
|
|
|
470
470
|
|
|
471
471
|
// center Y of node1 = 100+25 = 125
|
|
472
472
|
// Move node2 so its center Y is close: y + 25 ≈ 125 → y ≈ 100
|
|
473
|
-
const snap = flow.getSnapLines(
|
|
473
|
+
const snap = flow.getSnapLines('2', { x: 200, y: 102 })
|
|
474
474
|
expect(snap.snappedPosition.y).toBe(100) // snapped to center Y
|
|
475
475
|
})
|
|
476
476
|
|
|
477
|
-
it(
|
|
477
|
+
it('snaps to bottom edge alignment', () => {
|
|
478
478
|
const flow = createFlow({
|
|
479
479
|
nodes: [
|
|
480
480
|
{
|
|
481
|
-
id:
|
|
481
|
+
id: '1',
|
|
482
482
|
position: { x: 0, y: 0 },
|
|
483
483
|
width: 100,
|
|
484
484
|
height: 50,
|
|
485
485
|
data: {},
|
|
486
486
|
},
|
|
487
487
|
{
|
|
488
|
-
id:
|
|
488
|
+
id: '2',
|
|
489
489
|
position: { x: 200, y: 0 },
|
|
490
490
|
width: 100,
|
|
491
491
|
height: 50,
|
|
@@ -496,75 +496,75 @@ describe("createFlow — advanced", () => {
|
|
|
496
496
|
|
|
497
497
|
// bottom of node1 = 50, bottom of node2 = y + 50
|
|
498
498
|
// For snap: abs(y+50 - 50) < 5 → abs(y) < 5
|
|
499
|
-
const snap = flow.getSnapLines(
|
|
499
|
+
const snap = flow.getSnapLines('2', { x: 200, y: 3 })
|
|
500
500
|
expect(snap.snappedPosition.y).toBe(0) // snapped to bottom edge
|
|
501
501
|
})
|
|
502
502
|
|
|
503
|
-
it(
|
|
503
|
+
it('returns original position for missing node', () => {
|
|
504
504
|
const flow = createFlow()
|
|
505
|
-
const snap = flow.getSnapLines(
|
|
505
|
+
const snap = flow.getSnapLines('missing', { x: 100, y: 200 })
|
|
506
506
|
expect(snap.snappedPosition).toEqual({ x: 100, y: 200 })
|
|
507
507
|
})
|
|
508
508
|
})
|
|
509
509
|
|
|
510
|
-
describe(
|
|
511
|
-
it(
|
|
510
|
+
describe('reconnectEdge', () => {
|
|
511
|
+
it('changes edge source', () => {
|
|
512
512
|
const flow = createFlow({
|
|
513
513
|
nodes: [
|
|
514
|
-
{ id:
|
|
515
|
-
{ id:
|
|
516
|
-
{ id:
|
|
514
|
+
{ id: '1', position: { x: 0, y: 0 }, data: {} },
|
|
515
|
+
{ id: '2', position: { x: 100, y: 0 }, data: {} },
|
|
516
|
+
{ id: '3', position: { x: 200, y: 0 }, data: {} },
|
|
517
517
|
],
|
|
518
|
-
edges: [{ id:
|
|
518
|
+
edges: [{ id: 'e1', source: '1', target: '2' }],
|
|
519
519
|
})
|
|
520
520
|
|
|
521
|
-
flow.reconnectEdge(
|
|
522
|
-
expect(flow.getEdge(
|
|
523
|
-
expect(flow.getEdge(
|
|
521
|
+
flow.reconnectEdge('e1', { source: '3' })
|
|
522
|
+
expect(flow.getEdge('e1')!.source).toBe('3')
|
|
523
|
+
expect(flow.getEdge('e1')!.target).toBe('2') // unchanged
|
|
524
524
|
})
|
|
525
525
|
|
|
526
|
-
it(
|
|
526
|
+
it('changes edge target', () => {
|
|
527
527
|
const flow = createFlow({
|
|
528
528
|
nodes: [
|
|
529
|
-
{ id:
|
|
530
|
-
{ id:
|
|
531
|
-
{ 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: {} },
|
|
532
532
|
],
|
|
533
|
-
edges: [{ id:
|
|
533
|
+
edges: [{ id: 'e1', source: '1', target: '2' }],
|
|
534
534
|
})
|
|
535
535
|
|
|
536
|
-
flow.reconnectEdge(
|
|
537
|
-
expect(flow.getEdge(
|
|
536
|
+
flow.reconnectEdge('e1', { target: '3' })
|
|
537
|
+
expect(flow.getEdge('e1')!.target).toBe('3')
|
|
538
538
|
})
|
|
539
539
|
})
|
|
540
540
|
|
|
541
|
-
describe(
|
|
542
|
-
it(
|
|
541
|
+
describe('edge with custom id and handle', () => {
|
|
542
|
+
it('generates id from source/target handles', () => {
|
|
543
543
|
const flow = createFlow({
|
|
544
544
|
nodes: [
|
|
545
|
-
{ id:
|
|
546
|
-
{ id:
|
|
545
|
+
{ id: '1', position: { x: 0, y: 0 }, data: {} },
|
|
546
|
+
{ id: '2', position: { x: 100, y: 0 }, data: {} },
|
|
547
547
|
],
|
|
548
548
|
edges: [
|
|
549
549
|
{
|
|
550
|
-
source:
|
|
551
|
-
target:
|
|
552
|
-
sourceHandle:
|
|
553
|
-
targetHandle:
|
|
550
|
+
source: '1',
|
|
551
|
+
target: '2',
|
|
552
|
+
sourceHandle: 'out',
|
|
553
|
+
targetHandle: 'in',
|
|
554
554
|
},
|
|
555
555
|
],
|
|
556
556
|
})
|
|
557
557
|
|
|
558
|
-
expect(flow.edges()[0]!.id).toBe(
|
|
558
|
+
expect(flow.edges()[0]!.id).toBe('e-1-out-2-in')
|
|
559
559
|
})
|
|
560
560
|
})
|
|
561
561
|
|
|
562
|
-
describe(
|
|
563
|
-
it(
|
|
562
|
+
describe('fitView with initial config', () => {
|
|
563
|
+
it('fits view on creation when config.fitView is true', () => {
|
|
564
564
|
const flow = createFlow({
|
|
565
565
|
nodes: [
|
|
566
|
-
{ id:
|
|
567
|
-
{ id:
|
|
566
|
+
{ id: '1', position: { x: 0, y: 0 }, data: {} },
|
|
567
|
+
{ id: '2', position: { x: 500, y: 500 }, data: {} },
|
|
568
568
|
],
|
|
569
569
|
fitView: true,
|
|
570
570
|
})
|
|
@@ -575,19 +575,19 @@ describe("createFlow — advanced", () => {
|
|
|
575
575
|
})
|
|
576
576
|
})
|
|
577
577
|
|
|
578
|
-
describe(
|
|
579
|
-
it(
|
|
578
|
+
describe('proximity connect with connection rules', () => {
|
|
579
|
+
it('respects connection rules', () => {
|
|
580
580
|
const flow = createFlow({
|
|
581
581
|
nodes: [
|
|
582
582
|
{
|
|
583
|
-
id:
|
|
584
|
-
type:
|
|
583
|
+
id: '1',
|
|
584
|
+
type: 'output',
|
|
585
585
|
position: { x: 0, y: 0 },
|
|
586
586
|
data: {},
|
|
587
587
|
},
|
|
588
588
|
{
|
|
589
|
-
id:
|
|
590
|
-
type:
|
|
589
|
+
id: '2',
|
|
590
|
+
type: 'input',
|
|
591
591
|
position: { x: 100, y: 0 },
|
|
592
592
|
data: {},
|
|
593
593
|
},
|
|
@@ -597,37 +597,37 @@ describe("createFlow — advanced", () => {
|
|
|
597
597
|
},
|
|
598
598
|
})
|
|
599
599
|
|
|
600
|
-
expect(flow.getProximityConnection(
|
|
600
|
+
expect(flow.getProximityConnection('1', 200)).toBeNull()
|
|
601
601
|
})
|
|
602
602
|
})
|
|
603
603
|
|
|
604
|
-
describe(
|
|
605
|
-
it(
|
|
604
|
+
describe('undo/redo edge cases', () => {
|
|
605
|
+
it('undo with empty history does nothing', () => {
|
|
606
606
|
const flow = createFlow({
|
|
607
|
-
nodes: [{ id:
|
|
607
|
+
nodes: [{ id: '1', position: { x: 0, y: 0 }, data: {} }],
|
|
608
608
|
})
|
|
609
609
|
|
|
610
610
|
flow.undo()
|
|
611
611
|
expect(flow.nodes()).toHaveLength(1)
|
|
612
612
|
})
|
|
613
613
|
|
|
614
|
-
it(
|
|
614
|
+
it('redo with empty redo stack does nothing', () => {
|
|
615
615
|
const flow = createFlow({
|
|
616
|
-
nodes: [{ id:
|
|
616
|
+
nodes: [{ id: '1', position: { x: 0, y: 0 }, data: {} }],
|
|
617
617
|
})
|
|
618
618
|
|
|
619
619
|
flow.redo()
|
|
620
620
|
expect(flow.nodes()).toHaveLength(1)
|
|
621
621
|
})
|
|
622
622
|
|
|
623
|
-
it(
|
|
623
|
+
it('multiple undo/redo cycles', () => {
|
|
624
624
|
const flow = createFlow()
|
|
625
625
|
|
|
626
626
|
flow.pushHistory()
|
|
627
|
-
flow.addNode({ id:
|
|
627
|
+
flow.addNode({ id: '1', position: { x: 0, y: 0 }, data: {} })
|
|
628
628
|
|
|
629
629
|
flow.pushHistory()
|
|
630
|
-
flow.addNode({ id:
|
|
630
|
+
flow.addNode({ id: '2', position: { x: 100, y: 0 }, data: {} })
|
|
631
631
|
|
|
632
632
|
expect(flow.nodes()).toHaveLength(2)
|
|
633
633
|
|
|
@@ -645,22 +645,22 @@ describe("createFlow — advanced", () => {
|
|
|
645
645
|
})
|
|
646
646
|
})
|
|
647
647
|
|
|
648
|
-
describe(
|
|
649
|
-
it(
|
|
648
|
+
describe('copy/paste with edges', () => {
|
|
649
|
+
it('copies connected edges between selected nodes', () => {
|
|
650
650
|
const flow = createFlow({
|
|
651
651
|
nodes: [
|
|
652
|
-
{ id:
|
|
653
|
-
{ id:
|
|
654
|
-
{ id:
|
|
652
|
+
{ id: '1', position: { x: 0, y: 0 }, data: {} },
|
|
653
|
+
{ id: '2', position: { x: 100, y: 0 }, data: {} },
|
|
654
|
+
{ id: '3', position: { x: 200, y: 0 }, data: {} },
|
|
655
655
|
],
|
|
656
656
|
edges: [
|
|
657
|
-
{ source:
|
|
658
|
-
{ source:
|
|
657
|
+
{ source: '1', target: '2' },
|
|
658
|
+
{ source: '2', target: '3' },
|
|
659
659
|
],
|
|
660
660
|
})
|
|
661
661
|
|
|
662
|
-
flow.selectNode(
|
|
663
|
-
flow.selectNode(
|
|
662
|
+
flow.selectNode('1')
|
|
663
|
+
flow.selectNode('2', true)
|
|
664
664
|
flow.copySelected()
|
|
665
665
|
flow.paste()
|
|
666
666
|
|
|
@@ -670,9 +670,9 @@ describe("createFlow — advanced", () => {
|
|
|
670
670
|
expect(flow.edges()).toHaveLength(3)
|
|
671
671
|
})
|
|
672
672
|
|
|
673
|
-
it(
|
|
673
|
+
it('copy with no selection does nothing', () => {
|
|
674
674
|
const flow = createFlow({
|
|
675
|
-
nodes: [{ id:
|
|
675
|
+
nodes: [{ id: '1', position: { x: 0, y: 0 }, data: {} }],
|
|
676
676
|
})
|
|
677
677
|
|
|
678
678
|
flow.copySelected()
|
|
@@ -681,10 +681,10 @@ describe("createFlow — advanced", () => {
|
|
|
681
681
|
})
|
|
682
682
|
})
|
|
683
683
|
|
|
684
|
-
describe(
|
|
685
|
-
it(
|
|
684
|
+
describe('listener callbacks', () => {
|
|
685
|
+
it('onNodeDragStart/End can be registered', () => {
|
|
686
686
|
const flow = createFlow({
|
|
687
|
-
nodes: [{ id:
|
|
687
|
+
nodes: [{ id: '1', position: { x: 0, y: 0 }, data: {} }],
|
|
688
688
|
})
|
|
689
689
|
|
|
690
690
|
const starts: string[] = []
|
|
@@ -694,32 +694,32 @@ describe("createFlow — advanced", () => {
|
|
|
694
694
|
flow.onNodeDragEnd((n) => ends.push(n.id))
|
|
695
695
|
|
|
696
696
|
// Emit manually via _emit
|
|
697
|
-
flow._emit.nodeDragStart(flow.getNode(
|
|
698
|
-
flow._emit.nodeDragEnd(flow.getNode(
|
|
697
|
+
flow._emit.nodeDragStart(flow.getNode('1')!)
|
|
698
|
+
flow._emit.nodeDragEnd(flow.getNode('1')!)
|
|
699
699
|
|
|
700
|
-
expect(starts).toEqual([
|
|
701
|
-
expect(ends).toEqual([
|
|
700
|
+
expect(starts).toEqual(['1'])
|
|
701
|
+
expect(ends).toEqual(['1'])
|
|
702
702
|
})
|
|
703
703
|
|
|
704
|
-
it(
|
|
704
|
+
it('onNodeDoubleClick', () => {
|
|
705
705
|
const flow = createFlow({
|
|
706
|
-
nodes: [{ id:
|
|
706
|
+
nodes: [{ id: '1', position: { x: 0, y: 0 }, data: {} }],
|
|
707
707
|
})
|
|
708
708
|
|
|
709
709
|
const clicked: string[] = []
|
|
710
710
|
flow.onNodeDoubleClick((n) => clicked.push(n.id))
|
|
711
711
|
|
|
712
|
-
flow._emit.nodeDoubleClick(flow.getNode(
|
|
713
|
-
expect(clicked).toEqual([
|
|
712
|
+
flow._emit.nodeDoubleClick(flow.getNode('1')!)
|
|
713
|
+
expect(clicked).toEqual(['1'])
|
|
714
714
|
})
|
|
715
715
|
|
|
716
|
-
it(
|
|
716
|
+
it('onNodeClick / onEdgeClick', () => {
|
|
717
717
|
const flow = createFlow({
|
|
718
718
|
nodes: [
|
|
719
|
-
{ id:
|
|
720
|
-
{ id:
|
|
719
|
+
{ id: '1', position: { x: 0, y: 0 }, data: {} },
|
|
720
|
+
{ id: '2', position: { x: 100, y: 0 }, data: {} },
|
|
721
721
|
],
|
|
722
|
-
edges: [{ id:
|
|
722
|
+
edges: [{ id: 'e1', source: '1', target: '2' }],
|
|
723
723
|
})
|
|
724
724
|
|
|
725
725
|
const nodeClicks: string[] = []
|
|
@@ -728,29 +728,29 @@ describe("createFlow — advanced", () => {
|
|
|
728
728
|
flow.onNodeClick((n) => nodeClicks.push(n.id))
|
|
729
729
|
flow.onEdgeClick((e) => edgeClicks.push(e.id!))
|
|
730
730
|
|
|
731
|
-
flow._emit.nodeClick(flow.getNode(
|
|
732
|
-
flow._emit.edgeClick(flow.getEdge(
|
|
731
|
+
flow._emit.nodeClick(flow.getNode('1')!)
|
|
732
|
+
flow._emit.edgeClick(flow.getEdge('e1')!)
|
|
733
733
|
|
|
734
|
-
expect(nodeClicks).toEqual([
|
|
735
|
-
expect(edgeClicks).toEqual([
|
|
734
|
+
expect(nodeClicks).toEqual(['1'])
|
|
735
|
+
expect(edgeClicks).toEqual(['e1'])
|
|
736
736
|
})
|
|
737
737
|
})
|
|
738
738
|
|
|
739
|
-
describe(
|
|
740
|
-
it(
|
|
739
|
+
describe('containerSize', () => {
|
|
740
|
+
it('containerSize signal exists with defaults', () => {
|
|
741
741
|
const flow = createFlow()
|
|
742
742
|
expect(flow.containerSize()).toEqual({ width: 800, height: 600 })
|
|
743
743
|
})
|
|
744
744
|
|
|
745
|
-
it(
|
|
745
|
+
it('containerSize can be updated', () => {
|
|
746
746
|
const flow = createFlow()
|
|
747
747
|
flow.containerSize.set({ width: 1200, height: 900 })
|
|
748
748
|
expect(flow.containerSize()).toEqual({ width: 1200, height: 900 })
|
|
749
749
|
})
|
|
750
750
|
})
|
|
751
751
|
|
|
752
|
-
describe(
|
|
753
|
-
it(
|
|
752
|
+
describe('clampToExtent', () => {
|
|
753
|
+
it('returns position unchanged when no extent', () => {
|
|
754
754
|
const flow = createFlow()
|
|
755
755
|
expect(flow.clampToExtent({ x: -999, y: -999 })).toEqual({
|
|
756
756
|
x: -999,
|
|
@@ -758,7 +758,7 @@ describe("createFlow — advanced", () => {
|
|
|
758
758
|
})
|
|
759
759
|
})
|
|
760
760
|
|
|
761
|
-
it(
|
|
761
|
+
it('clamps to extent boundaries', () => {
|
|
762
762
|
const flow = createFlow({
|
|
763
763
|
nodeExtent: [
|
|
764
764
|
[0, 0],
|
|
@@ -776,18 +776,18 @@ describe("createFlow — advanced", () => {
|
|
|
776
776
|
})
|
|
777
777
|
})
|
|
778
778
|
|
|
779
|
-
describe(
|
|
780
|
-
it(
|
|
779
|
+
describe('edge default type', () => {
|
|
780
|
+
it('uses defaultEdgeType from config', () => {
|
|
781
781
|
const flow = createFlow({
|
|
782
782
|
nodes: [
|
|
783
|
-
{ id:
|
|
784
|
-
{ id:
|
|
783
|
+
{ id: '1', position: { x: 0, y: 0 }, data: {} },
|
|
784
|
+
{ id: '2', position: { x: 100, y: 0 }, data: {} },
|
|
785
785
|
],
|
|
786
|
-
edges: [{ source:
|
|
787
|
-
defaultEdgeType:
|
|
786
|
+
edges: [{ source: '1', target: '2' }],
|
|
787
|
+
defaultEdgeType: 'straight',
|
|
788
788
|
})
|
|
789
789
|
|
|
790
|
-
expect(flow.edges()[0]!.type).toBe(
|
|
790
|
+
expect(flow.edges()[0]!.type).toBe('straight')
|
|
791
791
|
})
|
|
792
792
|
})
|
|
793
793
|
})
|