@xiboplayer/renderer 0.6.13 → 0.7.1
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 -5
- package/src/index.d.ts +2 -0
- package/src/layout-pool.js +12 -0
- package/src/renderer-lite.js +96 -50
- package/src/renderer-lite.overlays.test.js +18 -9
- package/src/renderer-lite.test.js +32 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiboplayer/renderer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "RendererLite - Fast, efficient XLF layout rendering engine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -11,11 +11,10 @@
|
|
|
11
11
|
"./layout": "./src/layout.js"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"nanoevents": "^9.1.0",
|
|
15
14
|
"pdfjs-dist": "^4.10.38",
|
|
16
|
-
"@xiboplayer/cache": "0.
|
|
17
|
-
"@xiboplayer/schedule": "0.
|
|
18
|
-
"@xiboplayer/utils": "0.
|
|
15
|
+
"@xiboplayer/cache": "0.7.1",
|
|
16
|
+
"@xiboplayer/schedule": "0.7.1",
|
|
17
|
+
"@xiboplayer/utils": "0.7.1"
|
|
19
18
|
},
|
|
20
19
|
"devDependencies": {
|
|
21
20
|
"vitest": "^2.0.0",
|
package/src/index.d.ts
CHANGED
|
@@ -63,6 +63,8 @@ export class RendererLite {
|
|
|
63
63
|
resume(): void;
|
|
64
64
|
isPaused(): boolean;
|
|
65
65
|
resumeRegionMedia?(regionId: string): void;
|
|
66
|
+
showLayout(layoutId?: number): void;
|
|
67
|
+
getCurrentLayoutId(): number | null;
|
|
66
68
|
|
|
67
69
|
parseXlf(xlfXml: string): any;
|
|
68
70
|
parseWidget(mediaEl: Element): any;
|
package/src/layout-pool.js
CHANGED
|
@@ -279,6 +279,18 @@ export class LayoutPool {
|
|
|
279
279
|
return count;
|
|
280
280
|
}
|
|
281
281
|
|
|
282
|
+
/**
|
|
283
|
+
* Get the most recently added layout ID.
|
|
284
|
+
* @returns {number|undefined}
|
|
285
|
+
*/
|
|
286
|
+
getLatest() {
|
|
287
|
+
let latest;
|
|
288
|
+
for (const id of this.layouts.keys()) {
|
|
289
|
+
latest = id;
|
|
290
|
+
}
|
|
291
|
+
return latest;
|
|
292
|
+
}
|
|
293
|
+
|
|
282
294
|
/**
|
|
283
295
|
* Clear all entries (both hot and warm).
|
|
284
296
|
*/
|
package/src/renderer-lite.js
CHANGED
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
* ```
|
|
42
42
|
*/
|
|
43
43
|
|
|
44
|
-
import {
|
|
44
|
+
import { EventEmitter } from '@xiboplayer/utils';
|
|
45
45
|
import { createLogger, isDebug, PLAYER_API } from '@xiboplayer/utils';
|
|
46
46
|
import { parseLayoutDuration } from '@xiboplayer/schedule';
|
|
47
47
|
import { LayoutPool } from './layout-pool.js';
|
|
@@ -205,7 +205,7 @@ export class RendererLite {
|
|
|
205
205
|
this.log = createLogger('RendererLite', options.logLevel);
|
|
206
206
|
|
|
207
207
|
// Event emitter for lifecycle hooks
|
|
208
|
-
this.emitter =
|
|
208
|
+
this.emitter = new EventEmitter();
|
|
209
209
|
|
|
210
210
|
// State
|
|
211
211
|
this.currentLayout = null;
|
|
@@ -215,6 +215,7 @@ export class RendererLite {
|
|
|
215
215
|
this.layoutTimer = null;
|
|
216
216
|
this.layoutEndEmitted = false; // Prevents double layoutEnd on stop after timer
|
|
217
217
|
this._deferredTimerLayoutId = null; // Set when timer is deferred for dynamic layouts
|
|
218
|
+
this._deferredTimerFallback = null; // Safety timeout: starts layout timer if metadata never arrives
|
|
218
219
|
this._paused = false;
|
|
219
220
|
this._layoutTimerStartedAt = null; // Date.now() when layout timer started
|
|
220
221
|
this._layoutTimerDurationMs = null; // Total layout duration in ms
|
|
@@ -222,6 +223,10 @@ export class RendererLite {
|
|
|
222
223
|
this.layoutBlobUrls = new Map(); // layoutId => Set<blobUrl> (for lifecycle tracking)
|
|
223
224
|
this.audioOverlays = new Map(); // widgetId => [HTMLAudioElement] (audio overlays for widgets)
|
|
224
225
|
|
|
226
|
+
// Bound methods (avoid lambda allocation per call in startRegion/_advanceRegion)
|
|
227
|
+
this._stopWidgetBound = (rid, idx) => this.stopWidget(rid, idx);
|
|
228
|
+
this._renderWidgetBound = (rid, idx) => this.renderWidget(rid, idx);
|
|
229
|
+
|
|
225
230
|
// Scale state (for fitting layout to screen)
|
|
226
231
|
this.scaleFactor = 1;
|
|
227
232
|
this.offsetX = 0;
|
|
@@ -355,7 +360,7 @@ export class RendererLite {
|
|
|
355
360
|
* Event emitter interface (like XMR wrapper)
|
|
356
361
|
*/
|
|
357
362
|
on(event, callback) {
|
|
358
|
-
|
|
363
|
+
this.emitter.on(event, callback);
|
|
359
364
|
}
|
|
360
365
|
|
|
361
366
|
emit(event, ...args) {
|
|
@@ -720,6 +725,11 @@ export class RendererLite {
|
|
|
720
725
|
if (this._hasUnprobedVideos()) {
|
|
721
726
|
this.log.info(`Layout duration updated to ${maxRegionDuration}s but still has unprobed videos — keeping timer deferred`);
|
|
722
727
|
} else {
|
|
728
|
+
// Cancel safety fallback — metadata arrived in time
|
|
729
|
+
if (this._deferredTimerFallback) {
|
|
730
|
+
clearTimeout(this._deferredTimerFallback);
|
|
731
|
+
this._deferredTimerFallback = null;
|
|
732
|
+
}
|
|
723
733
|
const elapsed = Date.now() - (this._layoutTimerStartedAt || Date.now());
|
|
724
734
|
const remainingMs = Math.max(1000, maxRegionDuration * 1000 - elapsed);
|
|
725
735
|
this._deferredTimerLayoutId = null;
|
|
@@ -947,8 +957,8 @@ export class RendererLite {
|
|
|
947
957
|
const isMain = regionMap === this.regions;
|
|
948
958
|
this._startRegionCycle(
|
|
949
959
|
region, regionId,
|
|
950
|
-
isMain ?
|
|
951
|
-
isMain ?
|
|
960
|
+
isMain ? this._renderWidgetBound : this._renderWidgetBound,
|
|
961
|
+
isMain ? this._stopWidgetBound : this._stopWidgetBound,
|
|
952
962
|
isMain ? () => this.checkLayoutComplete() : undefined
|
|
953
963
|
);
|
|
954
964
|
}
|
|
@@ -1194,7 +1204,7 @@ export class RendererLite {
|
|
|
1194
1204
|
|
|
1195
1205
|
// Stop all region timers and widgets, then reset to first widget
|
|
1196
1206
|
this._clearRegionTimers(this.regions);
|
|
1197
|
-
this._stopAllRegionWidgets(this.regions,
|
|
1207
|
+
this._stopAllRegionWidgets(this.regions, this._stopWidgetBound);
|
|
1198
1208
|
for (const [, region] of this.regions) {
|
|
1199
1209
|
region.currentIndex = 0;
|
|
1200
1210
|
region.complete = false;
|
|
@@ -1205,8 +1215,13 @@ export class RendererLite {
|
|
|
1205
1215
|
clearTimeout(this.layoutTimer);
|
|
1206
1216
|
this.layoutTimer = null;
|
|
1207
1217
|
}
|
|
1208
|
-
|
|
1218
|
+
|
|
1209
1219
|
this.layoutEndEmitted = false;
|
|
1220
|
+
this._deferredTimerLayoutId = null;
|
|
1221
|
+
if (this._deferredTimerFallback) {
|
|
1222
|
+
clearTimeout(this._deferredTimerFallback);
|
|
1223
|
+
this._deferredTimerFallback = null;
|
|
1224
|
+
}
|
|
1210
1225
|
|
|
1211
1226
|
// DON'T call stopCurrentLayout() - keep elements alive!
|
|
1212
1227
|
// DON'T recreate regions/elements - already exist!
|
|
@@ -1378,8 +1393,8 @@ export class RendererLite {
|
|
|
1378
1393
|
const region = this.regions.get(regionId);
|
|
1379
1394
|
this._startRegionCycle(
|
|
1380
1395
|
region, regionId,
|
|
1381
|
-
|
|
1382
|
-
|
|
1396
|
+
this._renderWidgetBound,
|
|
1397
|
+
this._stopWidgetBound,
|
|
1383
1398
|
() => {
|
|
1384
1399
|
this.log.info(`Region ${regionId} completed one full cycle`);
|
|
1385
1400
|
this.checkLayoutComplete();
|
|
@@ -1599,12 +1614,24 @@ export class RendererLite {
|
|
|
1599
1614
|
}
|
|
1600
1615
|
|
|
1601
1616
|
// Dynamic layouts (useDuration=0 videos): defer timer until video metadata
|
|
1602
|
-
// provides real durations.
|
|
1603
|
-
//
|
|
1617
|
+
// provides real durations. Safety timeout ensures corrupt/missing videos
|
|
1618
|
+
// don't freeze the display forever.
|
|
1604
1619
|
if (layout.isDynamic && this._hasUnprobedVideos()) {
|
|
1605
1620
|
this._deferredTimerLayoutId = layoutId;
|
|
1606
1621
|
this._layoutTimerStartedAt = Date.now();
|
|
1607
1622
|
this.log.info(`Layout ${layoutId} has unprobed videos — deferring timer until metadata loads`);
|
|
1623
|
+
|
|
1624
|
+
// Safety: if metadata never arrives (corrupt file, codec error), start
|
|
1625
|
+
// the timer with the estimated duration after 30s so the display keeps cycling.
|
|
1626
|
+
this._deferredTimerFallback = setTimeout(() => {
|
|
1627
|
+
this._deferredTimerFallback = null;
|
|
1628
|
+
if (this._deferredTimerLayoutId === layoutId && !this.layoutTimer) {
|
|
1629
|
+
this.log.warn(`Layout ${layoutId}: metadata timeout after 30s — starting timer with ${layout.duration}s estimate`);
|
|
1630
|
+
this._deferredTimerLayoutId = null;
|
|
1631
|
+
this._startLayoutTimer(layoutId, layout);
|
|
1632
|
+
}
|
|
1633
|
+
}, 30000);
|
|
1634
|
+
|
|
1608
1635
|
return;
|
|
1609
1636
|
}
|
|
1610
1637
|
|
|
@@ -1628,6 +1655,10 @@ export class RendererLite {
|
|
|
1628
1655
|
*/
|
|
1629
1656
|
_startLayoutTimer(layoutId, layout) {
|
|
1630
1657
|
this._deferredTimerLayoutId = null;
|
|
1658
|
+
if (this._deferredTimerFallback) {
|
|
1659
|
+
clearTimeout(this._deferredTimerFallback);
|
|
1660
|
+
this._deferredTimerFallback = null;
|
|
1661
|
+
}
|
|
1631
1662
|
const layoutDurationMs = layout.duration * 1000;
|
|
1632
1663
|
this.log.info(`Layout ${layoutId} will end after ${layout.duration}s`);
|
|
1633
1664
|
|
|
@@ -2274,6 +2305,18 @@ export class RendererLite {
|
|
|
2274
2305
|
const errorCode = error?.code;
|
|
2275
2306
|
const errorMessage = error?.message || 'Unknown error';
|
|
2276
2307
|
this.log.warn(`Video error: ${storedAs}, code: ${errorCode}, time: ${video.currentTime.toFixed(1)}s, message: ${errorMessage}`);
|
|
2308
|
+
|
|
2309
|
+
// Set fallback duration so the deferred timer can proceed.
|
|
2310
|
+
// Without this, a corrupt video leaves widget.duration=0 forever,
|
|
2311
|
+
// _hasUnprobedVideos() stays true, and the deferred timer never unblocks.
|
|
2312
|
+
if (widget.useDuration === 0 && widget.duration === 0) {
|
|
2313
|
+
widget.duration = 60;
|
|
2314
|
+
this.log.info(`Set fallback duration 60s for errored widget ${widget.id}`);
|
|
2315
|
+
if (this.currentLayoutId === createdForLayoutId) {
|
|
2316
|
+
this.updateLayoutDuration();
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2277
2320
|
this.emit('videoError', { storedAs, fileId, errorCode, errorMessage, currentTime: video.currentTime });
|
|
2278
2321
|
};
|
|
2279
2322
|
video.addEventListener('error', onError);
|
|
@@ -2471,24 +2514,6 @@ export class RendererLite {
|
|
|
2471
2514
|
// Use cache URL — SW serves HTML and intercepts sub-resources
|
|
2472
2515
|
iframe.src = result.url;
|
|
2473
2516
|
|
|
2474
|
-
// On hard reload (Ctrl+Shift+R), iframe navigation bypasses SW → server 404
|
|
2475
|
-
// Detect and fall back to blob URL with original CMS signed URLs
|
|
2476
|
-
if (result.fallback) {
|
|
2477
|
-
const self = this;
|
|
2478
|
-
iframe.addEventListener('load', function() {
|
|
2479
|
-
try {
|
|
2480
|
-
// Our cached widget HTML has a <base> tag; server 404 page doesn't
|
|
2481
|
-
if (!iframe.contentDocument?.querySelector('base')) {
|
|
2482
|
-
self.log.warn('Cache URL failed (hard reload?), using original CMS URLs');
|
|
2483
|
-
const blob = new Blob([result.fallback], { type: 'text/html' });
|
|
2484
|
-
const blobUrl = URL.createObjectURL(blob);
|
|
2485
|
-
self.trackBlobUrl(blobUrl);
|
|
2486
|
-
iframe.src = blobUrl;
|
|
2487
|
-
}
|
|
2488
|
-
} catch (e) { /* cross-origin — should not happen */ }
|
|
2489
|
-
}, { once: true });
|
|
2490
|
-
}
|
|
2491
|
-
|
|
2492
2517
|
// Parse NUMITEMS/DURATION from fallback HTML (cache path)
|
|
2493
2518
|
if (result.fallback) {
|
|
2494
2519
|
this._parseDurationComments(result.fallback, widget);
|
|
@@ -2710,24 +2735,6 @@ export class RendererLite {
|
|
|
2710
2735
|
// Use cache URL — SW serves HTML and intercepts sub-resources
|
|
2711
2736
|
iframe.src = result.url;
|
|
2712
2737
|
|
|
2713
|
-
// On hard reload (Ctrl+Shift+R), iframe navigation bypasses SW → server 404
|
|
2714
|
-
// Detect and fall back to blob URL with original CMS signed URLs
|
|
2715
|
-
if (result.fallback) {
|
|
2716
|
-
const self = this;
|
|
2717
|
-
iframe.addEventListener('load', function() {
|
|
2718
|
-
try {
|
|
2719
|
-
// Our cached widget HTML has a <base> tag; server 404 page doesn't
|
|
2720
|
-
if (!iframe.contentDocument?.querySelector('base')) {
|
|
2721
|
-
self.log.warn('Cache URL failed (hard reload?), using original CMS URLs');
|
|
2722
|
-
const blob = new Blob([result.fallback], { type: 'text/html' });
|
|
2723
|
-
const blobUrl = URL.createObjectURL(blob);
|
|
2724
|
-
self.trackBlobUrl(blobUrl);
|
|
2725
|
-
iframe.src = blobUrl;
|
|
2726
|
-
}
|
|
2727
|
-
} catch (e) { /* cross-origin — should not happen */ }
|
|
2728
|
-
}, { once: true });
|
|
2729
|
-
}
|
|
2730
|
-
|
|
2731
2738
|
// Parse NUMITEMS/DURATION from fallback HTML (cache path)
|
|
2732
2739
|
if (result.fallback) {
|
|
2733
2740
|
this._parseDurationComments(result.fallback, widget);
|
|
@@ -3008,7 +3015,7 @@ export class RendererLite {
|
|
|
3008
3015
|
if (oldLayoutId && this.layoutPool.has(oldLayoutId)) {
|
|
3009
3016
|
// Stop all widgets before evicting (symmetric widgetEnd events)
|
|
3010
3017
|
this._clearRegionTimers(this.regions);
|
|
3011
|
-
this._stopAllRegionWidgets(this.regions,
|
|
3018
|
+
this._stopAllRegionWidgets(this.regions, this._stopWidgetBound);
|
|
3012
3019
|
// Old layout was preloaded — evict from pool (safe: removes its wrapper div)
|
|
3013
3020
|
this.layoutPool.evict(oldLayoutId);
|
|
3014
3021
|
} else {
|
|
@@ -3016,7 +3023,7 @@ export class RendererLite {
|
|
|
3016
3023
|
// Region elements live directly in this.container (not a wrapper),
|
|
3017
3024
|
// so we must remove them individually.
|
|
3018
3025
|
this._clearRegionTimers(this.regions);
|
|
3019
|
-
this._stopAllRegionWidgets(this.regions,
|
|
3026
|
+
this._stopAllRegionWidgets(this.regions, this._stopWidgetBound);
|
|
3020
3027
|
for (const [, region] of this.regions) {
|
|
3021
3028
|
// Release video/audio resources before removing from DOM
|
|
3022
3029
|
LayoutPool.releaseMediaElements(region.element);
|
|
@@ -3109,6 +3116,41 @@ export class RendererLite {
|
|
|
3109
3116
|
this.log.info(`Swapped to preloaded layout ${layoutId} (instant transition)`);
|
|
3110
3117
|
}
|
|
3111
3118
|
|
|
3119
|
+
/**
|
|
3120
|
+
* Get the currently showing layout ID.
|
|
3121
|
+
* @returns {number|null}
|
|
3122
|
+
*/
|
|
3123
|
+
getCurrentLayoutId() {
|
|
3124
|
+
return this.currentLayoutId;
|
|
3125
|
+
}
|
|
3126
|
+
|
|
3127
|
+
/**
|
|
3128
|
+
* Show a preloaded layout (swap from pool to visible).
|
|
3129
|
+
* If no layoutId, shows the most recently preloaded layout.
|
|
3130
|
+
* No-ops if the layout is not in the pool.
|
|
3131
|
+
* @param {number} [layoutId]
|
|
3132
|
+
*/
|
|
3133
|
+
showLayout(layoutId) {
|
|
3134
|
+
if (layoutId === undefined) {
|
|
3135
|
+
layoutId = this.layoutPool.getLatest();
|
|
3136
|
+
if (layoutId === undefined) {
|
|
3137
|
+
this.log.warn('showLayout: no preloaded layout to show');
|
|
3138
|
+
return;
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
// Same layout already showing — skip swap (self-swap would evict then fail).
|
|
3142
|
+
// Same-layout replay is handled by renderLayout's replay path instead.
|
|
3143
|
+
if (this.currentLayoutId === layoutId) {
|
|
3144
|
+
this.log.info(`showLayout: layout ${layoutId} already showing`);
|
|
3145
|
+
return;
|
|
3146
|
+
}
|
|
3147
|
+
if (!this.layoutPool.has(layoutId)) {
|
|
3148
|
+
this.log.warn(`showLayout: layout ${layoutId} not in preload pool`);
|
|
3149
|
+
return;
|
|
3150
|
+
}
|
|
3151
|
+
this._swapToPreloadedLayout(layoutId);
|
|
3152
|
+
}
|
|
3153
|
+
|
|
3112
3154
|
/**
|
|
3113
3155
|
* Check if all regions have completed one full cycle
|
|
3114
3156
|
* This is informational only - layout timer is authoritative
|
|
@@ -3144,6 +3186,10 @@ export class RendererLite {
|
|
|
3144
3186
|
|
|
3145
3187
|
this.layoutEndEmitted = false;
|
|
3146
3188
|
this._deferredTimerLayoutId = null;
|
|
3189
|
+
if (this._deferredTimerFallback) {
|
|
3190
|
+
clearTimeout(this._deferredTimerFallback);
|
|
3191
|
+
this._deferredTimerFallback = null;
|
|
3192
|
+
}
|
|
3147
3193
|
this.currentLayout = null;
|
|
3148
3194
|
this.currentLayoutId = null;
|
|
3149
3195
|
|
|
@@ -3178,7 +3224,7 @@ export class RendererLite {
|
|
|
3178
3224
|
|
|
3179
3225
|
// Stop all regions — use helper to stop ALL started widgets (canvas fix)
|
|
3180
3226
|
this._clearRegionTimers(this.regions);
|
|
3181
|
-
this._stopAllRegionWidgets(this.regions,
|
|
3227
|
+
this._stopAllRegionWidgets(this.regions, this._stopWidgetBound);
|
|
3182
3228
|
for (const [, region] of this.regions) {
|
|
3183
3229
|
// Release video/audio resources before removing from DOM
|
|
3184
3230
|
LayoutPool.releaseMediaElements(region.element);
|
|
@@ -9,15 +9,24 @@
|
|
|
9
9
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
10
10
|
import { RendererLite } from './renderer-lite.js';
|
|
11
11
|
|
|
12
|
-
// Mock logger
|
|
13
|
-
vi.mock('@xiboplayer/utils', () =>
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
|
|
12
|
+
// Mock logger (keep real EventEmitter for RendererLite construction)
|
|
13
|
+
vi.mock('@xiboplayer/utils', () => {
|
|
14
|
+
// Inline minimal EventEmitter — avoids async importActual which breaks vitest mock hoisting
|
|
15
|
+
class EventEmitter {
|
|
16
|
+
constructor() { this._listeners = new Map(); }
|
|
17
|
+
on(event, cb) { if (!this._listeners.has(event)) this._listeners.set(event, []); this._listeners.get(event).push(cb); }
|
|
18
|
+
emit(event, ...args) { for (const cb of this._listeners.get(event) || []) cb(...args); }
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
EventEmitter,
|
|
22
|
+
createLogger: () => ({
|
|
23
|
+
info: vi.fn(),
|
|
24
|
+
warn: vi.fn(),
|
|
25
|
+
error: vi.fn(),
|
|
26
|
+
debug: vi.fn()
|
|
27
|
+
})
|
|
28
|
+
};
|
|
29
|
+
});
|
|
21
30
|
|
|
22
31
|
describe('RendererLite - Overlay Rendering', () => {
|
|
23
32
|
let renderer;
|
|
@@ -1230,6 +1230,38 @@ describe('RendererLite', () => {
|
|
|
1230
1230
|
|
|
1231
1231
|
vi.useRealTimers();
|
|
1232
1232
|
});
|
|
1233
|
+
|
|
1234
|
+
it('should show a preloaded layout via showLayout()', async () => {
|
|
1235
|
+
const xlf = `<layout><region id="r1"></region></layout>`;
|
|
1236
|
+
const layoutStartHandler = vi.fn();
|
|
1237
|
+
renderer.on('layoutStart', layoutStartHandler);
|
|
1238
|
+
|
|
1239
|
+
// Preload layout hidden
|
|
1240
|
+
await renderer.preloadLayout(xlf, 42);
|
|
1241
|
+
expect(renderer.currentLayoutId).not.toBe(42);
|
|
1242
|
+
expect(layoutStartHandler).not.toHaveBeenCalled();
|
|
1243
|
+
|
|
1244
|
+
// Show it
|
|
1245
|
+
renderer.showLayout(42);
|
|
1246
|
+
expect(renderer.currentLayoutId).toBe(42);
|
|
1247
|
+
expect(layoutStartHandler).toHaveBeenCalledWith(42, expect.any(Object));
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
it('should show the latest preloaded layout when no id given', async () => {
|
|
1251
|
+
const xlf1 = `<layout bgcolor="#ff0000"><region id="r1"></region></layout>`;
|
|
1252
|
+
const xlf2 = `<layout bgcolor="#00ff00"><region id="r2"></region></layout>`;
|
|
1253
|
+
|
|
1254
|
+
await renderer.preloadLayout(xlf1, 10);
|
|
1255
|
+
await renderer.preloadLayout(xlf2, 20);
|
|
1256
|
+
|
|
1257
|
+
renderer.showLayout();
|
|
1258
|
+
expect(renderer.currentLayoutId).toBe(20);
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
it('should no-op showLayout when pool is empty', () => {
|
|
1262
|
+
renderer.showLayout(999);
|
|
1263
|
+
expect(renderer.currentLayoutId).toBeNull();
|
|
1264
|
+
});
|
|
1233
1265
|
});
|
|
1234
1266
|
|
|
1235
1267
|
describe('Layout Replay Optimization', () => {
|