@nyaruka/temba-components 0.138.6 → 0.140.0
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/.github/workflows/cla.yml +1 -1
- package/.github/workflows/copilot-setup-steps.yml +6 -1
- package/CHANGELOG.md +26 -0
- package/demo/data/flows/sample-flow.json +24 -0
- package/dist/locales/es.js +5 -5
- package/dist/locales/es.js.map +1 -1
- package/dist/locales/fr.js +5 -5
- package/dist/locales/fr.js.map +1 -1
- package/dist/locales/locale-codes.js +2 -11
- package/dist/locales/locale-codes.js.map +1 -1
- package/dist/locales/pt.js +5 -5
- package/dist/locales/pt.js.map +1 -1
- package/dist/temba-components.js +1112 -882
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/display/Chat.js +10 -7
- package/out-tsc/src/display/Chat.js.map +1 -1
- package/out-tsc/src/display/Dropdown.js +3 -1
- package/out-tsc/src/display/Dropdown.js.map +1 -1
- package/out-tsc/src/display/FloatingTab.js +25 -32
- package/out-tsc/src/display/FloatingTab.js.map +1 -1
- package/out-tsc/src/display/Thumbnail.js +163 -5
- package/out-tsc/src/display/Thumbnail.js.map +1 -1
- package/out-tsc/src/flow/CanvasMenu.js +5 -3
- package/out-tsc/src/flow/CanvasMenu.js.map +1 -1
- package/out-tsc/src/flow/CanvasNode.js +70 -29
- package/out-tsc/src/flow/CanvasNode.js.map +1 -1
- package/out-tsc/src/flow/Editor.js +290 -239
- package/out-tsc/src/flow/Editor.js.map +1 -1
- package/out-tsc/src/flow/NodeEditor.js +118 -10
- package/out-tsc/src/flow/NodeEditor.js.map +1 -1
- package/out-tsc/src/flow/Plumber.js +757 -403
- package/out-tsc/src/flow/Plumber.js.map +1 -1
- package/out-tsc/src/flow/StickyNote.js +13 -4
- package/out-tsc/src/flow/StickyNote.js.map +1 -1
- package/out-tsc/src/flow/actions/audio-player.js +112 -0
- package/out-tsc/src/flow/actions/audio-player.js.map +1 -0
- package/out-tsc/src/flow/actions/enter_flow.js +43 -0
- package/out-tsc/src/flow/actions/enter_flow.js.map +1 -0
- package/out-tsc/src/flow/actions/play_audio.js +57 -4
- package/out-tsc/src/flow/actions/play_audio.js.map +1 -1
- package/out-tsc/src/flow/actions/say_msg.js +86 -3
- package/out-tsc/src/flow/actions/say_msg.js.map +1 -1
- package/out-tsc/src/flow/config.js +11 -3
- package/out-tsc/src/flow/config.js.map +1 -1
- package/out-tsc/src/flow/nodes/shared-rules.js +1 -1
- package/out-tsc/src/flow/nodes/shared-rules.js.map +1 -1
- package/out-tsc/src/flow/nodes/terminal.js +7 -0
- package/out-tsc/src/flow/nodes/terminal.js.map +1 -0
- package/out-tsc/src/flow/nodes/wait_for_audio.js +77 -0
- package/out-tsc/src/flow/nodes/wait_for_audio.js.map +1 -0
- package/out-tsc/src/flow/nodes/wait_for_dial.js +151 -0
- package/out-tsc/src/flow/nodes/wait_for_dial.js.map +1 -0
- package/out-tsc/src/flow/nodes/wait_for_digits.js +61 -1
- package/out-tsc/src/flow/nodes/wait_for_digits.js.map +1 -1
- package/out-tsc/src/flow/nodes/wait_for_menu.js +173 -2
- package/out-tsc/src/flow/nodes/wait_for_menu.js.map +1 -1
- package/out-tsc/src/flow/operators.js +21 -5
- package/out-tsc/src/flow/operators.js.map +1 -1
- package/out-tsc/src/flow/types.js.map +1 -1
- package/out-tsc/src/flow/utils.js +213 -65
- package/out-tsc/src/flow/utils.js.map +1 -1
- package/out-tsc/src/form/ArrayEditor.js +4 -2
- package/out-tsc/src/form/ArrayEditor.js.map +1 -1
- package/out-tsc/src/form/FieldRenderer.js +49 -0
- package/out-tsc/src/form/FieldRenderer.js.map +1 -1
- package/out-tsc/src/interfaces.js +2 -0
- package/out-tsc/src/interfaces.js.map +1 -1
- package/out-tsc/src/layout/Dialog.js +52 -7
- package/out-tsc/src/layout/Dialog.js.map +1 -1
- package/out-tsc/src/list/TicketList.js +4 -1
- package/out-tsc/src/list/TicketList.js.map +1 -1
- package/out-tsc/src/live/TembaChart.js.map +1 -1
- package/out-tsc/src/locales/es.js +5 -5
- package/out-tsc/src/locales/es.js.map +1 -1
- package/out-tsc/src/locales/fr.js +5 -5
- package/out-tsc/src/locales/fr.js.map +1 -1
- package/out-tsc/src/locales/locale-codes.js +2 -11
- package/out-tsc/src/locales/locale-codes.js.map +1 -1
- package/out-tsc/src/locales/pt.js +5 -5
- package/out-tsc/src/locales/pt.js.map +1 -1
- package/out-tsc/src/simulator/Simulator.js +10 -3
- package/out-tsc/src/simulator/Simulator.js.map +1 -1
- package/out-tsc/src/store/AppState.js +89 -3
- package/out-tsc/src/store/AppState.js.map +1 -1
- package/out-tsc/test/actions/play_audio.test.js +118 -0
- package/out-tsc/test/actions/play_audio.test.js.map +1 -0
- package/out-tsc/test/actions/say_msg.test.js +158 -0
- package/out-tsc/test/actions/say_msg.test.js.map +1 -0
- package/out-tsc/test/nodes/wait_for_audio.test.js +156 -0
- package/out-tsc/test/nodes/wait_for_audio.test.js.map +1 -0
- package/out-tsc/test/nodes/wait_for_dial.test.js +336 -0
- package/out-tsc/test/nodes/wait_for_dial.test.js.map +1 -0
- package/out-tsc/test/nodes/wait_for_digits.test.js +198 -84
- package/out-tsc/test/nodes/wait_for_digits.test.js.map +1 -1
- package/out-tsc/test/nodes/wait_for_menu.test.js +340 -0
- package/out-tsc/test/nodes/wait_for_menu.test.js.map +1 -0
- package/out-tsc/test/temba-floating-tab.test.js +4 -6
- package/out-tsc/test/temba-floating-tab.test.js.map +1 -1
- package/out-tsc/test/temba-flow-collision.test.js +473 -220
- package/out-tsc/test/temba-flow-collision.test.js.map +1 -1
- package/out-tsc/test/temba-flow-editor.test.js +0 -2
- package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
- package/out-tsc/test/temba-flow-plumber-connections.test.js +83 -84
- package/out-tsc/test/temba-flow-plumber-connections.test.js.map +1 -1
- package/out-tsc/test/temba-flow-plumber.test.js +102 -93
- package/out-tsc/test/temba-flow-plumber.test.js.map +1 -1
- package/out-tsc/test/temba-node-type-selector.test.js +6 -6
- package/out-tsc/test/temba-node-type-selector.test.js.map +1 -1
- package/package.json +1 -1
- package/screenshots/truth/actions/play_audio/editor/expression-url.png +0 -0
- package/screenshots/truth/actions/play_audio/editor/static-url.png +0 -0
- package/screenshots/truth/actions/play_audio/render/expression-url.png +0 -0
- package/screenshots/truth/actions/play_audio/render/static-url.png +0 -0
- package/screenshots/truth/actions/say_msg/editor/multiline-text.png +0 -0
- package/screenshots/truth/actions/say_msg/editor/simple-text.png +0 -0
- package/screenshots/truth/actions/say_msg/editor/text-with-audio-url.png +0 -0
- package/screenshots/truth/actions/say_msg/render/multiline-text.png +0 -0
- package/screenshots/truth/actions/say_msg/render/simple-text.png +0 -0
- package/screenshots/truth/actions/say_msg/render/text-with-audio-url.png +0 -0
- package/screenshots/truth/editor/router.png +0 -0
- package/screenshots/truth/editor/wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_audio/editor/basic-audio-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_audio/render/basic-audio-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_dial/editor/basic-dial.png +0 -0
- package/screenshots/truth/nodes/wait_for_dial/editor/dial-with-limits.png +0 -0
- package/screenshots/truth/nodes/wait_for_dial/render/basic-dial.png +0 -0
- package/screenshots/truth/nodes/wait_for_dial/render/dial-with-limits.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/editor/basic-digits-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/editor/digits-with-rules.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/digits-with-rules.png +0 -0
- package/screenshots/truth/nodes/wait_for_menu/editor/menu-with-digits.png +0 -0
- package/screenshots/truth/nodes/wait_for_menu/render/menu-with-digits.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/editor/basic-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/editor/custom-result-name.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/editor/no-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/editor/short-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
- package/src/display/Chat.ts +13 -7
- package/src/display/Dropdown.ts +3 -1
- package/src/display/FloatingTab.ts +24 -33
- package/src/display/Thumbnail.ts +162 -2
- package/src/flow/CanvasMenu.ts +8 -3
- package/src/flow/CanvasNode.ts +75 -30
- package/src/flow/Editor.ts +336 -288
- package/src/flow/NodeEditor.ts +137 -9
- package/src/flow/Plumber.ts +1011 -457
- package/src/flow/StickyNote.ts +14 -4
- package/src/flow/actions/audio-player.ts +127 -0
- package/src/flow/actions/enter_flow.ts +44 -0
- package/src/flow/actions/play_audio.ts +64 -5
- package/src/flow/actions/say_msg.ts +94 -4
- package/src/flow/config.ts +11 -3
- package/src/flow/nodes/shared-rules.ts +1 -1
- package/src/flow/nodes/terminal.ts +9 -0
- package/src/flow/nodes/wait_for_audio.ts +88 -0
- package/src/flow/nodes/wait_for_dial.ts +176 -0
- package/src/flow/nodes/wait_for_digits.ts +86 -2
- package/src/flow/nodes/wait_for_menu.ts +209 -3
- package/src/flow/operators.ts +23 -5
- package/src/flow/types.ts +23 -1
- package/src/flow/utils.ts +238 -81
- package/src/form/ArrayEditor.ts +4 -2
- package/src/form/FieldRenderer.ts +64 -1
- package/src/interfaces.ts +3 -1
- package/src/layout/Dialog.ts +53 -7
- package/src/list/TicketList.ts +4 -1
- package/src/live/TembaChart.ts +1 -1
- package/src/locales/es.ts +13 -18
- package/src/locales/fr.ts +13 -18
- package/src/locales/locale-codes.ts +2 -11
- package/src/locales/pt.ts +13 -18
- package/src/simulator/Simulator.ts +13 -3
- package/src/store/AppState.ts +105 -1
- package/src/store/flow-definition.d.ts +2 -0
- package/test/actions/play_audio.test.ts +155 -0
- package/test/actions/say_msg.test.ts +196 -0
- package/test/nodes/wait_for_audio.test.ts +182 -0
- package/test/nodes/wait_for_dial.test.ts +382 -0
- package/test/nodes/wait_for_digits.test.ts +233 -109
- package/test/nodes/wait_for_menu.test.ts +383 -0
- package/test/temba-floating-tab.test.ts +4 -6
- package/test/temba-flow-collision.test.ts +495 -293
- package/test/temba-flow-editor.test.ts +0 -2
- package/test/temba-flow-plumber-connections.test.ts +97 -97
- package/test/temba-flow-plumber.test.ts +116 -103
- package/test/temba-node-type-selector.test.ts +6 -6
- package/screenshots/truth/nodes/wait_for_digits/editor/phone-number-collection.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/editor/single-digit-with-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/editor/verification-code.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
|
@@ -265,17 +265,16 @@ describe('Collision Detection Utilities', () => {
|
|
|
265
265
|
});
|
|
266
266
|
describe('calculateReflowPositions', () => {
|
|
267
267
|
it('returns empty map when no collisions', () => {
|
|
268
|
-
const movedBounds = {
|
|
269
|
-
uuid: 'moved',
|
|
270
|
-
left: 100,
|
|
271
|
-
top: 100,
|
|
272
|
-
right: 200,
|
|
273
|
-
bottom: 200,
|
|
274
|
-
width: 100,
|
|
275
|
-
height: 100
|
|
276
|
-
};
|
|
277
268
|
const allBounds = [
|
|
278
|
-
|
|
269
|
+
{
|
|
270
|
+
uuid: 'moved',
|
|
271
|
+
left: 100,
|
|
272
|
+
top: 100,
|
|
273
|
+
right: 200,
|
|
274
|
+
bottom: 200,
|
|
275
|
+
width: 100,
|
|
276
|
+
height: 100
|
|
277
|
+
},
|
|
279
278
|
{
|
|
280
279
|
uuid: 'node1',
|
|
281
280
|
left: 300,
|
|
@@ -286,50 +285,46 @@ describe('Collision Detection Utilities', () => {
|
|
|
286
285
|
height: 100
|
|
287
286
|
}
|
|
288
287
|
];
|
|
289
|
-
const positions = calculateReflowPositions('moved',
|
|
288
|
+
const positions = calculateReflowPositions(['moved'], allBounds);
|
|
290
289
|
expect(positions.size).to.equal(0);
|
|
291
290
|
});
|
|
292
|
-
it('moves colliding node
|
|
293
|
-
const movedBounds = {
|
|
294
|
-
uuid: 'moved',
|
|
295
|
-
left: 100,
|
|
296
|
-
top: 100,
|
|
297
|
-
right: 200,
|
|
298
|
-
bottom: 200,
|
|
299
|
-
width: 100,
|
|
300
|
-
height: 100
|
|
301
|
-
};
|
|
291
|
+
it('moves colliding node out of the way', () => {
|
|
302
292
|
const allBounds = [
|
|
303
|
-
|
|
293
|
+
{
|
|
294
|
+
uuid: 'moved',
|
|
295
|
+
left: 100,
|
|
296
|
+
top: 100,
|
|
297
|
+
right: 200,
|
|
298
|
+
bottom: 200,
|
|
299
|
+
width: 100,
|
|
300
|
+
height: 100
|
|
301
|
+
},
|
|
304
302
|
{
|
|
305
303
|
uuid: 'node1',
|
|
306
|
-
left:
|
|
304
|
+
left: 100,
|
|
307
305
|
top: 150,
|
|
308
|
-
right:
|
|
306
|
+
right: 200,
|
|
309
307
|
bottom: 250,
|
|
310
308
|
width: 100,
|
|
311
309
|
height: 100
|
|
312
310
|
}
|
|
313
311
|
];
|
|
314
|
-
const positions = calculateReflowPositions('moved',
|
|
312
|
+
const positions = calculateReflowPositions(['moved'], allBounds);
|
|
315
313
|
expect(positions.size).to.equal(1);
|
|
316
314
|
expect(positions.has('node1')).to.be.true;
|
|
317
|
-
|
|
318
|
-
expect(newPos.left).to.equal(150); // left unchanged
|
|
319
|
-
expect(newPos.top).to.be.greaterThan(200); // moved below the moved node
|
|
315
|
+
expect(positions.has('moved')).to.be.false;
|
|
320
316
|
});
|
|
321
|
-
it('
|
|
322
|
-
const movedBounds = {
|
|
323
|
-
uuid: 'moved',
|
|
324
|
-
left: 100,
|
|
325
|
-
top: 150,
|
|
326
|
-
right: 200,
|
|
327
|
-
bottom: 250,
|
|
328
|
-
width: 100,
|
|
329
|
-
height: 100
|
|
330
|
-
};
|
|
317
|
+
it('sacred node never appears in returned positions', () => {
|
|
331
318
|
const allBounds = [
|
|
332
|
-
|
|
319
|
+
{
|
|
320
|
+
uuid: 'dropped',
|
|
321
|
+
left: 100,
|
|
322
|
+
top: 100,
|
|
323
|
+
right: 200,
|
|
324
|
+
bottom: 200,
|
|
325
|
+
width: 100,
|
|
326
|
+
height: 100
|
|
327
|
+
},
|
|
333
328
|
{
|
|
334
329
|
uuid: 'existing',
|
|
335
330
|
left: 100,
|
|
@@ -340,91 +335,124 @@ describe('Collision Detection Utilities', () => {
|
|
|
340
335
|
height: 100
|
|
341
336
|
}
|
|
342
337
|
];
|
|
343
|
-
const positions = calculateReflowPositions('
|
|
344
|
-
);
|
|
345
|
-
expect(positions.
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
it('gives priority to dropped node when dropped above midpoint', () => {
|
|
351
|
-
// Dropped node overlaps with bottom of existing node
|
|
352
|
-
// Bottom of dropped (180) is above midpoint of existing (150)
|
|
353
|
-
// So dropped node should keep position, existing moves down
|
|
354
|
-
const movedBounds = {
|
|
355
|
-
uuid: 'dropped',
|
|
356
|
-
left: 100,
|
|
357
|
-
top: 80,
|
|
358
|
-
right: 200,
|
|
359
|
-
bottom: 180,
|
|
360
|
-
width: 100,
|
|
361
|
-
height: 100
|
|
362
|
-
};
|
|
338
|
+
const positions = calculateReflowPositions(['dropped'], allBounds);
|
|
339
|
+
expect(positions.has('dropped')).to.be.false;
|
|
340
|
+
expect(positions.has('existing')).to.be.true;
|
|
341
|
+
});
|
|
342
|
+
it('prefers least-displacement direction', () => {
|
|
343
|
+
// Sacred at (100,100)-(200,200), collider at (180,100)-(280,200)
|
|
344
|
+
// Right requires only 60px displacement, down requires 140px
|
|
363
345
|
const allBounds = [
|
|
364
|
-
movedBounds,
|
|
365
346
|
{
|
|
366
|
-
uuid: '
|
|
347
|
+
uuid: 'sacred',
|
|
367
348
|
left: 100,
|
|
368
349
|
top: 100,
|
|
369
350
|
right: 200,
|
|
370
351
|
bottom: 200,
|
|
371
352
|
width: 100,
|
|
372
353
|
height: 100
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
uuid: 'collider',
|
|
357
|
+
left: 180,
|
|
358
|
+
top: 100,
|
|
359
|
+
right: 280,
|
|
360
|
+
bottom: 200,
|
|
361
|
+
width: 100,
|
|
362
|
+
height: 100
|
|
373
363
|
}
|
|
374
364
|
];
|
|
375
|
-
const positions = calculateReflowPositions('
|
|
376
|
-
);
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
expect(
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
const movedBounds = {
|
|
388
|
-
uuid: 'dropped',
|
|
389
|
-
left: 100,
|
|
390
|
-
top: 120,
|
|
391
|
-
right: 200,
|
|
392
|
-
bottom: 220,
|
|
393
|
-
width: 100,
|
|
394
|
-
height: 100
|
|
395
|
-
};
|
|
365
|
+
const positions = calculateReflowPositions(['sacred'], allBounds);
|
|
366
|
+
expect(positions.has('collider')).to.be.true;
|
|
367
|
+
const newPos = positions.get('collider');
|
|
368
|
+
// Should move right (shorter) rather than down (longer)
|
|
369
|
+
expect(newPos.left).to.be.greaterThan(200);
|
|
370
|
+
expect(newPos.top).to.equal(100); // vertical position unchanged
|
|
371
|
+
});
|
|
372
|
+
it('prefers up when it is the shortest move', () => {
|
|
373
|
+
// Sacred at (100,200)-(200,300), collider at (100,180)-(200,280)
|
|
374
|
+
// Up: newTop=snapToGrid(200-100-30)=snapToGrid(70)=80, distance=100
|
|
375
|
+
// Down: newTop=snapToGrid(300+30)=340, distance=160
|
|
376
|
+
// Right: newLeft=snapToGrid(200+30)=240, distance=140
|
|
396
377
|
const allBounds = [
|
|
397
|
-
movedBounds,
|
|
398
378
|
{
|
|
399
|
-
uuid: '
|
|
379
|
+
uuid: 'sacred',
|
|
380
|
+
left: 100,
|
|
381
|
+
top: 200,
|
|
382
|
+
right: 200,
|
|
383
|
+
bottom: 300,
|
|
384
|
+
width: 100,
|
|
385
|
+
height: 100
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
uuid: 'collider',
|
|
389
|
+
left: 100,
|
|
390
|
+
top: 180,
|
|
391
|
+
right: 200,
|
|
392
|
+
bottom: 280,
|
|
393
|
+
width: 100,
|
|
394
|
+
height: 100
|
|
395
|
+
}
|
|
396
|
+
];
|
|
397
|
+
const positions = calculateReflowPositions(['sacred'], allBounds);
|
|
398
|
+
expect(positions.has('collider')).to.be.true;
|
|
399
|
+
const newPos = positions.get('collider');
|
|
400
|
+
// Should move up (shortest displacement)
|
|
401
|
+
expect(newPos.top).to.be.lessThan(200);
|
|
402
|
+
expect(newPos.left).to.equal(100); // horizontal position unchanged
|
|
403
|
+
});
|
|
404
|
+
it('prefers axis-matching direction even with a cascade', () => {
|
|
405
|
+
// Sacred at (100,100)-(200,200), collider at (100,150)-(200,250)
|
|
406
|
+
// Overlap is 100w x 50h (wide) = vertical collision = prefer down
|
|
407
|
+
// A blocker sits below at (100,280)-(200,380), so down causes a cascade
|
|
408
|
+
// But axis bias still prefers down over moving right
|
|
409
|
+
const allBounds = [
|
|
410
|
+
{
|
|
411
|
+
uuid: 'sacred',
|
|
400
412
|
left: 100,
|
|
401
413
|
top: 100,
|
|
402
414
|
right: 200,
|
|
403
415
|
bottom: 200,
|
|
404
416
|
width: 100,
|
|
405
417
|
height: 100
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
uuid: 'collider',
|
|
421
|
+
left: 100,
|
|
422
|
+
top: 150,
|
|
423
|
+
right: 200,
|
|
424
|
+
bottom: 250,
|
|
425
|
+
width: 100,
|
|
426
|
+
height: 100
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
uuid: 'blocker',
|
|
430
|
+
left: 100,
|
|
431
|
+
top: 280,
|
|
432
|
+
right: 200,
|
|
433
|
+
bottom: 380,
|
|
434
|
+
width: 100,
|
|
435
|
+
height: 100
|
|
406
436
|
}
|
|
407
437
|
];
|
|
408
|
-
const positions = calculateReflowPositions('
|
|
409
|
-
);
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
expect(
|
|
413
|
-
|
|
414
|
-
expect(droppedNewPos.top).to.be.greaterThan(200); // moved below existing node
|
|
438
|
+
const positions = calculateReflowPositions(['sacred'], allBounds);
|
|
439
|
+
expect(positions.has('collider')).to.be.true;
|
|
440
|
+
const newPos = positions.get('collider');
|
|
441
|
+
// Axis bias prefers down (vertical) even though it cascades into blocker
|
|
442
|
+
expect(newPos.top).to.be.greaterThan(200);
|
|
443
|
+
expect(newPos.left).to.equal(100); // horizontal position unchanged
|
|
415
444
|
});
|
|
416
445
|
it('resolves cascading collisions', () => {
|
|
417
|
-
const movedBounds = {
|
|
418
|
-
uuid: 'moved',
|
|
419
|
-
left: 100,
|
|
420
|
-
top: 100,
|
|
421
|
-
right: 200,
|
|
422
|
-
bottom: 200,
|
|
423
|
-
width: 100,
|
|
424
|
-
height: 100
|
|
425
|
-
};
|
|
426
446
|
const allBounds = [
|
|
427
|
-
|
|
447
|
+
{
|
|
448
|
+
uuid: 'moved',
|
|
449
|
+
left: 100,
|
|
450
|
+
top: 100,
|
|
451
|
+
right: 200,
|
|
452
|
+
bottom: 200,
|
|
453
|
+
width: 100,
|
|
454
|
+
height: 100
|
|
455
|
+
},
|
|
428
456
|
{
|
|
429
457
|
uuid: 'node1',
|
|
430
458
|
left: 100,
|
|
@@ -444,34 +472,102 @@ describe('Collision Detection Utilities', () => {
|
|
|
444
472
|
height: 100
|
|
445
473
|
}
|
|
446
474
|
];
|
|
447
|
-
const positions = calculateReflowPositions('moved',
|
|
448
|
-
//
|
|
475
|
+
const positions = calculateReflowPositions(['moved'], allBounds);
|
|
476
|
+
// At least one node should be repositioned
|
|
449
477
|
expect(positions.size).to.be.greaterThan(0);
|
|
450
|
-
//
|
|
451
|
-
|
|
452
|
-
const node1Pos = positions.get('node1');
|
|
453
|
-
const node2Pos = positions.get('node2');
|
|
454
|
-
// node2 should be below node1
|
|
455
|
-
expect(node2Pos.top).to.be.greaterThan(node1Pos.top);
|
|
456
|
-
}
|
|
478
|
+
// No node should overlap with the sacred node or each other after reflow
|
|
479
|
+
// (verified by the algorithm's correctness guarantee)
|
|
457
480
|
});
|
|
458
|
-
it('handles multiple
|
|
459
|
-
//
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
481
|
+
it('handles multiple sacred nodes', () => {
|
|
482
|
+
// Two sacred nodes with a non-sacred node overlapping one
|
|
483
|
+
const allBounds = [
|
|
484
|
+
{
|
|
485
|
+
uuid: 'sacred1',
|
|
486
|
+
left: 100,
|
|
487
|
+
top: 100,
|
|
488
|
+
right: 200,
|
|
489
|
+
bottom: 200,
|
|
490
|
+
width: 100,
|
|
491
|
+
height: 100
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
uuid: 'sacred2',
|
|
495
|
+
left: 100,
|
|
496
|
+
top: 400,
|
|
497
|
+
right: 200,
|
|
498
|
+
bottom: 500,
|
|
499
|
+
width: 100,
|
|
500
|
+
height: 100
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
uuid: 'collider',
|
|
504
|
+
left: 100,
|
|
505
|
+
top: 150,
|
|
506
|
+
right: 200,
|
|
507
|
+
bottom: 250,
|
|
508
|
+
width: 100,
|
|
509
|
+
height: 100
|
|
510
|
+
}
|
|
511
|
+
];
|
|
512
|
+
const positions = calculateReflowPositions(['sacred1', 'sacred2'], allBounds);
|
|
513
|
+
// Sacred nodes should never be moved
|
|
514
|
+
expect(positions.has('sacred1')).to.be.false;
|
|
515
|
+
expect(positions.has('sacred2')).to.be.false;
|
|
516
|
+
// Collider should be moved
|
|
517
|
+
expect(positions.has('collider')).to.be.true;
|
|
518
|
+
});
|
|
519
|
+
it('does not move a node into another sacred node', () => {
|
|
520
|
+
// Two sacred nodes close together with a collider between them
|
|
521
|
+
// Moving right would overlap sacred2, so it should choose another direction
|
|
522
|
+
const allBounds = [
|
|
523
|
+
{
|
|
524
|
+
uuid: 'sacred1',
|
|
525
|
+
left: 100,
|
|
526
|
+
top: 100,
|
|
527
|
+
right: 200,
|
|
528
|
+
bottom: 200,
|
|
529
|
+
width: 100,
|
|
530
|
+
height: 100
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
uuid: 'sacred2',
|
|
534
|
+
left: 240,
|
|
535
|
+
top: 100,
|
|
536
|
+
right: 340,
|
|
537
|
+
bottom: 200,
|
|
538
|
+
width: 100,
|
|
539
|
+
height: 100
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
uuid: 'collider',
|
|
543
|
+
left: 150,
|
|
544
|
+
top: 100,
|
|
545
|
+
right: 250,
|
|
546
|
+
bottom: 200,
|
|
547
|
+
width: 100,
|
|
548
|
+
height: 100
|
|
549
|
+
}
|
|
550
|
+
];
|
|
551
|
+
const positions = calculateReflowPositions(['sacred1', 'sacred2'], allBounds);
|
|
552
|
+
expect(positions.has('collider')).to.be.true;
|
|
553
|
+
const newPos = positions.get('collider');
|
|
554
|
+
// Should not overlap either sacred node after reflow
|
|
555
|
+
const newBounds = {
|
|
556
|
+
uuid: 'collider',
|
|
557
|
+
left: newPos.left,
|
|
558
|
+
top: newPos.top,
|
|
559
|
+
right: newPos.left + 100,
|
|
560
|
+
bottom: newPos.top + 100,
|
|
468
561
|
width: 100,
|
|
469
562
|
height: 100
|
|
470
563
|
};
|
|
564
|
+
expect(nodesOverlap(newBounds, allBounds[0])).to.be.false;
|
|
565
|
+
expect(nodesOverlap(newBounds, allBounds[1])).to.be.false;
|
|
566
|
+
});
|
|
567
|
+
it('snaps reflow positions to grid', () => {
|
|
471
568
|
const allBounds = [
|
|
472
|
-
movedBounds,
|
|
473
569
|
{
|
|
474
|
-
uuid: '
|
|
570
|
+
uuid: 'sacred',
|
|
475
571
|
left: 100,
|
|
476
572
|
top: 100,
|
|
477
573
|
right: 200,
|
|
@@ -480,151 +576,308 @@ describe('Collision Detection Utilities', () => {
|
|
|
480
576
|
height: 100
|
|
481
577
|
},
|
|
482
578
|
{
|
|
483
|
-
uuid: '
|
|
579
|
+
uuid: 'collider',
|
|
580
|
+
left: 100,
|
|
581
|
+
top: 150,
|
|
582
|
+
right: 200,
|
|
583
|
+
bottom: 250,
|
|
584
|
+
width: 100,
|
|
585
|
+
height: 100
|
|
586
|
+
}
|
|
587
|
+
];
|
|
588
|
+
const positions = calculateReflowPositions(['sacred'], allBounds);
|
|
589
|
+
expect(positions.has('collider')).to.be.true;
|
|
590
|
+
const newPos = positions.get('collider');
|
|
591
|
+
// Both coordinates should be multiples of 20 (grid size)
|
|
592
|
+
expect(newPos.left % 20).to.equal(0);
|
|
593
|
+
expect(newPos.top % 20).to.equal(0);
|
|
594
|
+
});
|
|
595
|
+
it('clamps positions to zero (no negative coordinates)', () => {
|
|
596
|
+
// Sacred node near top-left, collider above it
|
|
597
|
+
// Moving up would go negative, so it should fall back
|
|
598
|
+
const allBounds = [
|
|
599
|
+
{
|
|
600
|
+
uuid: 'sacred',
|
|
601
|
+
left: 0,
|
|
602
|
+
top: 0,
|
|
603
|
+
right: 200,
|
|
604
|
+
bottom: 200,
|
|
605
|
+
width: 200,
|
|
606
|
+
height: 200
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
uuid: 'collider',
|
|
610
|
+
left: 0,
|
|
611
|
+
top: 100,
|
|
612
|
+
right: 200,
|
|
613
|
+
bottom: 200,
|
|
614
|
+
width: 200,
|
|
615
|
+
height: 100
|
|
616
|
+
}
|
|
617
|
+
];
|
|
618
|
+
const positions = calculateReflowPositions(['sacred'], allBounds);
|
|
619
|
+
expect(positions.has('collider')).to.be.true;
|
|
620
|
+
const newPos = positions.get('collider');
|
|
621
|
+
expect(newPos.left).to.be.at.least(0);
|
|
622
|
+
expect(newPos.top).to.be.at.least(0);
|
|
623
|
+
});
|
|
624
|
+
it('does not move a lower node above the sacred node', () => {
|
|
625
|
+
// Collider is below sacred (collider.top > sacred.top)
|
|
626
|
+
// Up should be filtered, so collider goes down or to the side
|
|
627
|
+
const allBounds = [
|
|
628
|
+
{
|
|
629
|
+
uuid: 'sacred',
|
|
630
|
+
left: 100,
|
|
631
|
+
top: 100,
|
|
632
|
+
right: 200,
|
|
633
|
+
bottom: 200,
|
|
634
|
+
width: 100,
|
|
635
|
+
height: 100
|
|
636
|
+
},
|
|
637
|
+
{
|
|
638
|
+
uuid: 'collider',
|
|
484
639
|
left: 100,
|
|
485
640
|
top: 180,
|
|
486
641
|
right: 200,
|
|
487
642
|
bottom: 280,
|
|
488
643
|
width: 100,
|
|
489
644
|
height: 100
|
|
645
|
+
}
|
|
646
|
+
];
|
|
647
|
+
const positions = calculateReflowPositions(['sacred'], allBounds);
|
|
648
|
+
expect(positions.has('collider')).to.be.true;
|
|
649
|
+
const newPos = positions.get('collider');
|
|
650
|
+
// Should NOT move above the sacred node's top
|
|
651
|
+
expect(newPos.top).to.be.at.least(100);
|
|
652
|
+
});
|
|
653
|
+
it('does not move a right-of node left of the sacred node', () => {
|
|
654
|
+
// Collider is to the right of sacred (collider.left > sacred.left)
|
|
655
|
+
// Left should be filtered
|
|
656
|
+
const allBounds = [
|
|
657
|
+
{
|
|
658
|
+
uuid: 'sacred',
|
|
659
|
+
left: 100,
|
|
660
|
+
top: 100,
|
|
661
|
+
right: 200,
|
|
662
|
+
bottom: 200,
|
|
663
|
+
width: 100,
|
|
664
|
+
height: 100
|
|
665
|
+
},
|
|
666
|
+
{
|
|
667
|
+
uuid: 'collider',
|
|
668
|
+
left: 180,
|
|
669
|
+
top: 100,
|
|
670
|
+
right: 280,
|
|
671
|
+
bottom: 200,
|
|
672
|
+
width: 100,
|
|
673
|
+
height: 100
|
|
674
|
+
}
|
|
675
|
+
];
|
|
676
|
+
const positions = calculateReflowPositions(['sacred'], allBounds);
|
|
677
|
+
expect(positions.has('collider')).to.be.true;
|
|
678
|
+
const newPos = positions.get('collider');
|
|
679
|
+
// Should NOT move left of the sacred node's left
|
|
680
|
+
expect(newPos.left).to.be.at.least(100);
|
|
681
|
+
});
|
|
682
|
+
it('prefers vertical for wide overlap (vertical collision)', () => {
|
|
683
|
+
// Nodes stacked: same horizontal position, slight vertical overlap
|
|
684
|
+
// Overlap: 100w x 30h (wider than tall) = vertical collision = prefer up/down
|
|
685
|
+
const allBounds = [
|
|
686
|
+
{
|
|
687
|
+
uuid: 'sacred',
|
|
688
|
+
left: 100,
|
|
689
|
+
top: 100,
|
|
690
|
+
right: 200,
|
|
691
|
+
bottom: 200,
|
|
692
|
+
width: 100,
|
|
693
|
+
height: 100
|
|
694
|
+
},
|
|
695
|
+
{
|
|
696
|
+
uuid: 'collider',
|
|
697
|
+
left: 100,
|
|
698
|
+
top: 170,
|
|
699
|
+
right: 200,
|
|
700
|
+
bottom: 270,
|
|
701
|
+
width: 100,
|
|
702
|
+
height: 100
|
|
703
|
+
}
|
|
704
|
+
];
|
|
705
|
+
const positions = calculateReflowPositions(['sacred'], allBounds);
|
|
706
|
+
expect(positions.has('collider')).to.be.true;
|
|
707
|
+
const newPos = positions.get('collider');
|
|
708
|
+
// Should move vertically (down since collider is below)
|
|
709
|
+
expect(newPos.top).to.be.greaterThan(200);
|
|
710
|
+
expect(newPos.left).to.equal(100); // horizontal position unchanged
|
|
711
|
+
});
|
|
712
|
+
it('prefers horizontal for tall overlap (horizontal collision)', () => {
|
|
713
|
+
// Nodes side-by-side: same vertical position, slight horizontal overlap
|
|
714
|
+
// Overlap: 30w x 100h (taller than wide) = horizontal collision = prefer left/right
|
|
715
|
+
const allBounds = [
|
|
716
|
+
{
|
|
717
|
+
uuid: 'sacred',
|
|
718
|
+
left: 100,
|
|
719
|
+
top: 100,
|
|
720
|
+
right: 200,
|
|
721
|
+
bottom: 200,
|
|
722
|
+
width: 100,
|
|
723
|
+
height: 100
|
|
724
|
+
},
|
|
725
|
+
{
|
|
726
|
+
uuid: 'collider',
|
|
727
|
+
left: 170,
|
|
728
|
+
top: 100,
|
|
729
|
+
right: 270,
|
|
730
|
+
bottom: 200,
|
|
731
|
+
width: 100,
|
|
732
|
+
height: 100
|
|
733
|
+
}
|
|
734
|
+
];
|
|
735
|
+
const positions = calculateReflowPositions(['sacred'], allBounds);
|
|
736
|
+
expect(positions.has('collider')).to.be.true;
|
|
737
|
+
const newPos = positions.get('collider');
|
|
738
|
+
// Should move horizontally (right since collider is right of sacred)
|
|
739
|
+
expect(newPos.left).to.be.greaterThan(200);
|
|
740
|
+
expect(newPos.top).to.equal(100); // vertical position unchanged
|
|
741
|
+
});
|
|
742
|
+
it('axis bias tolerates a few cascading collisions', () => {
|
|
743
|
+
// Sacred at (100,100)-(200,200), collider at (100,170)-(200,270)
|
|
744
|
+
// Overlap: 100w x 30h = vertical collision = prefer down
|
|
745
|
+
// Two blockers below: moving down causes 2 cascades
|
|
746
|
+
// Moving right causes 0 cascades but is axis-mismatched
|
|
747
|
+
// Axis bias should still prefer down with 2 cascades
|
|
748
|
+
const allBounds = [
|
|
749
|
+
{
|
|
750
|
+
uuid: 'sacred',
|
|
751
|
+
left: 100,
|
|
752
|
+
top: 100,
|
|
753
|
+
right: 200,
|
|
754
|
+
bottom: 200,
|
|
755
|
+
width: 100,
|
|
756
|
+
height: 100
|
|
757
|
+
},
|
|
758
|
+
{
|
|
759
|
+
uuid: 'collider',
|
|
760
|
+
left: 100,
|
|
761
|
+
top: 170,
|
|
762
|
+
right: 200,
|
|
763
|
+
bottom: 270,
|
|
764
|
+
width: 100,
|
|
765
|
+
height: 100
|
|
490
766
|
},
|
|
491
767
|
{
|
|
492
|
-
uuid: '
|
|
768
|
+
uuid: 'blocker1',
|
|
493
769
|
left: 100,
|
|
494
770
|
top: 260,
|
|
495
771
|
right: 200,
|
|
496
772
|
bottom: 360,
|
|
497
773
|
width: 100,
|
|
498
774
|
height: 100
|
|
775
|
+
},
|
|
776
|
+
{
|
|
777
|
+
uuid: 'blocker2',
|
|
778
|
+
left: 100,
|
|
779
|
+
top: 350,
|
|
780
|
+
right: 200,
|
|
781
|
+
bottom: 450,
|
|
782
|
+
width: 100,
|
|
783
|
+
height: 100
|
|
499
784
|
}
|
|
500
785
|
];
|
|
501
|
-
const positions = calculateReflowPositions('
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
expect(
|
|
506
|
-
expect(
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
// Nodes should be stacked vertically with proper spacing
|
|
511
|
-
expect(node1Pos.top).to.be.at.least(170); // 150 (bottom of moved) + 20
|
|
512
|
-
expect(node2Pos.top).to.be.at.least(node1Pos.top + 120); // node1 top + height + spacing
|
|
513
|
-
expect(node3Pos.top).to.be.at.least(node2Pos.top + 120); // node2 top + height + spacing
|
|
514
|
-
});
|
|
515
|
-
it('handles nodes that collide with already repositioned nodes', () => {
|
|
516
|
-
// This test creates a scenario where node3 doesn't collide with the moved node,
|
|
517
|
-
// but collides with node2 which itself got repositioned due to collision with node1
|
|
518
|
-
const movedBounds = {
|
|
519
|
-
uuid: 'moved',
|
|
520
|
-
left: 100,
|
|
521
|
-
top: 100,
|
|
522
|
-
right: 200,
|
|
523
|
-
bottom: 200,
|
|
524
|
-
width: 100,
|
|
525
|
-
height: 100
|
|
526
|
-
};
|
|
786
|
+
const positions = calculateReflowPositions(['sacred'], allBounds);
|
|
787
|
+
expect(positions.has('collider')).to.be.true;
|
|
788
|
+
const newPos = positions.get('collider');
|
|
789
|
+
// Should still prefer down (axis match) despite 2 cascades
|
|
790
|
+
expect(newPos.top).to.be.greaterThan(200);
|
|
791
|
+
expect(newPos.left).to.equal(100);
|
|
792
|
+
});
|
|
793
|
+
it('sacred node yields to existing top node when dropped below its top', () => {
|
|
794
|
+
// Existing node at top of canvas, sacred dropped overlapping from below
|
|
527
795
|
const allBounds = [
|
|
528
|
-
movedBounds,
|
|
529
796
|
{
|
|
530
|
-
uuid: '
|
|
797
|
+
uuid: 'existing',
|
|
531
798
|
left: 100,
|
|
532
|
-
top:
|
|
799
|
+
top: 0,
|
|
533
800
|
right: 200,
|
|
534
|
-
bottom:
|
|
801
|
+
bottom: 100,
|
|
535
802
|
width: 100,
|
|
536
803
|
height: 100
|
|
537
804
|
},
|
|
538
805
|
{
|
|
539
|
-
uuid: '
|
|
806
|
+
uuid: 'dropped',
|
|
540
807
|
left: 100,
|
|
541
|
-
top:
|
|
808
|
+
top: 50,
|
|
542
809
|
right: 200,
|
|
543
|
-
bottom:
|
|
810
|
+
bottom: 150,
|
|
544
811
|
width: 100,
|
|
545
812
|
height: 100
|
|
546
813
|
}
|
|
547
814
|
];
|
|
548
|
-
const positions = calculateReflowPositions('
|
|
549
|
-
//
|
|
550
|
-
|
|
551
|
-
expect(positions.has('
|
|
552
|
-
expect(positions.has('
|
|
553
|
-
const
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
//
|
|
558
|
-
// This tests the code path where we check against already repositioned nodes
|
|
559
|
-
expect(node2Pos.top).to.be.at.least(node1Pos.top + 120);
|
|
560
|
-
});
|
|
561
|
-
it('maintains horizontal position while moving vertically', () => {
|
|
562
|
-
const movedBounds = {
|
|
563
|
-
uuid: 'moved',
|
|
564
|
-
left: 100,
|
|
565
|
-
top: 100,
|
|
566
|
-
right: 200,
|
|
567
|
-
bottom: 200,
|
|
568
|
-
width: 100,
|
|
569
|
-
height: 100
|
|
570
|
-
};
|
|
815
|
+
const positions = calculateReflowPositions(['dropped'], allBounds);
|
|
816
|
+
// Sacred (dropped) should yield since it didn't drop above existing
|
|
817
|
+
// and existing has no room to move up
|
|
818
|
+
expect(positions.has('dropped')).to.be.true;
|
|
819
|
+
expect(positions.has('existing')).to.be.false;
|
|
820
|
+
const newPos = positions.get('dropped');
|
|
821
|
+
expect(newPos.top).to.be.greaterThanOrEqual(100); // moved below existing
|
|
822
|
+
});
|
|
823
|
+
it('sacred keeps position when dropped above existing node', () => {
|
|
824
|
+
// Sacred node dropped above existing node - sacred gets priority
|
|
571
825
|
const allBounds = [
|
|
572
|
-
movedBounds,
|
|
573
826
|
{
|
|
574
|
-
uuid: '
|
|
575
|
-
left:
|
|
576
|
-
top:
|
|
577
|
-
right:
|
|
578
|
-
bottom:
|
|
827
|
+
uuid: 'existing',
|
|
828
|
+
left: 100,
|
|
829
|
+
top: 50,
|
|
830
|
+
right: 200,
|
|
831
|
+
bottom: 150,
|
|
832
|
+
width: 100,
|
|
833
|
+
height: 100
|
|
834
|
+
},
|
|
835
|
+
{
|
|
836
|
+
uuid: 'dropped',
|
|
837
|
+
left: 100,
|
|
838
|
+
top: 0,
|
|
839
|
+
right: 200,
|
|
840
|
+
bottom: 100,
|
|
579
841
|
width: 100,
|
|
580
842
|
height: 100
|
|
581
843
|
}
|
|
582
844
|
];
|
|
583
|
-
const positions = calculateReflowPositions('
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
expect(
|
|
845
|
+
const positions = calculateReflowPositions(['dropped'], allBounds);
|
|
846
|
+
// Sacred dropped above existing, so it keeps priority
|
|
847
|
+
expect(positions.has('dropped')).to.be.false;
|
|
848
|
+
expect(positions.has('existing')).to.be.true;
|
|
587
849
|
});
|
|
588
|
-
it('
|
|
589
|
-
|
|
590
|
-
uuid: 'moved',
|
|
591
|
-
left: 100,
|
|
592
|
-
top: 100,
|
|
593
|
-
right: 200,
|
|
594
|
-
bottom: 200,
|
|
595
|
-
width: 100,
|
|
596
|
-
height: 100
|
|
597
|
-
};
|
|
850
|
+
it('sacred keeps position when dropped at same top as existing node', () => {
|
|
851
|
+
// Both at top=0 - sacred keeps priority since it's not below existing
|
|
598
852
|
const allBounds = [
|
|
599
|
-
movedBounds,
|
|
600
853
|
{
|
|
601
|
-
uuid: '
|
|
854
|
+
uuid: 'existing',
|
|
602
855
|
left: 100,
|
|
603
|
-
top:
|
|
856
|
+
top: 0,
|
|
604
857
|
right: 200,
|
|
605
|
-
bottom:
|
|
858
|
+
bottom: 100,
|
|
859
|
+
width: 100,
|
|
860
|
+
height: 100
|
|
861
|
+
},
|
|
862
|
+
{
|
|
863
|
+
uuid: 'dropped',
|
|
864
|
+
left: 100,
|
|
865
|
+
top: 0,
|
|
866
|
+
right: 200,
|
|
867
|
+
bottom: 100,
|
|
606
868
|
width: 100,
|
|
607
869
|
height: 100
|
|
608
870
|
}
|
|
609
871
|
];
|
|
610
|
-
const positions = calculateReflowPositions('
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
expect(
|
|
872
|
+
const positions = calculateReflowPositions(['dropped'], allBounds);
|
|
873
|
+
// Sacred at same top keeps priority - existing node moves
|
|
874
|
+
expect(positions.has('dropped')).to.be.false;
|
|
875
|
+
expect(positions.has('existing')).to.be.true;
|
|
614
876
|
});
|
|
615
877
|
});
|
|
616
878
|
describe('edge cases', () => {
|
|
617
879
|
it('handles empty allBounds array', () => {
|
|
618
|
-
const
|
|
619
|
-
uuid: 'moved',
|
|
620
|
-
left: 100,
|
|
621
|
-
top: 100,
|
|
622
|
-
right: 200,
|
|
623
|
-
bottom: 200,
|
|
624
|
-
width: 100,
|
|
625
|
-
height: 100
|
|
626
|
-
};
|
|
627
|
-
const positions = calculateReflowPositions('moved', movedBounds, [], false);
|
|
880
|
+
const positions = calculateReflowPositions(['moved'], []);
|
|
628
881
|
expect(positions.size).to.equal(0);
|
|
629
882
|
});
|
|
630
883
|
it('handles single node (no other nodes to collide with)', () => {
|
|
@@ -637,7 +890,7 @@ describe('Collision Detection Utilities', () => {
|
|
|
637
890
|
width: 100,
|
|
638
891
|
height: 100
|
|
639
892
|
};
|
|
640
|
-
const positions = calculateReflowPositions('moved',
|
|
893
|
+
const positions = calculateReflowPositions(['moved'], [movedBounds]);
|
|
641
894
|
expect(positions.size).to.equal(0);
|
|
642
895
|
});
|
|
643
896
|
it('prevents infinite loops with complex collisions', () => {
|
|
@@ -664,7 +917,7 @@ describe('Collision Detection Utilities', () => {
|
|
|
664
917
|
});
|
|
665
918
|
}
|
|
666
919
|
// Should complete without hanging
|
|
667
|
-
const positions = calculateReflowPositions('moved',
|
|
920
|
+
const positions = calculateReflowPositions(['moved'], allBounds);
|
|
668
921
|
// Should have resolved some collisions
|
|
669
922
|
expect(positions.size).to.be.greaterThan(0);
|
|
670
923
|
});
|