@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.
- package/README.md +240 -25
- package/docs/RENDERER_COMPARISON.md +1 -1
- package/package.json +4 -4
- package/src/layout.js +6 -0
- package/src/renderer-lite.ic.test.js +203 -0
- package/src/renderer-lite.js +213 -12
- package/src/renderer-lite.test.js +493 -33
- package/vitest.config.js +3 -1
|
@@ -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
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
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
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
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
|
-
|
|
979
|
-
|
|
980
|
-
|
|
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
|
-
|
|
983
|
-
|
|
984
|
-
|
|
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
|
-
|
|
992
|
-
|
|
993
|
-
|
|
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
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
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
|
-
|
|
1002
|
-
|
|
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
|
-
|
|
1005
|
-
const
|
|
1166
|
+
const layout = renderer.parseXlf(xlf);
|
|
1167
|
+
const widget = layout.regions[0].widgets[0];
|
|
1006
1168
|
|
|
1007
|
-
|
|
1008
|
-
|
|
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
|
});
|