@xiboplayer/renderer 0.6.2 → 0.6.4

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.
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
9
- import { RendererLite } from './renderer-lite.js';
9
+ import { RendererLite, Transitions } from './renderer-lite.js';
10
10
 
11
11
  describe('RendererLite', () => {
12
12
  let container;
@@ -964,48 +964,218 @@ describe('RendererLite', () => {
964
964
  });
965
965
 
966
966
  describe('Transitions', () => {
967
- // Skip: jsdom doesn't support Web Animations API
968
- it.skip('should apply fade in transition', async () => {
969
- const element = document.createElement('div');
970
- element.style.opacity = '0';
967
+ let element;
968
+ let mockAnimate;
969
+ let capturedKeyframes;
970
+ let capturedTiming;
971
+
972
+ beforeEach(() => {
973
+ element = document.createElement('div');
974
+ capturedKeyframes = null;
975
+ capturedTiming = null;
976
+ mockAnimate = vi.fn((keyframes, timing) => {
977
+ capturedKeyframes = keyframes;
978
+ capturedTiming = timing;
979
+ return { onfinish: null, cancel: vi.fn() };
980
+ });
981
+ element.animate = mockAnimate;
982
+ });
971
983
 
972
- const transition = {
973
- type: 'fadeIn',
974
- duration: 1000,
975
- direction: 'N'
976
- };
984
+ it('should apply fade in transition', () => {
985
+ const result = Transitions.apply(element, { type: 'fadeIn', duration: 1000 }, true, 1920, 1080);
986
+
987
+ expect(result).toBeTruthy();
988
+ expect(capturedKeyframes).toEqual([{ opacity: 0 }, { opacity: 1 }]);
989
+ expect(capturedTiming.duration).toBe(1000);
990
+ expect(capturedTiming.easing).toBe('linear');
991
+ });
992
+
993
+ it('should apply fade out transition', () => {
994
+ const result = Transitions.apply(element, { type: 'fadeOut', duration: 800 }, false, 1920, 1080);
995
+
996
+ expect(result).toBeTruthy();
997
+ expect(capturedKeyframes).toEqual([{ opacity: 1 }, { opacity: 0 }]);
998
+ expect(capturedTiming.duration).toBe(800);
999
+ });
1000
+
1001
+ it('should apply generic "fade" as fadeIn when isIn=true', () => {
1002
+ const result = Transitions.apply(element, { type: 'fade', duration: 500 }, true, 1920, 1080);
1003
+
1004
+ expect(result).toBeTruthy();
1005
+ expect(capturedKeyframes).toEqual([{ opacity: 0 }, { opacity: 1 }]);
1006
+ });
1007
+
1008
+ it('should apply generic "fade" as fadeOut when isIn=false', () => {
1009
+ const result = Transitions.apply(element, { type: 'fade', duration: 500 }, false, 1920, 1080);
1010
+
1011
+ expect(result).toBeTruthy();
1012
+ expect(capturedKeyframes).toEqual([{ opacity: 1 }, { opacity: 0 }]);
1013
+ });
1014
+
1015
+ it('should apply fly in from North', () => {
1016
+ const result = Transitions.apply(
1017
+ element, { type: 'flyIn', duration: 500, direction: 'N' }, true, 1920, 1080
1018
+ );
977
1019
 
978
- // Import Transitions utility
979
- const { Transitions } = await import('./renderer-lite.js');
980
- const animation = Transitions.apply(element, transition, true, 1920, 1080);
1020
+ expect(result).toBeTruthy();
1021
+ expect(capturedKeyframes[0].transform).toBe('translate(0px, -1080px)');
1022
+ expect(capturedKeyframes[1].transform).toBe('translate(0, 0)');
1023
+ expect(capturedTiming.easing).toBe('ease-out');
1024
+ });
981
1025
 
982
- expect(animation).toBeTruthy();
983
- expect(animation.effect.getKeyframes()).toEqual(
984
- expect.arrayContaining([
985
- expect.objectContaining({ opacity: '0' }),
986
- expect.objectContaining({ opacity: '1' })
987
- ])
1026
+ it('should apply fly in from East', () => {
1027
+ Transitions.apply(
1028
+ element, { type: 'flyIn', duration: 500, direction: 'E' }, true, 1920, 1080
988
1029
  );
1030
+
1031
+ expect(capturedKeyframes[0].transform).toBe('translate(1920px, 0px)');
1032
+ expect(capturedKeyframes[1].transform).toBe('translate(0, 0)');
989
1033
  });
990
1034
 
991
- // Skip: jsdom doesn't support Web Animations API
992
- it.skip('should apply fly out transition with direction', async () => {
993
- const element = document.createElement('div');
1035
+ it('should apply fly in from South', () => {
1036
+ Transitions.apply(
1037
+ element, { type: 'flyIn', duration: 500, direction: 'S' }, true, 1920, 1080
1038
+ );
994
1039
 
995
- const transition = {
996
- type: 'flyOut',
997
- duration: 1500,
998
- direction: 'S' // South
999
- };
1040
+ expect(capturedKeyframes[0].transform).toBe('translate(0px, 1080px)');
1041
+ });
1042
+
1043
+ it('should apply fly in from West', () => {
1044
+ Transitions.apply(
1045
+ element, { type: 'flyIn', duration: 500, direction: 'W' }, true, 1920, 1080
1046
+ );
1047
+
1048
+ expect(capturedKeyframes[0].transform).toBe('translate(-1920px, 0px)');
1049
+ });
1050
+
1051
+ it('should apply fly in from diagonal directions (NE, SE, SW, NW)', () => {
1052
+ // NE
1053
+ Transitions.apply(element, { type: 'flyIn', duration: 500, direction: 'NE' }, true, 1920, 1080);
1054
+ expect(capturedKeyframes[0].transform).toBe('translate(1920px, -1080px)');
1055
+
1056
+ // SE
1057
+ Transitions.apply(element, { type: 'flyIn', duration: 500, direction: 'SE' }, true, 1920, 1080);
1058
+ expect(capturedKeyframes[0].transform).toBe('translate(1920px, 1080px)');
1000
1059
 
1001
- const { Transitions } = await import('./renderer-lite.js');
1002
- const animation = Transitions.apply(element, transition, false, 1920, 1080);
1060
+ // SW
1061
+ Transitions.apply(element, { type: 'flyIn', duration: 500, direction: 'SW' }, true, 1920, 1080);
1062
+ expect(capturedKeyframes[0].transform).toBe('translate(-1920px, 1080px)');
1063
+
1064
+ // NW
1065
+ Transitions.apply(element, { type: 'flyIn', duration: 500, direction: 'NW' }, true, 1920, 1080);
1066
+ expect(capturedKeyframes[0].transform).toBe('translate(-1920px, -1080px)');
1067
+ });
1068
+
1069
+ it('should apply fly out with direction S', () => {
1070
+ const result = Transitions.apply(
1071
+ element, { type: 'flyOut', duration: 1500, direction: 'S' }, false, 1920, 1080
1072
+ );
1073
+
1074
+ expect(result).toBeTruthy();
1075
+ expect(capturedKeyframes[0].transform).toBe('translate(0, 0)');
1076
+ expect(capturedKeyframes[1].transform).toBe('translate(0px, -1080px)');
1077
+ expect(capturedTiming.easing).toBe('ease-in');
1078
+ });
1079
+
1080
+ it('should apply generic "fly" as flyIn when isIn=true', () => {
1081
+ const result = Transitions.apply(
1082
+ element, { type: 'fly', duration: 500, direction: 'E' }, true, 1920, 1080
1083
+ );
1084
+
1085
+ expect(result).toBeTruthy();
1086
+ expect(capturedKeyframes[0].transform).toBe('translate(1920px, 0px)');
1087
+ expect(capturedKeyframes[1].transform).toBe('translate(0, 0)');
1088
+ expect(capturedTiming.easing).toBe('ease-out');
1089
+ });
1090
+
1091
+ it('should apply generic "fly" as flyOut when isIn=false', () => {
1092
+ const result = Transitions.apply(
1093
+ element, { type: 'fly', duration: 500, direction: 'W' }, false, 1920, 1080
1094
+ );
1095
+
1096
+ expect(result).toBeTruthy();
1097
+ expect(capturedKeyframes[0].transform).toBe('translate(0, 0)');
1098
+ expect(capturedKeyframes[1].transform).toContain('px');
1099
+ expect(capturedTiming.easing).toBe('ease-in');
1100
+ });
1101
+
1102
+ it('should not apply flyIn when isIn=false', () => {
1103
+ const result = Transitions.apply(
1104
+ element, { type: 'flyIn', duration: 500, direction: 'N' }, false, 1920, 1080
1105
+ );
1106
+ expect(result).toBeNull();
1107
+ });
1108
+
1109
+ it('should not apply flyOut when isIn=true', () => {
1110
+ const result = Transitions.apply(
1111
+ element, { type: 'flyOut', duration: 500, direction: 'N' }, true, 1920, 1080
1112
+ );
1113
+ expect(result).toBeNull();
1114
+ });
1115
+
1116
+ it('should default direction to N when missing', () => {
1117
+ Transitions.apply(element, { type: 'flyIn', duration: 500 }, true, 1920, 1080);
1118
+
1119
+ // N direction: translateY(-height)
1120
+ expect(capturedKeyframes[0].transform).toBe('translate(0px, -1080px)');
1121
+ });
1122
+
1123
+ it('should default duration to 1000 when missing', () => {
1124
+ Transitions.apply(element, { type: 'fadeIn' }, true, 1920, 1080);
1125
+
1126
+ expect(capturedTiming.duration).toBe(1000);
1127
+ });
1128
+
1129
+ it('should return null for unknown transition type', () => {
1130
+ const result = Transitions.apply(element, { type: 'slide' }, true, 1920, 1080);
1131
+ expect(result).toBeNull();
1132
+ });
1133
+
1134
+ it('should return null when config is null', () => {
1135
+ expect(Transitions.apply(element, null, true, 1920, 1080)).toBeNull();
1136
+ });
1137
+
1138
+ it('should return null when config has no type', () => {
1139
+ expect(Transitions.apply(element, { duration: 500 }, true, 1920, 1080)).toBeNull();
1140
+ });
1141
+
1142
+ it('should be case-insensitive for type matching', () => {
1143
+ const result = Transitions.apply(element, { type: 'FadeIn', duration: 500 }, true, 1920, 1080);
1144
+ expect(result).toBeTruthy();
1145
+ expect(capturedKeyframes).toEqual([{ opacity: 0 }, { opacity: 1 }]);
1146
+ });
1147
+
1148
+ it('should parse fly transitions from XLF with generic "fly" type', () => {
1149
+ const xlf = `
1150
+ <layout>
1151
+ <region id="r1">
1152
+ <media id="m1" type="image" duration="10">
1153
+ <options>
1154
+ <transIn>fly</transIn>
1155
+ <transInDuration>500</transInDuration>
1156
+ <transInDirection>E</transInDirection>
1157
+ <transOut>fly</transOut>
1158
+ <transOutDuration>500</transOutDuration>
1159
+ <transOutDirection>NW</transOutDirection>
1160
+ </options>
1161
+ </media>
1162
+ </region>
1163
+ </layout>
1164
+ `;
1003
1165
 
1004
- expect(animation).toBeTruthy();
1005
- const keyframes = animation.effect.getKeyframes();
1166
+ const layout = renderer.parseXlf(xlf);
1167
+ const widget = layout.regions[0].widgets[0];
1006
1168
 
1007
- // Should translate to south (positive Y)
1008
- expect(keyframes[1].transform).toContain('1080px'); // Height offset
1169
+ expect(widget.transitions.in).toEqual({
1170
+ type: 'fly',
1171
+ duration: 500,
1172
+ direction: 'E'
1173
+ });
1174
+ expect(widget.transitions.out).toEqual({
1175
+ type: 'fly',
1176
+ duration: 500,
1177
+ direction: 'NW'
1178
+ });
1009
1179
  });
1010
1180
  });
1011
1181
 
@@ -1789,6 +1959,57 @@ describe('RendererLite', () => {
1789
1959
 
1790
1960
  vi.useRealTimers();
1791
1961
  });
1962
+
1963
+ it('should hide multi-widget drawer after cycling through all widgets', async () => {
1964
+ vi.useFakeTimers();
1965
+
1966
+ const xlf = `
1967
+ <layout width="1920" height="1080" duration="60">
1968
+ <region id="r1" width="1920" height="1080" top="0" left="0">
1969
+ <media id="m1" type="image" duration="60" fileId="1">
1970
+ <options><uri>test.png</uri></options>
1971
+ </media>
1972
+ </region>
1973
+ <drawer id="d1" width="400" height="300" top="100" left="100">
1974
+ <media id="dm1" type="image" duration="3" fileId="2">
1975
+ <options><uri>d1.png</uri></options>
1976
+ </media>
1977
+ <media id="dm2" type="image" duration="3" fileId="3">
1978
+ <options><uri>d2.png</uri></options>
1979
+ </media>
1980
+ <media id="dm3" type="image" duration="3" fileId="4">
1981
+ <options><uri>d3.png</uri></options>
1982
+ </media>
1983
+ </drawer>
1984
+ </layout>
1985
+ `;
1986
+
1987
+ const renderPromise = renderer.renderLayout(xlf, 1);
1988
+ await vi.advanceTimersByTimeAsync(100);
1989
+ await renderPromise;
1990
+
1991
+ const drawerRegion = renderer.regions.get('d1');
1992
+ expect(drawerRegion.element.style.display).toBe('none');
1993
+
1994
+ // Navigate to first drawer widget
1995
+ renderer.navigateToWidget('dm1');
1996
+ expect(drawerRegion.element.style.display).toBe('');
1997
+ expect(drawerRegion.currentIndex).toBe(0);
1998
+
1999
+ // After dm1 duration → advances to dm2, still visible
2000
+ await vi.advanceTimersByTimeAsync(3100);
2001
+ expect(drawerRegion.element.style.display).toBe('');
2002
+
2003
+ // After dm2 duration → advances to dm3, still visible
2004
+ await vi.advanceTimersByTimeAsync(3100);
2005
+ expect(drawerRegion.element.style.display).toBe('');
2006
+
2007
+ // After dm3 duration → wraps to 0, drawer hidden
2008
+ await vi.advanceTimersByTimeAsync(3100);
2009
+ expect(drawerRegion.element.style.display).toBe('none');
2010
+
2011
+ vi.useRealTimers();
2012
+ });
1792
2013
  });
1793
2014
 
1794
2015
  describe('Sub-Playlist (#10)', () => {
@@ -1897,6 +2118,65 @@ describe('RendererLite', () => {
1897
2118
  expect(ids.some(id => id.startsWith('a'))).toBe(true);
1898
2119
  expect(ids.some(id => id.startsWith('b'))).toBe(true);
1899
2120
  });
2121
+
2122
+ it('should repeat widget playCount times before advancing (#188)', () => {
2123
+ const widgets = [
2124
+ { id: 'm1', type: 'image', duration: 10, parentWidgetId: 'sp1',
2125
+ displayOrder: 1, cyclePlayback: true, playCount: 2, isRandom: false },
2126
+ { id: 'm2', type: 'image', duration: 10, parentWidgetId: 'sp1',
2127
+ displayOrder: 2, cyclePlayback: true, playCount: 2, isRandom: false },
2128
+ ];
2129
+
2130
+ renderer._subPlaylistCycleIndex = new Map();
2131
+
2132
+ const r1 = renderer._applyCyclePlayback(widgets);
2133
+ const r2 = renderer._applyCyclePlayback(widgets);
2134
+ const r3 = renderer._applyCyclePlayback(widgets);
2135
+ const r4 = renderer._applyCyclePlayback(widgets);
2136
+
2137
+ // m1 plays twice, then m2 plays twice
2138
+ expect(r1[0].id).toBe('m1');
2139
+ expect(r2[0].id).toBe('m1');
2140
+ expect(r3[0].id).toBe('m2');
2141
+ expect(r4[0].id).toBe('m2');
2142
+ });
2143
+
2144
+ it('should treat playCount=0 or missing as 1 (#188)', () => {
2145
+ const widgets = [
2146
+ { id: 'm1', type: 'image', duration: 10, parentWidgetId: 'sp1',
2147
+ displayOrder: 1, cyclePlayback: true, playCount: 0, isRandom: false },
2148
+ { id: 'm2', type: 'image', duration: 10, parentWidgetId: 'sp1',
2149
+ displayOrder: 2, cyclePlayback: true, isRandom: false },
2150
+ ];
2151
+
2152
+ renderer._subPlaylistCycleIndex = new Map();
2153
+
2154
+ const r1 = renderer._applyCyclePlayback(widgets);
2155
+ const r2 = renderer._applyCyclePlayback(widgets);
2156
+
2157
+ // Should advance every cycle (playCount defaults to 1)
2158
+ expect(r1[0].id).toBe('m1');
2159
+ expect(r2[0].id).toBe('m2');
2160
+ });
2161
+
2162
+ it('should repeat playCount=3 times before advancing (#188)', () => {
2163
+ const widgets = [
2164
+ { id: 'm1', type: 'image', duration: 10, parentWidgetId: 'sp1',
2165
+ displayOrder: 1, cyclePlayback: true, playCount: 3, isRandom: false },
2166
+ { id: 'm2', type: 'image', duration: 10, parentWidgetId: 'sp1',
2167
+ displayOrder: 2, cyclePlayback: true, playCount: 3, isRandom: false },
2168
+ ];
2169
+
2170
+ renderer._subPlaylistCycleIndex = new Map();
2171
+
2172
+ const results = [];
2173
+ for (let i = 0; i < 6; i++) {
2174
+ results.push(renderer._applyCyclePlayback(widgets)[0].id);
2175
+ }
2176
+
2177
+ // m1 x3, m2 x3
2178
+ expect(results).toEqual(['m1', 'm1', 'm1', 'm2', 'm2', 'm2']);
2179
+ });
1900
2180
  });
1901
2181
 
1902
2182
  // ── Medium-Priority Spec Compliance ────────────────────────────────
@@ -2238,4 +2518,184 @@ describe('RendererLite', () => {
2238
2518
  expect(layout.regions[0].widgets[0].commands).toEqual([]);
2239
2519
  });
2240
2520
  });
2521
+
2522
+ describe('Canvas Regions (#186)', () => {
2523
+ it('should parse region with type="canvas" as isCanvas', () => {
2524
+ const xlf = `
2525
+ <layout width="1920" height="1080" duration="60">
2526
+ <region id="r1" type="canvas" width="1920" height="1080" top="0" left="0">
2527
+ <media id="m1" type="image" duration="10" fileId="1">
2528
+ <options><uri>img1.png</uri></options>
2529
+ </media>
2530
+ <media id="m2" type="image" duration="15" fileId="2">
2531
+ <options><uri>img2.png</uri></options>
2532
+ </media>
2533
+ </region>
2534
+ </layout>
2535
+ `;
2536
+
2537
+ const layout = renderer.parseXlf(xlf);
2538
+
2539
+ expect(layout.regions).toHaveLength(1);
2540
+ expect(layout.regions[0].isCanvas).toBe(true);
2541
+ expect(layout.regions[0].widgets).toHaveLength(2);
2542
+ });
2543
+
2544
+ it('should auto-detect canvas from type="global" widget', () => {
2545
+ const xlf = `
2546
+ <layout width="1920" height="1080" duration="60">
2547
+ <region id="r1" width="1920" height="1080" top="0" left="0">
2548
+ <media id="m1" type="global" duration="30" fileId="1">
2549
+ <options></options>
2550
+ </media>
2551
+ </region>
2552
+ </layout>
2553
+ `;
2554
+
2555
+ const layout = renderer.parseXlf(xlf);
2556
+
2557
+ expect(layout.regions[0].isCanvas).toBe(true);
2558
+ });
2559
+
2560
+ it('should NOT mark normal regions as canvas', () => {
2561
+ const xlf = `
2562
+ <layout width="1920" height="1080" duration="60">
2563
+ <region id="r1" width="1920" height="1080" top="0" left="0">
2564
+ <media id="m1" type="image" duration="10" fileId="1">
2565
+ <options><uri>test.png</uri></options>
2566
+ </media>
2567
+ </region>
2568
+ </layout>
2569
+ `;
2570
+
2571
+ const layout = renderer.parseXlf(xlf);
2572
+
2573
+ expect(layout.regions[0].isCanvas).toBe(false);
2574
+ });
2575
+
2576
+ it('should store isCanvas flag in region state after createRegion', async () => {
2577
+ const regionConfig = {
2578
+ id: 'r1',
2579
+ width: 1920,
2580
+ height: 1080,
2581
+ top: 0,
2582
+ left: 0,
2583
+ zindex: 0,
2584
+ isCanvas: true,
2585
+ widgets: []
2586
+ };
2587
+
2588
+ await renderer.createRegion(regionConfig);
2589
+
2590
+ const region = renderer.regions.get('r1');
2591
+ expect(region.isCanvas).toBe(true);
2592
+ });
2593
+
2594
+ it('should render all canvas widgets simultaneously', async () => {
2595
+ vi.useFakeTimers();
2596
+
2597
+ const xlf = `
2598
+ <layout width="1920" height="1080" duration="60">
2599
+ <region id="r1" type="canvas" width="1920" height="1080" top="0" left="0">
2600
+ <media id="m1" type="image" duration="10" fileId="1">
2601
+ <options><uri>img1.png</uri></options>
2602
+ </media>
2603
+ <media id="m2" type="image" duration="15" fileId="2">
2604
+ <options><uri>img2.png</uri></options>
2605
+ </media>
2606
+ <media id="m3" type="image" duration="20" fileId="3">
2607
+ <options><uri>img3.png</uri></options>
2608
+ </media>
2609
+ </region>
2610
+ </layout>
2611
+ `;
2612
+
2613
+ const renderPromise = renderer.renderLayout(xlf, 1);
2614
+ await vi.advanceTimersByTimeAsync(5000);
2615
+
2616
+ const region = renderer.regions.get('r1');
2617
+ expect(region).toBeDefined();
2618
+ expect(region.isCanvas).toBe(true);
2619
+
2620
+ // All 3 widgets should be visible simultaneously
2621
+ let visibleCount = 0;
2622
+ for (const [, el] of region.widgetElements) {
2623
+ if (el.style.visibility === 'visible') visibleCount++;
2624
+ }
2625
+ expect(visibleCount).toBe(3);
2626
+
2627
+ // Clean up
2628
+ await vi.advanceTimersByTimeAsync(60000);
2629
+ await renderPromise;
2630
+ vi.useRealTimers();
2631
+ });
2632
+
2633
+ it('should not cycle canvas region widgets', async () => {
2634
+ vi.useFakeTimers();
2635
+
2636
+ const xlf = `
2637
+ <layout width="1920" height="1080" duration="60">
2638
+ <region id="r1" type="canvas" width="1920" height="1080" top="0" left="0">
2639
+ <media id="m1" type="image" duration="5" fileId="1">
2640
+ <options><uri>img1.png</uri></options>
2641
+ </media>
2642
+ <media id="m2" type="image" duration="5" fileId="2">
2643
+ <options><uri>img2.png</uri></options>
2644
+ </media>
2645
+ </region>
2646
+ </layout>
2647
+ `;
2648
+
2649
+ const renderPromise = renderer.renderLayout(xlf, 1);
2650
+ await vi.advanceTimersByTimeAsync(2000);
2651
+
2652
+ const region = renderer.regions.get('r1');
2653
+
2654
+ // After widget durations expire, both should still be visible (no cycling)
2655
+ await vi.advanceTimersByTimeAsync(10000);
2656
+
2657
+ let visibleCount = 0;
2658
+ for (const [, el] of region.widgetElements) {
2659
+ if (el.style.visibility === 'visible') visibleCount++;
2660
+ }
2661
+ expect(visibleCount).toBe(2);
2662
+
2663
+ // Clean up
2664
+ await vi.advanceTimersByTimeAsync(60000);
2665
+ await renderPromise;
2666
+ vi.useRealTimers();
2667
+ });
2668
+
2669
+ it('should mark canvas region complete after max widget duration', async () => {
2670
+ vi.useFakeTimers();
2671
+
2672
+ const xlf = `
2673
+ <layout width="1920" height="1080">
2674
+ <region id="r1" type="canvas" width="1920" height="1080" top="0" left="0">
2675
+ <media id="m1" type="image" duration="5" fileId="1">
2676
+ <options><uri>img1.png</uri></options>
2677
+ </media>
2678
+ <media id="m2" type="image" duration="10" fileId="2">
2679
+ <options><uri>img2.png</uri></options>
2680
+ </media>
2681
+ </region>
2682
+ </layout>
2683
+ `;
2684
+
2685
+ const renderPromise = renderer.renderLayout(xlf, 1);
2686
+ await vi.advanceTimersByTimeAsync(2000);
2687
+
2688
+ const region = renderer.regions.get('r1');
2689
+ expect(region.complete).toBe(false);
2690
+
2691
+ // Advance past max widget duration (10s)
2692
+ await vi.advanceTimersByTimeAsync(9000);
2693
+ expect(region.complete).toBe(true);
2694
+
2695
+ // Clean up
2696
+ await vi.advanceTimersByTimeAsync(60000);
2697
+ await renderPromise;
2698
+ vi.useRealTimers();
2699
+ });
2700
+ });
2241
2701
  });
package/vitest.config.js CHANGED
@@ -9,7 +9,9 @@ export default defineConfig({
9
9
  alias: {
10
10
  // hls.js is an optional runtime dependency (dynamic import in renderVideo).
11
11
  // Alias to the monorepo mock so renderer tests work standalone.
12
- 'hls.js': new URL('../../vitest.hls-mock.js', import.meta.url).pathname
12
+ 'hls.js': new URL('../../vitest.hls-mock.js', import.meta.url).pathname,
13
+ '@xiboplayer/schedule': new URL('../schedule/src/index.js', import.meta.url).pathname,
14
+ '@xiboplayer/utils': new URL('../utils/src/index.js', import.meta.url).pathname
13
15
  }
14
16
  }
15
17
  });