@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
|
@@ -6,12 +6,11 @@
|
|
|
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;
|
|
13
13
|
let renderer;
|
|
14
|
-
let mockGetMediaUrl;
|
|
15
14
|
let mockGetWidgetHtml;
|
|
16
15
|
|
|
17
16
|
beforeEach(() => {
|
|
@@ -29,7 +28,6 @@ describe('RendererLite', () => {
|
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
// Mock callbacks
|
|
32
|
-
mockGetMediaUrl = vi.fn((fileId) => Promise.resolve(`blob://test-${fileId}`));
|
|
33
31
|
mockGetWidgetHtml = vi.fn((widget) => Promise.resolve(`<html>Widget ${widget.id}</html>`));
|
|
34
32
|
|
|
35
33
|
// Create renderer instance
|
|
@@ -37,7 +35,7 @@ describe('RendererLite', () => {
|
|
|
37
35
|
{ cmsUrl: 'https://test.com', hardwareKey: 'test-key' },
|
|
38
36
|
container,
|
|
39
37
|
{
|
|
40
|
-
|
|
38
|
+
fileIdToSaveAs: new Map(),
|
|
41
39
|
getWidgetHtml: mockGetWidgetHtml
|
|
42
40
|
}
|
|
43
41
|
);
|
|
@@ -415,7 +413,6 @@ describe('RendererLite', () => {
|
|
|
415
413
|
expect(element.className).toBe('renderer-lite-widget');
|
|
416
414
|
expect(element.style.width).toBe('100%');
|
|
417
415
|
expect(element.style.height).toBe('100%');
|
|
418
|
-
expect(mockGetMediaUrl).toHaveBeenCalledWith(1);
|
|
419
416
|
});
|
|
420
417
|
|
|
421
418
|
it('should default to objectFit contain and objectPosition center center', async () => {
|
|
@@ -566,7 +563,6 @@ describe('RendererLite', () => {
|
|
|
566
563
|
expect(element.muted).toBe(true);
|
|
567
564
|
// loop is intentionally false - handled manually via 'ended' event to avoid black frames
|
|
568
565
|
expect(element.loop).toBe(false);
|
|
569
|
-
expect(mockGetMediaUrl).toHaveBeenCalledWith(5);
|
|
570
566
|
});
|
|
571
567
|
|
|
572
568
|
it('should create text widget with iframe (blob fallback)', async () => {
|
|
@@ -968,66 +964,222 @@ describe('RendererLite', () => {
|
|
|
968
964
|
});
|
|
969
965
|
|
|
970
966
|
describe('Transitions', () => {
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
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
|
+
});
|
|
975
983
|
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
};
|
|
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);
|
|
981
995
|
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
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);
|
|
985
1010
|
|
|
986
|
-
expect(
|
|
987
|
-
expect(
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
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
|
|
992
1018
|
);
|
|
1019
|
+
|
|
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');
|
|
993
1024
|
});
|
|
994
1025
|
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1026
|
+
it('should apply fly in from East', () => {
|
|
1027
|
+
Transitions.apply(
|
|
1028
|
+
element, { type: 'flyIn', duration: 500, direction: 'E' }, true, 1920, 1080
|
|
1029
|
+
);
|
|
1030
|
+
|
|
1031
|
+
expect(capturedKeyframes[0].transform).toBe('translate(1920px, 0px)');
|
|
1032
|
+
expect(capturedKeyframes[1].transform).toBe('translate(0, 0)');
|
|
1033
|
+
});
|
|
998
1034
|
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
duration:
|
|
1002
|
-
|
|
1003
|
-
};
|
|
1035
|
+
it('should apply fly in from South', () => {
|
|
1036
|
+
Transitions.apply(
|
|
1037
|
+
element, { type: 'flyIn', duration: 500, direction: 'S' }, true, 1920, 1080
|
|
1038
|
+
);
|
|
1004
1039
|
|
|
1005
|
-
|
|
1006
|
-
|
|
1040
|
+
expect(capturedKeyframes[0].transform).toBe('translate(0px, 1080px)');
|
|
1041
|
+
});
|
|
1007
1042
|
|
|
1008
|
-
|
|
1009
|
-
|
|
1043
|
+
it('should apply fly in from West', () => {
|
|
1044
|
+
Transitions.apply(
|
|
1045
|
+
element, { type: 'flyIn', duration: 500, direction: 'W' }, true, 1920, 1080
|
|
1046
|
+
);
|
|
1010
1047
|
|
|
1011
|
-
|
|
1012
|
-
expect(keyframes[1].transform).toContain('1080px'); // Height offset
|
|
1048
|
+
expect(capturedKeyframes[0].transform).toBe('translate(-1920px, 0px)');
|
|
1013
1049
|
});
|
|
1014
|
-
});
|
|
1015
1050
|
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
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)');
|
|
1020
1055
|
|
|
1021
|
-
|
|
1022
|
-
|
|
1056
|
+
// SE
|
|
1057
|
+
Transitions.apply(element, { type: 'flyIn', duration: 500, direction: 'SE' }, true, 1920, 1080);
|
|
1058
|
+
expect(capturedKeyframes[0].transform).toBe('translate(1920px, 1080px)');
|
|
1023
1059
|
|
|
1024
|
-
//
|
|
1025
|
-
|
|
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
|
+
`;
|
|
1026
1165
|
|
|
1027
|
-
|
|
1028
|
-
|
|
1166
|
+
const layout = renderer.parseXlf(xlf);
|
|
1167
|
+
const widget = layout.regions[0].widgets[0];
|
|
1168
|
+
|
|
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
|
+
});
|
|
1029
1179
|
});
|
|
1180
|
+
});
|
|
1030
1181
|
|
|
1182
|
+
describe('Memory Management', () => {
|
|
1031
1183
|
it('should clear regions on stopCurrentLayout', async () => {
|
|
1032
1184
|
const xlf = `
|
|
1033
1185
|
<layout>
|
|
@@ -1111,8 +1263,22 @@ describe('RendererLite', () => {
|
|
|
1111
1263
|
});
|
|
1112
1264
|
});
|
|
1113
1265
|
|
|
1114
|
-
describe('
|
|
1115
|
-
it('should
|
|
1266
|
+
describe('Media URL construction via fileIdToSaveAs', () => {
|
|
1267
|
+
it('should construct media URLs using fileIdToSaveAs map', async () => {
|
|
1268
|
+
const fileIdToSaveAs = new Map([
|
|
1269
|
+
['1', '1.png'],
|
|
1270
|
+
['5', '5.mp4'],
|
|
1271
|
+
['7', '7.png']
|
|
1272
|
+
]);
|
|
1273
|
+
const r = new RendererLite(
|
|
1274
|
+
{ cmsUrl: 'https://test.com', hardwareKey: 'test-key' },
|
|
1275
|
+
container,
|
|
1276
|
+
{
|
|
1277
|
+
fileIdToSaveAs,
|
|
1278
|
+
getWidgetHtml: mockGetWidgetHtml
|
|
1279
|
+
}
|
|
1280
|
+
);
|
|
1281
|
+
|
|
1116
1282
|
const xlf = `
|
|
1117
1283
|
<layout>
|
|
1118
1284
|
<region id="r1">
|
|
@@ -1129,16 +1295,11 @@ describe('RendererLite', () => {
|
|
|
1129
1295
|
</layout>
|
|
1130
1296
|
`;
|
|
1131
1297
|
|
|
1132
|
-
await
|
|
1133
|
-
|
|
1134
|
-
// All media URLs should have been fetched
|
|
1135
|
-
expect(mockGetMediaUrl).toHaveBeenCalledTimes(3);
|
|
1136
|
-
expect(mockGetMediaUrl).toHaveBeenCalledWith(1);
|
|
1137
|
-
expect(mockGetMediaUrl).toHaveBeenCalledWith(5);
|
|
1138
|
-
expect(mockGetMediaUrl).toHaveBeenCalledWith(7);
|
|
1298
|
+
await r.renderLayout(xlf, 1);
|
|
1139
1299
|
|
|
1140
|
-
//
|
|
1141
|
-
expect(
|
|
1300
|
+
// fileIdToSaveAs should have all 3 entries
|
|
1301
|
+
expect(fileIdToSaveAs.size).toBe(3);
|
|
1302
|
+
r.cleanup();
|
|
1142
1303
|
});
|
|
1143
1304
|
});
|
|
1144
1305
|
|
|
@@ -1798,6 +1959,57 @@ describe('RendererLite', () => {
|
|
|
1798
1959
|
|
|
1799
1960
|
vi.useRealTimers();
|
|
1800
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
|
+
});
|
|
1801
2013
|
});
|
|
1802
2014
|
|
|
1803
2015
|
describe('Sub-Playlist (#10)', () => {
|
|
@@ -1906,6 +2118,65 @@ describe('RendererLite', () => {
|
|
|
1906
2118
|
expect(ids.some(id => id.startsWith('a'))).toBe(true);
|
|
1907
2119
|
expect(ids.some(id => id.startsWith('b'))).toBe(true);
|
|
1908
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
|
+
});
|
|
1909
2180
|
});
|
|
1910
2181
|
|
|
1911
2182
|
// ── Medium-Priority Spec Compliance ────────────────────────────────
|
|
@@ -2247,4 +2518,184 @@ describe('RendererLite', () => {
|
|
|
2247
2518
|
expect(layout.regions[0].widgets[0].commands).toEqual([]);
|
|
2248
2519
|
});
|
|
2249
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
|
+
});
|
|
2250
2701
|
});
|
package/vitest.config.js
CHANGED
|
@@ -4,5 +4,14 @@ export default defineConfig({
|
|
|
4
4
|
test: {
|
|
5
5
|
environment: 'jsdom',
|
|
6
6
|
globals: true
|
|
7
|
+
},
|
|
8
|
+
resolve: {
|
|
9
|
+
alias: {
|
|
10
|
+
// hls.js is an optional runtime dependency (dynamic import in renderVideo).
|
|
11
|
+
// Alias to the monorepo mock so renderer tests work standalone.
|
|
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
|
|
15
|
+
}
|
|
7
16
|
}
|
|
8
17
|
});
|