@nyaruka/temba-components 0.133.0 → 0.134.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.
Files changed (72) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/demo/components/webchat/example.html +1 -1
  3. package/dist/locales/es.js +5 -5
  4. package/dist/locales/es.js.map +1 -1
  5. package/dist/locales/fr.js +5 -5
  6. package/dist/locales/fr.js.map +1 -1
  7. package/dist/locales/locale-codes.js +2 -11
  8. package/dist/locales/locale-codes.js.map +1 -1
  9. package/dist/locales/pt.js +5 -5
  10. package/dist/locales/pt.js.map +1 -1
  11. package/dist/temba-components.js +307 -259
  12. package/dist/temba-components.js.map +1 -1
  13. package/out-tsc/src/display/Chat.js +223 -90
  14. package/out-tsc/src/display/Chat.js.map +1 -1
  15. package/out-tsc/src/display/TembaUser.js +3 -3
  16. package/out-tsc/src/display/TembaUser.js.map +1 -1
  17. package/out-tsc/src/events.js.map +1 -1
  18. package/out-tsc/src/flow/CanvasNode.js +8 -0
  19. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  20. package/out-tsc/src/flow/Editor.js +117 -28
  21. package/out-tsc/src/flow/Editor.js.map +1 -1
  22. package/out-tsc/src/flow/utils.js +141 -0
  23. package/out-tsc/src/flow/utils.js.map +1 -1
  24. package/out-tsc/src/interfaces.js.map +1 -1
  25. package/out-tsc/src/live/ContactChat.js +122 -170
  26. package/out-tsc/src/live/ContactChat.js.map +1 -1
  27. package/out-tsc/src/locales/es.js +5 -5
  28. package/out-tsc/src/locales/es.js.map +1 -1
  29. package/out-tsc/src/locales/fr.js +5 -5
  30. package/out-tsc/src/locales/fr.js.map +1 -1
  31. package/out-tsc/src/locales/locale-codes.js +2 -11
  32. package/out-tsc/src/locales/locale-codes.js.map +1 -1
  33. package/out-tsc/src/locales/pt.js +5 -5
  34. package/out-tsc/src/locales/pt.js.map +1 -1
  35. package/out-tsc/src/store/AppState.js +3 -0
  36. package/out-tsc/src/store/AppState.js.map +1 -1
  37. package/out-tsc/src/store/Store.js +5 -5
  38. package/out-tsc/src/store/Store.js.map +1 -1
  39. package/out-tsc/src/webchat/WebChat.js +22 -9
  40. package/out-tsc/src/webchat/WebChat.js.map +1 -1
  41. package/out-tsc/test/actions/send_broadcast.test.js +9 -4
  42. package/out-tsc/test/actions/send_broadcast.test.js.map +1 -1
  43. package/out-tsc/test/temba-flow-collision.test.js +673 -0
  44. package/out-tsc/test/temba-flow-collision.test.js.map +1 -0
  45. package/out-tsc/test/temba-flow-editor-node.test.js +128 -42
  46. package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
  47. package/package.json +1 -1
  48. package/screenshots/truth/contacts/chat-failure.png +0 -0
  49. package/screenshots/truth/contacts/chat-for-archived-contact.png +0 -0
  50. package/screenshots/truth/contacts/chat-for-blocked-contact.png +0 -0
  51. package/screenshots/truth/contacts/chat-for-stopped-contact.png +0 -0
  52. package/screenshots/truth/contacts/chat-sends-attachments-only.png +0 -0
  53. package/screenshots/truth/contacts/chat-sends-text-and-attachments.png +0 -0
  54. package/screenshots/truth/contacts/chat-sends-text-only.png +0 -0
  55. package/src/display/Chat.ts +303 -129
  56. package/src/display/TembaUser.ts +3 -2
  57. package/src/events.ts +11 -8
  58. package/src/flow/CanvasNode.ts +10 -0
  59. package/src/flow/Editor.ts +156 -28
  60. package/src/flow/utils.ts +207 -1
  61. package/src/interfaces.ts +7 -0
  62. package/src/live/ContactChat.ts +129 -180
  63. package/src/locales/es.ts +13 -18
  64. package/src/locales/fr.ts +13 -18
  65. package/src/locales/locale-codes.ts +2 -11
  66. package/src/locales/pt.ts +13 -18
  67. package/src/store/AppState.ts +2 -0
  68. package/src/store/Store.ts +5 -5
  69. package/src/webchat/WebChat.ts +24 -10
  70. package/test/actions/send_broadcast.test.ts +2 -1
  71. package/test/temba-flow-collision.test.ts +833 -0
  72. package/test/temba-flow-editor-node.test.ts +142 -47
