@dragonworks/ngx-dashboard 20.0.4 → 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 -2192
  77. package/fesm2022/dragonworks-ngx-dashboard.mjs.map +0 -1
  78. package/index.d.ts +0 -678
@@ -0,0 +1,220 @@
1
+ // dashboard-bridge.service.spec.ts
2
+ import { TestBed } from '@angular/core/testing';
3
+ import { Component } from '@angular/core';
4
+ import { DashboardBridgeService } from '../dashboard-bridge.service';
5
+ import { DashboardStore } from '../../store/dashboard-store';
6
+ import { DragData, WidgetMetadata } from '../../models';
7
+
8
+ // Mock dashboard store for testing
9
+ function createMockDashboardStore(dashboardId = 'test-dashboard-1') {
10
+ const mockStore = {
11
+ dashboardId: jasmine.createSpy('dashboardId').and.returnValue(dashboardId),
12
+ gridCellDimensions: jasmine.createSpy('gridCellDimensions').and.returnValue({ width: 120, height: 80 }),
13
+ startDrag: jasmine.createSpy('startDrag'),
14
+ endDrag: jasmine.createSpy('endDrag')
15
+ } as any;
16
+ return mockStore;
17
+ }
18
+
19
+ // Mock widget metadata for testing
20
+ function createMockWidgetMetadata(): WidgetMetadata {
21
+ return {
22
+ widgetTypeid: 'test-widget',
23
+ name: 'Test Widget',
24
+ description: 'A test widget for testing',
25
+ svgIcon: '<svg><rect width="10" height="10"/></svg>'
26
+ };
27
+ }
28
+
29
+ describe('DashboardBridgeService', () => {
30
+ let service: DashboardBridgeService;
31
+
32
+ beforeEach(() => {
33
+ TestBed.configureTestingModule({
34
+ providers: [DashboardBridgeService]
35
+ });
36
+ service = TestBed.inject(DashboardBridgeService);
37
+ });
38
+
39
+ describe('Initial State', () => {
40
+ it('should be created', () => {
41
+ expect(service).toBeTruthy();
42
+ });
43
+
44
+ it('should start with zero dashboards', () => {
45
+ expect(service.dashboardCount()).toBe(0);
46
+ expect(service.hasDashboards()).toBe(false);
47
+ expect(service.registeredDashboards()).toEqual([]);
48
+ });
49
+
50
+ it('should provide fallback dimensions when no dashboards exist', () => {
51
+ expect(service.availableDimensions()).toEqual({ width: 100, height: 100 });
52
+ });
53
+ });
54
+
55
+ describe('Dashboard Registration', () => {
56
+ it('should register a dashboard with its ID', () => {
57
+ const mockStore = createMockDashboardStore('test-dashboard-1');
58
+
59
+ service.registerDashboard(mockStore);
60
+
61
+ expect(service.dashboardCount()).toBe(1);
62
+ expect(service.hasDashboards()).toBe(true);
63
+ expect(service.registeredDashboards()).toContain('test-dashboard-1');
64
+ });
65
+
66
+ it('should register multiple dashboards with different IDs', () => {
67
+ const store1 = createMockDashboardStore('dashboard-1');
68
+ const store2 = createMockDashboardStore('dashboard-2');
69
+
70
+ service.registerDashboard(store1);
71
+ service.registerDashboard(store2);
72
+
73
+ expect(service.dashboardCount()).toBe(2);
74
+ expect(service.registeredDashboards()).toContain('dashboard-1');
75
+ expect(service.registeredDashboards()).toContain('dashboard-2');
76
+ });
77
+
78
+ it('should not register dashboard without ID', () => {
79
+ const mockStore = createMockDashboardStore('');
80
+ mockStore.dashboardId.and.returnValue('');
81
+
82
+ service.registerDashboard(mockStore);
83
+
84
+ expect(service.dashboardCount()).toBe(0);
85
+ });
86
+
87
+ });
88
+
89
+ describe('Dashboard Unregistration', () => {
90
+ it('should unregister a dashboard', () => {
91
+ const mockStore = createMockDashboardStore('test-dashboard-1');
92
+ service.registerDashboard(mockStore);
93
+
94
+ expect(service.dashboardCount()).toBe(1);
95
+
96
+ service.unregisterDashboard(mockStore);
97
+
98
+ expect(service.dashboardCount()).toBe(0);
99
+ expect(service.hasDashboards()).toBe(false);
100
+ expect(service.registeredDashboards()).not.toContain('test-dashboard-1');
101
+ });
102
+
103
+ it('should handle unregistering dashboard without ID gracefully', () => {
104
+ const mockStore = createMockDashboardStore('');
105
+ mockStore.dashboardId.and.returnValue('');
106
+
107
+ expect(() => service.unregisterDashboard(mockStore)).not.toThrow();
108
+ expect(service.dashboardCount()).toBe(0);
109
+ });
110
+
111
+
112
+ it('should return to fallback dimensions when all dashboards removed', () => {
113
+ const mockStore = createMockDashboardStore('test-dashboard-1');
114
+
115
+ service.registerDashboard(mockStore);
116
+ service.unregisterDashboard(mockStore);
117
+
118
+ expect(service.availableDimensions()).toEqual({ width: 100, height: 100 });
119
+ });
120
+ });
121
+
122
+ describe('Specific Dashboard Dimensions', () => {
123
+ it('should return fallback dimensions for non-existent dashboard ID', () => {
124
+ expect(service.getDashboardDimensions('non-existent')).toEqual({ width: 100, height: 100 });
125
+ });
126
+ });
127
+
128
+ describe('Drag Operations', () => {
129
+ it('should not start drag when no dashboards available', () => {
130
+ const dragData: DragData = { kind: 'widget', content: createMockWidgetMetadata() };
131
+ expect(() => service.startDrag(dragData)).not.toThrow();
132
+ });
133
+
134
+ it('should not throw when ending drag with no dashboards', () => {
135
+ expect(() => service.endDrag()).not.toThrow();
136
+ });
137
+ });
138
+
139
+ describe('Reactive Properties', () => {
140
+ it('should reactively update dashboard count', () => {
141
+ expect(service.dashboardCount()).toBe(0);
142
+
143
+ const store1 = createMockDashboardStore('dashboard-1');
144
+ service.registerDashboard(store1);
145
+ expect(service.dashboardCount()).toBe(1);
146
+
147
+ const store2 = createMockDashboardStore('dashboard-2');
148
+ service.registerDashboard(store2);
149
+ expect(service.dashboardCount()).toBe(2);
150
+
151
+ service.unregisterDashboard(store1);
152
+ expect(service.dashboardCount()).toBe(1);
153
+
154
+ service.unregisterDashboard(store2);
155
+ expect(service.dashboardCount()).toBe(0);
156
+ });
157
+ });
158
+
159
+ describe('Edge Cases', () => {
160
+ it('should handle dashboard with undefined dimensions', () => {
161
+ const mockStore = createMockDashboardStore('test-dashboard-1');
162
+ mockStore.gridCellDimensions.and.returnValue(undefined);
163
+
164
+ service.registerDashboard(mockStore);
165
+
166
+ expect(service.availableDimensions()).toEqual({ width: 100, height: 100 });
167
+ expect(service.getDashboardDimensions('test-dashboard-1')).toEqual({ width: 100, height: 100 });
168
+ });
169
+
170
+ it('should handle registration of many dashboards', () => {
171
+ const stores: any[] = [];
172
+
173
+ // Register 10 dashboards
174
+ for (let i = 0; i < 10; i++) {
175
+ const store = createMockDashboardStore(`dashboard-${i}`);
176
+ stores.push(store);
177
+ service.registerDashboard(store);
178
+ }
179
+ expect(service.dashboardCount()).toBe(10);
180
+
181
+ // Unregister all
182
+ stores.forEach(store => {
183
+ service.unregisterDashboard(store);
184
+ });
185
+ expect(service.dashboardCount()).toBe(0);
186
+ expect(service.hasDashboards()).toBe(false);
187
+ });
188
+
189
+ it('should handle mixed registration and unregistration cycles', () => {
190
+ const store1 = createMockDashboardStore('dashboard-1');
191
+ const store2 = createMockDashboardStore('dashboard-2');
192
+ const store3 = createMockDashboardStore('dashboard-3');
193
+
194
+ service.registerDashboard(store1);
195
+ expect(service.dashboardCount()).toBe(1);
196
+
197
+ service.registerDashboard(store2);
198
+ expect(service.dashboardCount()).toBe(2);
199
+
200
+ service.unregisterDashboard(store1);
201
+ expect(service.dashboardCount()).toBe(1);
202
+
203
+ service.registerDashboard(store3);
204
+ expect(service.dashboardCount()).toBe(2);
205
+
206
+ service.unregisterDashboard(store2);
207
+ service.unregisterDashboard(store3);
208
+ expect(service.dashboardCount()).toBe(0);
209
+ });
210
+
211
+ it('should handle updateDashboardRegistration method', () => {
212
+ const mockStore = createMockDashboardStore('test-dashboard-1');
213
+
214
+ service.updateDashboardRegistration(mockStore);
215
+
216
+ expect(service.dashboardCount()).toBe(1);
217
+ expect(service.registeredDashboards()).toContain('test-dashboard-1');
218
+ });
219
+ });
220
+ });
@@ -0,0 +1,362 @@
1
+ import { TestBed } from '@angular/core/testing';
2
+ import { PLATFORM_ID } from '@angular/core';
3
+ import { DashboardViewportService } from '../dashboard-viewport.service';
4
+ import { DashboardStore } from '../../store/dashboard-store';
5
+ import {
6
+ ReservedSpace,
7
+ DEFAULT_RESERVED_SPACE,
8
+ } from '../../models/reserved-space';
9
+
10
+ // Mock ResizeObserver for testing
11
+ class MockResizeObserver {
12
+ private callbacks: Array<(entries: any[]) => void> = [];
13
+
14
+ constructor(callback: (entries: any[]) => void) {
15
+ this.callbacks.push(callback);
16
+ // Store reference for manual triggering in tests
17
+ (MockResizeObserver as any).instances =
18
+ (MockResizeObserver as any).instances || [];
19
+ (MockResizeObserver as any).instances.push(this);
20
+ }
21
+
22
+ observe() {}
23
+ disconnect() {}
24
+
25
+ static triggerResize(width: number, height: number) {
26
+ const instances = (MockResizeObserver as any).instances || [];
27
+ instances.forEach((instance: MockResizeObserver) => {
28
+ instance.callbacks.forEach((callback) => {
29
+ callback([
30
+ {
31
+ contentBoxSize: [{ inlineSize: width, blockSize: height }],
32
+ },
33
+ ]);
34
+ });
35
+ });
36
+ }
37
+
38
+ static reset() {
39
+ (MockResizeObserver as any).instances = [];
40
+ }
41
+ }
42
+
43
+ // Mock window dimensions
44
+ Object.defineProperty(window, 'innerWidth', {
45
+ writable: true,
46
+ configurable: true,
47
+ value: 1024,
48
+ });
49
+
50
+ Object.defineProperty(window, 'innerHeight', {
51
+ writable: true,
52
+ configurable: true,
53
+ value: 768,
54
+ });
55
+
56
+ describe('DashboardViewportService', () => {
57
+ let service: DashboardViewportService;
58
+ let store: InstanceType<typeof DashboardStore>;
59
+
60
+ beforeEach(() => {
61
+ // Mock ResizeObserver globally
62
+ (window as any).ResizeObserver = MockResizeObserver;
63
+ MockResizeObserver.reset();
64
+
65
+ TestBed.configureTestingModule({
66
+ providers: [
67
+ DashboardViewportService,
68
+ DashboardStore,
69
+ { provide: PLATFORM_ID, useValue: 'browser' },
70
+ ],
71
+ });
72
+
73
+ service = TestBed.inject(DashboardViewportService);
74
+ store = TestBed.inject(DashboardStore);
75
+
76
+ // Initialize store with dashboard data
77
+ store.initializeFromDto({
78
+ version: '1.0.0',
79
+ dashboardId: 'test-dashboard',
80
+ rows: 8,
81
+ columns: 16,
82
+ gutterSize: '1em',
83
+ cells: [],
84
+ });
85
+
86
+ // Set initial viewport size
87
+ MockResizeObserver.triggerResize(1024, 768);
88
+ });
89
+
90
+ afterEach(() => {
91
+ MockResizeObserver.reset();
92
+ });
93
+
94
+ describe('Constraint Calculations', () => {
95
+ it('should constrain by height when grid aspect ratio is wide', () => {
96
+ // Setup: Wide grid (16x8 = 2:1 aspect ratio) in landscape viewport (1024x768)
97
+ // Available space: 1024x768 (no reserved space)
98
+ // Max width from height: 768 * 2 = 1536 > 1024 (available width)
99
+ // So width constrains: maxWidth = 1024, maxHeight = 1024/2 = 512
100
+
101
+ const constraints = service.constraints();
102
+
103
+ expect(constraints.constrainedBy).toBe('width');
104
+ expect(constraints.maxWidth).toBe(1024);
105
+ expect(constraints.maxHeight).toBe(512); // 1024 / 2 (aspect ratio)
106
+ });
107
+
108
+ it('should constrain by height when grid aspect ratio is tall', () => {
109
+ // Setup: Tall grid (8x16 = 1:2 aspect ratio) in landscape viewport
110
+ store.setGridConfig({ rows: 16, columns: 8 });
111
+
112
+ // Available space: 1024x768
113
+ // Aspect ratio: 8/16 = 0.5
114
+ // Max width from height: 768 * 0.5 = 384 < 1024 (available width)
115
+ // So height constrains: maxHeight = 768, maxWidth = 384
116
+
117
+ const constraints = service.constraints();
118
+
119
+ expect(constraints.constrainedBy).toBe('height');
120
+ expect(constraints.maxWidth).toBe(384); // 768 * 0.5 (aspect ratio)
121
+ expect(constraints.maxHeight).toBe(768);
122
+ });
123
+
124
+ it('should constrain by width in portrait viewport with wide grid', () => {
125
+ // Setup: Portrait viewport (768x1024) with wide grid (16x8)
126
+ MockResizeObserver.triggerResize(768, 1024);
127
+
128
+ // Available space: 768x1024
129
+ // Aspect ratio: 16/8 = 2
130
+ // Max width from height: 1024 * 2 = 2048 > 768 (available width)
131
+ // So width constrains: maxWidth = 768, maxHeight = 768/2 = 384
132
+
133
+ const constraints = service.constraints();
134
+
135
+ expect(constraints.constrainedBy).toBe('width');
136
+ expect(constraints.maxWidth).toBe(768);
137
+ expect(constraints.maxHeight).toBe(384); // 768 / 2
138
+ });
139
+
140
+ it('should constrain by height in portrait viewport with tall grid', () => {
141
+ // Setup: Portrait viewport (768x1024) with tall grid (8x16)
142
+ MockResizeObserver.triggerResize(768, 1024);
143
+ store.setGridConfig({ rows: 16, columns: 8 });
144
+
145
+ // Available space: 768x1024
146
+ // Aspect ratio: 8/16 = 0.5
147
+ // Max width from height: 1024 * 0.5 = 512 < 768 (available width)
148
+ // So height constrains: maxHeight = 1024, maxWidth = 512
149
+
150
+ const constraints = service.constraints();
151
+
152
+ expect(constraints.constrainedBy).toBe('height');
153
+ expect(constraints.maxWidth).toBe(512); // 1024 * 0.5
154
+ expect(constraints.maxHeight).toBe(1024);
155
+ });
156
+
157
+ it('should handle square grid correctly', () => {
158
+ // Setup: Square grid (8x8 = 1:1 aspect ratio)
159
+ store.setGridConfig({ rows: 8, columns: 8 });
160
+
161
+ // Available space: 1024x768
162
+ // Aspect ratio: 8/8 = 1
163
+ // Max width from height: 768 * 1 = 768 < 1024 (available width)
164
+ // So height constrains: maxHeight = 768, maxWidth = 768
165
+
166
+ const constraints = service.constraints();
167
+
168
+ expect(constraints.constrainedBy).toBe('height');
169
+ expect(constraints.maxWidth).toBe(768);
170
+ expect(constraints.maxHeight).toBe(768);
171
+ });
172
+
173
+ it('should return available space when grid has zero rows', () => {
174
+ // Edge case: Invalid grid dimensions
175
+ store.setGridConfig({ rows: 0, columns: 8 });
176
+
177
+ const constraints = service.constraints();
178
+
179
+ expect(constraints.constrainedBy).toBe('none');
180
+ expect(constraints.maxWidth).toBe(1024);
181
+ expect(constraints.maxHeight).toBe(768);
182
+ });
183
+
184
+ it('should return available space when grid has zero columns', () => {
185
+ // Edge case: Invalid grid dimensions
186
+ store.setGridConfig({ rows: 8, columns: 0 });
187
+
188
+ const constraints = service.constraints();
189
+
190
+ expect(constraints.constrainedBy).toBe('none');
191
+ expect(constraints.maxWidth).toBe(1024);
192
+ expect(constraints.maxHeight).toBe(768);
193
+ });
194
+
195
+ it('should handle very small viewport dimensions', () => {
196
+ // Edge case: Tiny viewport
197
+ MockResizeObserver.triggerResize(100, 100);
198
+
199
+ // Square grid should fit exactly
200
+ store.setGridConfig({ rows: 4, columns: 4 });
201
+
202
+ const constraints = service.constraints();
203
+
204
+ expect(constraints.maxWidth).toBe(100);
205
+ expect(constraints.maxHeight).toBe(100);
206
+ expect(constraints.constrainedBy).toBe('height'); // or 'width' - both are equivalent for square
207
+ });
208
+
209
+ it('should handle very large grid dimensions', () => {
210
+ // Edge case: Huge grid
211
+ store.setGridConfig({ rows: 100, columns: 200 });
212
+
213
+ // Aspect ratio: 200/100 = 2
214
+ // Should still calculate correctly
215
+ const constraints = service.constraints();
216
+
217
+ expect(constraints.constrainedBy).toBe('width');
218
+ expect(constraints.maxWidth).toBe(1024);
219
+ expect(constraints.maxHeight).toBe(512); // 1024 / 2
220
+ });
221
+ });
222
+
223
+ describe('Reserved Space Integration', () => {
224
+ it('should account for reserved space in constraint calculations', () => {
225
+ // Setup: Reserve space on all sides
226
+ const reservedSpace: ReservedSpace = {
227
+ top: 64, // Toolbar
228
+ right: 320, // Widget list
229
+ bottom: 32, // Padding
230
+ left: 32, // Padding
231
+ };
232
+
233
+ service.setReservedSpace(reservedSpace);
234
+
235
+ // Available space: (1024 - 32 - 320) x (768 - 64 - 32) = 672 x 672
236
+ // Square available space with 2:1 aspect ratio grid
237
+ // Max width from height: 672 * 2 = 1344 > 672 (available width)
238
+ // So width constrains: maxWidth = 672, maxHeight = 336
239
+
240
+ const constraints = service.constraints();
241
+
242
+ expect(constraints.constrainedBy).toBe('width');
243
+ expect(constraints.maxWidth).toBe(672); // 1024 - 32 - 320
244
+ expect(constraints.maxHeight).toBe(336); // 672 / 2
245
+ });
246
+
247
+ it('should handle asymmetric reserved space', () => {
248
+ // Setup: Only reserve top and right space
249
+ const reservedSpace: ReservedSpace = {
250
+ top: 100,
251
+ right: 200,
252
+ bottom: 0,
253
+ left: 0,
254
+ };
255
+
256
+ service.setReservedSpace(reservedSpace);
257
+
258
+ // Available space: (1024 - 200) x (768 - 100) = 824 x 668
259
+ // With 2:1 aspect ratio: height constrains
260
+ // Max width from height: 668 * 2 = 1336 > 824
261
+ // So width constrains: maxWidth = 824, maxHeight = 412
262
+
263
+ const constraints = service.constraints();
264
+
265
+ expect(constraints.constrainedBy).toBe('width');
266
+ expect(constraints.maxWidth).toBe(824);
267
+ expect(constraints.maxHeight).toBe(412); // 824 / 2
268
+ });
269
+
270
+ it('should not allow negative available space', () => {
271
+ // Edge case: Reserved space larger than viewport
272
+ const reservedSpace: ReservedSpace = {
273
+ top: 400,
274
+ right: 600,
275
+ bottom: 400,
276
+ left: 600,
277
+ };
278
+
279
+ service.setReservedSpace(reservedSpace);
280
+
281
+ // Available space would be negative, should be clamped to 0
282
+ const constraints = service.constraints();
283
+
284
+ expect(constraints.maxWidth).toBe(0);
285
+ expect(constraints.maxHeight).toBe(0);
286
+ });
287
+
288
+ it('should update constraints reactively when reserved space changes', () => {
289
+ // Initial state: no reserved space
290
+ let constraints = service.constraints();
291
+ const initialMaxWidth = constraints.maxWidth;
292
+
293
+ // Add reserved space
294
+ service.setReservedSpace({
295
+ top: 50,
296
+ right: 100,
297
+ bottom: 50,
298
+ left: 100,
299
+ });
300
+
301
+ // Should recalculate automatically
302
+ constraints = service.constraints();
303
+
304
+ expect(constraints.maxWidth).toBeLessThan(initialMaxWidth);
305
+ expect(constraints.maxWidth).toBe(1024 - 200); // 1024 - left - right
306
+ });
307
+ });
308
+
309
+ describe('Signal Reactivity', () => {
310
+ it('should recalculate constraints when grid dimensions change', () => {
311
+ // Initial: 16x8 grid (wide)
312
+ let constraints = service.constraints();
313
+ expect(constraints.constrainedBy).toBe('width');
314
+ const initialMaxHeight = constraints.maxHeight;
315
+
316
+ // Change to 8x16 grid (tall)
317
+ store.setGridConfig({ rows: 16, columns: 8 });
318
+
319
+ // Should recalculate automatically
320
+ constraints = service.constraints();
321
+ expect(constraints.constrainedBy).toBe('height');
322
+ expect(constraints.maxHeight).toBeGreaterThan(initialMaxHeight);
323
+ });
324
+
325
+ it('should recalculate constraints when viewport size changes', () => {
326
+ // Initial viewport
327
+ let constraints = service.constraints();
328
+ const initialMaxWidth = constraints.maxWidth;
329
+
330
+ // Resize viewport to be smaller
331
+ MockResizeObserver.triggerResize(800, 600);
332
+
333
+ // Should recalculate automatically
334
+ constraints = service.constraints();
335
+ expect(constraints.maxWidth).toBeLessThan(initialMaxWidth);
336
+ expect(constraints.maxWidth).toBe(800);
337
+ });
338
+ });
339
+
340
+ describe('Available Space Calculation', () => {
341
+ it('should calculate available space correctly with default reserved space', () => {
342
+ const availableSpace = service.availableSpace();
343
+
344
+ expect(availableSpace.width).toBe(1024);
345
+ expect(availableSpace.height).toBe(768);
346
+ });
347
+
348
+ it('should calculate available space correctly with custom reserved space', () => {
349
+ service.setReservedSpace({
350
+ top: 60,
351
+ right: 300,
352
+ bottom: 40,
353
+ left: 20,
354
+ });
355
+
356
+ const availableSpace = service.availableSpace();
357
+
358
+ expect(availableSpace.width).toBe(704); // 1024 - 20 - 300
359
+ expect(availableSpace.height).toBe(668); // 768 - 60 - 40
360
+ });
361
+ });
362
+ });