@xiboplayer/renderer 0.6.1 → 0.6.3
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/package.json +4 -3
- package/src/index.d.ts +1 -1
- package/src/layout-pool.test.js +1 -14
- package/src/layout.js +6 -0
- package/src/renderer-lite.ic.test.js +203 -0
- package/src/renderer-lite.js +349 -292
- package/src/renderer-lite.overlays.test.js +1 -27
- package/src/renderer-lite.test.js +511 -60
- package/vitest.config.js +9 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiboplayer/renderer",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"description": "RendererLite - Fast, efficient XLF layout rendering engine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"nanoevents": "^9.1.0",
|
|
15
15
|
"pdfjs-dist": "^4.10.38",
|
|
16
|
-
"@xiboplayer/
|
|
17
|
-
"@xiboplayer/
|
|
16
|
+
"@xiboplayer/utils": "0.6.3",
|
|
17
|
+
"@xiboplayer/cache": "0.6.3",
|
|
18
|
+
"@xiboplayer/schedule": "0.6.3"
|
|
18
19
|
},
|
|
19
20
|
"devDependencies": {
|
|
20
21
|
"vitest": "^2.0.0",
|
package/src/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export interface RendererConfig {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
export interface RendererOptions {
|
|
9
|
-
|
|
9
|
+
fileIdToSaveAs?: Map<string, string>;
|
|
10
10
|
getWidgetHtml?: (widget: any) => Promise<string | { url: string; fallback?: string }>;
|
|
11
11
|
logLevel?: string;
|
|
12
12
|
}
|
package/src/layout-pool.test.js
CHANGED
|
@@ -33,8 +33,7 @@ describe('LayoutPool', () => {
|
|
|
33
33
|
container,
|
|
34
34
|
layout: { width: 1920, height: 1080, duration: 60, bgcolor: '#000', regions: [] },
|
|
35
35
|
regions: new Map(),
|
|
36
|
-
blobUrls: new Set()
|
|
37
|
-
mediaUrlCache: new Map()
|
|
36
|
+
blobUrls: new Set()
|
|
38
37
|
};
|
|
39
38
|
}
|
|
40
39
|
|
|
@@ -170,18 +169,6 @@ describe('LayoutPool', () => {
|
|
|
170
169
|
expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:test-2');
|
|
171
170
|
});
|
|
172
171
|
|
|
173
|
-
it('should revoke media blob URLs on eviction', () => {
|
|
174
|
-
const entry = createMockEntry(1);
|
|
175
|
-
entry.mediaUrlCache.set(10, 'blob:media-10');
|
|
176
|
-
entry.mediaUrlCache.set(20, 'blob:media-20');
|
|
177
|
-
|
|
178
|
-
pool.add(1, entry);
|
|
179
|
-
pool.evict(1);
|
|
180
|
-
|
|
181
|
-
expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:media-10');
|
|
182
|
-
expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:media-20');
|
|
183
|
-
});
|
|
184
|
-
|
|
185
172
|
it('should remove container from DOM', () => {
|
|
186
173
|
const entry = createMockEntry(1);
|
|
187
174
|
pool.add(1, entry);
|
package/src/layout.js
CHANGED
|
@@ -303,10 +303,16 @@ window.Transitions = {
|
|
|
303
303
|
const direction = transitionConfig.direction || 'N';
|
|
304
304
|
|
|
305
305
|
switch (type) {
|
|
306
|
+
case 'fade':
|
|
307
|
+
return isIn ? this.fadeIn(element, duration) : this.fadeOut(element, duration);
|
|
306
308
|
case 'fadein':
|
|
307
309
|
return isIn ? this.fadeIn(element, duration) : null;
|
|
308
310
|
case 'fadeout':
|
|
309
311
|
return isIn ? null : this.fadeOut(element, duration);
|
|
312
|
+
case 'fly':
|
|
313
|
+
return isIn
|
|
314
|
+
? this.flyIn(element, duration, direction, regionWidth, regionHeight)
|
|
315
|
+
: this.flyOut(element, duration, direction, regionWidth, regionHeight);
|
|
310
316
|
case 'flyin':
|
|
311
317
|
return isIn ? this.flyIn(element, duration, direction, regionWidth, regionHeight) : null;
|
|
312
318
|
case 'flyout':
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RendererLite Interactive Control (XIC) Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for the XIC event handlers: interactiveTrigger, widgetExpire,
|
|
5
|
+
* widgetExtendDuration, widgetSetDuration.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
9
|
+
import { RendererLite } from './renderer-lite.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create a minimal RendererLite instance with stubbed DOM and methods.
|
|
13
|
+
*/
|
|
14
|
+
function createRenderer() {
|
|
15
|
+
const container = document.createElement('div');
|
|
16
|
+
const renderer = new RendererLite(
|
|
17
|
+
{ cmsUrl: 'http://localhost', hardwareKey: 'test' },
|
|
18
|
+
container,
|
|
19
|
+
{ logLevel: 'silent' }
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
// Stub methods that touch DOM or async operations
|
|
23
|
+
renderer.renderWidget = vi.fn();
|
|
24
|
+
renderer.stopWidget = vi.fn();
|
|
25
|
+
renderer.checkLayoutComplete = vi.fn();
|
|
26
|
+
renderer._startRegionCycle = vi.fn();
|
|
27
|
+
renderer.navigateToWidget = vi.fn();
|
|
28
|
+
|
|
29
|
+
return renderer;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Populate renderer with a fake region containing widgets.
|
|
34
|
+
*/
|
|
35
|
+
function addRegion(renderer, regionId, widgets) {
|
|
36
|
+
renderer.regions.set(regionId, {
|
|
37
|
+
element: document.createElement('div'),
|
|
38
|
+
config: { id: regionId },
|
|
39
|
+
widgets,
|
|
40
|
+
currentIndex: 0,
|
|
41
|
+
timer: null,
|
|
42
|
+
width: 100,
|
|
43
|
+
height: 100,
|
|
44
|
+
complete: false,
|
|
45
|
+
isDrawer: false,
|
|
46
|
+
widgetElements: new Map()
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('RendererLite XIC', () => {
|
|
51
|
+
let renderer;
|
|
52
|
+
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
renderer = createRenderer();
|
|
55
|
+
addRegion(renderer, 'region-1', [
|
|
56
|
+
{ id: 'w1', type: 'text', duration: 10, options: {} },
|
|
57
|
+
{ id: 'w2', type: 'image', duration: 20, options: {} },
|
|
58
|
+
{ id: 'w3', type: 'video', duration: 30, options: {} }
|
|
59
|
+
]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('_findRegionByWidgetId', () => {
|
|
63
|
+
it('should find a widget in main regions', () => {
|
|
64
|
+
const result = renderer._findRegionByWidgetId('w2');
|
|
65
|
+
expect(result).not.toBeNull();
|
|
66
|
+
expect(result.regionId).toBe('region-1');
|
|
67
|
+
expect(result.widgetIndex).toBe(1);
|
|
68
|
+
expect(result.widget.id).toBe('w2');
|
|
69
|
+
expect(result.regionMap).toBe(renderer.regions);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should return null for unknown widget', () => {
|
|
73
|
+
const result = renderer._findRegionByWidgetId('w-unknown');
|
|
74
|
+
expect(result).toBeNull();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should find a widget in overlay regions', () => {
|
|
78
|
+
const overlayRegions = new Map();
|
|
79
|
+
overlayRegions.set('overlay-r1', {
|
|
80
|
+
element: document.createElement('div'),
|
|
81
|
+
config: { id: 'overlay-r1' },
|
|
82
|
+
widgets: [{ id: 'ow1', type: 'text', duration: 5, options: {} }],
|
|
83
|
+
currentIndex: 0,
|
|
84
|
+
timer: null,
|
|
85
|
+
width: 50,
|
|
86
|
+
height: 50,
|
|
87
|
+
complete: false,
|
|
88
|
+
isDrawer: false,
|
|
89
|
+
widgetElements: new Map()
|
|
90
|
+
});
|
|
91
|
+
renderer.activeOverlays.set(100, { regions: overlayRegions });
|
|
92
|
+
|
|
93
|
+
const result = renderer._findRegionByWidgetId('ow1');
|
|
94
|
+
expect(result).not.toBeNull();
|
|
95
|
+
expect(result.regionId).toBe('overlay-r1');
|
|
96
|
+
expect(result.widgetIndex).toBe(0);
|
|
97
|
+
expect(result.regionMap).toBe(overlayRegions);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('_handleInteractiveTrigger', () => {
|
|
102
|
+
it('should call navigateToWidget when target exists', () => {
|
|
103
|
+
renderer.emit('interactiveTrigger', { targetId: 'w2', triggerCode: 'btn1' });
|
|
104
|
+
expect(renderer.navigateToWidget).toHaveBeenCalledWith('w2');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should not call navigateToWidget when target is unknown', () => {
|
|
108
|
+
renderer.emit('interactiveTrigger', { targetId: 'w-missing', triggerCode: 'btn1' });
|
|
109
|
+
expect(renderer.navigateToWidget).not.toHaveBeenCalled();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('_handleWidgetExpire', () => {
|
|
114
|
+
it('should clear timer, stop widget, and advance region', () => {
|
|
115
|
+
const region = renderer.regions.get('region-1');
|
|
116
|
+
region.timer = setTimeout(() => {}, 99999);
|
|
117
|
+
region.currentIndex = 0;
|
|
118
|
+
|
|
119
|
+
renderer.emit('widgetExpire', { widgetId: 'w1' });
|
|
120
|
+
|
|
121
|
+
expect(region.timer).toBeNull();
|
|
122
|
+
expect(renderer.stopWidget).toHaveBeenCalledWith('region-1', 0);
|
|
123
|
+
expect(renderer._startRegionCycle).toHaveBeenCalled();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should do nothing for unknown widget', () => {
|
|
127
|
+
renderer.emit('widgetExpire', { widgetId: 'w-missing' });
|
|
128
|
+
expect(renderer.stopWidget).not.toHaveBeenCalled();
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('_handleWidgetExtendDuration', () => {
|
|
133
|
+
it('should clear existing timer and re-arm with extended duration', () => {
|
|
134
|
+
vi.useFakeTimers();
|
|
135
|
+
const region = renderer.regions.get('region-1');
|
|
136
|
+
region.timer = setTimeout(() => {}, 99999);
|
|
137
|
+
|
|
138
|
+
renderer.emit('widgetExtendDuration', { widgetId: 'w1', duration: 15 });
|
|
139
|
+
|
|
140
|
+
// Timer should be re-armed (not null)
|
|
141
|
+
expect(region.timer).not.toBeNull();
|
|
142
|
+
// stopWidget should NOT have been called yet (timer hasn't fired)
|
|
143
|
+
expect(renderer.stopWidget).not.toHaveBeenCalled();
|
|
144
|
+
|
|
145
|
+
// Advance time to fire the new timer
|
|
146
|
+
vi.advanceTimersByTime(15000);
|
|
147
|
+
expect(renderer.stopWidget).toHaveBeenCalledWith('region-1', 0);
|
|
148
|
+
|
|
149
|
+
vi.useRealTimers();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should do nothing for unknown widget', () => {
|
|
153
|
+
renderer.emit('widgetExtendDuration', { widgetId: 'w-missing', duration: 10 });
|
|
154
|
+
expect(renderer.stopWidget).not.toHaveBeenCalled();
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe('_handleWidgetSetDuration', () => {
|
|
159
|
+
it('should clear existing timer and set absolute duration', () => {
|
|
160
|
+
vi.useFakeTimers();
|
|
161
|
+
const region = renderer.regions.get('region-1');
|
|
162
|
+
region.timer = setTimeout(() => {}, 99999);
|
|
163
|
+
|
|
164
|
+
renderer.emit('widgetSetDuration', { widgetId: 'w2', duration: 5 });
|
|
165
|
+
|
|
166
|
+
expect(region.timer).not.toBeNull();
|
|
167
|
+
expect(renderer.stopWidget).not.toHaveBeenCalled();
|
|
168
|
+
|
|
169
|
+
vi.advanceTimersByTime(5000);
|
|
170
|
+
// w2 is at index 1, but the region's currentIndex determines what gets stopped
|
|
171
|
+
expect(renderer.stopWidget).toHaveBeenCalled();
|
|
172
|
+
|
|
173
|
+
vi.useRealTimers();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('should do nothing for unknown widget', () => {
|
|
177
|
+
renderer.emit('widgetSetDuration', { widgetId: 'w-missing', duration: 10 });
|
|
178
|
+
expect(renderer.stopWidget).not.toHaveBeenCalled();
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe('_advanceRegion', () => {
|
|
183
|
+
it('should increment currentIndex and call _startRegionCycle', () => {
|
|
184
|
+
const region = renderer.regions.get('region-1');
|
|
185
|
+
region.currentIndex = 0;
|
|
186
|
+
|
|
187
|
+
renderer._advanceRegion('region-1', renderer.regions);
|
|
188
|
+
|
|
189
|
+
expect(region.currentIndex).toBe(1);
|
|
190
|
+
expect(renderer._startRegionCycle).toHaveBeenCalled();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should wrap around at end of widget list', () => {
|
|
194
|
+
const region = renderer.regions.get('region-1');
|
|
195
|
+
region.currentIndex = 2; // last widget
|
|
196
|
+
|
|
197
|
+
renderer._advanceRegion('region-1', renderer.regions);
|
|
198
|
+
|
|
199
|
+
expect(region.currentIndex).toBe(0);
|
|
200
|
+
expect(renderer._startRegionCycle).toHaveBeenCalled();
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
});
|