@xiboplayer/renderer 0.1.0
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/docs/PERFORMANCE_OPTIMIZATIONS.md +451 -0
- package/docs/README.md +98 -0
- package/docs/RENDERER_COMPARISON.md +483 -0
- package/docs/TRANSITIONS.md +180 -0
- package/package.json +40 -0
- package/src/index.js +4 -0
- package/src/layout-pool.js +245 -0
- package/src/layout-pool.test.js +373 -0
- package/src/layout.js +1073 -0
- package/src/renderer-lite.js +2637 -0
- package/src/renderer-lite.overlays.test.js +493 -0
- package/src/renderer-lite.test.js +901 -0
- package/vitest.config.js +8 -0
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for RendererLite overlay rendering
|
|
3
|
+
*
|
|
4
|
+
* Tests overlay layout rendering on top of main layouts
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
8
|
+
import { RendererLite } from './renderer-lite.js';
|
|
9
|
+
|
|
10
|
+
// Mock logger
|
|
11
|
+
vi.mock('@xiboplayer/utils', () => ({
|
|
12
|
+
createLogger: () => ({
|
|
13
|
+
info: vi.fn(),
|
|
14
|
+
warn: vi.fn(),
|
|
15
|
+
error: vi.fn(),
|
|
16
|
+
debug: vi.fn()
|
|
17
|
+
})
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
describe('RendererLite - Overlay Rendering', () => {
|
|
21
|
+
let renderer;
|
|
22
|
+
let container;
|
|
23
|
+
let blobCounter;
|
|
24
|
+
|
|
25
|
+
// Sample XLF for overlay
|
|
26
|
+
const overlayXLF = `<?xml version="1.0"?>
|
|
27
|
+
<layout width="1920" height="1080" bgcolor="#00000080">
|
|
28
|
+
<region id="overlay-1" width="400" height="200" top="50" left="50" zindex="10">
|
|
29
|
+
<media id="1" type="text" duration="10">
|
|
30
|
+
<raw><![CDATA[<p>Overlay Content</p>]]></raw>
|
|
31
|
+
</media>
|
|
32
|
+
</region>
|
|
33
|
+
</layout>`;
|
|
34
|
+
|
|
35
|
+
// Sample XLF for main layout
|
|
36
|
+
const mainLayoutXLF = `<?xml version="1.0"?>
|
|
37
|
+
<layout width="1920" height="1080" bgcolor="#000000">
|
|
38
|
+
<region id="main-1" width="1920" height="1080" top="0" left="0" zindex="0">
|
|
39
|
+
<media id="1" type="text" duration="30">
|
|
40
|
+
<raw><![CDATA[<p>Main Content</p>]]></raw>
|
|
41
|
+
</media>
|
|
42
|
+
</region>
|
|
43
|
+
</layout>`;
|
|
44
|
+
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
// Mock URL.createObjectURL and URL.revokeObjectURL (not available in jsdom)
|
|
47
|
+
blobCounter = 0;
|
|
48
|
+
if (!URL.createObjectURL) {
|
|
49
|
+
URL.createObjectURL = vi.fn(() => `blob:test/${++blobCounter}`);
|
|
50
|
+
}
|
|
51
|
+
if (!URL.revokeObjectURL) {
|
|
52
|
+
URL.revokeObjectURL = vi.fn();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Create container
|
|
56
|
+
container = document.createElement('div');
|
|
57
|
+
container.id = 'test-container';
|
|
58
|
+
document.body.appendChild(container);
|
|
59
|
+
|
|
60
|
+
// Create renderer
|
|
61
|
+
renderer = new RendererLite(
|
|
62
|
+
{
|
|
63
|
+
cmsUrl: 'http://test.local',
|
|
64
|
+
hardwareKey: 'test-key'
|
|
65
|
+
},
|
|
66
|
+
container,
|
|
67
|
+
{
|
|
68
|
+
getMediaUrl: async (fileId) => `http://test.local/media/${fileId}`,
|
|
69
|
+
getWidgetHtml: async (widget) => widget.raw || '<p>Widget HTML</p>'
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
afterEach(() => {
|
|
75
|
+
renderer.cleanup();
|
|
76
|
+
// Container may have been removed from DOM by layoutPool.evict() during cleanup
|
|
77
|
+
if (container.parentNode) {
|
|
78
|
+
container.parentNode.removeChild(container);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('Overlay Container Setup', () => {
|
|
83
|
+
it('should create overlay container on init', () => {
|
|
84
|
+
const overlayContainer = container.querySelector('#overlay-container');
|
|
85
|
+
|
|
86
|
+
expect(overlayContainer).toBeDefined();
|
|
87
|
+
expect(overlayContainer).not.toBeNull();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should set correct z-index for overlay container', () => {
|
|
91
|
+
const overlayContainer = container.querySelector('#overlay-container');
|
|
92
|
+
|
|
93
|
+
expect(overlayContainer.style.zIndex).toBe('1000');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should set overlay container to full size', () => {
|
|
97
|
+
const overlayContainer = container.querySelector('#overlay-container');
|
|
98
|
+
|
|
99
|
+
expect(overlayContainer.style.width).toBe('100%');
|
|
100
|
+
expect(overlayContainer.style.height).toBe('100%');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should set pointer-events to none on overlay container', () => {
|
|
104
|
+
const overlayContainer = container.querySelector('#overlay-container');
|
|
105
|
+
|
|
106
|
+
expect(overlayContainer.style.pointerEvents).toBe('none');
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('renderOverlay()', () => {
|
|
111
|
+
it('should render overlay on top of main layout', async () => {
|
|
112
|
+
// Render main layout first
|
|
113
|
+
await renderer.renderLayout(mainLayoutXLF, 100);
|
|
114
|
+
|
|
115
|
+
// Render overlay
|
|
116
|
+
await renderer.renderOverlay(overlayXLF, 200, 10);
|
|
117
|
+
|
|
118
|
+
// Check overlay exists
|
|
119
|
+
const overlayDiv = container.querySelector('#overlay_200');
|
|
120
|
+
expect(overlayDiv).toBeDefined();
|
|
121
|
+
expect(overlayDiv).not.toBeNull();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should set correct z-index based on priority', async () => {
|
|
125
|
+
await renderer.renderOverlay(overlayXLF, 200, 15);
|
|
126
|
+
|
|
127
|
+
const overlayDiv = container.querySelector('#overlay_200');
|
|
128
|
+
expect(overlayDiv.style.zIndex).toBe('1015'); // 1000 + priority
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should create regions for overlay', async () => {
|
|
132
|
+
await renderer.renderOverlay(overlayXLF, 200, 10);
|
|
133
|
+
|
|
134
|
+
const region = container.querySelector('#overlay_200_region_overlay-1');
|
|
135
|
+
expect(region).toBeDefined();
|
|
136
|
+
expect(region).not.toBeNull();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should emit overlayStart event', async () => {
|
|
140
|
+
const startListener = vi.fn();
|
|
141
|
+
renderer.on('overlayStart', startListener);
|
|
142
|
+
|
|
143
|
+
await renderer.renderOverlay(overlayXLF, 200, 10);
|
|
144
|
+
|
|
145
|
+
expect(startListener).toHaveBeenCalledWith(200, expect.any(Object));
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should store overlay in activeOverlays', async () => {
|
|
149
|
+
await renderer.renderOverlay(overlayXLF, 200, 10);
|
|
150
|
+
|
|
151
|
+
expect(renderer.activeOverlays.has(200)).toBe(true);
|
|
152
|
+
const overlayState = renderer.activeOverlays.get(200);
|
|
153
|
+
expect(overlayState.priority).toBe(10);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should skip if overlay already active', async () => {
|
|
157
|
+
await renderer.renderOverlay(overlayXLF, 200, 10);
|
|
158
|
+
const firstOverlayDiv = container.querySelector('#overlay_200');
|
|
159
|
+
|
|
160
|
+
// Try to render same overlay again
|
|
161
|
+
await renderer.renderOverlay(overlayXLF, 200, 10);
|
|
162
|
+
|
|
163
|
+
// Should still be the same element
|
|
164
|
+
const secondOverlayDiv = container.querySelector('#overlay_200');
|
|
165
|
+
expect(secondOverlayDiv).toBe(firstOverlayDiv);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should pre-fetch media URLs for overlay widgets', async () => {
|
|
169
|
+
const getMediaUrl = vi.fn(async (fileId) => `http://test.local/media/${fileId}`);
|
|
170
|
+
|
|
171
|
+
const customRenderer = new RendererLite(
|
|
172
|
+
{ cmsUrl: 'http://test.local', hardwareKey: 'test-key' },
|
|
173
|
+
container,
|
|
174
|
+
{
|
|
175
|
+
getMediaUrl,
|
|
176
|
+
getWidgetHtml: async (widget) => widget.raw
|
|
177
|
+
}
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const xlfWithMedia = `<?xml version="1.0"?>
|
|
181
|
+
<layout width="1920" height="1080" bgcolor="#000000">
|
|
182
|
+
<region id="1" width="400" height="200" top="0" left="0" zindex="0">
|
|
183
|
+
<media id="10" fileId="555" type="image" duration="10">
|
|
184
|
+
<options><uri>test.jpg</uri></options>
|
|
185
|
+
</media>
|
|
186
|
+
</region>
|
|
187
|
+
</layout>`;
|
|
188
|
+
|
|
189
|
+
await customRenderer.renderOverlay(xlfWithMedia, 300, 10);
|
|
190
|
+
|
|
191
|
+
expect(getMediaUrl).toHaveBeenCalledWith(555);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should set overlay timer based on duration', async () => {
|
|
195
|
+
vi.useFakeTimers();
|
|
196
|
+
|
|
197
|
+
const endListener = vi.fn();
|
|
198
|
+
renderer.on('overlayEnd', endListener);
|
|
199
|
+
|
|
200
|
+
const xlfWith60s = `<?xml version="1.0"?>
|
|
201
|
+
<layout width="1920" height="1080" bgcolor="#000000" duration="60">
|
|
202
|
+
<region id="1" width="400" height="200" top="0" left="0" zindex="0">
|
|
203
|
+
<media id="1" type="text" duration="30">
|
|
204
|
+
<raw><![CDATA[<p>Test</p>]]></raw>
|
|
205
|
+
</media>
|
|
206
|
+
</region>
|
|
207
|
+
</layout>`;
|
|
208
|
+
|
|
209
|
+
await renderer.renderOverlay(xlfWith60s, 400, 10);
|
|
210
|
+
|
|
211
|
+
// Fast forward 60 seconds
|
|
212
|
+
vi.advanceTimersByTime(60000);
|
|
213
|
+
|
|
214
|
+
expect(endListener).toHaveBeenCalledWith(400);
|
|
215
|
+
|
|
216
|
+
vi.useRealTimers();
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe('Multiple overlays', () => {
|
|
221
|
+
it('should render multiple overlays simultaneously', async () => {
|
|
222
|
+
await renderer.renderOverlay(overlayXLF, 201, 10);
|
|
223
|
+
await renderer.renderOverlay(overlayXLF, 202, 20);
|
|
224
|
+
await renderer.renderOverlay(overlayXLF, 203, 5);
|
|
225
|
+
|
|
226
|
+
expect(renderer.activeOverlays.size).toBe(3);
|
|
227
|
+
expect(container.querySelector('#overlay_201')).not.toBeNull();
|
|
228
|
+
expect(container.querySelector('#overlay_202')).not.toBeNull();
|
|
229
|
+
expect(container.querySelector('#overlay_203')).not.toBeNull();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should set correct z-index for multiple overlays by priority', async () => {
|
|
233
|
+
await renderer.renderOverlay(overlayXLF, 201, 10);
|
|
234
|
+
await renderer.renderOverlay(overlayXLF, 202, 20);
|
|
235
|
+
await renderer.renderOverlay(overlayXLF, 203, 5);
|
|
236
|
+
|
|
237
|
+
const overlay1 = container.querySelector('#overlay_201');
|
|
238
|
+
const overlay2 = container.querySelector('#overlay_202');
|
|
239
|
+
const overlay3 = container.querySelector('#overlay_203');
|
|
240
|
+
|
|
241
|
+
expect(overlay1.style.zIndex).toBe('1010'); // 1000 + 10
|
|
242
|
+
expect(overlay2.style.zIndex).toBe('1020'); // 1000 + 20 (highest)
|
|
243
|
+
expect(overlay3.style.zIndex).toBe('1005'); // 1000 + 5 (lowest)
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('should return all active overlay IDs', async () => {
|
|
247
|
+
await renderer.renderOverlay(overlayXLF, 201, 10);
|
|
248
|
+
await renderer.renderOverlay(overlayXLF, 202, 20);
|
|
249
|
+
|
|
250
|
+
const activeIds = renderer.getActiveOverlays();
|
|
251
|
+
|
|
252
|
+
expect(activeIds).toContain(201);
|
|
253
|
+
expect(activeIds).toContain(202);
|
|
254
|
+
expect(activeIds.length).toBe(2);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe('stopOverlay()', () => {
|
|
259
|
+
it('should remove overlay from DOM', async () => {
|
|
260
|
+
await renderer.renderOverlay(overlayXLF, 200, 10);
|
|
261
|
+
expect(container.querySelector('#overlay_200')).not.toBeNull();
|
|
262
|
+
|
|
263
|
+
renderer.stopOverlay(200);
|
|
264
|
+
|
|
265
|
+
expect(container.querySelector('#overlay_200')).toBeNull();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('should remove overlay from activeOverlays', async () => {
|
|
269
|
+
await renderer.renderOverlay(overlayXLF, 200, 10);
|
|
270
|
+
expect(renderer.activeOverlays.has(200)).toBe(true);
|
|
271
|
+
|
|
272
|
+
renderer.stopOverlay(200);
|
|
273
|
+
|
|
274
|
+
expect(renderer.activeOverlays.has(200)).toBe(false);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('should clear overlay timer', async () => {
|
|
278
|
+
vi.useFakeTimers();
|
|
279
|
+
|
|
280
|
+
await renderer.renderOverlay(overlayXLF, 200, 10);
|
|
281
|
+
const overlayState = renderer.activeOverlays.get(200);
|
|
282
|
+
expect(overlayState.timer).toBeDefined();
|
|
283
|
+
|
|
284
|
+
renderer.stopOverlay(200);
|
|
285
|
+
|
|
286
|
+
vi.advanceTimersByTime(100000); // Timer should not fire
|
|
287
|
+
|
|
288
|
+
vi.useRealTimers();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('should emit overlayEnd event', async () => {
|
|
292
|
+
const endListener = vi.fn();
|
|
293
|
+
renderer.on('overlayEnd', endListener);
|
|
294
|
+
|
|
295
|
+
await renderer.renderOverlay(overlayXLF, 200, 10);
|
|
296
|
+
renderer.stopOverlay(200);
|
|
297
|
+
|
|
298
|
+
expect(endListener).toHaveBeenCalledWith(200);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('should warn if overlay not active', () => {
|
|
302
|
+
const warnSpy = vi.spyOn(renderer.log, 'warn');
|
|
303
|
+
|
|
304
|
+
renderer.stopOverlay(999);
|
|
305
|
+
|
|
306
|
+
expect(warnSpy).toHaveBeenCalled();
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('should stop only specified overlay, leaving others active', async () => {
|
|
310
|
+
await renderer.renderOverlay(overlayXLF, 201, 10);
|
|
311
|
+
await renderer.renderOverlay(overlayXLF, 202, 20);
|
|
312
|
+
|
|
313
|
+
renderer.stopOverlay(201);
|
|
314
|
+
|
|
315
|
+
expect(renderer.activeOverlays.has(201)).toBe(false);
|
|
316
|
+
expect(renderer.activeOverlays.has(202)).toBe(true);
|
|
317
|
+
expect(container.querySelector('#overlay_201')).toBeNull();
|
|
318
|
+
expect(container.querySelector('#overlay_202')).not.toBeNull();
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
describe('stopAllOverlays()', () => {
|
|
323
|
+
it('should stop all active overlays', async () => {
|
|
324
|
+
await renderer.renderOverlay(overlayXLF, 201, 10);
|
|
325
|
+
await renderer.renderOverlay(overlayXLF, 202, 20);
|
|
326
|
+
await renderer.renderOverlay(overlayXLF, 203, 5);
|
|
327
|
+
|
|
328
|
+
expect(renderer.activeOverlays.size).toBe(3);
|
|
329
|
+
|
|
330
|
+
renderer.stopAllOverlays();
|
|
331
|
+
|
|
332
|
+
expect(renderer.activeOverlays.size).toBe(0);
|
|
333
|
+
expect(container.querySelector('#overlay_201')).toBeNull();
|
|
334
|
+
expect(container.querySelector('#overlay_202')).toBeNull();
|
|
335
|
+
expect(container.querySelector('#overlay_203')).toBeNull();
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('should emit overlayEnd for each overlay', async () => {
|
|
339
|
+
const endListener = vi.fn();
|
|
340
|
+
renderer.on('overlayEnd', endListener);
|
|
341
|
+
|
|
342
|
+
await renderer.renderOverlay(overlayXLF, 201, 10);
|
|
343
|
+
await renderer.renderOverlay(overlayXLF, 202, 20);
|
|
344
|
+
|
|
345
|
+
renderer.stopAllOverlays();
|
|
346
|
+
|
|
347
|
+
expect(endListener).toHaveBeenCalledTimes(2);
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
describe('Overlay with main layout', () => {
|
|
352
|
+
it('should render overlay on top of active main layout', async () => {
|
|
353
|
+
// Render main layout
|
|
354
|
+
await renderer.renderLayout(mainLayoutXLF, 100);
|
|
355
|
+
|
|
356
|
+
// Render overlay
|
|
357
|
+
await renderer.renderOverlay(overlayXLF, 200, 10);
|
|
358
|
+
|
|
359
|
+
// Both should exist
|
|
360
|
+
expect(renderer.currentLayoutId).toBe(100);
|
|
361
|
+
expect(renderer.activeOverlays.has(200)).toBe(true);
|
|
362
|
+
|
|
363
|
+
// Overlay should be in overlay container
|
|
364
|
+
const overlayDiv = container.querySelector('#overlay_200');
|
|
365
|
+
const overlayContainer = container.querySelector('#overlay-container');
|
|
366
|
+
expect(overlayContainer.contains(overlayDiv)).toBe(true);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('should keep overlay active when main layout changes', async () => {
|
|
370
|
+
// Render main layout
|
|
371
|
+
await renderer.renderLayout(mainLayoutXLF, 100);
|
|
372
|
+
|
|
373
|
+
// Render overlay
|
|
374
|
+
await renderer.renderOverlay(overlayXLF, 200, 10);
|
|
375
|
+
|
|
376
|
+
// Change main layout
|
|
377
|
+
await renderer.renderLayout(mainLayoutXLF, 101);
|
|
378
|
+
|
|
379
|
+
// Overlay should still be active
|
|
380
|
+
expect(renderer.activeOverlays.has(200)).toBe(true);
|
|
381
|
+
expect(container.querySelector('#overlay_200')).not.toBeNull();
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('should allow removing overlay while main layout is active', async () => {
|
|
385
|
+
// Render main layout
|
|
386
|
+
await renderer.renderLayout(mainLayoutXLF, 100);
|
|
387
|
+
|
|
388
|
+
// Render overlay
|
|
389
|
+
await renderer.renderOverlay(overlayXLF, 200, 10);
|
|
390
|
+
|
|
391
|
+
// Stop overlay
|
|
392
|
+
renderer.stopOverlay(200);
|
|
393
|
+
|
|
394
|
+
// Main layout should still be active
|
|
395
|
+
expect(renderer.currentLayoutId).toBe(100);
|
|
396
|
+
expect(renderer.activeOverlays.has(200)).toBe(false);
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
describe('Overlay region and widget rendering', () => {
|
|
401
|
+
it('should start rendering overlay widgets', async () => {
|
|
402
|
+
const widgetStartListener = vi.fn();
|
|
403
|
+
renderer.on('overlayWidgetStart', widgetStartListener);
|
|
404
|
+
|
|
405
|
+
await renderer.renderOverlay(overlayXLF, 200, 10);
|
|
406
|
+
|
|
407
|
+
expect(widgetStartListener).toHaveBeenCalled();
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it('should create widget elements for overlay regions', async () => {
|
|
411
|
+
await renderer.renderOverlay(overlayXLF, 200, 10);
|
|
412
|
+
|
|
413
|
+
const overlayState = renderer.activeOverlays.get(200);
|
|
414
|
+
const region = overlayState.regions.get('overlay-1');
|
|
415
|
+
|
|
416
|
+
expect(region.widgetElements.size).toBeGreaterThan(0);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('should emit overlayWidgetEnd when widget stops', async () => {
|
|
420
|
+
vi.useFakeTimers();
|
|
421
|
+
|
|
422
|
+
const widgetEndListener = vi.fn();
|
|
423
|
+
renderer.on('overlayWidgetEnd', widgetEndListener);
|
|
424
|
+
|
|
425
|
+
// Need 2+ widgets for cycling (single widget has no timer/cycling)
|
|
426
|
+
const multiWidgetOverlayXLF = `<?xml version="1.0"?>
|
|
427
|
+
<layout width="1920" height="1080" bgcolor="#00000080">
|
|
428
|
+
<region id="overlay-1" width="400" height="200" top="50" left="50" zindex="10">
|
|
429
|
+
<media id="1" type="text" duration="10">
|
|
430
|
+
<raw><![CDATA[<p>Widget 1</p>]]></raw>
|
|
431
|
+
</media>
|
|
432
|
+
<media id="2" type="text" duration="10">
|
|
433
|
+
<raw><![CDATA[<p>Widget 2</p>]]></raw>
|
|
434
|
+
</media>
|
|
435
|
+
</region>
|
|
436
|
+
</layout>`;
|
|
437
|
+
|
|
438
|
+
await renderer.renderOverlay(multiWidgetOverlayXLF, 200, 10);
|
|
439
|
+
|
|
440
|
+
// Fast forward past first widget duration (10s) to trigger cycling
|
|
441
|
+
vi.advanceTimersByTime(11000);
|
|
442
|
+
|
|
443
|
+
// Widget should have cycled, emitting end event
|
|
444
|
+
expect(widgetEndListener).toHaveBeenCalled();
|
|
445
|
+
|
|
446
|
+
vi.useRealTimers();
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
describe('cleanup()', () => {
|
|
451
|
+
it('should stop all overlays on cleanup', async () => {
|
|
452
|
+
await renderer.renderOverlay(overlayXLF, 201, 10);
|
|
453
|
+
await renderer.renderOverlay(overlayXLF, 202, 20);
|
|
454
|
+
|
|
455
|
+
renderer.cleanup();
|
|
456
|
+
|
|
457
|
+
expect(renderer.activeOverlays.size).toBe(0);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it('should remove overlay container on cleanup', async () => {
|
|
461
|
+
await renderer.renderOverlay(overlayXLF, 200, 10);
|
|
462
|
+
|
|
463
|
+
renderer.cleanup();
|
|
464
|
+
|
|
465
|
+
expect(container.innerHTML).toBe('');
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
describe('Memory management', () => {
|
|
470
|
+
it('should revoke blob URLs when stopping overlay', async () => {
|
|
471
|
+
const revokeObjectURLSpy = vi.spyOn(URL, 'revokeObjectURL');
|
|
472
|
+
|
|
473
|
+
await renderer.renderOverlay(overlayXLF, 200, 10);
|
|
474
|
+
|
|
475
|
+
// Manually track blob URLs for the overlay layout ID, since trackBlobUrl()
|
|
476
|
+
// is scoped to currentLayoutId (main layout) and doesn't track overlay blobs
|
|
477
|
+
renderer.layoutBlobUrls.set(200, new Set(['blob:test/overlay-1', 'blob:test/overlay-2']));
|
|
478
|
+
|
|
479
|
+
renderer.stopOverlay(200);
|
|
480
|
+
|
|
481
|
+
// stopOverlay calls revokeBlobUrlsForLayout(layoutId) which revokes tracked URLs
|
|
482
|
+
expect(revokeObjectURLSpy).toHaveBeenCalledWith('blob:test/overlay-1');
|
|
483
|
+
expect(revokeObjectURLSpy).toHaveBeenCalledWith('blob:test/overlay-2');
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it('should track blob URLs per overlay layout', async () => {
|
|
487
|
+
await renderer.renderOverlay(overlayXLF, 200, 10);
|
|
488
|
+
|
|
489
|
+
// Blob URLs should be tracked
|
|
490
|
+
expect(renderer.layoutBlobUrls.has(200) || renderer.layoutBlobUrls.size === 0).toBe(true);
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
});
|