@xiboplayer/core 0.1.0
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/CAMPAIGNS.md +254 -0
- package/README.md +163 -0
- package/TESTING_STATUS.md +281 -0
- package/TEST_STANDARDIZATION_COMPLETE.md +287 -0
- package/docs/ARCHITECTURE.md +714 -0
- package/docs/README.md +92 -0
- package/examples/dayparting-schedule-example.json +190 -0
- package/index.html +262 -0
- package/package.json +53 -0
- package/proxy.js +72 -0
- package/public/manifest.json +22 -0
- package/public/sw.js +218 -0
- package/setup.html +220 -0
- package/src/data-connectors.js +198 -0
- package/src/index.js +4 -0
- package/src/main.js +580 -0
- package/src/player-core.js +1120 -0
- package/src/player-core.test.js +1796 -0
- package/src/state.js +54 -0
- package/src/state.test.js +206 -0
- package/src/test-utils.js +217 -0
- package/src/xmds-test.html +109 -0
- package/src/xmds.test.js +516 -0
- package/vite.config.js +51 -0
- package/vitest.config.js +35 -0
package/src/xmds.test.js
ADDED
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XMDS Client Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for XMDS campaign parsing
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect } from 'vitest';
|
|
8
|
+
import { parseScheduleResponse } from '../../xmds/src/schedule-parser.js';
|
|
9
|
+
|
|
10
|
+
describe('Schedule Parsing', () => {
|
|
11
|
+
describe('Campaign Parsing', () => {
|
|
12
|
+
it('should parse schedule with campaigns', () => {
|
|
13
|
+
const xml = `
|
|
14
|
+
<schedule>
|
|
15
|
+
<default file="0"/>
|
|
16
|
+
<campaign id="5" priority="10" fromdt="2026-01-30 00:00:00" todt="2026-01-31 23:59:59" scheduleid="15">
|
|
17
|
+
<layout file="100"/>
|
|
18
|
+
<layout file="101"/>
|
|
19
|
+
<layout file="102"/>
|
|
20
|
+
</campaign>
|
|
21
|
+
<layout file="200" priority="5" fromdt="2026-01-30 00:00:00" todt="2026-01-31 23:59:59" scheduleid="20"/>
|
|
22
|
+
<campaign id="6" priority="8" fromdt="2026-01-30 00:00:00" todt="2026-01-31 23:59:59" scheduleid="25">
|
|
23
|
+
<layout file="300"/>
|
|
24
|
+
<layout file="301"/>
|
|
25
|
+
</campaign>
|
|
26
|
+
</schedule>
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
const schedule = parseScheduleResponse(xml);
|
|
30
|
+
|
|
31
|
+
// Check default
|
|
32
|
+
expect(schedule.default).toBe('0');
|
|
33
|
+
|
|
34
|
+
// Check campaigns
|
|
35
|
+
expect(schedule.campaigns).toHaveLength(2);
|
|
36
|
+
|
|
37
|
+
const campaign1 = schedule.campaigns[0];
|
|
38
|
+
expect(campaign1.id).toBe('5');
|
|
39
|
+
expect(campaign1.priority).toBe(10);
|
|
40
|
+
expect(campaign1.layouts).toHaveLength(3);
|
|
41
|
+
expect(campaign1.layouts[0].file).toBe('100');
|
|
42
|
+
expect(campaign1.layouts[1].file).toBe('101');
|
|
43
|
+
expect(campaign1.layouts[2].file).toBe('102');
|
|
44
|
+
expect(campaign1.layouts[0].priority).toBe(10);
|
|
45
|
+
expect(campaign1.layouts[0].campaignId).toBe('5');
|
|
46
|
+
|
|
47
|
+
const campaign2 = schedule.campaigns[1];
|
|
48
|
+
expect(campaign2.id).toBe('6');
|
|
49
|
+
expect(campaign2.priority).toBe(8);
|
|
50
|
+
expect(campaign2.layouts).toHaveLength(2);
|
|
51
|
+
|
|
52
|
+
// Check standalone layouts
|
|
53
|
+
expect(schedule.layouts).toHaveLength(1);
|
|
54
|
+
expect(schedule.layouts[0].file).toBe('200');
|
|
55
|
+
expect(schedule.layouts[0].priority).toBe(5);
|
|
56
|
+
expect(schedule.layouts[0].campaignId).toBeNull();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should parse schedule with only standalone layouts (backward compatible)', () => {
|
|
60
|
+
const xml = `
|
|
61
|
+
<schedule>
|
|
62
|
+
<default file="0"/>
|
|
63
|
+
<layout file="100" priority="10" fromdt="2026-01-30 00:00:00" todt="2026-01-31 23:59:59" scheduleid="10"/>
|
|
64
|
+
<layout file="101" priority="5" fromdt="2026-01-30 00:00:00" todt="2026-01-31 23:59:59" scheduleid="11"/>
|
|
65
|
+
</schedule>
|
|
66
|
+
`;
|
|
67
|
+
|
|
68
|
+
const schedule = parseScheduleResponse(xml);
|
|
69
|
+
|
|
70
|
+
expect(schedule.default).toBe('0');
|
|
71
|
+
expect(schedule.campaigns).toHaveLength(0);
|
|
72
|
+
expect(schedule.layouts).toHaveLength(2);
|
|
73
|
+
expect(schedule.layouts[0].file).toBe('100');
|
|
74
|
+
expect(schedule.layouts[0].priority).toBe(10);
|
|
75
|
+
expect(schedule.layouts[1].file).toBe('101');
|
|
76
|
+
expect(schedule.layouts[1].priority).toBe(5);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should parse empty schedule', () => {
|
|
80
|
+
const xml = `
|
|
81
|
+
<schedule>
|
|
82
|
+
<default file="999"/>
|
|
83
|
+
</schedule>
|
|
84
|
+
`;
|
|
85
|
+
|
|
86
|
+
const schedule = parseScheduleResponse(xml);
|
|
87
|
+
|
|
88
|
+
expect(schedule.default).toBe('999');
|
|
89
|
+
expect(schedule.campaigns).toHaveLength(0);
|
|
90
|
+
expect(schedule.layouts).toHaveLength(0);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('Campaign Layout Timing Inheritance', () => {
|
|
95
|
+
it('should allow layouts to inherit timing from campaign', () => {
|
|
96
|
+
const xml = `
|
|
97
|
+
<schedule>
|
|
98
|
+
<default file="0"/>
|
|
99
|
+
<campaign id="1" priority="10" fromdt="2026-01-30 00:00:00" todt="2026-01-31 23:59:59" scheduleid="5">
|
|
100
|
+
<layout file="100"/>
|
|
101
|
+
<layout file="101" fromdt="2026-01-30 12:00:00" todt="2026-01-30 18:00:00"/>
|
|
102
|
+
</campaign>
|
|
103
|
+
</schedule>
|
|
104
|
+
`;
|
|
105
|
+
|
|
106
|
+
const schedule = parseScheduleResponse(xml);
|
|
107
|
+
|
|
108
|
+
const campaign = schedule.campaigns[0];
|
|
109
|
+
|
|
110
|
+
// First layout inherits campaign timing
|
|
111
|
+
expect(campaign.layouts[0].fromdt).toBe('2026-01-30 00:00:00');
|
|
112
|
+
expect(campaign.layouts[0].todt).toBe('2026-01-31 23:59:59');
|
|
113
|
+
|
|
114
|
+
// Second layout has its own timing
|
|
115
|
+
expect(campaign.layouts[1].fromdt).toBe('2026-01-30 12:00:00');
|
|
116
|
+
expect(campaign.layouts[1].todt).toBe('2026-01-30 18:00:00');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should allow layouts to inherit priority from campaign', () => {
|
|
120
|
+
const xml = `
|
|
121
|
+
<schedule>
|
|
122
|
+
<default file="0"/>
|
|
123
|
+
<campaign id="1" priority="15" fromdt="2026-01-30 00:00:00" todt="2026-01-31 23:59:59">
|
|
124
|
+
<layout file="100"/>
|
|
125
|
+
<layout file="101"/>
|
|
126
|
+
</campaign>
|
|
127
|
+
</schedule>
|
|
128
|
+
`;
|
|
129
|
+
|
|
130
|
+
const schedule = parseScheduleResponse(xml);
|
|
131
|
+
|
|
132
|
+
const campaign = schedule.campaigns[0];
|
|
133
|
+
|
|
134
|
+
expect(campaign.layouts[0].priority).toBe(15);
|
|
135
|
+
expect(campaign.layouts[1].priority).toBe(15);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should associate layouts with their campaign ID', () => {
|
|
139
|
+
const xml = `
|
|
140
|
+
<schedule>
|
|
141
|
+
<default file="0"/>
|
|
142
|
+
<campaign id="42" priority="10" fromdt="2026-01-30 00:00:00" todt="2026-01-31 23:59:59">
|
|
143
|
+
<layout file="100"/>
|
|
144
|
+
<layout file="101"/>
|
|
145
|
+
</campaign>
|
|
146
|
+
<layout file="200" priority="5" fromdt="2026-01-30 00:00:00" todt="2026-01-31 23:59:59"/>
|
|
147
|
+
</schedule>
|
|
148
|
+
`;
|
|
149
|
+
|
|
150
|
+
const schedule = parseScheduleResponse(xml);
|
|
151
|
+
|
|
152
|
+
// Campaign layouts should have campaignId
|
|
153
|
+
expect(schedule.campaigns[0].layouts[0].campaignId).toBe('42');
|
|
154
|
+
expect(schedule.campaigns[0].layouts[1].campaignId).toBe('42');
|
|
155
|
+
|
|
156
|
+
// Standalone layout should not have campaignId
|
|
157
|
+
expect(schedule.layouts[0].campaignId).toBeNull();
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe('Schedule ID Parsing', () => {
|
|
162
|
+
it('should parse scheduleid attribute', () => {
|
|
163
|
+
const xml = `
|
|
164
|
+
<schedule>
|
|
165
|
+
<default file="0"/>
|
|
166
|
+
<layout file="100" priority="10" fromdt="2026-01-30 00:00:00" todt="2026-01-31 23:59:59" scheduleid="123"/>
|
|
167
|
+
<campaign id="1" priority="10" fromdt="2026-01-30 00:00:00" todt="2026-01-31 23:59:59" scheduleid="456">
|
|
168
|
+
<layout file="200"/>
|
|
169
|
+
</campaign>
|
|
170
|
+
</schedule>
|
|
171
|
+
`;
|
|
172
|
+
|
|
173
|
+
const schedule = parseScheduleResponse(xml);
|
|
174
|
+
|
|
175
|
+
expect(schedule.layouts[0].scheduleid).toBe('123');
|
|
176
|
+
expect(schedule.campaigns[0].scheduleid).toBe('456');
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe('Action Parsing', () => {
|
|
181
|
+
it('should parse action elements from schedule', () => {
|
|
182
|
+
const xml = `
|
|
183
|
+
<schedule>
|
|
184
|
+
<default file="0"/>
|
|
185
|
+
<actions>
|
|
186
|
+
<action actionType="navLayout" triggerCode="tc1" layoutCode="42" fromdt="2026-01-01 00:00:00" todt="2030-12-31 23:59:59" priority="5" scheduleid="10"/>
|
|
187
|
+
</actions>
|
|
188
|
+
</schedule>
|
|
189
|
+
`;
|
|
190
|
+
|
|
191
|
+
const schedule = parseScheduleResponse(xml);
|
|
192
|
+
|
|
193
|
+
expect(schedule.actions).toHaveLength(1);
|
|
194
|
+
expect(schedule.actions[0].actionType).toBe('navLayout');
|
|
195
|
+
expect(schedule.actions[0].triggerCode).toBe('tc1');
|
|
196
|
+
expect(schedule.actions[0].layoutCode).toBe('42');
|
|
197
|
+
expect(schedule.actions[0].fromDt).toBe('2026-01-01 00:00:00');
|
|
198
|
+
expect(schedule.actions[0].toDt).toBe('2030-12-31 23:59:59');
|
|
199
|
+
expect(schedule.actions[0].priority).toBe(5);
|
|
200
|
+
expect(schedule.actions[0].scheduleId).toBe('10');
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('should parse multiple actions', () => {
|
|
204
|
+
const xml = `
|
|
205
|
+
<schedule>
|
|
206
|
+
<default file="0"/>
|
|
207
|
+
<actions>
|
|
208
|
+
<action actionType="navLayout" triggerCode="tc1" layoutCode="42" fromdt="2026-01-01 00:00:00" todt="2030-12-31 23:59:59" priority="1" scheduleid="1"/>
|
|
209
|
+
<action actionType="command" triggerCode="tc2" commandCode="restart" fromdt="2026-02-01 00:00:00" todt="2026-12-31 23:59:59" priority="10" scheduleid="2"/>
|
|
210
|
+
<action actionType="navigateToWidget" triggerCode="tc3" layoutCode="99" fromdt="2026-01-01 00:00:00" todt="2027-01-01 00:00:00" priority="3" scheduleid="3"/>
|
|
211
|
+
</actions>
|
|
212
|
+
</schedule>
|
|
213
|
+
`;
|
|
214
|
+
|
|
215
|
+
const schedule = parseScheduleResponse(xml);
|
|
216
|
+
|
|
217
|
+
expect(schedule.actions).toHaveLength(3);
|
|
218
|
+
expect(schedule.actions[0].triggerCode).toBe('tc1');
|
|
219
|
+
expect(schedule.actions[1].triggerCode).toBe('tc2');
|
|
220
|
+
expect(schedule.actions[1].commandCode).toBe('restart');
|
|
221
|
+
expect(schedule.actions[2].triggerCode).toBe('tc3');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('should parse action with geoLocation attributes', () => {
|
|
225
|
+
const xml = `
|
|
226
|
+
<schedule>
|
|
227
|
+
<default file="0"/>
|
|
228
|
+
<actions>
|
|
229
|
+
<action actionType="navLayout" triggerCode="geo1" layoutCode="50" fromdt="2026-01-01 00:00:00" todt="2030-12-31 23:59:59" isGeoAware="1" geoLocation="41.3851,2.1734" scheduleid="5"/>
|
|
230
|
+
</actions>
|
|
231
|
+
</schedule>
|
|
232
|
+
`;
|
|
233
|
+
|
|
234
|
+
const schedule = parseScheduleResponse(xml);
|
|
235
|
+
|
|
236
|
+
expect(schedule.actions[0].isGeoAware).toBe(true);
|
|
237
|
+
expect(schedule.actions[0].geoLocation).toBe('41.3851,2.1734');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('should initialize actions array as empty when no actions element', () => {
|
|
241
|
+
const xml = `
|
|
242
|
+
<schedule>
|
|
243
|
+
<default file="0"/>
|
|
244
|
+
<layout file="100" priority="10" fromdt="2026-01-30 00:00:00" todt="2026-01-31 23:59:59" scheduleid="1"/>
|
|
245
|
+
</schedule>
|
|
246
|
+
`;
|
|
247
|
+
|
|
248
|
+
const schedule = parseScheduleResponse(xml);
|
|
249
|
+
|
|
250
|
+
expect(schedule.actions).toEqual([]);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
describe('Command Parsing', () => {
|
|
255
|
+
it('should parse command elements from schedule', () => {
|
|
256
|
+
const xml = `
|
|
257
|
+
<schedule>
|
|
258
|
+
<default file="0"/>
|
|
259
|
+
<command command="collectNow" date="2026-01-01"/>
|
|
260
|
+
</schedule>
|
|
261
|
+
`;
|
|
262
|
+
|
|
263
|
+
const schedule = parseScheduleResponse(xml);
|
|
264
|
+
|
|
265
|
+
expect(schedule.commands).toHaveLength(1);
|
|
266
|
+
expect(schedule.commands[0].code).toBe('collectNow');
|
|
267
|
+
expect(schedule.commands[0].date).toBe('2026-01-01');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('should parse multiple commands', () => {
|
|
271
|
+
const xml = `
|
|
272
|
+
<schedule>
|
|
273
|
+
<default file="0"/>
|
|
274
|
+
<command command="collectNow" date="2026-02-11"/>
|
|
275
|
+
<command command="reboot" date="2026-02-12"/>
|
|
276
|
+
</schedule>
|
|
277
|
+
`;
|
|
278
|
+
|
|
279
|
+
const schedule = parseScheduleResponse(xml);
|
|
280
|
+
|
|
281
|
+
expect(schedule.commands).toHaveLength(2);
|
|
282
|
+
expect(schedule.commands[0].code).toBe('collectNow');
|
|
283
|
+
expect(schedule.commands[0].date).toBe('2026-02-11');
|
|
284
|
+
expect(schedule.commands[1].code).toBe('reboot');
|
|
285
|
+
expect(schedule.commands[1].date).toBe('2026-02-12');
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('should initialize commands array as empty when no command elements', () => {
|
|
289
|
+
const xml = `
|
|
290
|
+
<schedule>
|
|
291
|
+
<default file="0"/>
|
|
292
|
+
</schedule>
|
|
293
|
+
`;
|
|
294
|
+
|
|
295
|
+
const schedule = parseScheduleResponse(xml);
|
|
296
|
+
|
|
297
|
+
expect(schedule.commands).toEqual([]);
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
describe('Criteria Parsing', () => {
|
|
302
|
+
it('should parse criteria from standalone layout', () => {
|
|
303
|
+
const xml = `
|
|
304
|
+
<schedule>
|
|
305
|
+
<default file="0"/>
|
|
306
|
+
<layout file="100" priority="5" fromdt="2026-01-30 00:00:00" todt="2026-01-31 23:59:59" scheduleid="1">
|
|
307
|
+
<criteria metric="dayOfWeek" condition="equals" type="string">Monday</criteria>
|
|
308
|
+
<criteria metric="temperature" condition="greaterThan" type="number">25</criteria>
|
|
309
|
+
</layout>
|
|
310
|
+
</schedule>
|
|
311
|
+
`;
|
|
312
|
+
|
|
313
|
+
const schedule = parseScheduleResponse(xml);
|
|
314
|
+
|
|
315
|
+
expect(schedule.layouts).toHaveLength(1);
|
|
316
|
+
expect(schedule.layouts[0].criteria).toHaveLength(2);
|
|
317
|
+
expect(schedule.layouts[0].criteria[0]).toEqual({
|
|
318
|
+
metric: 'dayOfWeek',
|
|
319
|
+
condition: 'equals',
|
|
320
|
+
type: 'string',
|
|
321
|
+
value: 'Monday'
|
|
322
|
+
});
|
|
323
|
+
expect(schedule.layouts[0].criteria[1]).toEqual({
|
|
324
|
+
metric: 'temperature',
|
|
325
|
+
condition: 'greaterThan',
|
|
326
|
+
type: 'number',
|
|
327
|
+
value: '25'
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('should parse criteria from campaign layout', () => {
|
|
332
|
+
const xml = `
|
|
333
|
+
<schedule>
|
|
334
|
+
<default file="0"/>
|
|
335
|
+
<campaign id="1" priority="10" fromdt="2026-01-30 00:00:00" todt="2026-01-31 23:59:59" scheduleid="2">
|
|
336
|
+
<layout file="200">
|
|
337
|
+
<criteria metric="displayProperty" condition="contains" type="string">building-A</criteria>
|
|
338
|
+
</layout>
|
|
339
|
+
</campaign>
|
|
340
|
+
</schedule>
|
|
341
|
+
`;
|
|
342
|
+
|
|
343
|
+
const schedule = parseScheduleResponse(xml);
|
|
344
|
+
|
|
345
|
+
expect(schedule.campaigns[0].layouts[0].criteria).toHaveLength(1);
|
|
346
|
+
expect(schedule.campaigns[0].layouts[0].criteria[0].metric).toBe('displayProperty');
|
|
347
|
+
expect(schedule.campaigns[0].layouts[0].criteria[0].value).toBe('building-A');
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('should parse criteria from overlay', () => {
|
|
351
|
+
const xml = `
|
|
352
|
+
<schedule>
|
|
353
|
+
<default file="0"/>
|
|
354
|
+
<overlays>
|
|
355
|
+
<overlay file="300" duration="30" fromdt="2026-01-01 00:00:00" todt="2026-12-31 23:59:59" priority="5" scheduleid="3">
|
|
356
|
+
<criteria metric="timeOfDay" condition="between" type="string">09:00-17:00</criteria>
|
|
357
|
+
</overlay>
|
|
358
|
+
</overlays>
|
|
359
|
+
</schedule>
|
|
360
|
+
`;
|
|
361
|
+
|
|
362
|
+
const schedule = parseScheduleResponse(xml);
|
|
363
|
+
|
|
364
|
+
expect(schedule.overlays[0].criteria).toHaveLength(1);
|
|
365
|
+
expect(schedule.overlays[0].criteria[0].metric).toBe('timeOfDay');
|
|
366
|
+
expect(schedule.overlays[0].criteria[0].condition).toBe('between');
|
|
367
|
+
expect(schedule.overlays[0].criteria[0].value).toBe('09:00-17:00');
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('should return empty criteria array when no criteria elements', () => {
|
|
371
|
+
const xml = `
|
|
372
|
+
<schedule>
|
|
373
|
+
<default file="0"/>
|
|
374
|
+
<layout file="100" priority="5" fromdt="2026-01-30 00:00:00" todt="2026-01-31 23:59:59" scheduleid="1"/>
|
|
375
|
+
</schedule>
|
|
376
|
+
`;
|
|
377
|
+
|
|
378
|
+
const schedule = parseScheduleResponse(xml);
|
|
379
|
+
|
|
380
|
+
expect(schedule.layouts[0].criteria).toEqual([]);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('should parse geoLocation and isGeoAware from layouts', () => {
|
|
384
|
+
const xml = `
|
|
385
|
+
<schedule>
|
|
386
|
+
<default file="0"/>
|
|
387
|
+
<layout file="100" priority="5" fromdt="2026-01-30 00:00:00" todt="2026-01-31 23:59:59" scheduleid="1" isGeoAware="1" geoLocation="41.3851,2.1734"/>
|
|
388
|
+
</schedule>
|
|
389
|
+
`;
|
|
390
|
+
|
|
391
|
+
const schedule = parseScheduleResponse(xml);
|
|
392
|
+
|
|
393
|
+
expect(schedule.layouts[0].isGeoAware).toBe(true);
|
|
394
|
+
expect(schedule.layouts[0].geoLocation).toBe('41.3851,2.1734');
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('should default isGeoAware to false and geoLocation to empty', () => {
|
|
398
|
+
const xml = `
|
|
399
|
+
<schedule>
|
|
400
|
+
<default file="0"/>
|
|
401
|
+
<layout file="100" priority="5" fromdt="2026-01-30 00:00:00" todt="2026-01-31 23:59:59" scheduleid="1"/>
|
|
402
|
+
</schedule>
|
|
403
|
+
`;
|
|
404
|
+
|
|
405
|
+
const schedule = parseScheduleResponse(xml);
|
|
406
|
+
|
|
407
|
+
expect(schedule.layouts[0].isGeoAware).toBe(false);
|
|
408
|
+
expect(schedule.layouts[0].geoLocation).toBe('');
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
describe('Sync Event Parsing', () => {
|
|
413
|
+
it('should parse syncEvent from standalone layout', () => {
|
|
414
|
+
const xml = `
|
|
415
|
+
<schedule>
|
|
416
|
+
<default file="0"/>
|
|
417
|
+
<layout file="100" priority="5" fromdt="2026-01-30 00:00:00" todt="2026-01-31 23:59:59" scheduleid="1" syncEvent="1"/>
|
|
418
|
+
<layout file="200" priority="5" fromdt="2026-01-30 00:00:00" todt="2026-01-31 23:59:59" scheduleid="2" syncEvent="0"/>
|
|
419
|
+
</schedule>
|
|
420
|
+
`;
|
|
421
|
+
|
|
422
|
+
const schedule = parseScheduleResponse(xml);
|
|
423
|
+
|
|
424
|
+
expect(schedule.layouts[0].syncEvent).toBe(true);
|
|
425
|
+
expect(schedule.layouts[1].syncEvent).toBe(false);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it('should parse syncEvent from campaign layout', () => {
|
|
429
|
+
const xml = `
|
|
430
|
+
<schedule>
|
|
431
|
+
<default file="0"/>
|
|
432
|
+
<campaign id="1" priority="10" fromdt="2026-01-30 00:00:00" todt="2026-01-31 23:59:59" scheduleid="5">
|
|
433
|
+
<layout file="100" syncEvent="1"/>
|
|
434
|
+
<layout file="101" syncEvent="0"/>
|
|
435
|
+
</campaign>
|
|
436
|
+
</schedule>
|
|
437
|
+
`;
|
|
438
|
+
|
|
439
|
+
const schedule = parseScheduleResponse(xml);
|
|
440
|
+
|
|
441
|
+
expect(schedule.campaigns[0].layouts[0].syncEvent).toBe(true);
|
|
442
|
+
expect(schedule.campaigns[0].layouts[1].syncEvent).toBe(false);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it('should parse syncEvent from overlay', () => {
|
|
446
|
+
const xml = `
|
|
447
|
+
<schedule>
|
|
448
|
+
<default file="0"/>
|
|
449
|
+
<overlays>
|
|
450
|
+
<overlay file="300" duration="30" fromdt="2026-01-01 00:00:00" todt="2026-12-31 23:59:59" priority="5" scheduleid="3" syncEvent="1"/>
|
|
451
|
+
</overlays>
|
|
452
|
+
</schedule>
|
|
453
|
+
`;
|
|
454
|
+
|
|
455
|
+
const schedule = parseScheduleResponse(xml);
|
|
456
|
+
|
|
457
|
+
expect(schedule.overlays[0].syncEvent).toBe(true);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it('should default syncEvent to false when not present', () => {
|
|
461
|
+
const xml = `
|
|
462
|
+
<schedule>
|
|
463
|
+
<default file="0"/>
|
|
464
|
+
<layout file="100" priority="5" fromdt="2026-01-30 00:00:00" todt="2026-01-31 23:59:59" scheduleid="1"/>
|
|
465
|
+
</schedule>
|
|
466
|
+
`;
|
|
467
|
+
|
|
468
|
+
const schedule = parseScheduleResponse(xml);
|
|
469
|
+
|
|
470
|
+
expect(schedule.layouts[0].syncEvent).toBe(false);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it('should parse shareOfVoice from layouts', () => {
|
|
474
|
+
const xml = `
|
|
475
|
+
<schedule>
|
|
476
|
+
<default file="0"/>
|
|
477
|
+
<layout file="100" priority="5" fromdt="2026-01-30 00:00:00" todt="2026-01-31 23:59:59" scheduleid="1" shareOfVoice="30"/>
|
|
478
|
+
<layout file="200" priority="5" fromdt="2026-01-30 00:00:00" todt="2026-01-31 23:59:59" scheduleid="2"/>
|
|
479
|
+
</schedule>
|
|
480
|
+
`;
|
|
481
|
+
|
|
482
|
+
const schedule = parseScheduleResponse(xml);
|
|
483
|
+
|
|
484
|
+
expect(schedule.layouts[0].shareOfVoice).toBe(30);
|
|
485
|
+
expect(schedule.layouts[1].shareOfVoice).toBe(0);
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
describe('Actions and Commands Together', () => {
|
|
490
|
+
it('should parse both actions and commands in same schedule', () => {
|
|
491
|
+
const xml = `
|
|
492
|
+
<schedule>
|
|
493
|
+
<default file="0"/>
|
|
494
|
+
<layout file="100" priority="5" fromdt="2026-01-30 00:00:00" todt="2026-01-31 23:59:59" scheduleid="1"/>
|
|
495
|
+
<campaign id="1" priority="10" fromdt="2026-01-30 00:00:00" todt="2026-01-31 23:59:59" scheduleid="2">
|
|
496
|
+
<layout file="200"/>
|
|
497
|
+
</campaign>
|
|
498
|
+
<actions>
|
|
499
|
+
<action actionType="navLayout" triggerCode="tc1" layoutCode="42" fromdt="2026-01-01 00:00:00" todt="2030-12-31 23:59:59" priority="1" scheduleid="10"/>
|
|
500
|
+
</actions>
|
|
501
|
+
<command command="collectNow" date="2026-02-11"/>
|
|
502
|
+
</schedule>
|
|
503
|
+
`;
|
|
504
|
+
|
|
505
|
+
const schedule = parseScheduleResponse(xml);
|
|
506
|
+
|
|
507
|
+
expect(schedule.default).toBe('0');
|
|
508
|
+
expect(schedule.layouts).toHaveLength(1);
|
|
509
|
+
expect(schedule.campaigns).toHaveLength(1);
|
|
510
|
+
expect(schedule.actions).toHaveLength(1);
|
|
511
|
+
expect(schedule.actions[0].triggerCode).toBe('tc1');
|
|
512
|
+
expect(schedule.commands).toHaveLength(1);
|
|
513
|
+
expect(schedule.commands[0].code).toBe('collectNow');
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
});
|
package/vite.config.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import { copyFileSync, mkdirSync, existsSync } from 'fs';
|
|
3
|
+
import { join, dirname } from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = dirname(__filename);
|
|
8
|
+
|
|
9
|
+
export default defineConfig({
|
|
10
|
+
base: '/player/',
|
|
11
|
+
build: {
|
|
12
|
+
outDir: 'dist',
|
|
13
|
+
rollupOptions: {
|
|
14
|
+
input: {
|
|
15
|
+
main: './index.html',
|
|
16
|
+
setup: './setup.html'
|
|
17
|
+
},
|
|
18
|
+
output: {
|
|
19
|
+
manualChunks: {
|
|
20
|
+
'pdfjs': ['pdfjs-dist']
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
server: {
|
|
26
|
+
port: 5173,
|
|
27
|
+
strictPort: false
|
|
28
|
+
},
|
|
29
|
+
plugins: [
|
|
30
|
+
{
|
|
31
|
+
name: 'copy-pdfjs-worker',
|
|
32
|
+
closeBundle() {
|
|
33
|
+
// Copy PDF.js worker to dist after build
|
|
34
|
+
try {
|
|
35
|
+
// Try local node_modules first, then parent (workspace root)
|
|
36
|
+
let workerSrc = join(__dirname, 'node_modules', 'pdfjs-dist', 'build', 'pdf.worker.min.mjs');
|
|
37
|
+
if (!existsSync(workerSrc)) {
|
|
38
|
+
workerSrc = join(__dirname, '..', '..', 'node_modules', 'pdfjs-dist', 'build', 'pdf.worker.min.mjs');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const workerDest = join(__dirname, 'dist', 'pdf.worker.min.mjs');
|
|
42
|
+
mkdirSync(dirname(workerDest), { recursive: true });
|
|
43
|
+
copyFileSync(workerSrc, workerDest);
|
|
44
|
+
console.log('✓ Copied PDF.js worker to dist/');
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.warn('⚠ Could not copy PDF.js worker:', error.message);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
});
|
package/vitest.config.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
// Environment
|
|
6
|
+
environment: 'jsdom', // Browser-like environment
|
|
7
|
+
|
|
8
|
+
// Test files
|
|
9
|
+
include: ['src/**/*.test.js'],
|
|
10
|
+
exclude: ['node_modules', 'dist'],
|
|
11
|
+
|
|
12
|
+
// Coverage
|
|
13
|
+
coverage: {
|
|
14
|
+
provider: 'v8',
|
|
15
|
+
reporter: ['text', 'json', 'html'],
|
|
16
|
+
include: ['src/**/*.js'],
|
|
17
|
+
exclude: ['src/**/*.test.js', 'src/**/*.spec.js'],
|
|
18
|
+
thresholds: {
|
|
19
|
+
lines: 80,
|
|
20
|
+
functions: 80,
|
|
21
|
+
branches: 75,
|
|
22
|
+
statements: 80
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
// Globals
|
|
27
|
+
globals: true,
|
|
28
|
+
|
|
29
|
+
// Timeout
|
|
30
|
+
testTimeout: 10000,
|
|
31
|
+
|
|
32
|
+
// Reporters
|
|
33
|
+
reporters: ['verbose']
|
|
34
|
+
}
|
|
35
|
+
});
|