@xiboplayer/xmds 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/docs/README.md +60 -0
- package/package.json +36 -0
- package/src/index.js +4 -0
- package/src/rest-client.js +342 -0
- package/src/schedule-parser.js +172 -0
- package/src/xmds-client.js +373 -0
- package/src/xmds.overlays.test.js +170 -0
- package/src/xmds.rest.integration.test.js +659 -0
- package/src/xmds.rest.test.js +676 -0
- package/src/xmds.test.js +831 -0
- package/vitest.config.js +8 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XMDS SOAP transport client for Xibo CMS.
|
|
3
|
+
*
|
|
4
|
+
* Uses the traditional SOAP/XML endpoint (xmds.php) for full protocol
|
|
5
|
+
* compatibility with all Xibo CMS versions.
|
|
6
|
+
*
|
|
7
|
+
* Protocol: https://github.com/linuxnow/xibo_players_docs
|
|
8
|
+
*/
|
|
9
|
+
import { createLogger, fetchWithRetry } from '@xiboplayer/utils';
|
|
10
|
+
import { parseScheduleResponse } from './schedule-parser.js';
|
|
11
|
+
|
|
12
|
+
const log = createLogger('XMDS');
|
|
13
|
+
|
|
14
|
+
export class XmdsClient {
|
|
15
|
+
constructor(config) {
|
|
16
|
+
this.config = config;
|
|
17
|
+
this.schemaVersion = 5;
|
|
18
|
+
this.retryOptions = config.retryOptions || { maxRetries: 2, baseDelayMs: 2000 };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ─── SOAP transport helpers ─────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Build SOAP envelope for a given method and parameters
|
|
25
|
+
*/
|
|
26
|
+
buildEnvelope(method, params) {
|
|
27
|
+
const paramElements = Object.entries(params)
|
|
28
|
+
.map(([key, value]) => {
|
|
29
|
+
const escaped = String(value)
|
|
30
|
+
.replace(/&/g, '&')
|
|
31
|
+
.replace(/</g, '<')
|
|
32
|
+
.replace(/>/g, '>')
|
|
33
|
+
.replace(/"/g, '"');
|
|
34
|
+
return `<${key} xsi:type="xsd:string">${escaped}</${key}>`;
|
|
35
|
+
})
|
|
36
|
+
.join('\n ');
|
|
37
|
+
|
|
38
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
39
|
+
<soap:Envelope
|
|
40
|
+
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
|
|
41
|
+
xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"
|
|
42
|
+
xmlns:tns="urn:xmds"
|
|
43
|
+
xmlns:types="urn:xmds/encodedTypes"
|
|
44
|
+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
45
|
+
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
|
46
|
+
<soap:Body soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
|
|
47
|
+
<tns:${method}>
|
|
48
|
+
${paramElements}
|
|
49
|
+
</tns:${method}>
|
|
50
|
+
</soap:Body>
|
|
51
|
+
</soap:Envelope>`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Rewrite XMDS URL for Electron proxy.
|
|
56
|
+
* If running inside the Electron shell, use the local proxy to avoid CORS.
|
|
57
|
+
* Detection: preload.js exposes window.electronAPI.isElectron = true,
|
|
58
|
+
* or fallback to checking localhost:8765 (default Electron server port).
|
|
59
|
+
*/
|
|
60
|
+
rewriteXmdsUrl(cmsUrl) {
|
|
61
|
+
if (typeof window !== 'undefined' &&
|
|
62
|
+
(window.electronAPI?.isElectron ||
|
|
63
|
+
(window.location.hostname === 'localhost' && window.location.port === '8765'))) {
|
|
64
|
+
const encodedCmsUrl = encodeURIComponent(cmsUrl);
|
|
65
|
+
return `/xmds-proxy?cms=${encodedCmsUrl}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return `${cmsUrl}/xmds.php`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Call XMDS SOAP method
|
|
73
|
+
*/
|
|
74
|
+
async call(method, params = {}) {
|
|
75
|
+
const xmdsUrl = this.rewriteXmdsUrl(this.config.cmsAddress);
|
|
76
|
+
const url = `${xmdsUrl}${xmdsUrl.includes('?') ? '&' : '?'}v=${this.schemaVersion}`;
|
|
77
|
+
const body = this.buildEnvelope(method, params);
|
|
78
|
+
|
|
79
|
+
log.debug(`${method}`, params);
|
|
80
|
+
log.debug(`URL: ${url}`);
|
|
81
|
+
|
|
82
|
+
const response = await fetchWithRetry(url, {
|
|
83
|
+
method: 'POST',
|
|
84
|
+
headers: {
|
|
85
|
+
'Content-Type': 'text/xml; charset=utf-8'
|
|
86
|
+
},
|
|
87
|
+
body
|
|
88
|
+
}, this.retryOptions);
|
|
89
|
+
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
throw new Error(`XMDS ${method} failed: ${response.status} ${response.statusText}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const xml = await response.text();
|
|
95
|
+
return this.parseResponse(xml, method);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Parse SOAP response
|
|
100
|
+
*/
|
|
101
|
+
parseResponse(xml, method) {
|
|
102
|
+
const parser = new DOMParser();
|
|
103
|
+
const doc = parser.parseFromString(xml, 'text/xml');
|
|
104
|
+
|
|
105
|
+
// Check for SOAP fault (handle namespace prefix like soap:Fault)
|
|
106
|
+
let fault = doc.querySelector('Fault');
|
|
107
|
+
if (!fault) {
|
|
108
|
+
fault = Array.from(doc.querySelectorAll('*')).find(
|
|
109
|
+
el => el.localName === 'Fault' || el.tagName.endsWith(':Fault')
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
if (fault) {
|
|
113
|
+
const faultString = fault.querySelector('faultstring')?.textContent
|
|
114
|
+
|| Array.from(fault.querySelectorAll('*')).find(el => el.localName === 'faultstring')?.textContent
|
|
115
|
+
|| 'Unknown SOAP fault';
|
|
116
|
+
throw new Error(`SOAP Fault: ${faultString}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Extract response element (handle namespace prefixes like ns1:MethodResponse)
|
|
120
|
+
const responseTag = `${method}Response`;
|
|
121
|
+
let responseEl = doc.querySelector(responseTag);
|
|
122
|
+
if (!responseEl) {
|
|
123
|
+
responseEl = Array.from(doc.querySelectorAll('*')).find(
|
|
124
|
+
el => el.localName === responseTag || el.tagName.endsWith(':' + responseTag)
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!responseEl) {
|
|
129
|
+
throw new Error(`No ${responseTag} element in SOAP response`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const returnEl = responseEl.firstElementChild;
|
|
133
|
+
if (!returnEl) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return returnEl.textContent;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ─── Public API ─────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* RegisterDisplay - authenticate and get settings
|
|
144
|
+
*/
|
|
145
|
+
async registerDisplay() {
|
|
146
|
+
const os = `${navigator.platform} ${navigator.userAgent}`;
|
|
147
|
+
|
|
148
|
+
const xml = await this.call('RegisterDisplay', {
|
|
149
|
+
serverKey: this.config.cmsKey,
|
|
150
|
+
hardwareKey: this.config.hardwareKey,
|
|
151
|
+
displayName: this.config.displayName,
|
|
152
|
+
clientType: 'chromeOS',
|
|
153
|
+
clientVersion: '0.1.0',
|
|
154
|
+
clientCode: '1',
|
|
155
|
+
operatingSystem: os,
|
|
156
|
+
macAddress: this.config.macAddress || 'n/a',
|
|
157
|
+
xmrChannel: this.config.xmrChannel,
|
|
158
|
+
xmrPubKey: ''
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
return this.parseRegisterDisplayResponse(xml);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Parse RegisterDisplay XML response
|
|
166
|
+
*/
|
|
167
|
+
parseRegisterDisplayResponse(xml) {
|
|
168
|
+
const parser = new DOMParser();
|
|
169
|
+
const doc = parser.parseFromString(xml, 'text/xml');
|
|
170
|
+
|
|
171
|
+
const display = doc.querySelector('display');
|
|
172
|
+
if (!display) {
|
|
173
|
+
throw new Error('Invalid RegisterDisplay response: no <display> element');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const code = display.getAttribute('code');
|
|
177
|
+
const message = display.getAttribute('message');
|
|
178
|
+
|
|
179
|
+
if (code !== 'READY') {
|
|
180
|
+
return { code, message, settings: null };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const settings = {};
|
|
184
|
+
for (const child of display.children) {
|
|
185
|
+
if (!['commands', 'file'].includes(child.tagName.toLowerCase())) {
|
|
186
|
+
settings[child.tagName] = child.textContent;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const checkRf = display.getAttribute('checkRf') || '';
|
|
191
|
+
const checkSchedule = display.getAttribute('checkSchedule') || '';
|
|
192
|
+
|
|
193
|
+
return { code, message, settings, checkRf, checkSchedule };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* RequiredFiles - get list of files to download
|
|
198
|
+
*/
|
|
199
|
+
async requiredFiles() {
|
|
200
|
+
const xml = await this.call('RequiredFiles', {
|
|
201
|
+
serverKey: this.config.cmsKey,
|
|
202
|
+
hardwareKey: this.config.hardwareKey
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
return this.parseRequiredFilesResponse(xml);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Parse RequiredFiles XML response
|
|
210
|
+
*/
|
|
211
|
+
parseRequiredFilesResponse(xml) {
|
|
212
|
+
const parser = new DOMParser();
|
|
213
|
+
const doc = parser.parseFromString(xml, 'text/xml');
|
|
214
|
+
|
|
215
|
+
const files = [];
|
|
216
|
+
for (const fileEl of doc.querySelectorAll('file')) {
|
|
217
|
+
files.push({
|
|
218
|
+
type: fileEl.getAttribute('type'),
|
|
219
|
+
id: fileEl.getAttribute('id'),
|
|
220
|
+
size: parseInt(fileEl.getAttribute('size') || '0'),
|
|
221
|
+
md5: fileEl.getAttribute('md5'),
|
|
222
|
+
download: fileEl.getAttribute('download'),
|
|
223
|
+
path: fileEl.getAttribute('path'),
|
|
224
|
+
code: fileEl.getAttribute('code'),
|
|
225
|
+
layoutid: fileEl.getAttribute('layoutid'),
|
|
226
|
+
regionid: fileEl.getAttribute('regionid'),
|
|
227
|
+
mediaid: fileEl.getAttribute('mediaid')
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return files;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Schedule - get layout schedule
|
|
236
|
+
*/
|
|
237
|
+
async schedule() {
|
|
238
|
+
const xml = await this.call('Schedule', {
|
|
239
|
+
serverKey: this.config.cmsKey,
|
|
240
|
+
hardwareKey: this.config.hardwareKey
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
return parseScheduleResponse(xml);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* GetResource - get rendered widget HTML
|
|
248
|
+
*/
|
|
249
|
+
async getResource(layoutId, regionId, mediaId) {
|
|
250
|
+
const xml = await this.call('GetResource', {
|
|
251
|
+
serverKey: this.config.cmsKey,
|
|
252
|
+
hardwareKey: this.config.hardwareKey,
|
|
253
|
+
layoutId: String(layoutId),
|
|
254
|
+
regionId: String(regionId),
|
|
255
|
+
mediaId: String(mediaId)
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
return xml;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* NotifyStatus - report current status
|
|
263
|
+
* @param {Object} status - Status object with currentLayoutId, deviceName, etc.
|
|
264
|
+
*/
|
|
265
|
+
async notifyStatus(status) {
|
|
266
|
+
// Enrich with storage estimate if available
|
|
267
|
+
if (typeof navigator !== 'undefined' && navigator.storage?.estimate) {
|
|
268
|
+
try {
|
|
269
|
+
const estimate = await navigator.storage.estimate();
|
|
270
|
+
status.availableSpace = estimate.quota - estimate.usage;
|
|
271
|
+
status.totalSpace = estimate.quota;
|
|
272
|
+
} catch (_) { /* storage estimate not supported */ }
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Add timezone if not already provided
|
|
276
|
+
if (!status.timeZone && typeof Intl !== 'undefined') {
|
|
277
|
+
status.timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return await this.call('NotifyStatus', {
|
|
281
|
+
serverKey: this.config.cmsKey,
|
|
282
|
+
hardwareKey: this.config.hardwareKey,
|
|
283
|
+
status: JSON.stringify(status)
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* MediaInventory - report downloaded files
|
|
289
|
+
*/
|
|
290
|
+
async mediaInventory(inventoryXml) {
|
|
291
|
+
return await this.call('MediaInventory', {
|
|
292
|
+
serverKey: this.config.cmsKey,
|
|
293
|
+
hardwareKey: this.config.hardwareKey,
|
|
294
|
+
mediaInventory: inventoryXml
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* BlackList - report broken media to CMS
|
|
300
|
+
* @param {string} mediaId - The media file ID
|
|
301
|
+
* @param {string} type - File type ('media' or 'layout')
|
|
302
|
+
* @param {string} reason - Reason for blacklisting
|
|
303
|
+
* @returns {Promise<boolean>}
|
|
304
|
+
*/
|
|
305
|
+
async blackList(mediaId, type, reason) {
|
|
306
|
+
try {
|
|
307
|
+
const xml = await this.call('BlackList', {
|
|
308
|
+
serverKey: this.config.cmsKey,
|
|
309
|
+
hardwareKey: this.config.hardwareKey,
|
|
310
|
+
mediaId: String(mediaId),
|
|
311
|
+
type: type || 'media',
|
|
312
|
+
reason: reason || 'Failed to render'
|
|
313
|
+
});
|
|
314
|
+
log.info(`BlackListed ${type}/${mediaId}: ${reason}`);
|
|
315
|
+
return xml === 'true';
|
|
316
|
+
} catch (error) {
|
|
317
|
+
log.warn('BlackList failed:', error);
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* SubmitLog - submit player logs to CMS for remote debugging
|
|
324
|
+
* @param {string} logXml - XML string containing log entries
|
|
325
|
+
* @returns {Promise<boolean>} - true if logs were successfully submitted
|
|
326
|
+
*/
|
|
327
|
+
async submitLog(logXml) {
|
|
328
|
+
const xml = await this.call('SubmitLog', {
|
|
329
|
+
serverKey: this.config.cmsKey,
|
|
330
|
+
hardwareKey: this.config.hardwareKey,
|
|
331
|
+
logXml: logXml
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
return xml === 'true';
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* SubmitScreenShot - submit screenshot to CMS for display verification
|
|
339
|
+
* @param {string} base64Image - Base64-encoded PNG image data
|
|
340
|
+
* @returns {Promise<boolean>} - true if screenshot was successfully submitted
|
|
341
|
+
*/
|
|
342
|
+
async submitScreenShot(base64Image) {
|
|
343
|
+
const xml = await this.call('SubmitScreenShot', {
|
|
344
|
+
serverKey: this.config.cmsKey,
|
|
345
|
+
hardwareKey: this.config.hardwareKey,
|
|
346
|
+
screenShot: base64Image
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
return xml === 'true';
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* SubmitStats - submit proof of play statistics
|
|
354
|
+
* @param {string} statsXml - XML-encoded stats string
|
|
355
|
+
* @returns {Promise<boolean>} - true if stats were successfully submitted
|
|
356
|
+
*/
|
|
357
|
+
async submitStats(statsXml) {
|
|
358
|
+
try {
|
|
359
|
+
const xml = await this.call('SubmitStats', {
|
|
360
|
+
serverKey: this.config.cmsKey,
|
|
361
|
+
hardwareKey: this.config.hardwareKey,
|
|
362
|
+
statXml: statsXml
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
const success = xml === 'true';
|
|
366
|
+
log.info(`SubmitStats result: ${success}`);
|
|
367
|
+
return success;
|
|
368
|
+
} catch (error) {
|
|
369
|
+
log.error('SubmitStats failed:', error);
|
|
370
|
+
throw error;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for XMDS overlay parsing
|
|
3
|
+
*
|
|
4
|
+
* Tests that overlays are correctly parsed from Schedule XML response
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect } from 'vitest';
|
|
8
|
+
import { parseScheduleResponse } from './schedule-parser.js';
|
|
9
|
+
|
|
10
|
+
describe('Schedule Parsing - Overlays', () => {
|
|
11
|
+
describe('parseScheduleResponse()', () => {
|
|
12
|
+
it('should parse overlays from Schedule XML', () => {
|
|
13
|
+
const xml = `<?xml version="1.0"?>
|
|
14
|
+
<schedule>
|
|
15
|
+
<default file="1.xlf"/>
|
|
16
|
+
<overlays>
|
|
17
|
+
<overlay duration="60" file="101.xlf" fromdt="2026-01-01 00:00:00" todt="2026-12-31 23:59:59" priority="10" scheduleid="555" isGeoAware="0" geoLocation=""/>
|
|
18
|
+
</overlays>
|
|
19
|
+
</schedule>`;
|
|
20
|
+
|
|
21
|
+
const schedule = parseScheduleResponse(xml);
|
|
22
|
+
|
|
23
|
+
expect(schedule.overlays).toBeDefined();
|
|
24
|
+
expect(schedule.overlays.length).toBe(1);
|
|
25
|
+
expect(schedule.overlays[0].file).toBe('101.xlf');
|
|
26
|
+
expect(schedule.overlays[0].duration).toBe(60);
|
|
27
|
+
expect(schedule.overlays[0].priority).toBe(10);
|
|
28
|
+
expect(schedule.overlays[0].fromDt).toBe('2026-01-01 00:00:00');
|
|
29
|
+
expect(schedule.overlays[0].toDt).toBe('2026-12-31 23:59:59');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should parse multiple overlays', () => {
|
|
33
|
+
const xml = `<?xml version="1.0"?>
|
|
34
|
+
<schedule>
|
|
35
|
+
<default file="1.xlf"/>
|
|
36
|
+
<overlays>
|
|
37
|
+
<overlay duration="60" file="101.xlf" fromdt="2026-01-01 00:00:00" todt="2026-12-31 23:59:59" priority="10" scheduleid="555" isGeoAware="0" geoLocation=""/>
|
|
38
|
+
<overlay duration="30" file="102.xlf" fromdt="2026-01-01 00:00:00" todt="2026-12-31 23:59:59" priority="5" scheduleid="556" isGeoAware="0" geoLocation=""/>
|
|
39
|
+
<overlay duration="120" file="103.xlf" fromdt="2026-01-01 00:00:00" todt="2026-12-31 23:59:59" priority="20" scheduleid="557" isGeoAware="1" geoLocation="location-1"/>
|
|
40
|
+
</overlays>
|
|
41
|
+
</schedule>`;
|
|
42
|
+
|
|
43
|
+
const schedule = parseScheduleResponse(xml);
|
|
44
|
+
|
|
45
|
+
expect(schedule.overlays.length).toBe(3);
|
|
46
|
+
expect(schedule.overlays[0].file).toBe('101.xlf');
|
|
47
|
+
expect(schedule.overlays[1].file).toBe('102.xlf');
|
|
48
|
+
expect(schedule.overlays[2].file).toBe('103.xlf');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should parse geo-aware overlay', () => {
|
|
52
|
+
const xml = `<?xml version="1.0"?>
|
|
53
|
+
<schedule>
|
|
54
|
+
<overlays>
|
|
55
|
+
<overlay duration="60" file="101.xlf" fromdt="2026-01-01 00:00:00" todt="2026-12-31 23:59:59" priority="10" scheduleid="555" isGeoAware="1" geoLocation="geo-fence-1"/>
|
|
56
|
+
</overlays>
|
|
57
|
+
</schedule>`;
|
|
58
|
+
|
|
59
|
+
const schedule = parseScheduleResponse(xml);
|
|
60
|
+
|
|
61
|
+
expect(schedule.overlays[0].isGeoAware).toBe(true);
|
|
62
|
+
expect(schedule.overlays[0].geoLocation).toBe('geo-fence-1');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should handle overlays with no geo-awareness', () => {
|
|
66
|
+
const xml = `<?xml version="1.0"?>
|
|
67
|
+
<schedule>
|
|
68
|
+
<overlays>
|
|
69
|
+
<overlay duration="60" file="101.xlf" fromdt="2026-01-01 00:00:00" todt="2026-12-31 23:59:59" priority="10" scheduleid="555" isGeoAware="0" geoLocation=""/>
|
|
70
|
+
</overlays>
|
|
71
|
+
</schedule>`;
|
|
72
|
+
|
|
73
|
+
const schedule = parseScheduleResponse(xml);
|
|
74
|
+
|
|
75
|
+
expect(schedule.overlays[0].isGeoAware).toBe(false);
|
|
76
|
+
expect(schedule.overlays[0].geoLocation).toBe('');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should handle schedule with no overlays element', () => {
|
|
80
|
+
const xml = `<?xml version="1.0"?>
|
|
81
|
+
<schedule>
|
|
82
|
+
<default file="1.xlf"/>
|
|
83
|
+
<layout file="2.xlf" fromdt="2026-01-01 00:00:00" todt="2026-12-31 23:59:59" scheduleid="123" priority="0"/>
|
|
84
|
+
</schedule>`;
|
|
85
|
+
|
|
86
|
+
const schedule = parseScheduleResponse(xml);
|
|
87
|
+
|
|
88
|
+
expect(schedule.overlays).toBeDefined();
|
|
89
|
+
expect(schedule.overlays.length).toBe(0);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should handle empty overlays element', () => {
|
|
93
|
+
const xml = `<?xml version="1.0"?>
|
|
94
|
+
<schedule>
|
|
95
|
+
<default file="1.xlf"/>
|
|
96
|
+
<overlays></overlays>
|
|
97
|
+
</schedule>`;
|
|
98
|
+
|
|
99
|
+
const schedule = parseScheduleResponse(xml);
|
|
100
|
+
|
|
101
|
+
expect(schedule.overlays).toBeDefined();
|
|
102
|
+
expect(schedule.overlays.length).toBe(0);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should parse overlay priorities correctly', () => {
|
|
106
|
+
const xml = `<?xml version="1.0"?>
|
|
107
|
+
<schedule>
|
|
108
|
+
<overlays>
|
|
109
|
+
<overlay duration="60" file="101.xlf" fromdt="2026-01-01 00:00:00" todt="2026-12-31 23:59:59" priority="100" scheduleid="555" isGeoAware="0" geoLocation=""/>
|
|
110
|
+
<overlay duration="60" file="102.xlf" fromdt="2026-01-01 00:00:00" todt="2026-12-31 23:59:59" priority="0" scheduleid="556" isGeoAware="0" geoLocation=""/>
|
|
111
|
+
</overlays>
|
|
112
|
+
</schedule>`;
|
|
113
|
+
|
|
114
|
+
const schedule = parseScheduleResponse(xml);
|
|
115
|
+
|
|
116
|
+
expect(schedule.overlays[0].priority).toBe(100);
|
|
117
|
+
expect(schedule.overlays[1].priority).toBe(0);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should combine overlays with regular layouts and campaigns', () => {
|
|
121
|
+
const xml = `<?xml version="1.0"?>
|
|
122
|
+
<schedule>
|
|
123
|
+
<default file="1.xlf"/>
|
|
124
|
+
<layout file="2.xlf" fromdt="2026-01-01 00:00:00" todt="2026-12-31 23:59:59" scheduleid="123" priority="5"/>
|
|
125
|
+
<campaign id="1" priority="10" fromdt="2026-01-01 00:00:00" todt="2026-12-31 23:59:59" scheduleid="456">
|
|
126
|
+
<layout file="3.xlf"/>
|
|
127
|
+
<layout file="4.xlf"/>
|
|
128
|
+
</campaign>
|
|
129
|
+
<overlays>
|
|
130
|
+
<overlay duration="60" file="101.xlf" fromdt="2026-01-01 00:00:00" todt="2026-12-31 23:59:59" priority="10" scheduleid="555" isGeoAware="0" geoLocation=""/>
|
|
131
|
+
</overlays>
|
|
132
|
+
</schedule>`;
|
|
133
|
+
|
|
134
|
+
const schedule = parseScheduleResponse(xml);
|
|
135
|
+
|
|
136
|
+
expect(schedule.default).toBe('1.xlf');
|
|
137
|
+
expect(schedule.layouts.length).toBe(1);
|
|
138
|
+
expect(schedule.campaigns.length).toBe(1);
|
|
139
|
+
expect(schedule.overlays.length).toBe(1);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should parse overlay durations correctly', () => {
|
|
143
|
+
const xml = `<?xml version="1.0"?>
|
|
144
|
+
<schedule>
|
|
145
|
+
<overlays>
|
|
146
|
+
<overlay duration="30" file="101.xlf" fromdt="2026-01-01 00:00:00" todt="2026-12-31 23:59:59" priority="10" scheduleid="555" isGeoAware="0" geoLocation=""/>
|
|
147
|
+
<overlay duration="120" file="102.xlf" fromdt="2026-01-01 00:00:00" todt="2026-12-31 23:59:59" priority="5" scheduleid="556" isGeoAware="0" geoLocation=""/>
|
|
148
|
+
</overlays>
|
|
149
|
+
</schedule>`;
|
|
150
|
+
|
|
151
|
+
const schedule = parseScheduleResponse(xml);
|
|
152
|
+
|
|
153
|
+
expect(schedule.overlays[0].duration).toBe(30);
|
|
154
|
+
expect(schedule.overlays[1].duration).toBe(120);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should use default duration if not specified', () => {
|
|
158
|
+
const xml = `<?xml version="1.0"?>
|
|
159
|
+
<schedule>
|
|
160
|
+
<overlays>
|
|
161
|
+
<overlay file="101.xlf" fromdt="2026-01-01 00:00:00" todt="2026-12-31 23:59:59" priority="10" scheduleid="555" isGeoAware="0" geoLocation=""/>
|
|
162
|
+
</overlays>
|
|
163
|
+
</schedule>`;
|
|
164
|
+
|
|
165
|
+
const schedule = parseScheduleResponse(xml);
|
|
166
|
+
|
|
167
|
+
expect(schedule.overlays[0].duration).toBe(60); // Default
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
});
|