@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.
- 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 -2192
- package/fesm2022/dragonworks-ngx-dashboard.mjs.map +0 -1
- 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
|
+
});
|