@@ -0,0 +1,673 @@
1
+ import { expect } from '@open-wc/testing';
2
+ import { getNodeBounds, nodesOverlap, detectCollisions, calculateReflowPositions } from '../src/flow/utils';
3
+ describe('Collision Detection Utilities', () => {
4
+ describe('getNodeBounds', () => {
5
+ it('returns null when element not found', () => {
6
+ const position = { left: 100, top: 200 };
7
+ const bounds = getNodeBounds('nonexistent-uuid', position);
8
+ expect(bounds).to.be.null;
9
+ });
10
+ it('calculates bounds correctly from element', () => {
11
+ // Create a mock element
12
+ const mockElement = document.createElement('div');
13
+ mockElement.id = 'test-node';
14
+ mockElement.style.width = '200px';
15
+ mockElement.style.height = '150px';
16
+ document.body.appendChild(mockElement);
17
+ const position = { left: 100, top: 200 };
18
+ const bounds = getNodeBounds('test-node', position, mockElement);
19
+ expect(bounds).to.not.be.null;
20
+ expect(bounds.uuid).to.equal('test-node');
21
+ expect(bounds.left).to.equal(100);
22
+ expect(bounds.top).to.equal(200);
23
+ expect(bounds.right).to.equal(300); // left + width
24
+ expect(bounds.bottom).to.equal(350); // top + height
25
+ expect(bounds.width).to.equal(200);
26
+ expect(bounds.height).to.equal(150);
27
+ document.body.removeChild(mockElement);
28
+ });
29
+ });
30
+ describe('nodesOverlap', () => {
31
+ it('detects overlapping nodes horizontally and vertically', () => {
32
+ const bounds1 = {
33
+ uuid: 'node1',
34
+ left: 100,
35
+ top: 100,
36
+ right: 200,
37
+ bottom: 200,
38
+ width: 100,
39
+ height: 100
40
+ };
41
+ const bounds2 = {
42
+ uuid: 'node2',
43
+ left: 150,
44
+ top: 150,
45
+ right: 250,
46
+ bottom: 250,
47
+ width: 100,
48
+ height: 100
49
+ };
50
+ expect(nodesOverlap(bounds1, bounds2)).to.be.true;
51
+ });
52
+ it('detects non-overlapping nodes to the right', () => {
53
+ const bounds1 = {
54
+ uuid: 'node1',
55
+ left: 100,
56
+ top: 100,
57
+ right: 200,
58
+ bottom: 200,
59
+ width: 100,
60
+ height: 100
61
+ };
62
+ const bounds2 = {
63
+ uuid: 'node2',
64
+ left: 300,
65
+ top: 100,
66
+ right: 400,
67
+ bottom: 200,
68
+ width: 100,
69
+ height: 100
70
+ };
71
+ expect(nodesOverlap(bounds1, bounds2)).to.be.false;
72
+ });
73
+ it('detects non-overlapping nodes below', () => {
74
+ const bounds1 = {
75
+ uuid: 'node1',
76
+ left: 100,
77
+ top: 100,
78
+ right: 200,
79
+ bottom: 200,
80
+ width: 100,
81
+ height: 100
82
+ };
83
+ const bounds2 = {
84
+ uuid: 'node2',
85
+ left: 100,
86
+ top: 300,
87
+ right: 200,
88
+ bottom: 400,
89
+ width: 100,
90
+ height: 100
91
+ };
92
+ expect(nodesOverlap(bounds1, bounds2)).to.be.false;
93
+ });
94
+ it('detects edge touching as overlapping (within buffer)', () => {
95
+ const bounds1 = {
96
+ uuid: 'node1',
97
+ left: 100,
98
+ top: 100,
99
+ right: 200,
100
+ bottom: 200,
101
+ width: 100,
102
+ height: 100
103
+ };
104
+ const bounds2 = {
105
+ uuid: 'node2',
106
+ left: 200,
107
+ top: 100,
108
+ right: 300,
109
+ bottom: 200,
110
+ width: 100,
111
+ height: 100
112
+ };
113
+ // edges touching should be considered overlapping due to buffer
114
+ expect(nodesOverlap(bounds1, bounds2)).to.be.true;
115
+ });
116
+ it('detects partial vertical overlap', () => {
117
+ const bounds1 = {
118
+ uuid: 'node1',
119
+ left: 100,
120
+ top: 100,
121
+ right: 200,
122
+ bottom: 200,
123
+ width: 100,
124
+ height: 100
125
+ };
126
+ const bounds2 = {
127
+ uuid: 'node2',
128
+ left: 150,
129
+ top: 50,
130
+ right: 250,
131
+ bottom: 150,
132
+ width: 100,
133
+ height: 100
134
+ };
135
+ expect(nodesOverlap(bounds1, bounds2)).to.be.true;
136
+ });
137
+ });
138
+ describe('detectCollisions', () => {
139
+ it('returns empty array when no collisions', () => {
140
+ const targetBounds = {
141
+ uuid: 'target',
142
+ left: 100,
143
+ top: 100,
144
+ right: 200,
145
+ bottom: 200,
146
+ width: 100,
147
+ height: 100
148
+ };
149
+ const allBounds = [
150
+ {
151
+ uuid: 'node1',
152
+ left: 300,
153
+ top: 100,
154
+ right: 400,
155
+ bottom: 200,
156
+ width: 100,
157
+ height: 100
158
+ },
159
+ {
160
+ uuid: 'node2',
161
+ left: 100,
162
+ top: 300,
163
+ right: 200,
164
+ bottom: 400,
165
+ width: 100,
166
+ height: 100
167
+ }
168
+ ];
169
+ const collisions = detectCollisions(targetBounds, allBounds);
170
+ expect(collisions).to.have.length(0);
171
+ });
172
+ it('detects single collision', () => {
173
+ const targetBounds = {
174
+ uuid: 'target',
175
+ left: 100,
176
+ top: 100,
177
+ right: 200,
178
+ bottom: 200,
179
+ width: 100,
180
+ height: 100
181
+ };
182
+ const allBounds = [
183
+ {
184
+ uuid: 'node1',
185
+ left: 150,
186
+ top: 150,
187
+ right: 250,
188
+ bottom: 250,
189
+ width: 100,
190
+ height: 100
191
+ },
192
+ {
193
+ uuid: 'node2',
194
+ left: 300,
195
+ top: 300,
196
+ right: 400,
197
+ bottom: 400,
198
+ width: 100,
199
+ height: 100
200
+ }
201
+ ];
202
+ const collisions = detectCollisions(targetBounds, allBounds);
203
+ expect(collisions).to.have.length(1);
204
+ expect(collisions[0].uuid).to.equal('node1');
205
+ });
206
+ it('detects multiple collisions', () => {
207
+ const targetBounds = {
208
+ uuid: 'target',
209
+ left: 100,
210
+ top: 100,
211
+ right: 200,
212
+ bottom: 200,
213
+ width: 100,
214
+ height: 100
215
+ };
216
+ const allBounds = [
217
+ {
218
+ uuid: 'node1',
219
+ left: 150,
220
+ top: 150,
221
+ right: 250,
222
+ bottom: 250,
223
+ width: 100,
224
+ height: 100
225
+ },
226
+ {
227
+ uuid: 'node2',
228
+ left: 50,
229
+ top: 50,
230
+ right: 150,
231
+ bottom: 150,
232
+ width: 100,
233
+ height: 100
234
+ }
235
+ ];
236
+ const collisions = detectCollisions(targetBounds, allBounds);
237
+ expect(collisions).to.have.length(2);
238
+ });
239
+ it('excludes target node from collisions', () => {
240
+ const targetBounds = {
241
+ uuid: 'target',
242
+ left: 100,
243
+ top: 100,
244
+ right: 200,
245
+ bottom: 200,
246
+ width: 100,
247
+ height: 100
248
+ };
249
+ const allBounds = [
250
+ targetBounds,
251
+ {
252
+ uuid: 'node1',
253
+ left: 150,
254
+ top: 150,
255
+ right: 250,
256
+ bottom: 250,
257
+ width: 100,
258
+ height: 100
259
+ }
260
+ ];
261
+ const collisions = detectCollisions(targetBounds, allBounds);
262
+ expect(collisions).to.have.length(1);
263
+ expect(collisions[0].uuid).to.equal('node1');
264
+ });
265
+ });
266
+ describe('calculateReflowPositions', () => {
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
+ const allBounds = [
278
+ movedBounds,
279
+ {
280
+ uuid: 'node1',
281
+ left: 300,
282
+ top: 100,
283
+ right: 400,
284
+ bottom: 200,
285
+ width: 100,
286
+ height: 100
287
+ }
288
+ ];
289
+ const positions = calculateReflowPositions('moved', movedBounds, allBounds, false);
290
+ expect(positions.size).to.equal(0);
291
+ });
292
+ it('moves colliding node down', () => {
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
+ };
302
+ const allBounds = [
303
+ movedBounds,
304
+ {
305
+ uuid: 'node1',
306
+ left: 150,
307
+ top: 150,
308
+ right: 250,
309
+ bottom: 250,
310
+ width: 100,
311
+ height: 100
312
+ }
313
+ ];
314
+ const positions = calculateReflowPositions('moved', movedBounds, allBounds, false);
315
+ expect(positions.size).to.equal(1);
316
+ expect(positions.has('node1')).to.be.true;
317
+ const newPos = positions.get('node1');
318
+ expect(newPos.left).to.equal(150); // left unchanged
319
+ expect(newPos.top).to.be.greaterThan(200); // moved below the moved node
320
+ });
321
+ it('handles droppedBelowMidpoint by moving dropped node down', () => {
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
+ };
331
+ const allBounds = [
332
+ movedBounds,
333
+ {
334
+ uuid: 'existing',
335
+ left: 100,
336
+ top: 100,
337
+ right: 200,
338
+ bottom: 200,
339
+ width: 100,
340
+ height: 100
341
+ }
342
+ ];
343
+ const positions = calculateReflowPositions('moved', movedBounds, allBounds, true // droppedBelowMidpoint
344
+ );
345
+ expect(positions.size).to.equal(1);
346
+ expect(positions.has('moved')).to.be.true;
347
+ const newPos = positions.get('moved');
348
+ expect(newPos.top).to.be.greaterThan(200); // moved below existing node
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
+ };
363
+ const allBounds = [
364
+ movedBounds,
365
+ {
366
+ uuid: 'existing',
367
+ left: 100,
368
+ top: 100,
369
+ right: 200,
370
+ bottom: 200,
371
+ width: 100,
372
+ height: 100
373
+ }
374
+ ];
375
+ const positions = calculateReflowPositions('dropped', movedBounds, allBounds, false // dropped node's bottom is above target midpoint, dropped node gets priority
376
+ );
377
+ // Existing node should be moved down
378
+ expect(positions.has('existing')).to.be.true;
379
+ expect(positions.has('dropped')).to.be.false; // dropped keeps its position
380
+ const existingNewPos = positions.get('existing');
381
+ expect(existingNewPos.top).to.be.greaterThan(180); // moved below dropped node
382
+ });
383
+ it('gives priority to existing node when dropped below midpoint', () => {
384
+ // Dropped node overlaps with top of existing node
385
+ // Bottom of dropped (220) is below midpoint of existing (150)
386
+ // So existing node keeps position, dropped moves down
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
+ };
396
+ const allBounds = [
397
+ movedBounds,
398
+ {
399
+ uuid: 'existing',
400
+ left: 100,
401
+ top: 100,
402
+ right: 200,
403
+ bottom: 200,
404
+ width: 100,
405
+ height: 100
406
+ }
407
+ ];
408
+ const positions = calculateReflowPositions('dropped', movedBounds, allBounds, true // dropped node's bottom is below target midpoint, target gets priority
409
+ );
410
+ // Dropped node should be moved down
411
+ expect(positions.has('dropped')).to.be.true;
412
+ expect(positions.has('existing')).to.be.false; // existing keeps its position
413
+ const droppedNewPos = positions.get('dropped');
414
+ expect(droppedNewPos.top).to.be.greaterThan(200); // moved below existing node
415
+ });
416
+ 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
+ const allBounds = [
427
+ movedBounds,
428
+ {
429
+ uuid: 'node1',
430
+ left: 100,
431
+ top: 150,
432
+ right: 200,
433
+ bottom: 250,
434
+ width: 100,
435
+ height: 100
436
+ },
437
+ {
438
+ uuid: 'node2',
439
+ left: 100,
440
+ top: 200,
441
+ right: 200,
442
+ bottom: 300,
443
+ width: 100,
444
+ height: 100
445
+ }
446
+ ];
447
+ const positions = calculateReflowPositions('moved', movedBounds, allBounds, false);
448
+ // Both nodes should be repositioned to avoid collision
449
+ expect(positions.size).to.be.greaterThan(0);
450
+ // Check that moved nodes maintain vertical order and spacing
451
+ if (positions.has('node1') && positions.has('node2')) {
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
+ }
457
+ });
458
+ it('handles multiple iterations for complex cascading collisions', () => {
459
+ // Test case that requires multiple iterations to resolve all collisions
460
+ // This scenario has nodes that initially don't collide with moved node,
461
+ // but will collide with other nodes that get moved
462
+ const movedBounds = {
463
+ uuid: 'moved',
464
+ left: 100,
465
+ top: 50,
466
+ right: 200,
467
+ bottom: 150,
468
+ width: 100,
469
+ height: 100
470
+ };
471
+ const allBounds = [
472
+ movedBounds,
473
+ {
474
+ uuid: 'node1',
475
+ left: 100,
476
+ top: 100,
477
+ right: 200,
478
+ bottom: 200,
479
+ width: 100,
480
+ height: 100
481
+ },
482
+ {
483
+ uuid: 'node2',
484
+ left: 100,
485
+ top: 180,
486
+ right: 200,
487
+ bottom: 280,
488
+ width: 100,
489
+ height: 100
490
+ },
491
+ {
492
+ uuid: 'node3',
493
+ left: 100,
494
+ top: 260,
495
+ right: 200,
496
+ bottom: 360,
497
+ width: 100,
498
+ height: 100
499
+ }
500
+ ];
501
+ const positions = calculateReflowPositions('moved', movedBounds, allBounds, false);
502
+ // All three nodes should be repositioned to be stacked below the moved node
503
+ expect(positions.size).to.equal(3);
504
+ expect(positions.has('node1')).to.be.true;
505
+ expect(positions.has('node2')).to.be.true;
506
+ expect(positions.has('node3')).to.be.true;
507
+ const node1Pos = positions.get('node1');
508
+ const node2Pos = positions.get('node2');
509
+ const node3Pos = positions.get('node3');
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
+ };
527
+ const allBounds = [
528
+ movedBounds,
529
+ {
530
+ uuid: 'node1',
531
+ left: 100,
532
+ top: 150,
533
+ right: 200,
534
+ bottom: 250,
535
+ width: 100,
536
+ height: 100
537
+ },
538
+ {
539
+ uuid: 'node2',
540
+ left: 100,
541
+ top: 240,
542
+ right: 200,
543
+ bottom: 340,
544
+ width: 100,
545
+ height: 100
546
+ }
547
+ ];
548
+ const positions = calculateReflowPositions('moved', movedBounds, allBounds, false);
549
+ // Both nodes should be moved
550
+ expect(positions.size).to.equal(2);
551
+ expect(positions.has('node1')).to.be.true;
552
+ expect(positions.has('node2')).to.be.true;
553
+ const node1Pos = positions.get('node1');
554
+ const node2Pos = positions.get('node2');
555
+ // node1 should be moved below moved node
556
+ expect(node1Pos.top).to.be.at.least(220); // 200 + 20
557
+ // node2 should be moved below the new position of node1
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
+ };
571
+ const allBounds = [
572
+ movedBounds,
573
+ {
574
+ uuid: 'node1',
575
+ left: 150,
576
+ top: 150,
577
+ right: 250,
578
+ bottom: 250,
579
+ width: 100,
580
+ height: 100
581
+ }
582
+ ];
583
+ const positions = calculateReflowPositions('moved', movedBounds, allBounds, false);
584
+ const newPos = positions.get('node1');
585
+ // Horizontal position should remain unchanged
586
+ expect(newPos.left).to.equal(150);
587
+ });
588
+ it('adds minimum spacing between nodes', () => {
589
+ const movedBounds = {
590
+ uuid: 'moved',
591
+ left: 100,
592
+ top: 100,
593
+ right: 200,
594
+ bottom: 200,
595
+ width: 100,
596
+ height: 100
597
+ };
598
+ const allBounds = [
599
+ movedBounds,
600
+ {
601
+ uuid: 'node1',
602
+ left: 100,
603
+ top: 150,
604
+ right: 200,
605
+ bottom: 250,
606
+ width: 100,
607
+ height: 100
608
+ }
609
+ ];
610
+ const positions = calculateReflowPositions('moved', movedBounds, allBounds, false);
611
+ const newPos = positions.get('node1');
612
+ // Should have at least 20px spacing (MIN_NODE_SPACING)
613
+ expect(newPos.top).to.be.at.least(220); // 200 (bottom of moved) + 20
614
+ });
615
+ });
616
+ describe('edge cases', () => {
617
+ it('handles empty allBounds array', () => {
618
+ const movedBounds = {
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);
628
+ expect(positions.size).to.equal(0);
629
+ });
630
+ it('handles single node (no other nodes to collide with)', () => {
631
+ const movedBounds = {
632
+ uuid: 'moved',
633
+ left: 100,
634
+ top: 100,
635
+ right: 200,
636
+ bottom: 200,
637
+ width: 100,
638
+ height: 100
639
+ };
640
+ const positions = calculateReflowPositions('moved', movedBounds, [movedBounds], false);
641
+ expect(positions.size).to.equal(0);
642
+ });
643
+ it('prevents infinite loops with complex collisions', () => {
644
+ const movedBounds = {
645
+ uuid: 'moved',
646
+ left: 100,
647
+ top: 100,
648
+ right: 200,
649
+ bottom: 200,
650
+ width: 100,
651
+ height: 100
652
+ };
653
+ // Create a complex scenario with many overlapping nodes
654
+ const allBounds = [movedBounds];
655
+ for (let i = 0; i < 20; i++) {
656
+ allBounds.push({
657
+ uuid: `node${i}`,
658
+ left: 100 + i * 10,
659
+ top: 100 + i * 10,
660
+ right: 200 + i * 10,
661
+ bottom: 200 + i * 10,
662
+ width: 100,
663
+ height: 100
664
+ });
665
+ }
666
+ // Should complete without hanging
667
+ const positions = calculateReflowPositions('moved', movedBounds, allBounds, false);
668
+ // Should have resolved some collisions
669
+ expect(positions.size).to.be.greaterThan(0);
670
+ });
671
+ });
672
+ });
673
+ //# sourceMappingURL=temba-flow-collision.test.js.map