@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.
- package/ng-package.json +7 -0
- package/package.json +34 -45
- package/src/lib/__tests__/dashboard-component-widget-state-integration.spec.ts +537 -0
- package/src/lib/cell/__tests__/cell-resize.component.spec.ts +442 -0
- package/src/lib/cell/__tests__/cell.component.spec.ts +541 -0
- package/src/lib/cell/cell-context-menu.component.ts +138 -0
- package/src/lib/cell/cell-context-menu.service.ts +36 -0
- package/src/lib/cell/cell.component.html +37 -0
- package/src/lib/cell/cell.component.scss +198 -0
- package/src/lib/cell/cell.component.ts +375 -0
- package/src/lib/dashboard/dashboard.component.html +18 -0
- package/src/lib/dashboard/dashboard.component.scss +17 -0
- package/src/lib/dashboard/dashboard.component.ts +187 -0
- package/src/lib/dashboard-editor/dashboard-editor.component.html +57 -0
- package/src/lib/dashboard-editor/dashboard-editor.component.scss +87 -0
- package/src/lib/dashboard-editor/dashboard-editor.component.ts +219 -0
- package/src/lib/dashboard-viewer/__tests__/dashboard-viewer.component.spec.ts +258 -0
- package/src/lib/dashboard-viewer/dashboard-viewer.component.html +20 -0
- package/src/lib/dashboard-viewer/dashboard-viewer.component.scss +50 -0
- package/src/lib/dashboard-viewer/dashboard-viewer.component.ts +70 -0
- package/src/lib/drop-zone/__tests__/drop-zone.component.spec.ts +465 -0
- package/src/lib/drop-zone/drop-zone.component.html +20 -0
- package/src/lib/drop-zone/drop-zone.component.scss +67 -0
- package/src/lib/drop-zone/drop-zone.component.ts +122 -0
- package/src/lib/internal-widgets/unknown-widget/unknown-widget.component.ts +72 -0
- package/src/lib/models/cell-data.ts +13 -0
- package/src/lib/models/cell-dialog.ts +7 -0
- package/src/lib/models/cell-id.ts +85 -0
- package/src/lib/models/cell-position.ts +15 -0
- package/src/lib/models/dashboard-data.dto.ts +44 -0
- package/src/lib/models/dashboard-data.utils.ts +49 -0
- package/src/lib/models/drag-data.ts +6 -0
- package/src/lib/models/index.ts +11 -0
- package/src/lib/models/reserved-space.ts +24 -0
- package/src/lib/models/widget-factory.ts +33 -0
- package/src/lib/models/widget-id.ts +70 -0
- package/src/lib/models/widget.ts +21 -0
- package/src/lib/providers/cell-settings-dialog/cell-settings-dialog.component.ts +127 -0
- package/src/lib/providers/cell-settings-dialog/cell-settings-dialog.provider.ts +15 -0
- package/src/lib/providers/cell-settings-dialog/cell-settings-dialog.tokens.ts +20 -0
- package/src/lib/providers/cell-settings-dialog/default-cell-settings-dialog.provider.ts +32 -0
- package/src/lib/providers/cell-settings-dialog/index.ts +3 -0
- package/src/lib/providers/index.ts +1 -0
- package/src/lib/services/__tests__/dashboard-bridge.service.spec.ts +220 -0
- package/src/lib/services/__tests__/dashboard-viewport.service.spec.ts +362 -0
- package/src/lib/services/dashboard-bridge.service.ts +155 -0
- package/src/lib/services/dashboard-viewport.service.ts +148 -0
- package/src/lib/services/dashboard.service.ts +62 -0
- package/src/lib/store/__tests__/dashboard-store-collision-detection.spec.ts +756 -0
- package/src/lib/store/__tests__/dashboard-store-computed-properties.spec.ts +974 -0
- package/src/lib/store/__tests__/dashboard-store-drag-drop.spec.ts +279 -0
- package/src/lib/store/__tests__/dashboard-store-export-import.spec.ts +780 -0
- package/src/lib/store/__tests__/dashboard-store-grid-config.spec.ts +128 -0
- package/src/lib/store/__tests__/dashboard-store-query-methods.spec.ts +229 -0
- package/src/lib/store/__tests__/dashboard-store-resize-operations.spec.ts +652 -0
- package/src/lib/store/__tests__/dashboard-store-widget-management.spec.ts +461 -0
- package/src/lib/store/__tests__/dashboard-store-widget-state-preservation.spec.ts +369 -0
- package/src/lib/store/dashboard-store.ts +239 -0
- package/src/lib/store/features/drag-drop.feature.ts +140 -0
- package/src/lib/store/features/grid-config.feature.ts +43 -0
- package/src/lib/store/features/resize.feature.ts +140 -0
- package/src/lib/store/features/utils/collision.utils.ts +89 -0
- package/src/lib/store/features/utils/grid-query-internal.utils.ts +37 -0
- package/src/lib/store/features/utils/resize.utils.ts +165 -0
- package/src/lib/store/features/widget-management.feature.ts +158 -0
- package/src/lib/styles/_dashboard-grid-vars.scss +11 -0
- package/src/lib/widget-list/__tests__/widget-list-bridge-integration.spec.ts +137 -0
- package/src/lib/widget-list/widget-list.component.html +22 -0
- package/src/lib/widget-list/widget-list.component.scss +154 -0
- package/src/lib/widget-list/widget-list.component.ts +106 -0
- package/src/public-api.ts +21 -0
- package/src/test-setup.ts +10 -0
- package/tsconfig.lib.json +15 -0
- package/tsconfig.lib.prod.json +11 -0
- package/tsconfig.spec.json +14 -0
- package/fesm2022/dragonworks-ngx-dashboard.mjs +0 -2178
- package/fesm2022/dragonworks-ngx-dashboard.mjs.map +0 -1
- 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
|
+
});
|