@dragonworks/ngx-dashboard 20.0.5 → 20.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/ng-package.json +7 -0
  2. package/package.json +34 -45
  3. package/src/lib/__tests__/dashboard-component-widget-state-integration.spec.ts +537 -0
  4. package/src/lib/cell/__tests__/cell-resize.component.spec.ts +442 -0
  5. package/src/lib/cell/__tests__/cell.component.spec.ts +541 -0
  6. package/src/lib/cell/cell-context-menu.component.ts +138 -0
  7. package/src/lib/cell/cell-context-menu.service.ts +36 -0
  8. package/src/lib/cell/cell.component.html +37 -0
  9. package/src/lib/cell/cell.component.scss +198 -0
  10. package/src/lib/cell/cell.component.ts +375 -0
  11. package/src/lib/dashboard/dashboard.component.html +18 -0
  12. package/src/lib/dashboard/dashboard.component.scss +17 -0
  13. package/src/lib/dashboard/dashboard.component.ts +187 -0
  14. package/src/lib/dashboard-editor/dashboard-editor.component.html +57 -0
  15. package/src/lib/dashboard-editor/dashboard-editor.component.scss +87 -0
  16. package/src/lib/dashboard-editor/dashboard-editor.component.ts +219 -0
  17. package/src/lib/dashboard-viewer/__tests__/dashboard-viewer.component.spec.ts +258 -0
  18. package/src/lib/dashboard-viewer/dashboard-viewer.component.html +20 -0
  19. package/src/lib/dashboard-viewer/dashboard-viewer.component.scss +50 -0
  20. package/src/lib/dashboard-viewer/dashboard-viewer.component.ts +70 -0
  21. package/src/lib/drop-zone/__tests__/drop-zone.component.spec.ts +465 -0
  22. package/src/lib/drop-zone/drop-zone.component.html +20 -0
  23. package/src/lib/drop-zone/drop-zone.component.scss +67 -0
  24. package/src/lib/drop-zone/drop-zone.component.ts +122 -0
  25. package/src/lib/internal-widgets/unknown-widget/unknown-widget.component.ts +72 -0
  26. package/src/lib/models/cell-data.ts +13 -0
  27. package/src/lib/models/cell-dialog.ts +7 -0
  28. package/src/lib/models/cell-id.ts +85 -0
  29. package/src/lib/models/cell-position.ts +15 -0
  30. package/src/lib/models/dashboard-data.dto.ts +44 -0
  31. package/src/lib/models/dashboard-data.utils.ts +49 -0
  32. package/src/lib/models/drag-data.ts +6 -0
  33. package/src/lib/models/index.ts +11 -0
  34. package/src/lib/models/reserved-space.ts +24 -0
  35. package/src/lib/models/widget-factory.ts +33 -0
  36. package/src/lib/models/widget-id.ts +70 -0
  37. package/src/lib/models/widget.ts +21 -0
  38. package/src/lib/providers/cell-settings-dialog/cell-settings-dialog.component.ts +127 -0
  39. package/src/lib/providers/cell-settings-dialog/cell-settings-dialog.provider.ts +15 -0
  40. package/src/lib/providers/cell-settings-dialog/cell-settings-dialog.tokens.ts +20 -0
  41. package/src/lib/providers/cell-settings-dialog/default-cell-settings-dialog.provider.ts +32 -0
  42. package/src/lib/providers/cell-settings-dialog/index.ts +3 -0
  43. package/src/lib/providers/index.ts +1 -0
  44. package/src/lib/services/__tests__/dashboard-bridge.service.spec.ts +220 -0
  45. package/src/lib/services/__tests__/dashboard-viewport.service.spec.ts +362 -0
  46. package/src/lib/services/dashboard-bridge.service.ts +155 -0
  47. package/src/lib/services/dashboard-viewport.service.ts +148 -0
  48. package/src/lib/services/dashboard.service.ts +62 -0
  49. package/src/lib/store/__tests__/dashboard-store-collision-detection.spec.ts +756 -0
  50. package/src/lib/store/__tests__/dashboard-store-computed-properties.spec.ts +974 -0
  51. package/src/lib/store/__tests__/dashboard-store-drag-drop.spec.ts +279 -0
  52. package/src/lib/store/__tests__/dashboard-store-export-import.spec.ts +780 -0
  53. package/src/lib/store/__tests__/dashboard-store-grid-config.spec.ts +128 -0
  54. package/src/lib/store/__tests__/dashboard-store-query-methods.spec.ts +229 -0
  55. package/src/lib/store/__tests__/dashboard-store-resize-operations.spec.ts +652 -0
  56. package/src/lib/store/__tests__/dashboard-store-widget-management.spec.ts +461 -0
  57. package/src/lib/store/__tests__/dashboard-store-widget-state-preservation.spec.ts +369 -0
  58. package/src/lib/store/dashboard-store.ts +239 -0
  59. package/src/lib/store/features/drag-drop.feature.ts +140 -0
  60. package/src/lib/store/features/grid-config.feature.ts +43 -0
  61. package/src/lib/store/features/resize.feature.ts +140 -0
  62. package/src/lib/store/features/utils/collision.utils.ts +89 -0
  63. package/src/lib/store/features/utils/grid-query-internal.utils.ts +37 -0
  64. package/src/lib/store/features/utils/resize.utils.ts +165 -0
  65. package/src/lib/store/features/widget-management.feature.ts +158 -0
  66. package/src/lib/styles/_dashboard-grid-vars.scss +11 -0
  67. package/src/lib/widget-list/__tests__/widget-list-bridge-integration.spec.ts +137 -0
  68. package/src/lib/widget-list/widget-list.component.html +22 -0
  69. package/src/lib/widget-list/widget-list.component.scss +154 -0
  70. package/src/lib/widget-list/widget-list.component.ts +106 -0
  71. package/src/public-api.ts +21 -0
  72. package/src/test-setup.ts +10 -0
  73. package/tsconfig.lib.json +15 -0
  74. package/tsconfig.lib.prod.json +11 -0
  75. package/tsconfig.spec.json +14 -0
  76. package/fesm2022/dragonworks-ngx-dashboard.mjs +0 -2178
  77. package/fesm2022/dragonworks-ngx-dashboard.mjs.map +0 -1
  78. package/index.d.ts +0 -678
