@xiboplayer/renderer 0.6.6 → 0.6.8
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 -4
- package/src/renderer-lite.js +46 -20
- package/src/renderer-lite.test.js +219 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiboplayer/renderer",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.8",
|
|
4
4
|
"description": "RendererLite - Fast, efficient XLF layout rendering engine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"nanoevents": "^9.1.0",
|
|
15
15
|
"pdfjs-dist": "^4.10.38",
|
|
16
|
-
"@xiboplayer/cache": "0.6.
|
|
17
|
-
"@xiboplayer/
|
|
18
|
-
"@xiboplayer/
|
|
16
|
+
"@xiboplayer/cache": "0.6.8",
|
|
17
|
+
"@xiboplayer/utils": "0.6.8",
|
|
18
|
+
"@xiboplayer/schedule": "0.6.8"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"vitest": "^2.0.0",
|
package/src/renderer-lite.js
CHANGED
|
@@ -236,6 +236,9 @@ export class RendererLite {
|
|
|
236
236
|
// Sub-playlist cycle state (round-robin per parentWidgetId group)
|
|
237
237
|
this._subPlaylistCycleIndex = new Map();
|
|
238
238
|
|
|
239
|
+
// Widget lifecycle tracking — ensures symmetric start/stop
|
|
240
|
+
this._startedWidgets = new Set(); // "regionId:widgetIndex" keys
|
|
241
|
+
|
|
239
242
|
// Layout preload pool (2-layout pool for instant transitions)
|
|
240
243
|
this.layoutPool = new LayoutPool(2);
|
|
241
244
|
this.preloadTimer = null;
|
|
@@ -707,7 +710,8 @@ export class RendererLite {
|
|
|
707
710
|
this.currentLayout.duration = maxRegionDuration;
|
|
708
711
|
|
|
709
712
|
this.log.info(`Layout duration updated: ${oldDuration}s → ${maxRegionDuration}s (based on video metadata)`);
|
|
710
|
-
this.
|
|
713
|
+
const final_ = !this._hasUnprobedVideos();
|
|
714
|
+
this.emit('layoutDurationUpdated', this.currentLayoutId, maxRegionDuration, final_);
|
|
711
715
|
|
|
712
716
|
// Deferred timer: video metadata arrived, start the timer now
|
|
713
717
|
if (this._deferredTimerLayoutId === this.currentLayoutId && !this.layoutTimer) {
|
|
@@ -1186,10 +1190,12 @@ export class RendererLite {
|
|
|
1186
1190
|
// OPTIMIZATION: Reuse existing elements for same layout (Arexibo pattern)
|
|
1187
1191
|
this.log.info(`Replaying layout ${layoutId} - reusing elements (no recreation!)`);
|
|
1188
1192
|
|
|
1189
|
-
// Stop all region timers and reset to first widget
|
|
1193
|
+
// Stop all region timers and widgets, then reset to first widget
|
|
1190
1194
|
this._clearRegionTimers(this.regions);
|
|
1195
|
+
this._stopAllRegionWidgets(this.regions, (rid, idx) => this.stopWidget(rid, idx));
|
|
1191
1196
|
for (const [, region] of this.regions) {
|
|
1192
1197
|
region.currentIndex = 0;
|
|
1198
|
+
region.complete = false;
|
|
1193
1199
|
}
|
|
1194
1200
|
|
|
1195
1201
|
// Clear layout timer
|
|
@@ -2041,6 +2047,7 @@ export class RendererLite {
|
|
|
2041
2047
|
const widget = await this._showWidget(region, widgetIndex);
|
|
2042
2048
|
if (widget) {
|
|
2043
2049
|
this.log.info(`Showing widget ${widget.type} (${widget.id}) in region ${regionId}`);
|
|
2050
|
+
this._startedWidgets.add(`${regionId}:${widgetIndex}`);
|
|
2044
2051
|
this.emit('widgetStart', {
|
|
2045
2052
|
widgetId: widget.id, regionId, layoutId: this.currentLayoutId,
|
|
2046
2053
|
mediaId: parseInt(widget.fileId || widget.id) || null,
|
|
@@ -2073,6 +2080,9 @@ export class RendererLite {
|
|
|
2073
2080
|
* @param {number} widgetIndex - Widget index
|
|
2074
2081
|
*/
|
|
2075
2082
|
async stopWidget(regionId, widgetIndex) {
|
|
2083
|
+
const key = `${regionId}:${widgetIndex}`;
|
|
2084
|
+
if (!this._startedWidgets.delete(key)) return; // idempotent: already stopped
|
|
2085
|
+
|
|
2076
2086
|
const region = this.regions.get(regionId);
|
|
2077
2087
|
if (!region) return;
|
|
2078
2088
|
|
|
@@ -2088,6 +2098,24 @@ export class RendererLite {
|
|
|
2088
2098
|
}
|
|
2089
2099
|
}
|
|
2090
2100
|
|
|
2101
|
+
/**
|
|
2102
|
+
* Stop all started widgets across regions (symmetric counterpart to startRegion)
|
|
2103
|
+
* Canvas regions start ALL widgets; non-canvas regions have one active widget.
|
|
2104
|
+
* @param {Map} regions - Region map
|
|
2105
|
+
* @param {Function} stopFn - (regionId, widgetIndex) => void
|
|
2106
|
+
*/
|
|
2107
|
+
_stopAllRegionWidgets(regions, stopFn) {
|
|
2108
|
+
for (const [regionId, region] of regions) {
|
|
2109
|
+
if (region.isCanvas) {
|
|
2110
|
+
for (let i = 0; i < region.widgets.length; i++) {
|
|
2111
|
+
stopFn(regionId, i);
|
|
2112
|
+
}
|
|
2113
|
+
} else if (region.widgets.length > 0) {
|
|
2114
|
+
stopFn(regionId, region.currentIndex);
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2091
2119
|
/**
|
|
2092
2120
|
* Render image widget
|
|
2093
2121
|
*/
|
|
@@ -2933,6 +2961,7 @@ export class RendererLite {
|
|
|
2933
2961
|
}
|
|
2934
2962
|
|
|
2935
2963
|
const oldLayoutId = this.currentLayoutId;
|
|
2964
|
+
const alreadyEmittedEnd = this.layoutEndEmitted;
|
|
2936
2965
|
|
|
2937
2966
|
this.layoutEndEmitted = false;
|
|
2938
2967
|
this.currentLayout = null;
|
|
@@ -2946,6 +2975,7 @@ export class RendererLite {
|
|
|
2946
2975
|
// Region elements live directly in this.container (not a wrapper),
|
|
2947
2976
|
// so we must remove them individually.
|
|
2948
2977
|
this._clearRegionTimers(this.regions);
|
|
2978
|
+
this._stopAllRegionWidgets(this.regions, (rid, idx) => this.stopWidget(rid, idx));
|
|
2949
2979
|
for (const [, region] of this.regions) {
|
|
2950
2980
|
// Release video/audio resources before removing from DOM
|
|
2951
2981
|
LayoutPool.releaseMediaElements(region.element);
|
|
@@ -2986,7 +3016,8 @@ export class RendererLite {
|
|
|
2986
3016
|
// Emit layoutEnd for old layout AFTER setting new currentLayoutId —
|
|
2987
3017
|
// the listener guard in main.ts sees the new layout already playing
|
|
2988
3018
|
// and skips advance, while stats/tracking still run.
|
|
2989
|
-
if (
|
|
3019
|
+
// Skip if the layout timer already emitted layoutEnd (avoids double stats).
|
|
3020
|
+
if (oldLayoutId && !alreadyEmittedEnd) {
|
|
2990
3021
|
this.emit('layoutEnd', oldLayoutId);
|
|
2991
3022
|
}
|
|
2992
3023
|
|
|
@@ -3101,14 +3132,10 @@ export class RendererLite {
|
|
|
3101
3132
|
this.revokeBlobUrlsForLayout(endedLayoutId);
|
|
3102
3133
|
}
|
|
3103
3134
|
|
|
3104
|
-
// Stop all regions
|
|
3135
|
+
// Stop all regions — use helper to stop ALL started widgets (canvas fix)
|
|
3105
3136
|
this._clearRegionTimers(this.regions);
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
if (region.widgets.length > 0) {
|
|
3109
|
-
this.stopWidget(regionId, region.currentIndex);
|
|
3110
|
-
}
|
|
3111
|
-
|
|
3137
|
+
this._stopAllRegionWidgets(this.regions, (rid, idx) => this.stopWidget(rid, idx));
|
|
3138
|
+
for (const [, region] of this.regions) {
|
|
3112
3139
|
// Release video/audio resources before removing from DOM
|
|
3113
3140
|
LayoutPool.releaseMediaElements(region.element);
|
|
3114
3141
|
|
|
@@ -3300,6 +3327,7 @@ export class RendererLite {
|
|
|
3300
3327
|
const widget = await this._showWidget(region, widgetIndex);
|
|
3301
3328
|
if (widget) {
|
|
3302
3329
|
this.log.info(`Showing overlay widget ${widget.type} (${widget.id}) in overlay ${overlayId} region ${regionId}`);
|
|
3330
|
+
this._startedWidgets.add(`overlay:${overlayId}:${regionId}:${widgetIndex}`);
|
|
3303
3331
|
this.emit('overlayWidgetStart', {
|
|
3304
3332
|
overlayId, widgetId: widget.id, regionId,
|
|
3305
3333
|
type: widget.type, duration: widget.duration
|
|
@@ -3318,6 +3346,9 @@ export class RendererLite {
|
|
|
3318
3346
|
* @param {number} widgetIndex - Widget index
|
|
3319
3347
|
*/
|
|
3320
3348
|
async stopOverlayWidget(overlayId, regionId, widgetIndex) {
|
|
3349
|
+
const key = `overlay:${overlayId}:${regionId}:${widgetIndex}`;
|
|
3350
|
+
if (!this._startedWidgets.delete(key)) return; // idempotent
|
|
3351
|
+
|
|
3321
3352
|
const overlayState = this.activeOverlays.get(overlayId);
|
|
3322
3353
|
if (!overlayState) return;
|
|
3323
3354
|
|
|
@@ -3353,17 +3384,11 @@ export class RendererLite {
|
|
|
3353
3384
|
}
|
|
3354
3385
|
|
|
3355
3386
|
// Stop all overlay regions
|
|
3356
|
-
for (const [
|
|
3357
|
-
if (region.timer) {
|
|
3358
|
-
clearTimeout(region.timer);
|
|
3359
|
-
region.timer = null;
|
|
3360
|
-
}
|
|
3361
|
-
|
|
3362
|
-
// Stop current widget
|
|
3363
|
-
if (region.widgets.length > 0) {
|
|
3364
|
-
this.stopOverlayWidget(layoutId, regionId, region.currentIndex);
|
|
3365
|
-
}
|
|
3387
|
+
for (const [, region] of overlayState.regions) {
|
|
3388
|
+
if (region.timer) { clearTimeout(region.timer); region.timer = null; }
|
|
3366
3389
|
}
|
|
3390
|
+
this._stopAllRegionWidgets(overlayState.regions,
|
|
3391
|
+
(rid, idx) => this.stopOverlayWidget(layoutId, rid, idx));
|
|
3367
3392
|
|
|
3368
3393
|
// Remove overlay container from DOM
|
|
3369
3394
|
if (overlayState.container) {
|
|
@@ -3466,6 +3491,7 @@ export class RendererLite {
|
|
|
3466
3491
|
cleanup() {
|
|
3467
3492
|
this.stopAllOverlays();
|
|
3468
3493
|
this.stopCurrentLayout();
|
|
3494
|
+
this._startedWidgets.clear();
|
|
3469
3495
|
|
|
3470
3496
|
// Clean up any remaining audio overlays
|
|
3471
3497
|
for (const widgetId of this.audioOverlays.keys()) {
|
|
@@ -2698,4 +2698,223 @@ describe('RendererLite', () => {
|
|
|
2698
2698
|
vi.useRealTimers();
|
|
2699
2699
|
});
|
|
2700
2700
|
});
|
|
2701
|
+
|
|
2702
|
+
describe('Widget Lifecycle Symmetry', () => {
|
|
2703
|
+
it('should emit symmetric widgetStart/widgetEnd for single-widget layout', async () => {
|
|
2704
|
+
vi.useFakeTimers();
|
|
2705
|
+
const starts = [];
|
|
2706
|
+
const ends = [];
|
|
2707
|
+
renderer.on('widgetStart', (e) => starts.push(e));
|
|
2708
|
+
renderer.on('widgetEnd', (e) => ends.push(e));
|
|
2709
|
+
|
|
2710
|
+
const xlf = `
|
|
2711
|
+
<layout width="1920" height="1080" duration="10">
|
|
2712
|
+
<region id="r1" width="1920" height="1080" top="0" left="0">
|
|
2713
|
+
<media id="m1" type="image" duration="10" fileId="1">
|
|
2714
|
+
<options><uri>test.png</uri></options>
|
|
2715
|
+
</media>
|
|
2716
|
+
</region>
|
|
2717
|
+
</layout>
|
|
2718
|
+
`;
|
|
2719
|
+
|
|
2720
|
+
const renderPromise = renderer.renderLayout(xlf, 1);
|
|
2721
|
+
await vi.advanceTimersByTimeAsync(2000);
|
|
2722
|
+
|
|
2723
|
+
expect(starts).toHaveLength(1);
|
|
2724
|
+
expect(ends).toHaveLength(0);
|
|
2725
|
+
|
|
2726
|
+
renderer.stopCurrentLayout();
|
|
2727
|
+
|
|
2728
|
+
expect(ends).toHaveLength(1);
|
|
2729
|
+
expect(ends[0].widgetId).toBe(starts[0].widgetId);
|
|
2730
|
+
expect(renderer._startedWidgets.size).toBe(0);
|
|
2731
|
+
|
|
2732
|
+
await vi.advanceTimersByTimeAsync(60000);
|
|
2733
|
+
await renderPromise;
|
|
2734
|
+
vi.useRealTimers();
|
|
2735
|
+
});
|
|
2736
|
+
|
|
2737
|
+
it('should stop ALL widgets in canvas region on teardown', async () => {
|
|
2738
|
+
vi.useFakeTimers();
|
|
2739
|
+
const starts = [];
|
|
2740
|
+
const ends = [];
|
|
2741
|
+
renderer.on('widgetStart', (e) => starts.push(e));
|
|
2742
|
+
renderer.on('widgetEnd', (e) => ends.push(e));
|
|
2743
|
+
|
|
2744
|
+
const xlf = `
|
|
2745
|
+
<layout width="1920" height="1080" duration="30">
|
|
2746
|
+
<region id="r1" type="canvas" width="1920" height="1080" top="0" left="0">
|
|
2747
|
+
<media id="m1" type="image" duration="10" fileId="1">
|
|
2748
|
+
<options><uri>img1.png</uri></options>
|
|
2749
|
+
</media>
|
|
2750
|
+
<media id="m2" type="image" duration="10" fileId="2">
|
|
2751
|
+
<options><uri>img2.png</uri></options>
|
|
2752
|
+
</media>
|
|
2753
|
+
<media id="m3" type="image" duration="10" fileId="3">
|
|
2754
|
+
<options><uri>img3.png</uri></options>
|
|
2755
|
+
</media>
|
|
2756
|
+
</region>
|
|
2757
|
+
</layout>
|
|
2758
|
+
`;
|
|
2759
|
+
|
|
2760
|
+
const renderPromise = renderer.renderLayout(xlf, 1);
|
|
2761
|
+
await vi.advanceTimersByTimeAsync(2000);
|
|
2762
|
+
|
|
2763
|
+
expect(starts).toHaveLength(3);
|
|
2764
|
+
expect(ends).toHaveLength(0);
|
|
2765
|
+
|
|
2766
|
+
renderer.stopCurrentLayout();
|
|
2767
|
+
|
|
2768
|
+
expect(ends).toHaveLength(3);
|
|
2769
|
+
expect(renderer._startedWidgets.size).toBe(0);
|
|
2770
|
+
|
|
2771
|
+
await vi.advanceTimersByTimeAsync(60000);
|
|
2772
|
+
await renderPromise;
|
|
2773
|
+
vi.useRealTimers();
|
|
2774
|
+
});
|
|
2775
|
+
|
|
2776
|
+
it('should stop widgets before restarting on same-layout replay', async () => {
|
|
2777
|
+
vi.useFakeTimers();
|
|
2778
|
+
const events = [];
|
|
2779
|
+
renderer.on('widgetStart', (e) => events.push({ type: 'start', id: e.widgetId }));
|
|
2780
|
+
renderer.on('widgetEnd', (e) => events.push({ type: 'end', id: e.widgetId }));
|
|
2781
|
+
|
|
2782
|
+
const xlf = `
|
|
2783
|
+
<layout width="1920" height="1080" duration="10">
|
|
2784
|
+
<region id="r1" width="1920" height="1080" top="0" left="0">
|
|
2785
|
+
<media id="m1" type="image" duration="10" fileId="1">
|
|
2786
|
+
<options><uri>test.png</uri></options>
|
|
2787
|
+
</media>
|
|
2788
|
+
</region>
|
|
2789
|
+
</layout>
|
|
2790
|
+
`;
|
|
2791
|
+
|
|
2792
|
+
// First render
|
|
2793
|
+
const p1 = renderer.renderLayout(xlf, 1);
|
|
2794
|
+
await vi.advanceTimersByTimeAsync(2000);
|
|
2795
|
+
expect(events).toHaveLength(1); // 1 start
|
|
2796
|
+
|
|
2797
|
+
// Same-layout replay
|
|
2798
|
+
events.length = 0;
|
|
2799
|
+
const p2 = renderer.renderLayout(xlf, 1);
|
|
2800
|
+
await vi.advanceTimersByTimeAsync(2000);
|
|
2801
|
+
|
|
2802
|
+
// widgetEnd should fire before widgetStart for the replay
|
|
2803
|
+
expect(events.length).toBeGreaterThanOrEqual(2);
|
|
2804
|
+
const endIdx = events.findIndex(e => e.type === 'end');
|
|
2805
|
+
const startIdx = events.findIndex(e => e.type === 'start');
|
|
2806
|
+
expect(endIdx).toBeLessThan(startIdx);
|
|
2807
|
+
expect(renderer._startedWidgets.size).toBe(1);
|
|
2808
|
+
|
|
2809
|
+
await vi.advanceTimersByTimeAsync(60000);
|
|
2810
|
+
await p1;
|
|
2811
|
+
await p2;
|
|
2812
|
+
vi.useRealTimers();
|
|
2813
|
+
});
|
|
2814
|
+
|
|
2815
|
+
it('should be idempotent on double stopWidget calls', async () => {
|
|
2816
|
+
vi.useFakeTimers();
|
|
2817
|
+
const ends = [];
|
|
2818
|
+
renderer.on('widgetEnd', (e) => ends.push(e));
|
|
2819
|
+
|
|
2820
|
+
const xlf = `
|
|
2821
|
+
<layout width="1920" height="1080" duration="10">
|
|
2822
|
+
<region id="r1" width="1920" height="1080" top="0" left="0">
|
|
2823
|
+
<media id="m1" type="image" duration="10" fileId="1">
|
|
2824
|
+
<options><uri>test.png</uri></options>
|
|
2825
|
+
</media>
|
|
2826
|
+
</region>
|
|
2827
|
+
</layout>
|
|
2828
|
+
`;
|
|
2829
|
+
|
|
2830
|
+
const renderPromise = renderer.renderLayout(xlf, 1);
|
|
2831
|
+
await vi.advanceTimersByTimeAsync(2000);
|
|
2832
|
+
|
|
2833
|
+
// Stop twice
|
|
2834
|
+
await renderer.stopWidget('r1', 0);
|
|
2835
|
+
await renderer.stopWidget('r1', 0);
|
|
2836
|
+
|
|
2837
|
+
expect(ends).toHaveLength(1); // only 1 widgetEnd, not 2
|
|
2838
|
+
|
|
2839
|
+
await vi.advanceTimersByTimeAsync(60000);
|
|
2840
|
+
await renderPromise;
|
|
2841
|
+
vi.useRealTimers();
|
|
2842
|
+
});
|
|
2843
|
+
|
|
2844
|
+
it('should not double-emit widgetEnd on layout timer + stopCurrentLayout', async () => {
|
|
2845
|
+
vi.useFakeTimers();
|
|
2846
|
+
const ends = [];
|
|
2847
|
+
renderer.on('widgetEnd', (e) => ends.push(e));
|
|
2848
|
+
|
|
2849
|
+
const xlf = `
|
|
2850
|
+
<layout width="1920" height="1080" duration="5">
|
|
2851
|
+
<region id="r1" width="1920" height="1080" top="0" left="0">
|
|
2852
|
+
<media id="m1" type="image" duration="5" fileId="1">
|
|
2853
|
+
<options><uri>test.png</uri></options>
|
|
2854
|
+
</media>
|
|
2855
|
+
</region>
|
|
2856
|
+
</layout>
|
|
2857
|
+
`;
|
|
2858
|
+
|
|
2859
|
+
const renderPromise = renderer.renderLayout(xlf, 1);
|
|
2860
|
+
await vi.advanceTimersByTimeAsync(2000);
|
|
2861
|
+
|
|
2862
|
+
// Advance past layout duration (layout timer fires, which triggers stopCurrentLayout internally via layoutEnd)
|
|
2863
|
+
await vi.advanceTimersByTimeAsync(5000);
|
|
2864
|
+
|
|
2865
|
+
// Now explicitly stop (as renderLayout(next) would do)
|
|
2866
|
+
renderer.stopCurrentLayout();
|
|
2867
|
+
|
|
2868
|
+
// Should only have 1 widgetEnd total, not 2
|
|
2869
|
+
expect(ends).toHaveLength(1);
|
|
2870
|
+
|
|
2871
|
+
await vi.advanceTimersByTimeAsync(60000);
|
|
2872
|
+
await renderPromise;
|
|
2873
|
+
vi.useRealTimers();
|
|
2874
|
+
});
|
|
2875
|
+
|
|
2876
|
+
it('should balance starts and ends during multi-widget cycling', async () => {
|
|
2877
|
+
vi.useFakeTimers();
|
|
2878
|
+
let startCount = 0;
|
|
2879
|
+
let endCount = 0;
|
|
2880
|
+
renderer.on('widgetStart', () => startCount++);
|
|
2881
|
+
renderer.on('widgetEnd', () => endCount++);
|
|
2882
|
+
|
|
2883
|
+
const xlf = `
|
|
2884
|
+
<layout width="1920" height="1080" duration="30">
|
|
2885
|
+
<region id="r1" width="1920" height="1080" top="0" left="0">
|
|
2886
|
+
<media id="m1" type="image" duration="2" fileId="1">
|
|
2887
|
+
<options><uri>img1.png</uri></options>
|
|
2888
|
+
</media>
|
|
2889
|
+
<media id="m2" type="image" duration="2" fileId="2">
|
|
2890
|
+
<options><uri>img2.png</uri></options>
|
|
2891
|
+
</media>
|
|
2892
|
+
<media id="m3" type="image" duration="2" fileId="3">
|
|
2893
|
+
<options><uri>img3.png</uri></options>
|
|
2894
|
+
</media>
|
|
2895
|
+
</region>
|
|
2896
|
+
</layout>
|
|
2897
|
+
`;
|
|
2898
|
+
|
|
2899
|
+
const renderPromise = renderer.renderLayout(xlf, 1);
|
|
2900
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
2901
|
+
|
|
2902
|
+
// Cycle through several widgets (3 widgets × 2s each = 6s per cycle)
|
|
2903
|
+
for (let i = 0; i < 12; i++) {
|
|
2904
|
+
await vi.advanceTimersByTimeAsync(2000);
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
// At any point, starts should be >= ends
|
|
2908
|
+
expect(startCount).toBeGreaterThanOrEqual(endCount);
|
|
2909
|
+
|
|
2910
|
+
// After full teardown, they should balance
|
|
2911
|
+
renderer.stopCurrentLayout();
|
|
2912
|
+
expect(startCount).toBe(endCount);
|
|
2913
|
+
expect(renderer._startedWidgets.size).toBe(0);
|
|
2914
|
+
|
|
2915
|
+
await vi.advanceTimersByTimeAsync(60000);
|
|
2916
|
+
await renderPromise;
|
|
2917
|
+
vi.useRealTimers();
|
|
2918
|
+
});
|
|
2919
|
+
});
|
|
2701
2920
|
});
|