@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/state.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized player state
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for player status, dimensions, and display info.
|
|
5
|
+
* Avoids scattered state across PlayerCore, renderer, and platform layer.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { EventEmitter } from '@xiboplayer/utils';
|
|
9
|
+
|
|
10
|
+
export class PlayerState extends EventEmitter {
|
|
11
|
+
constructor() {
|
|
12
|
+
super();
|
|
13
|
+
this.currentLayoutId = null;
|
|
14
|
+
this.currentScheduleId = null;
|
|
15
|
+
this.displayName = '';
|
|
16
|
+
this.hardwareKey = '';
|
|
17
|
+
this.playerType = 'pwa';
|
|
18
|
+
this.displayStatus = 'idle'; // idle | collecting | rendering | error
|
|
19
|
+
this.screenWidth = 0;
|
|
20
|
+
this.screenHeight = 0;
|
|
21
|
+
this.lastCollectionTime = null;
|
|
22
|
+
this.lastHeartbeat = null;
|
|
23
|
+
this.isRegistered = false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Update a state property and emit change event
|
|
28
|
+
*/
|
|
29
|
+
set(key, value) {
|
|
30
|
+
if (this[key] === value) return;
|
|
31
|
+
const old = this[key];
|
|
32
|
+
this[key] = value;
|
|
33
|
+
this.emit('change', key, value, old);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get snapshot of current state (for status reporting)
|
|
38
|
+
*/
|
|
39
|
+
toJSON() {
|
|
40
|
+
return {
|
|
41
|
+
currentLayoutId: this.currentLayoutId,
|
|
42
|
+
currentScheduleId: this.currentScheduleId,
|
|
43
|
+
displayName: this.displayName,
|
|
44
|
+
hardwareKey: this.hardwareKey,
|
|
45
|
+
playerType: this.playerType,
|
|
46
|
+
displayStatus: this.displayStatus,
|
|
47
|
+
screenWidth: this.screenWidth,
|
|
48
|
+
screenHeight: this.screenHeight,
|
|
49
|
+
lastCollectionTime: this.lastCollectionTime,
|
|
50
|
+
lastHeartbeat: this.lastHeartbeat,
|
|
51
|
+
isRegistered: this.isRegistered
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PlayerState Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for centralized player state management
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
8
|
+
import { PlayerState } from './state.js';
|
|
9
|
+
|
|
10
|
+
describe('PlayerState', () => {
|
|
11
|
+
let state;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
state = new PlayerState();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('Initial State', () => {
|
|
18
|
+
it('should start with null currentLayoutId', () => {
|
|
19
|
+
expect(state.currentLayoutId).toBeNull();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should start with null currentScheduleId', () => {
|
|
23
|
+
expect(state.currentScheduleId).toBeNull();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should start with empty displayName', () => {
|
|
27
|
+
expect(state.displayName).toBe('');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should start with empty hardwareKey', () => {
|
|
31
|
+
expect(state.hardwareKey).toBe('');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should start with pwa playerType', () => {
|
|
35
|
+
expect(state.playerType).toBe('pwa');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should start with idle displayStatus', () => {
|
|
39
|
+
expect(state.displayStatus).toBe('idle');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should start with zero screen dimensions', () => {
|
|
43
|
+
expect(state.screenWidth).toBe(0);
|
|
44
|
+
expect(state.screenHeight).toBe(0);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should start with null lastCollectionTime', () => {
|
|
48
|
+
expect(state.lastCollectionTime).toBeNull();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should start with null lastHeartbeat', () => {
|
|
52
|
+
expect(state.lastHeartbeat).toBeNull();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should start with isRegistered false', () => {
|
|
56
|
+
expect(state.isRegistered).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('set()', () => {
|
|
61
|
+
it('should update a property value', () => {
|
|
62
|
+
state.set('displayName', 'Test Display');
|
|
63
|
+
|
|
64
|
+
expect(state.displayName).toBe('Test Display');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should emit change event with key, new value, and old value', () => {
|
|
68
|
+
const spy = vi.fn();
|
|
69
|
+
state.on('change', spy);
|
|
70
|
+
|
|
71
|
+
state.set('displayStatus', 'rendering');
|
|
72
|
+
|
|
73
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
74
|
+
expect(spy).toHaveBeenCalledWith('displayStatus', 'rendering', 'idle');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should not emit change event when value is the same', () => {
|
|
78
|
+
const spy = vi.fn();
|
|
79
|
+
state.on('change', spy);
|
|
80
|
+
|
|
81
|
+
state.set('playerType', 'pwa'); // Same as initial value
|
|
82
|
+
|
|
83
|
+
expect(spy).not.toHaveBeenCalled();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should not emit change event for same null value', () => {
|
|
87
|
+
const spy = vi.fn();
|
|
88
|
+
state.on('change', spy);
|
|
89
|
+
|
|
90
|
+
state.set('currentLayoutId', null); // Same as initial value
|
|
91
|
+
|
|
92
|
+
expect(spy).not.toHaveBeenCalled();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should emit change event when changing from null to a value', () => {
|
|
96
|
+
const spy = vi.fn();
|
|
97
|
+
state.on('change', spy);
|
|
98
|
+
|
|
99
|
+
state.set('currentLayoutId', 42);
|
|
100
|
+
|
|
101
|
+
expect(spy).toHaveBeenCalledWith('currentLayoutId', 42, null);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should emit change event when changing from a value to null', () => {
|
|
105
|
+
state.set('currentLayoutId', 42);
|
|
106
|
+
|
|
107
|
+
const spy = vi.fn();
|
|
108
|
+
state.on('change', spy);
|
|
109
|
+
|
|
110
|
+
state.set('currentLayoutId', null);
|
|
111
|
+
|
|
112
|
+
expect(spy).toHaveBeenCalledWith('currentLayoutId', null, 42);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should handle multiple sequential updates', () => {
|
|
116
|
+
const spy = vi.fn();
|
|
117
|
+
state.on('change', spy);
|
|
118
|
+
|
|
119
|
+
state.set('displayStatus', 'collecting');
|
|
120
|
+
state.set('displayStatus', 'rendering');
|
|
121
|
+
state.set('displayStatus', 'idle');
|
|
122
|
+
|
|
123
|
+
expect(spy).toHaveBeenCalledTimes(3);
|
|
124
|
+
expect(spy).toHaveBeenNthCalledWith(1, 'displayStatus', 'collecting', 'idle');
|
|
125
|
+
expect(spy).toHaveBeenNthCalledWith(2, 'displayStatus', 'rendering', 'collecting');
|
|
126
|
+
expect(spy).toHaveBeenNthCalledWith(3, 'displayStatus', 'idle', 'rendering');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should update numeric properties', () => {
|
|
130
|
+
state.set('screenWidth', 1920);
|
|
131
|
+
state.set('screenHeight', 1080);
|
|
132
|
+
|
|
133
|
+
expect(state.screenWidth).toBe(1920);
|
|
134
|
+
expect(state.screenHeight).toBe(1080);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should update boolean properties', () => {
|
|
138
|
+
const spy = vi.fn();
|
|
139
|
+
state.on('change', spy);
|
|
140
|
+
|
|
141
|
+
state.set('isRegistered', true);
|
|
142
|
+
|
|
143
|
+
expect(state.isRegistered).toBe(true);
|
|
144
|
+
expect(spy).toHaveBeenCalledWith('isRegistered', true, false);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should not emit when setting same boolean value', () => {
|
|
148
|
+
const spy = vi.fn();
|
|
149
|
+
state.on('change', spy);
|
|
150
|
+
|
|
151
|
+
state.set('isRegistered', false); // Same as initial
|
|
152
|
+
|
|
153
|
+
expect(spy).not.toHaveBeenCalled();
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('toJSON()', () => {
|
|
158
|
+
it('should return a snapshot of all state properties', () => {
|
|
159
|
+
const json = state.toJSON();
|
|
160
|
+
|
|
161
|
+
expect(json).toEqual({
|
|
162
|
+
currentLayoutId: null,
|
|
163
|
+
currentScheduleId: null,
|
|
164
|
+
displayName: '',
|
|
165
|
+
hardwareKey: '',
|
|
166
|
+
playerType: 'pwa',
|
|
167
|
+
displayStatus: 'idle',
|
|
168
|
+
screenWidth: 0,
|
|
169
|
+
screenHeight: 0,
|
|
170
|
+
lastCollectionTime: null,
|
|
171
|
+
lastHeartbeat: null,
|
|
172
|
+
isRegistered: false
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('should reflect updated values', () => {
|
|
177
|
+
state.set('displayName', 'My Display');
|
|
178
|
+
state.set('currentLayoutId', 123);
|
|
179
|
+
state.set('isRegistered', true);
|
|
180
|
+
state.set('screenWidth', 1920);
|
|
181
|
+
state.set('screenHeight', 1080);
|
|
182
|
+
|
|
183
|
+
const json = state.toJSON();
|
|
184
|
+
|
|
185
|
+
expect(json.displayName).toBe('My Display');
|
|
186
|
+
expect(json.currentLayoutId).toBe(123);
|
|
187
|
+
expect(json.isRegistered).toBe(true);
|
|
188
|
+
expect(json.screenWidth).toBe(1920);
|
|
189
|
+
expect(json.screenHeight).toBe(1080);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should return a plain object (not the state instance)', () => {
|
|
193
|
+
const json = state.toJSON();
|
|
194
|
+
|
|
195
|
+
expect(json).not.toBe(state);
|
|
196
|
+
expect(json.constructor).toBe(Object);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('should return independent snapshot (mutations do not affect original)', () => {
|
|
200
|
+
const json = state.toJSON();
|
|
201
|
+
json.displayName = 'Modified';
|
|
202
|
+
|
|
203
|
+
expect(state.displayName).toBe(''); // Original unchanged
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
});
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test Utilities for Xibo Player Core
|
|
3
|
+
*
|
|
4
|
+
* Provides mocking utilities, test helpers, and fixtures for unit tests.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { vi } from 'vitest';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Mock fetch with controllable responses
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* mockFetch({
|
|
14
|
+
* 'http://example.com/file.mp4': {
|
|
15
|
+
* blob: createTestBlob(1024),
|
|
16
|
+
* headers: { 'Content-Length': '1024' }
|
|
17
|
+
* }
|
|
18
|
+
* });
|
|
19
|
+
*/
|
|
20
|
+
export function mockFetch(responses = {}) {
|
|
21
|
+
global.fetch = vi.fn((url, options) => {
|
|
22
|
+
const method = options?.method || 'GET';
|
|
23
|
+
const key = `${method} ${url}`;
|
|
24
|
+
const response = responses[key] || responses[url];
|
|
25
|
+
|
|
26
|
+
if (!response) {
|
|
27
|
+
return Promise.resolve({
|
|
28
|
+
ok: false,
|
|
29
|
+
status: 404,
|
|
30
|
+
statusText: 'Not Found',
|
|
31
|
+
headers: {
|
|
32
|
+
get: () => null
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return Promise.resolve({
|
|
38
|
+
ok: response.ok !== false,
|
|
39
|
+
status: response.status || 200,
|
|
40
|
+
statusText: response.statusText || 'OK',
|
|
41
|
+
headers: {
|
|
42
|
+
get: (name) => response.headers?.[name] || null
|
|
43
|
+
},
|
|
44
|
+
blob: () => Promise.resolve(response.blob || new Blob()),
|
|
45
|
+
text: () => Promise.resolve(response.text || ''),
|
|
46
|
+
json: () => Promise.resolve(response.json || {}),
|
|
47
|
+
arrayBuffer: () => Promise.resolve(response.arrayBuffer || new ArrayBuffer(0))
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return global.fetch;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Mock Service Worker navigator
|
|
56
|
+
*
|
|
57
|
+
* Usage:
|
|
58
|
+
* mockServiceWorker({ ready: true, controller: {} });
|
|
59
|
+
* mockServiceWorker({ supported: false }); // Simulate no SW support
|
|
60
|
+
*/
|
|
61
|
+
export function mockServiceWorker(config = {}) {
|
|
62
|
+
const {
|
|
63
|
+
ready = true,
|
|
64
|
+
controller = {},
|
|
65
|
+
supported = true
|
|
66
|
+
} = config;
|
|
67
|
+
|
|
68
|
+
if (supported) {
|
|
69
|
+
global.navigator.serviceWorker = {
|
|
70
|
+
ready: ready ? Promise.resolve({ active: {} }) : Promise.reject(new Error('Not ready')),
|
|
71
|
+
controller: controller,
|
|
72
|
+
register: vi.fn(() => Promise.resolve({ scope: '/' })),
|
|
73
|
+
addEventListener: vi.fn(),
|
|
74
|
+
removeEventListener: vi.fn()
|
|
75
|
+
};
|
|
76
|
+
} else {
|
|
77
|
+
delete global.navigator.serviceWorker;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Mock CacheManager for DirectCacheBackend tests
|
|
83
|
+
*
|
|
84
|
+
* Returns a mock object with all cache.js methods
|
|
85
|
+
*/
|
|
86
|
+
export function mockCacheManager() {
|
|
87
|
+
return {
|
|
88
|
+
init: vi.fn(() => Promise.resolve()),
|
|
89
|
+
getCachedFile: vi.fn(() => Promise.resolve(null)),
|
|
90
|
+
getCachedResponse: vi.fn(() => Promise.resolve(null)),
|
|
91
|
+
downloadFile: vi.fn(() => Promise.resolve({ id: '1', size: 100 })),
|
|
92
|
+
getFile: vi.fn(() => Promise.resolve(null)),
|
|
93
|
+
getCacheKey: vi.fn((type, id) => `/cache/${type}/${id}`),
|
|
94
|
+
cache: {
|
|
95
|
+
match: vi.fn(() => Promise.resolve(null)),
|
|
96
|
+
put: vi.fn(() => Promise.resolve()),
|
|
97
|
+
delete: vi.fn(() => Promise.resolve(true))
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Create test blob
|
|
104
|
+
*
|
|
105
|
+
* Usage:
|
|
106
|
+
* const blob = createTestBlob(1024); // 1KB blob
|
|
107
|
+
* const blob = createTestBlob(1024, 'video/mp4'); // Typed blob
|
|
108
|
+
*/
|
|
109
|
+
export function createTestBlob(size = 1024, type = 'application/octet-stream') {
|
|
110
|
+
const buffer = new ArrayBuffer(size);
|
|
111
|
+
return new Blob([buffer], { type });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Wait for condition to be true
|
|
116
|
+
*
|
|
117
|
+
* Usage:
|
|
118
|
+
* await waitFor(() => task.state === 'complete', 5000);
|
|
119
|
+
*/
|
|
120
|
+
export async function waitFor(condition, timeout = 5000) {
|
|
121
|
+
const start = Date.now();
|
|
122
|
+
while (!condition()) {
|
|
123
|
+
if (Date.now() - start > timeout) {
|
|
124
|
+
throw new Error('waitFor timeout');
|
|
125
|
+
}
|
|
126
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Wait for a specific time (for testing timing-dependent logic)
|
|
132
|
+
*
|
|
133
|
+
* Usage:
|
|
134
|
+
* await wait(100); // Wait 100ms
|
|
135
|
+
*/
|
|
136
|
+
export async function wait(ms) {
|
|
137
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Create a spy that tracks calls but doesn't interfere
|
|
142
|
+
*
|
|
143
|
+
* Usage:
|
|
144
|
+
* const spy = createSpy();
|
|
145
|
+
* obj.on('event', spy);
|
|
146
|
+
* // ... trigger event
|
|
147
|
+
* expect(spy).toHaveBeenCalledWith('arg1', 'arg2');
|
|
148
|
+
*/
|
|
149
|
+
export function createSpy() {
|
|
150
|
+
return vi.fn();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Mock MessageChannel for Service Worker tests
|
|
155
|
+
*/
|
|
156
|
+
export function mockMessageChannel() {
|
|
157
|
+
class MockMessagePort {
|
|
158
|
+
constructor() {
|
|
159
|
+
this.onmessage = null;
|
|
160
|
+
this._listeners = [];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
addEventListener(event, callback) {
|
|
164
|
+
if (event === 'message') {
|
|
165
|
+
this._listeners.push(callback);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
removeEventListener(event, callback) {
|
|
170
|
+
if (event === 'message') {
|
|
171
|
+
const index = this._listeners.indexOf(callback);
|
|
172
|
+
if (index !== -1) {
|
|
173
|
+
this._listeners.splice(index, 1);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
postMessage(data) {
|
|
179
|
+
// Simulate async message passing
|
|
180
|
+
setTimeout(() => {
|
|
181
|
+
if (this.paired) {
|
|
182
|
+
const event = { data };
|
|
183
|
+
|
|
184
|
+
// Call onmessage if set
|
|
185
|
+
if (this.paired.onmessage) {
|
|
186
|
+
this.paired.onmessage(event);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Call event listeners
|
|
190
|
+
this.paired._listeners.forEach(listener => listener(event));
|
|
191
|
+
}
|
|
192
|
+
}, 0);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
class MockMessageChannel {
|
|
197
|
+
constructor() {
|
|
198
|
+
this.port1 = new MockMessagePort();
|
|
199
|
+
this.port2 = new MockMessagePort();
|
|
200
|
+
this.port1.paired = this.port2;
|
|
201
|
+
this.port2.paired = this.port1;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
global.MessageChannel = MockMessageChannel;
|
|
206
|
+
return MockMessageChannel;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Reset all mocks
|
|
211
|
+
*/
|
|
212
|
+
export function resetMocks() {
|
|
213
|
+
vi.clearAllMocks();
|
|
214
|
+
delete global.fetch;
|
|
215
|
+
delete global.navigator.serviceWorker;
|
|
216
|
+
delete global.MessageChannel;
|
|
217
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>XMDS Campaign Parsing Tests</title>
|
|
6
|
+
<style>
|
|
7
|
+
body {
|
|
8
|
+
font-family: monospace;
|
|
9
|
+
padding: 20px;
|
|
10
|
+
background: #1e1e1e;
|
|
11
|
+
color: #d4d4d4;
|
|
12
|
+
}
|
|
13
|
+
.pass { color: #4ec9b0; }
|
|
14
|
+
.fail { color: #f48771; }
|
|
15
|
+
pre { background: #2d2d2d; padding: 10px; border-left: 3px solid #4ec9b0; }
|
|
16
|
+
</style>
|
|
17
|
+
</head>
|
|
18
|
+
<body>
|
|
19
|
+
<h1>XMDS Campaign Parsing Tests</h1>
|
|
20
|
+
<div id="output"></div>
|
|
21
|
+
|
|
22
|
+
<script type="module">
|
|
23
|
+
import { XmdsClient } from './xmds.js';
|
|
24
|
+
|
|
25
|
+
const output = document.getElementById('output');
|
|
26
|
+
|
|
27
|
+
function log(msg, isPass = true) {
|
|
28
|
+
const div = document.createElement('div');
|
|
29
|
+
div.className = isPass ? 'pass' : 'fail';
|
|
30
|
+
div.textContent = msg;
|
|
31
|
+
output.appendChild(div);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Test XML with campaigns
|
|
35
|
+
const scheduleXmlWithCampaigns = `
|
|
36
|
+
<schedule>
|
|
37
|
+
<default file="0"/>
|
|
38
|
+
|
|
39
|
+
<!-- Campaign with 3 layouts -->
|
|
40
|
+
<campaign id="5" priority="10" fromdt="2026-01-30 00:00:00" todt="2026-01-31 23:59:59" scheduleid="15">
|
|
41
|
+
<layout file="100"/>
|
|
42
|
+
<layout file="101"/>
|
|
43
|
+
<layout file="102"/>
|
|
44
|
+
</campaign>
|
|
45
|
+
|
|
46
|
+
<!-- Standalone layout -->
|
|
47
|
+
<layout file="200" priority="5" fromdt="2026-01-30 00:00:00" todt="2026-01-31 23:59:59" scheduleid="20"/>
|
|
48
|
+
|
|
49
|
+
<!-- Another campaign with 2 layouts -->
|
|
50
|
+
<campaign id="6" priority="8" fromdt="2026-01-30 00:00:00" todt="2026-01-31 23:59:59" scheduleid="25">
|
|
51
|
+
<layout file="300"/>
|
|
52
|
+
<layout file="301"/>
|
|
53
|
+
</campaign>
|
|
54
|
+
</schedule>
|
|
55
|
+
`;
|
|
56
|
+
|
|
57
|
+
// Test XML with only standalone layouts (backward compatibility)
|
|
58
|
+
const scheduleXmlStandalone = `
|
|
59
|
+
<schedule>
|
|
60
|
+
<default file="0"/>
|
|
61
|
+
<layout file="100" priority="10" fromdt="2026-01-30 00:00:00" todt="2026-01-31 23:59:59" scheduleid="10"/>
|
|
62
|
+
<layout file="101" priority="5" fromdt="2026-01-30 00:00:00" todt="2026-01-31 23:59:59" scheduleid="11"/>
|
|
63
|
+
</schedule>
|
|
64
|
+
`;
|
|
65
|
+
|
|
66
|
+
function testParseScheduleWithCampaigns() {
|
|
67
|
+
const client = new XmdsClient({ cmsAddress: 'http://test', cmsKey: 'key', hardwareKey: 'hw' });
|
|
68
|
+
const schedule = client.parseScheduleResponse(scheduleXmlWithCampaigns);
|
|
69
|
+
|
|
70
|
+
console.log('Parsed schedule:', schedule);
|
|
71
|
+
|
|
72
|
+
console.assert(schedule.default === '0', 'Default should be 0');
|
|
73
|
+
console.assert(schedule.campaigns.length === 2, 'Should have 2 campaigns');
|
|
74
|
+
|
|
75
|
+
const campaign1 = schedule.campaigns[0];
|
|
76
|
+
console.assert(campaign1.id === '5', 'Campaign 1 ID should be 5');
|
|
77
|
+
console.assert(campaign1.priority === 10, 'Campaign 1 priority should be 10');
|
|
78
|
+
console.assert(campaign1.layouts.length === 3, 'Campaign 1 should have 3 layouts');
|
|
79
|
+
console.assert(campaign1.layouts[0].file === '100', 'Campaign 1 layout 1 file should be 100');
|
|
80
|
+
|
|
81
|
+
console.assert(schedule.layouts.length === 1, 'Should have 1 standalone layout');
|
|
82
|
+
console.assert(schedule.layouts[0].file === '200', 'Standalone layout file should be 200');
|
|
83
|
+
|
|
84
|
+
log('✓ Test 1 passed: Parse schedule with campaigns');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function testParseScheduleStandalone() {
|
|
88
|
+
const client = new XmdsClient({ cmsAddress: 'http://test', cmsKey: 'key', hardwareKey: 'hw' });
|
|
89
|
+
const schedule = client.parseScheduleResponse(scheduleXmlStandalone);
|
|
90
|
+
|
|
91
|
+
console.assert(schedule.campaigns.length === 0, 'Should have 0 campaigns');
|
|
92
|
+
console.assert(schedule.layouts.length === 2, 'Should have 2 standalone layouts');
|
|
93
|
+
|
|
94
|
+
log('✓ Test 2 passed: Parse schedule with standalone layouts (backward compatible)');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Run tests
|
|
98
|
+
log('\n=== Running XMDS Campaign Parsing Tests ===\n');
|
|
99
|
+
try {
|
|
100
|
+
testParseScheduleWithCampaigns();
|
|
101
|
+
testParseScheduleStandalone();
|
|
102
|
+
log('\n=== All XMDS tests passed! ===\n');
|
|
103
|
+
} catch (e) {
|
|
104
|
+
log('✗ Test failed: ' + e.message, false);
|
|
105
|
+
console.error(e);
|
|
106
|
+
}
|
|
107
|
+
</script>
|
|
108
|
+
</body>
|
|
109
|
+
</html>
|