@@ -0,0 +1,756 @@
1
+ import { TestBed } from '@angular/core/testing';
2
+ import { DashboardStore } from '../dashboard-store';
3
+ import { CellIdUtils, WidgetIdUtils, CellData, DragData, WidgetMetadata, WidgetFactory } from '../../models';
4
+ import { DashboardService } from '../../services/dashboard.service';
5
+
6
+ describe('DashboardStore - Collision Detection', () => {
7
+ let store: InstanceType<typeof DashboardStore>;
8
+ let dashboardService: jasmine.SpyObj<DashboardService>;
9
+
10
+ beforeEach(() => {
11
+ const spy = jasmine.createSpyObj('DashboardService', ['getFactory']);
12
+
13
+ TestBed.configureTestingModule({
14
+ providers: [
15
+ DashboardStore,
16
+ { provide: DashboardService, useValue: spy }
17
+ ]
18
+ });
19
+
20
+ store = TestBed.inject(DashboardStore);
21
+ dashboardService = TestBed.inject(DashboardService) as jasmine.SpyObj<DashboardService>;
22
+ });
23
+
24
+ describe('isValidPlacement', () => {
25
+ let testWidgetMetadata: WidgetMetadata;
26
+ let mockWidgetFactory: WidgetFactory;
27
+
28
+ beforeEach(() => {
29
+ // Set up a 16x16 grid for real-world testing
30
+ store.setGridConfig({ rows: 16, columns: 16 });
31
+
32
+ // Create test widget metadata
33
+ testWidgetMetadata = {
34
+ widgetTypeid: 'test-widget',
35
+ name: 'Test Widget',
36
+ description: 'A test widget for unit tests',
37
+ svgIcon: '<svg></svg>'
38
+ };
39
+
40
+ // Create mock widget factory
41
+ mockWidgetFactory = {
42
+ widgetTypeid: 'test-widget',
43
+ createComponent: jasmine.createSpy('createComponent')
44
+ } as any;
45
+ });
46
+
47
+ it('should return true when no drag data is present', () => {
48
+ expect(store.isValidPlacement()).toBe(true);
49
+ });
50
+
51
+ it('should return true when no hovered drop zone is present', () => {
52
+ const dragData: DragData = {
53
+ kind: 'widget',
54
+ content: testWidgetMetadata
55
+ };
56
+ store.startDrag(dragData);
57
+ expect(store.isValidPlacement()).toBe(true);
58
+ });
59
+
60
+ it('should return true for valid widget placement in empty grid', () => {
61
+ const dragData: DragData = {
62
+ kind: 'widget',
63
+ content: testWidgetMetadata
64
+ };
65
+ store.startDrag(dragData);
66
+ store.setHoveredDropZone({ row: 8, col: 8 });
67
+ expect(store.isValidPlacement()).toBe(true);
68
+ });
69
+
70
+ it('should return false for out-of-bounds placement', () => {
71
+ const dragData: DragData = {
72
+ kind: 'widget',
73
+ content: testWidgetMetadata
74
+ };
75
+ store.startDrag(dragData);
76
+ store.setHoveredDropZone({ row: 17, col: 8 }); // Row 17 is out of bounds
77
+ expect(store.isValidPlacement()).toBe(false);
78
+ });
79
+
80
+ it('should return false for collision with existing widget', () => {
81
+ // Add an existing widget at position (5,5)
82
+ const existingCell: CellData = {
83
+ widgetId: WidgetIdUtils.generate(),
84
+ cellId: CellIdUtils.create(5, 5),
85
+ row: 5,
86
+ col: 5,
87
+ rowSpan: 1,
88
+ colSpan: 1,
89
+ widgetFactory: mockWidgetFactory,
90
+ widgetState: {},
91
+ };
92
+ store.addWidget(existingCell);
93
+
94
+ // Try to place a new widget at the same position
95
+ const dragData: DragData = {
96
+ kind: 'widget',
97
+ content: testWidgetMetadata
98
+ };
99
+ store.startDrag(dragData);
100
+ store.setHoveredDropZone({ row: 5, col: 5 });
101
+ expect(store.isValidPlacement()).toBe(false);
102
+ });
103
+
104
+ it('should return true for self-overlap when moving 3x3 widget one step right', () => {
105
+ // Add a 3x3 widget at position (5,5) - occupies (5,5) to (7,7)
106
+ const existingCell: CellData = {
107
+ widgetId: WidgetIdUtils.generate(),
108
+ cellId: CellIdUtils.create(5, 5),
109
+ row: 5,
110
+ col: 5,
111
+ rowSpan: 3,
112
+ colSpan: 3,
113
+ widgetFactory: mockWidgetFactory,
114
+ widgetState: {},
115
+ };
116
+ store.addWidget(existingCell);
117
+
118
+ // Try to move the same widget one position to the right (partial overlap)
119
+ // New position would be (5,6) to (7,8) - overlaps with original (5,5) to (7,7)
120
+ const dragData: DragData = {
121
+ kind: 'cell',
122
+ content: {
123
+ cellId: CellIdUtils.create(5, 5),
124
+ widgetId: existingCell.widgetId, // Use the SAME widgetId
125
+ row: 5,
126
+ col: 5,
127
+ rowSpan: 3,
128
+ colSpan: 3,
129
+ }
130
+ };
131
+ store.startDrag(dragData);
132
+ store.setHoveredDropZone({ row: 5, col: 6 }); // Move one column right
133
+ expect(store.isValidPlacement()).toBe(true);
134
+ });
135
+
136
+ it('should return true for self-overlap when moving 3x3 widget one step down', () => {
137
+ // Add a 3x3 widget at position (5,5) - occupies (5,5) to (7,7)
138
+ const existingCell: CellData = {
139
+ widgetId: WidgetIdUtils.generate(),
140
+ cellId: CellIdUtils.create(5, 5),
141
+ row: 5,
142
+ col: 5,
143
+ rowSpan: 3,
144
+ colSpan: 3,
145
+ widgetFactory: mockWidgetFactory,
146
+ widgetState: {},
147
+ };
148
+ store.addWidget(existingCell);
149
+
150
+ // Try to move the same widget one position down (partial overlap)
151
+ // New position would be (6,5) to (8,7) - overlaps with original (5,5) to (7,7)
152
+ const dragData: DragData = {
153
+ kind: 'cell',
154
+ content: {
155
+ cellId: CellIdUtils.create(5, 5),
156
+ widgetId: existingCell.widgetId, // Use the SAME widgetId
157
+ row: 5,
158
+ col: 5,
159
+ rowSpan: 3,
160
+ colSpan: 3,
161
+ }
162
+ };
163
+ store.startDrag(dragData);
164
+ store.setHoveredDropZone({ row: 6, col: 5 }); // Move one row down
165
+ expect(store.isValidPlacement()).toBe(true);
166
+ });
167
+
168
+ it('should return false when moving widget would collide with other widget', () => {
169
+ // Add two widgets with space between them
170
+ const widget1: CellData = {
171
+ widgetId: WidgetIdUtils.generate(),
172
+ cellId: CellIdUtils.create(3, 3),
173
+ row: 3,
174
+ col: 3,
175
+ rowSpan: 2,
176
+ colSpan: 2,
177
+ widgetFactory: mockWidgetFactory,
178
+ widgetState: {},
179
+ };
180
+ const widget2: CellData = {
181
+ widgetId: WidgetIdUtils.generate(),
182
+ cellId: CellIdUtils.create(10, 10),
183
+ row: 10,
184
+ col: 10,
185
+ rowSpan: 2,
186
+ colSpan: 2,
187
+ widgetFactory: mockWidgetFactory,
188
+ widgetState: {},
189
+ };
190
+ store.addWidget(widget1);
191
+ store.addWidget(widget2);
192
+
193
+ // Try to move widget-1 to overlap with widget-2
194
+ const dragData: DragData = {
195
+ kind: 'cell',
196
+ content: {
197
+ cellId: CellIdUtils.create(3, 3),
198
+ widgetId: WidgetIdUtils.generate(),
199
+ row: 3,
200
+ col: 3,
201
+ rowSpan: 2,
202
+ colSpan: 2,
203
+ }
204
+ };
205
+ store.startDrag(dragData);
206
+ store.setHoveredDropZone({ row: 10, col: 10 }); // Same position as widget-2
207
+ expect(store.isValidPlacement()).toBe(false);
208
+ });
209
+
210
+ it('should return false when large widget goes out of bounds', () => {
211
+ const dragData: DragData = {
212
+ kind: 'cell',
213
+ content: {
214
+ cellId: CellIdUtils.create(5, 5),
215
+ widgetId: WidgetIdUtils.generate(),
216
+ row: 5,
217
+ col: 5,
218
+ rowSpan: 4,
219
+ colSpan: 4,
220
+ }
221
+ };
222
+ store.startDrag(dragData);
223
+ store.setHoveredDropZone({ row: 14, col: 14 }); // 4x4 widget at (14,14) would extend to (17,17), out of bounds
224
+ expect(store.isValidPlacement()).toBe(false);
225
+ });
226
+
227
+ it('should return true when large widget is placed at valid boundary position', () => {
228
+ const dragData: DragData = {
229
+ kind: 'cell',
230
+ content: {
231
+ cellId: CellIdUtils.create(5, 5),
232
+ widgetId: WidgetIdUtils.generate(),
233
+ row: 5,
234
+ col: 5,
235
+ rowSpan: 4,
236
+ colSpan: 4,
237
+ }
238
+ };
239
+ store.startDrag(dragData);
240
+ store.setHoveredDropZone({ row: 13, col: 13 }); // 4x4 widget at (13,13) extends to (16,16), which is valid
241
+ expect(store.isValidPlacement()).toBe(true);
242
+ });
243
+
244
+ it('should handle complex self-overlap scenario with 2x5 widget', () => {
245
+ // Add a 2x5 horizontal widget at position (8,6) - occupies (8,6) to (9,10)
246
+ const existingCell: CellData = {
247
+ widgetId: WidgetIdUtils.generate(),
248
+ cellId: CellIdUtils.create(8, 6),
249
+ row: 8,
250
+ col: 6,
251
+ rowSpan: 2,
252
+ colSpan: 5,
253
+ widgetFactory: mockWidgetFactory,
254
+ widgetState: {},
255
+ };
256
+ store.addWidget(existingCell);
257
+
258
+ // Try to move it 2 columns to the right
259
+ // New position would be (8,8) to (9,12) - partial overlap with original
260
+ const dragData: DragData = {
261
+ kind: 'cell',
262
+ content: {
263
+ cellId: CellIdUtils.create(8, 6),
264
+ widgetId: existingCell.widgetId, // Use the SAME widgetId
265
+ row: 8,
266
+ col: 6,
267
+ rowSpan: 2,
268
+ colSpan: 5,
269
+ }
270
+ };
271
+ store.startDrag(dragData);
272
+ store.setHoveredDropZone({ row: 8, col: 8 }); // Move 2 columns right
273
+ expect(store.isValidPlacement()).toBe(true);
274
+ });
275
+
276
+ it('should prevent collision between different widgets even with complex shapes', () => {
277
+ // Add an L-shaped arrangement using multiple widgets
278
+ const widget1: CellData = {
279
+ widgetId: WidgetIdUtils.generate(),
280
+ cellId: CellIdUtils.create(5, 5),
281
+ row: 5,
282
+ col: 5,
283
+ rowSpan: 1,
284
+ colSpan: 3, // Horizontal bar: (5,5) to (5,7)
285
+ widgetFactory: mockWidgetFactory,
286
+ widgetState: {},
287
+ };
288
+ const widget2: CellData = {
289
+ widgetId: WidgetIdUtils.generate(),
290
+ cellId: CellIdUtils.create(6, 5),
291
+ row: 6,
292
+ col: 5,
293
+ rowSpan: 2,
294
+ colSpan: 1, // Vertical bar: (6,5) to (7,5)
295
+ widgetFactory: mockWidgetFactory,
296
+ widgetState: {},
297
+ };
298
+ store.addWidget(widget1);
299
+ store.addWidget(widget2);
300
+
301
+ // Try to place a 2x2 widget that would overlap with both
302
+ const dragData: DragData = {
303
+ kind: 'widget',
304
+ content: testWidgetMetadata
305
+ };
306
+ store.startDrag(dragData);
307
+ store.setHoveredDropZone({ row: 5, col: 6 }); // Would overlap with widget-1
308
+ expect(store.isValidPlacement()).toBe(false);
309
+ });
310
+
311
+ it('should allow 4x4 widget self-overlap when moved 1 right and 1 down', () => {
312
+ // Add a 4x4 widget at position (6,6) - occupies (6,6) to (9,9)
313
+ const existingCell: CellData = {
314
+ widgetId: WidgetIdUtils.generate(),
315
+ cellId: CellIdUtils.create(6, 6),
316
+ row: 6,
317
+ col: 6,
318
+ rowSpan: 4,
319
+ colSpan: 4,
320
+ widgetFactory: mockWidgetFactory,
321
+ widgetState: {},
322
+ };
323
+ store.addWidget(existingCell);
324
+
325
+ // Try to move the same widget 1 right and 1 down (diagonal movement)
326
+ // New position would be (7,7) to (10,10) - significant overlap with original (6,6) to (9,9)
327
+ const dragData: DragData = {
328
+ kind: 'cell',
329
+ content: {
330
+ cellId: CellIdUtils.create(6, 6),
331
+ widgetId: existingCell.widgetId,
332
+ row: 6,
333
+ col: 6,
334
+ rowSpan: 4,
335
+ colSpan: 4,
336
+ }
337
+ };
338
+ store.startDrag(dragData);
339
+ store.setHoveredDropZone({ row: 7, col: 7 }); // Move one row down and one column right
340
+ expect(store.isValidPlacement()).toBe(true);
341
+ });
342
+
343
+ it('should allow 4x4 widget self-overlap when moved 2 right and 2 down', () => {
344
+ // Add a 4x4 widget at position (4,4) - occupies (4,4) to (7,7)
345
+ const existingCell: CellData = {
346
+ widgetId: WidgetIdUtils.generate(),
347
+ cellId: CellIdUtils.create(4, 4),
348
+ row: 4,
349
+ col: 4,
350
+ rowSpan: 4,
351
+ colSpan: 4,
352
+ widgetFactory: mockWidgetFactory,
353
+ widgetState: {},
354
+ };
355
+ store.addWidget(existingCell);
356
+
357
+ // Try to move the same widget 2 right and 2 down
358
+ // New position would be (6,6) to (9,9) - partial overlap with original (4,4) to (7,7)
359
+ const dragData: DragData = {
360
+ kind: 'cell',
361
+ content: {
362
+ cellId: CellIdUtils.create(4, 4),
363
+ widgetId: existingCell.widgetId,
364
+ row: 4,
365
+ col: 4,
366
+ rowSpan: 4,
367
+ colSpan: 4,
368
+ }
369
+ };
370
+ store.startDrag(dragData);
371
+ store.setHoveredDropZone({ row: 6, col: 6 }); // Move 2 rows down and 2 columns right
372
+ expect(store.isValidPlacement()).toBe(true);
373
+ });
374
+
375
+ it('should allow 4x4 widget movement to non-overlapping position', () => {
376
+ // Add a 4x4 widget at position (2,2) - occupies (2,2) to (5,5)
377
+ const existingCell: CellData = {
378
+ widgetId: WidgetIdUtils.generate(),
379
+ cellId: CellIdUtils.create(2, 2),
380
+ row: 2,
381
+ col: 2,
382
+ rowSpan: 4,
383
+ colSpan: 4,
384
+ widgetFactory: mockWidgetFactory,
385
+ widgetState: {},
386
+ };
387
+ store.addWidget(existingCell);
388
+
389
+ // Try to move the same widget 4 positions right (no overlap)
390
+ // New position would be (2,6) to (5,9) - no overlap with original (2,2) to (5,5)
391
+ const dragData: DragData = {
392
+ kind: 'cell',
393
+ content: {
394
+ cellId: CellIdUtils.create(2, 2),
395
+ widgetId: existingCell.widgetId,
396
+ row: 2,
397
+ col: 2,
398
+ rowSpan: 4,
399
+ colSpan: 4,
400
+ }
401
+ };
402
+ store.startDrag(dragData);
403
+ store.setHoveredDropZone({ row: 2, col: 6 }); // Move 4 columns right, no vertical movement
404
+ expect(store.isValidPlacement()).toBe(true);
405
+ });
406
+
407
+ it('should allow 4x4 widget minimal self-overlap (corner-to-corner)', () => {
408
+ // Add a 4x4 widget at position (5,5) - occupies (5,5) to (8,8)
409
+ const existingCell: CellData = {
410
+ widgetId: WidgetIdUtils.generate(),
411
+ cellId: CellIdUtils.create(5, 5),
412
+ row: 5,
413
+ col: 5,
414
+ rowSpan: 4,
415
+ colSpan: 4,
416
+ widgetFactory: mockWidgetFactory,
417
+ widgetState: {},
418
+ };
419
+ store.addWidget(existingCell);
420
+
421
+ // Try to move the same widget 3 right and 3 down
422
+ // New position would be (8,8) to (11,11) - minimal overlap at corner (8,8)
423
+ const dragData: DragData = {
424
+ kind: 'cell',
425
+ content: {
426
+ cellId: CellIdUtils.create(5, 5),
427
+ widgetId: existingCell.widgetId,
428
+ row: 5,
429
+ col: 5,
430
+ rowSpan: 4,
431
+ colSpan: 4,
432
+ }
433
+ };
434
+ store.startDrag(dragData);
435
+ store.setHoveredDropZone({ row: 8, col: 8 }); // Move to create minimal corner overlap
436
+ expect(store.isValidPlacement()).toBe(true);
437
+ });
438
+
439
+ it('should prevent 4x4 widget collision with different widget when moved diagonally', () => {
440
+ // Add a 4x4 widget at position (2,2) - occupies (2,2) to (5,5)
441
+ const widget1: CellData = {
442
+ widgetId: WidgetIdUtils.generate(),
443
+ cellId: CellIdUtils.create(2, 2),
444
+ row: 2,
445
+ col: 2,
446
+ rowSpan: 4,
447
+ colSpan: 4,
448
+ widgetFactory: mockWidgetFactory,
449
+ widgetState: {},
450
+ };
451
+
452
+ // Add another 2x2 widget at position (8,8) - occupies (8,8) to (9,9)
453
+ const widget2: CellData = {
454
+ widgetId: WidgetIdUtils.generate(),
455
+ cellId: CellIdUtils.create(8, 8),
456
+ row: 8,
457
+ col: 8,
458
+ rowSpan: 2,
459
+ colSpan: 2,
460
+ widgetFactory: mockWidgetFactory,
461
+ widgetState: {},
462
+ };
463
+
464
+ store.addWidget(widget1);
465
+ store.addWidget(widget2);
466
+
467
+ // Try to move widget1 to overlap with widget2
468
+ // Moving to (7,7) would make widget1 occupy (7,7) to (10,10), overlapping widget2
469
+ const dragData: DragData = {
470
+ kind: 'cell',
471
+ content: {
472
+ cellId: CellIdUtils.create(2, 2),
473
+ widgetId: widget1.widgetId,
474
+ row: 2,
475
+ col: 2,
476
+ rowSpan: 4,
477
+ colSpan: 4,
478
+ }
479
+ };
480
+ store.startDrag(dragData);
481
+ store.setHoveredDropZone({ row: 7, col: 7 }); // Would overlap with widget2
482
+ expect(store.isValidPlacement()).toBe(false);
483
+ });
484
+
485
+ it('should expose the cellId-based exclusion bug when widget moves to completely different position', () => {
486
+ // Add a 4x4 widget at position (1,1) - occupies (1,1) to (4,4)
487
+ const existingCell: CellData = {
488
+ widgetId: WidgetIdUtils.generate(),
489
+ cellId: CellIdUtils.create(1, 1),
490
+ row: 1,
491
+ col: 1,
492
+ rowSpan: 4,
493
+ colSpan: 4,
494
+ widgetFactory: mockWidgetFactory,
495
+ widgetState: {},
496
+ };
497
+ store.addWidget(existingCell);
498
+
499
+ // Try to move the same widget to a completely different area (8,8) to (11,11)
500
+ // This should be valid since it's the same widget, but cellId-based exclusion might fail
501
+ const dragData: DragData = {
502
+ kind: 'cell',
503
+ content: {
504
+ cellId: CellIdUtils.create(1, 1), // Original position
505
+ widgetId: existingCell.widgetId, // Same widget
506
+ row: 1,
507
+ col: 1,
508
+ rowSpan: 4,
509
+ colSpan: 4,
510
+ }
511
+ };
512
+ store.startDrag(dragData);
513
+ store.setHoveredDropZone({ row: 8, col: 8 }); // Move to completely different position
514
+
515
+ // This should be TRUE since it's the same widget moving to an empty area
516
+ // But if cellId-based exclusion is buggy, it might incorrectly detect collision
517
+ expect(store.isValidPlacement()).toBe(true);
518
+ });
519
+
520
+ it('should fail when cellId exclusion does not match the widget being moved after position update', () => {
521
+ // Create a widget
522
+ const originalWidget: CellData = {
523
+ widgetId: WidgetIdUtils.generate(),
524
+ cellId: CellIdUtils.create(2, 2),
525
+ row: 2,
526
+ col: 2,
527
+ rowSpan: 3,
528
+ colSpan: 3,
529
+ widgetFactory: mockWidgetFactory,
530
+ widgetState: {},
531
+ };
532
+ store.addWidget(originalWidget);
533
+
534
+ // Simulate that the widget has been moved to a new position (3,3) in the store
535
+ // This updates the cellId to match the new position
536
+ store.updateWidgetPosition(originalWidget.widgetId, 3, 3);
537
+
538
+ // Now try to drag the widget again, but the drag data still has the OLD cellId
539
+ // This simulates the real-world scenario where drag starts before position update
540
+ const dragData: DragData = {
541
+ kind: 'cell',
542
+ content: {
543
+ cellId: CellIdUtils.create(2, 2), // OLD position (no longer matches stored widget)
544
+ widgetId: originalWidget.widgetId, // Same widget ID
545
+ row: 2, // OLD position
546
+ col: 2, // OLD position
547
+ rowSpan: 3,
548
+ colSpan: 3,
549
+ }
550
+ };
551
+
552
+ store.startDrag(dragData);
553
+ store.setHoveredDropZone({ row: 4, col: 4 }); // Try to move to (4,4)
554
+
555
+ // This SHOULD be valid since it's the same widget
556
+ // But cellId-based exclusion will fail because the drag data cellId (2,2)
557
+ // doesn't match the stored widget's cellId (3,3)
558
+ // So it will think this is a different widget colliding with the stored one
559
+ expect(store.isValidPlacement()).toBe(true);
560
+ });
561
+ });
562
+
563
+ describe('handleDrop', () => {
564
+ let testWidgetMetadata: WidgetMetadata;
565
+ let mockWidgetFactory: WidgetFactory;
566
+
567
+ beforeEach(() => {
568
+ // Set up a 16x16 grid for real-world testing
569
+ store.setGridConfig({ rows: 16, columns: 16 });
570
+
571
+ // Create test widget metadata
572
+ testWidgetMetadata = {
573
+ widgetTypeid: 'test-widget',
574
+ name: 'Test Widget',
575
+ description: 'A test widget for unit tests',
576
+ svgIcon: '<svg></svg>'
577
+ };
578
+
579
+ // Create mock widget factory
580
+ mockWidgetFactory = {
581
+ widgetTypeid: 'test-widget',
582
+ createComponent: jasmine.createSpy('createComponent')
583
+ } as any;
584
+
585
+ // Mock the dashboard service getFactory method
586
+ dashboardService.getFactory.and.returnValue(mockWidgetFactory);
587
+ });
588
+
589
+ it('should create widget when dropping widget drag data on empty space', () => {
590
+ const dragData: DragData = {
591
+ kind: 'widget',
592
+ content: testWidgetMetadata
593
+ };
594
+
595
+ const result = store.handleDrop(dragData, { row: 8, col: 8 });
596
+
597
+ expect(result).toBe(true);
598
+ expect(store.cells().length).toBe(1);
599
+ expect(store.cells()[0].row).toBe(8);
600
+ expect(store.cells()[0].col).toBe(8);
601
+ });
602
+
603
+ it('should move cell when dropping cell drag data on valid position', () => {
604
+ // Add an existing widget at position (5,5)
605
+ const existingCell: CellData = {
606
+ widgetId: WidgetIdUtils.generate(),
607
+ cellId: CellIdUtils.create(5, 5),
608
+ row: 5,
609
+ col: 5,
610
+ rowSpan: 3,
611
+ colSpan: 3,
612
+ widgetFactory: mockWidgetFactory,
613
+ widgetState: {},
614
+ };
615
+ store.addWidget(existingCell);
616
+
617
+ // Drag data for moving the cell - use the same widgetId
618
+ const dragData: DragData = {
619
+ kind: 'cell',
620
+ content: {
621
+ cellId: CellIdUtils.create(5, 5),
622
+ widgetId: existingCell.widgetId,
623
+ row: 5,
624
+ col: 5,
625
+ rowSpan: 3,
626
+ colSpan: 3,
627
+ }
628
+ };
629
+
630
+ const result = store.handleDrop(dragData, { row: 6, col: 6 });
631
+
632
+ expect(result).toBe(true);
633
+ expect(store.cells().length).toBe(1);
634
+ expect(store.cells()[0].row).toBe(6);
635
+ expect(store.cells()[0].col).toBe(6);
636
+ });
637
+
638
+ it('should return false for invalid placements due to collision', () => {
639
+ // Add two widgets
640
+ const widget1: CellData = {
641
+ widgetId: WidgetIdUtils.generate(),
642
+ cellId: CellIdUtils.create(3, 3),
643
+ row: 3,
644
+ col: 3,
645
+ rowSpan: 2,
646
+ colSpan: 2,
647
+ widgetFactory: mockWidgetFactory,
648
+ widgetState: {},
649
+ };
650
+ const widget2: CellData = {
651
+ widgetId: WidgetIdUtils.generate(),
652
+ cellId: CellIdUtils.create(10, 10),
653
+ row: 10,
654
+ col: 10,
655
+ rowSpan: 2,
656
+ colSpan: 2,
657
+ widgetFactory: mockWidgetFactory,
658
+ widgetState: {},
659
+ };
660
+ store.addWidget(widget1);
661
+ store.addWidget(widget2);
662
+
663
+ // Try to move widget1 to overlap with widget2
664
+ const dragData: DragData = {
665
+ kind: 'cell',
666
+ content: {
667
+ cellId: CellIdUtils.create(3, 3),
668
+ widgetId: WidgetIdUtils.generate(),
669
+ row: 3,
670
+ col: 3,
671
+ rowSpan: 2,
672
+ colSpan: 2,
673
+ }
674
+ };
675
+
676
+ const result = store.handleDrop(dragData, { row: 10, col: 10 });
677
+
678
+ expect(result).toBe(false);
679
+ // Verify original positions unchanged
680
+ expect(store.cells()[0].row).toBe(3);
681
+ expect(store.cells()[0].col).toBe(3);
682
+ expect(store.cells()[1].row).toBe(10);
683
+ expect(store.cells()[1].col).toBe(10);
684
+ });
685
+
686
+ it('should return false for out-of-bounds placement', () => {
687
+ const dragData: DragData = {
688
+ kind: 'widget',
689
+ content: testWidgetMetadata
690
+ };
691
+
692
+ const result = store.handleDrop(dragData, { row: 17, col: 8 }); // Row 17 is out of bounds
693
+
694
+ expect(result).toBe(false);
695
+ expect(store.cells().length).toBe(0);
696
+ });
697
+
698
+ it('should end drag state regardless of success or failure', () => {
699
+ const dragData: DragData = {
700
+ kind: 'widget',
701
+ content: testWidgetMetadata
702
+ };
703
+
704
+ // Start drag first
705
+ store.startDrag(dragData);
706
+ expect(store.dragData()).toBeTruthy();
707
+
708
+ // Handle valid drop
709
+ store.handleDrop(dragData, { row: 8, col: 8 });
710
+ expect(store.dragData()).toBeNull();
711
+
712
+ // Start drag again
713
+ store.startDrag(dragData);
714
+ expect(store.dragData()).toBeTruthy();
715
+
716
+ // Handle invalid drop (out of bounds)
717
+ store.handleDrop(dragData, { row: 17, col: 8 });
718
+ expect(store.dragData()).toBeNull();
719
+ });
720
+
721
+ it('should allow self-overlap when moving cell to partially overlapping position', () => {
722
+ // Add a 3x3 widget at position (5,5)
723
+ const existingCell: CellData = {
724
+ widgetId: WidgetIdUtils.generate(),
725
+ cellId: CellIdUtils.create(5, 5),
726
+ row: 5,
727
+ col: 5,
728
+ rowSpan: 3,
729
+ colSpan: 3,
730
+ widgetFactory: mockWidgetFactory,
731
+ widgetState: {},
732
+ };
733
+ store.addWidget(existingCell);
734
+
735
+ // Try to move it one position right (self-overlap scenario) - use the same widgetId
736
+ const dragData: DragData = {
737
+ kind: 'cell',
738
+ content: {
739
+ cellId: CellIdUtils.create(5, 5),
740
+ widgetId: existingCell.widgetId,
741
+ row: 5,
742
+ col: 5,
743
+ rowSpan: 3,
744
+ colSpan: 3,
745
+ }
746
+ };
747
+
748
+ const result = store.handleDrop(dragData, { row: 5, col: 6 });
749
+
750
+ expect(result).toBe(true);
751
+ expect(store.cells()[0].row).toBe(5);
752
+ expect(store.cells()[0].col).toBe(6);
753
+ });
754
+ });
755
+
756
+ });