@xiboplayer/xmr 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.
@@ -0,0 +1,509 @@
1
+ # XMR Testing Guide
2
+
3
+ This guide explains how to test XMR (Xibo Message Relay) integration in the Xibo player.
4
+
5
+ ## Table of Contents
6
+
7
+ 1. [Unit Tests](#unit-tests)
8
+ 2. [Integration Testing](#integration-testing)
9
+ 3. [Manual Testing](#manual-testing)
10
+ 4. [Real XMR Server Testing](#real-xmr-server-testing)
11
+ 5. [Troubleshooting](#troubleshooting)
12
+
13
+ ---
14
+
15
+ ## Unit Tests
16
+
17
+ The XMR package includes comprehensive unit tests covering all functionality.
18
+
19
+ ### Running Tests
20
+
21
+ ```bash
22
+ # From xmr package directory
23
+ cd packages/xmr
24
+ npm test
25
+
26
+ # With coverage report
27
+ npm run test:coverage
28
+
29
+ # Watch mode (auto-run on file changes)
30
+ npm run test:watch
31
+ ```
32
+
33
+ ### Test Coverage
34
+
35
+ Current coverage: **48 test cases**
36
+
37
+ Categories:
38
+ - ✅ **Constructor** (2 tests) - Initialization and state
39
+ - ✅ **Connection lifecycle** (8 tests) - Start, stop, reconnect
40
+ - ✅ **Connection events** (4 tests) - connected, disconnected, error
41
+ - ✅ **CMS commands** (14 tests) - All 7 commands + error handling
42
+ - ✅ **Reconnection logic** (4 tests) - Exponential backoff, max attempts
43
+ - ✅ **stop() method** (4 tests) - Cleanup, error handling
44
+ - ✅ **isConnected()** (3 tests) - Connection state queries
45
+ - ✅ **send()** (4 tests) - Sending messages to CMS
46
+ - ✅ **Edge cases** (3 tests) - Simultaneous commands, rapid cycles
47
+ - ✅ **Memory management** (2 tests) - Timer cleanup, garbage collection
48
+
49
+ ### Test Structure
50
+
51
+ Tests use Vitest with:
52
+ - **Mocking**: `vi.mock()` for @xibosignage/xibo-communication-framework
53
+ - **Fake timers**: `vi.useFakeTimers()` for reconnection testing
54
+ - **Async handling**: `vi.runAllTimersAsync()` for event handlers
55
+
56
+ Example test:
57
+ ```javascript
58
+ it('should handle collectNow command', async () => {
59
+ await wrapper.start('wss://test.xmr.com', 'cms-key-123');
60
+ const xmr = wrapper.xmr;
61
+
62
+ xmr.simulateCommand('collectNow');
63
+ await vi.runAllTimersAsync();
64
+
65
+ expect(mockPlayer.collect).toHaveBeenCalled();
66
+ });
67
+ ```
68
+
69
+ ---
70
+
71
+ ## Integration Testing
72
+
73
+ ### Mock XMR Server
74
+
75
+ For integration testing without a real CMS, create a mock XMR server:
76
+
77
+ ```javascript
78
+ // test-xmr-server.js
79
+ import { WebSocketServer } from 'ws';
80
+
81
+ const wss = new WebSocketServer({ port: 9505 });
82
+
83
+ wss.on('connection', (ws) => {
84
+ console.log('Player connected');
85
+
86
+ // Send test command after 2 seconds
87
+ setTimeout(() => {
88
+ ws.send(JSON.stringify({
89
+ action: 'collectNow'
90
+ }));
91
+ }, 2000);
92
+
93
+ ws.on('message', (data) => {
94
+ console.log('Received from player:', data.toString());
95
+ });
96
+ });
97
+
98
+ console.log('Mock XMR server listening on ws://localhost:9505');
99
+ ```
100
+
101
+ Run:
102
+ ```bash
103
+ node test-xmr-server.js
104
+ ```
105
+
106
+ Configure player to connect:
107
+ ```javascript
108
+ await xmrWrapper.start('ws://localhost:9505', 'test-key');
109
+ ```
110
+
111
+ ### Testing Commands
112
+
113
+ Send commands from mock server:
114
+
115
+ ```javascript
116
+ // collectNow
117
+ ws.send(JSON.stringify({ action: 'collectNow' }));
118
+
119
+ // screenShot
120
+ ws.send(JSON.stringify({ action: 'screenShot' }));
121
+
122
+ // changeLayout
123
+ ws.send(JSON.stringify({
124
+ action: 'changeLayout',
125
+ layoutId: 'layout-123'
126
+ }));
127
+
128
+ // criteriaUpdate
129
+ ws.send(JSON.stringify({
130
+ action: 'criteriaUpdate',
131
+ data: { displayId: '123', criteria: 'new-criteria' }
132
+ }));
133
+
134
+ // currentGeoLocation
135
+ ws.send(JSON.stringify({
136
+ action: 'currentGeoLocation',
137
+ data: { latitude: 40.7128, longitude: -74.0060 }
138
+ }));
139
+ ```
140
+
141
+ ---
142
+
143
+ ## Manual Testing
144
+
145
+ ### Prerequisites
146
+
147
+ 1. **XMR-enabled CMS**: Xibo CMS 2.3+ with XMR configured
148
+ 2. **Player**: Xibo PWA/Linux player with XMR package
149
+ 3. **Network**: Player can reach CMS XMR endpoint (typically port 9505)
150
+
151
+ ### Testing Steps
152
+
153
+ #### 1. Enable XMR in CMS
154
+
155
+ 1. Go to **Administration** → **Settings** → **Displays**
156
+ 2. Set **Enable XMR** to **Yes**
157
+ 3. Configure **XMR Address** (e.g., `ws://your-cms.com:9505`)
158
+ 4. Save settings
159
+
160
+ #### 2. Configure Player
161
+
162
+ Player should receive XMR settings automatically from `registerDisplay`:
163
+
164
+ ```javascript
165
+ // In registerDisplay response
166
+ {
167
+ settings: {
168
+ xmrWebSocketAddress: 'wss://cms.example.com:9505',
169
+ xmrCmsKey: 'your-cms-key-here'
170
+ }
171
+ }
172
+ ```
173
+
174
+ #### 3. Verify Connection
175
+
176
+ Check player logs:
177
+ ```
178
+ [XMR] Initializing connection to: wss://cms.example.com:9505
179
+ [XMR] WebSocket connected
180
+ [XMR] Connected successfully
181
+ ```
182
+
183
+ Check CMS logs (XMR server):
184
+ ```
185
+ Player connected: player-hw-key-123
186
+ ```
187
+
188
+ #### 4. Test Commands
189
+
190
+ From CMS display management page:
191
+
192
+ **Test collectNow:**
193
+ 1. Click display name
194
+ 2. Click **Send Command** → **Collect Now**
195
+ 3. Verify player logs:
196
+ ```
197
+ [XMR] Received collectNow command from CMS
198
+ [XMR] collectNow completed successfully
199
+ ```
200
+
201
+ **Test screenShot:**
202
+ 1. Click **Send Command** → **Request Screenshot**
203
+ 2. Verify screenshot appears in display's **Screenshots** tab
204
+
205
+ **Test changeLayout:**
206
+ 1. Click **Send Command** → **Change Layout**
207
+ 2. Select layout
208
+ 3. Verify player switches immediately
209
+
210
+ #### 5. Test Reconnection
211
+
212
+ **Simulate connection loss:**
213
+ 1. Stop XMR server on CMS
214
+ 2. Verify player logs:
215
+ ```
216
+ [XMR] WebSocket disconnected
217
+ [XMR] Connection lost, scheduling reconnection...
218
+ [XMR] Scheduling reconnect attempt 1/10 in 5000ms
219
+ ```
220
+ 3. Restart XMR server
221
+ 4. Verify player reconnects automatically
222
+
223
+ ---
224
+
225
+ ## Real XMR Server Testing
226
+
227
+ ### Setup
228
+
229
+ 1. **Install Xibo CMS** with Docker:
230
+ ```bash
231
+ git clone https://github.com/xibosignage/xibo-docker
232
+ cd xibo-docker
233
+ docker-compose up -d
234
+ ```
235
+
236
+ 2. **Enable XMR** in CMS settings (see Manual Testing above)
237
+
238
+ 3. **Configure player** with CMS credentials:
239
+ ```javascript
240
+ const config = {
241
+ cmsAddress: 'http://localhost',
242
+ hardwareKey: 'test-player-123',
243
+ serverKey: 'your-server-key'
244
+ };
245
+ ```
246
+
247
+ ### Test Scenarios
248
+
249
+ #### Scenario 1: Basic Commands
250
+
251
+ 1. Register player with CMS
252
+ 2. Send collectNow via CMS display page
253
+ 3. Verify XMDS collection triggered
254
+ 4. Send screenShot
255
+ 5. Verify screenshot uploaded
256
+ 6. Send changeLayout
257
+ 7. Verify layout changes
258
+
259
+ **Expected**: All commands execute successfully
260
+
261
+ #### Scenario 2: Network Interruption
262
+
263
+ 1. Establish XMR connection
264
+ 2. Block port 9505 with firewall:
265
+ ```bash
266
+ sudo iptables -A OUTPUT -p tcp --dport 9505 -j DROP
267
+ ```
268
+ 3. Wait for reconnection attempts
269
+ 4. Unblock port:
270
+ ```bash
271
+ sudo iptables -D OUTPUT -p tcp --dport 9505 -j DROP
272
+ ```
273
+ 5. Verify automatic reconnection
274
+
275
+ **Expected**: Player reconnects within 50 seconds (max 10 attempts × 5s)
276
+
277
+ #### Scenario 3: Multiple Players
278
+
279
+ 1. Start 5 players with different hardware keys
280
+ 2. Send collectNow to all
281
+ 3. Verify all receive command
282
+ 4. Send changeLayout to specific player
283
+ 5. Verify only that player changes layout
284
+
285
+ **Expected**: Commands routed to correct players
286
+
287
+ #### Scenario 4: High Frequency Commands
288
+
289
+ 1. Send 10 collectNow commands in 10 seconds
290
+ 2. Verify all execute without dropping
291
+ 3. Monitor memory usage
292
+
293
+ **Expected**: No memory leaks, all commands processed
294
+
295
+ #### Scenario 5: CMS Upgrade
296
+
297
+ 1. Establish XMR connection
298
+ 2. Upgrade CMS (restart XMR server)
299
+ 3. Verify player reconnects after upgrade
300
+
301
+ **Expected**: Automatic reconnection within 5-10 seconds
302
+
303
+ ---
304
+
305
+ ## Troubleshooting
306
+
307
+ ### Connection Issues
308
+
309
+ **Problem**: XMR won't connect
310
+
311
+ **Checks**:
312
+ 1. Verify XMR enabled in CMS settings
313
+ 2. Check firewall allows port 9505
314
+ 3. Verify `xmrWebSocketAddress` in registerDisplay response
315
+ 4. Check player logs for errors
316
+
317
+ **Solution**:
318
+ ```bash
319
+ # Test XMR connectivity
320
+ wscat -c wss://cms.example.com:9505
321
+
322
+ # Check XMR server status
323
+ docker logs xibo-xmr
324
+ ```
325
+
326
+ ---
327
+
328
+ ### Commands Not Executing
329
+
330
+ **Problem**: collectNow sent but not executed
331
+
332
+ **Checks**:
333
+ 1. Verify XMR connected (`wrapper.isConnected() === true`)
334
+ 2. Check player logs for command reception
335
+ 3. Verify player.collect() method exists
336
+ 4. Check for errors in command handler
337
+
338
+ **Solution**:
339
+ ```javascript
340
+ // Add debug logging
341
+ this.xmr.on('collectNow', async () => {
342
+ console.log('[XMR] collectNow handler triggered');
343
+ console.log('[XMR] player.collect:', typeof this.player.collect);
344
+ // ...
345
+ });
346
+ ```
347
+
348
+ ---
349
+
350
+ ### Reconnection Loops
351
+
352
+ **Problem**: Player keeps reconnecting endlessly
353
+
354
+ **Checks**:
355
+ 1. Verify XMR server is actually running
356
+ 2. Check for auth errors (wrong cmsKey)
357
+ 3. Monitor reconnectAttempts counter
358
+
359
+ **Solution**:
360
+ ```javascript
361
+ // Check reconnect state
362
+ console.log('Reconnect attempts:', wrapper.reconnectAttempts);
363
+ console.log('Max attempts:', wrapper.maxReconnectAttempts);
364
+
365
+ // Manually stop reconnecting
366
+ await wrapper.stop();
367
+ ```
368
+
369
+ ---
370
+
371
+ ### Memory Leaks
372
+
373
+ **Problem**: Memory usage grows over time
374
+
375
+ **Checks**:
376
+ 1. Verify `stop()` clears timers
377
+ 2. Check for event listener leaks
378
+ 3. Monitor with Chrome DevTools heap snapshots
379
+
380
+ **Solution**:
381
+ ```javascript
382
+ // Ensure cleanup on shutdown
383
+ await wrapper.stop();
384
+ expect(wrapper.reconnectTimer).toBeNull();
385
+
386
+ // Check event listeners
387
+ console.log('XMR listeners:', wrapper.xmr?.events?.size);
388
+ ```
389
+
390
+ ---
391
+
392
+ ## Performance Benchmarks
393
+
394
+ Expected performance metrics:
395
+
396
+ | Metric | Target | Acceptable |
397
+ |--------|--------|------------|
398
+ | Connection time | < 1s | < 3s |
399
+ | Command execution | < 100ms | < 500ms |
400
+ | Reconnection time | < 5s | < 30s |
401
+ | Memory overhead | < 5MB | < 10MB |
402
+ | CPU usage | < 1% | < 5% |
403
+
404
+ Measure with:
405
+ ```javascript
406
+ // Connection time
407
+ const start = Date.now();
408
+ await wrapper.start(url, key);
409
+ console.log('Connect time:', Date.now() - start);
410
+
411
+ // Command execution
412
+ const cmdStart = Date.now();
413
+ xmr.simulateCommand('collectNow');
414
+ await waitFor(() => mockPlayer.collect.called);
415
+ console.log('Exec time:', Date.now() - cmdStart);
416
+
417
+ // Memory
418
+ const baseline = process.memoryUsage().heapUsed;
419
+ await wrapper.start(url, key);
420
+ const withXmr = process.memoryUsage().heapUsed;
421
+ console.log('Memory overhead:', (withXmr - baseline) / 1024 / 1024, 'MB');
422
+ ```
423
+
424
+ ---
425
+
426
+ ## Continuous Integration
427
+
428
+ ### GitHub Actions
429
+
430
+ ```yaml
431
+ name: XMR Tests
432
+
433
+ on: [push, pull_request]
434
+
435
+ jobs:
436
+ test:
437
+ runs-on: ubuntu-latest
438
+ steps:
439
+ - uses: actions/checkout@v2
440
+ - uses: actions/setup-node@v2
441
+ with:
442
+ node-version: '18'
443
+ - run: npm install
444
+ - run: npm test
445
+ working-directory: packages/xmr
446
+ - run: npm run test:coverage
447
+ working-directory: packages/xmr
448
+ - uses: codecov/codecov-action@v2
449
+ with:
450
+ files: packages/xmr/coverage/lcov.info
451
+ ```
452
+
453
+ ---
454
+
455
+ ## Test Data
456
+
457
+ ### Sample registerDisplay Response
458
+
459
+ ```json
460
+ {
461
+ "displayName": "Test Display",
462
+ "settings": {
463
+ "collectInterval": "300",
464
+ "xmrWebSocketAddress": "wss://cms.example.com:9505",
465
+ "xmrCmsKey": "abcdef123456",
466
+ "xmrChannel": "player-hw-key-123"
467
+ }
468
+ }
469
+ ```
470
+
471
+ ### Sample XMR Messages
472
+
473
+ ```javascript
474
+ // collectNow
475
+ { "action": "collectNow" }
476
+
477
+ // screenShot
478
+ { "action": "screenShot" }
479
+
480
+ // changeLayout
481
+ { "action": "changeLayout", "layoutId": "42" }
482
+
483
+ // criteriaUpdate
484
+ {
485
+ "action": "criteriaUpdate",
486
+ "data": {
487
+ "displayId": "123",
488
+ "criteria": "tag:urgent,location:lobby"
489
+ }
490
+ }
491
+
492
+ // currentGeoLocation
493
+ {
494
+ "action": "currentGeoLocation",
495
+ "data": {
496
+ "latitude": 40.7128,
497
+ "longitude": -74.0060
498
+ }
499
+ }
500
+ ```
501
+
502
+ ---
503
+
504
+ ## References
505
+
506
+ - XMR Commands: [XMR_COMMANDS.md](./XMR_COMMANDS.md)
507
+ - XMR Library: [@xibosignage/xibo-communication-framework](https://www.npmjs.com/package/@xibosignage/xibo-communication-framework)
508
+ - Xibo CMS: https://xibosignage.com
509
+ - WebSocket Testing: https://github.com/websockets/wscat
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@xiboplayer/xmr",
3
+ "version": "0.1.0",
4
+ "description": "XMR WebSocket client for real-time Xibo CMS commands",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js"
9
+ },
10
+ "dependencies": {
11
+ "@xibosignage/xibo-communication-framework": "^0.0.6",
12
+ "@xiboplayer/utils": "0.1.0"
13
+ },
14
+ "devDependencies": {
15
+ "vitest": "^2.0.0"
16
+ },
17
+ "keywords": [
18
+ "xibo",
19
+ "digital-signage",
20
+ "xmr",
21
+ "websocket",
22
+ "real-time"
23
+ ],
24
+ "author": "Pau Aliagas <linuxnow@gmail.com>",
25
+ "license": "AGPL-3.0-or-later",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/xibo-players/xiboplayer.git",
29
+ "directory": "packages/xmr"
30
+ },
31
+ "scripts": {
32
+ "test": "vitest run",
33
+ "test:watch": "vitest",
34
+ "test:coverage": "vitest run --coverage"
35
+ }
36
+ }
package/src/index.js ADDED
@@ -0,0 +1,2 @@
1
+ // @xiboplayer/xmr - XMR WebSocket client
2
+ export { XmrWrapper } from './xmr-wrapper.js';
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Test Utilities for XMR Package
3
+ *
4
+ * Provides mocking utilities, test helpers, and fixtures for XMR tests.
5
+ */
6
+
7
+ import { vi } from 'vitest';
8
+
9
+ /**
10
+ * Create a spy that tracks calls but doesn't interfere
11
+ *
12
+ * Usage:
13
+ * const spy = createSpy();
14
+ * xmr.on('event', spy);
15
+ * // ... trigger event
16
+ * expect(spy).toHaveBeenCalledWith('arg1', 'arg2');
17
+ */
18
+ export function createSpy() {
19
+ return vi.fn();
20
+ }
21
+
22
+ /**
23
+ * Mock Xmr class from @xibosignage/xibo-communication-framework
24
+ *
25
+ * Usage:
26
+ * const MockXmr = mockXmr();
27
+ * // Use in tests
28
+ */
29
+ export function mockXmr() {
30
+ class MockXmr {
31
+ constructor(channel) {
32
+ this.channel = channel;
33
+ this.events = new Map();
34
+ this.connected = false;
35
+ this.init = vi.fn(() => Promise.resolve());
36
+ this.start = vi.fn(() => {
37
+ this.connected = true;
38
+ this.emit('connected');
39
+ return Promise.resolve();
40
+ });
41
+ this.stop = vi.fn(() => {
42
+ this.connected = false;
43
+ this.emit('disconnected');
44
+ return Promise.resolve();
45
+ });
46
+ this.send = vi.fn(() => Promise.resolve());
47
+ }
48
+
49
+ on(event, callback) {
50
+ if (!this.events.has(event)) {
51
+ this.events.set(event, []);
52
+ }
53
+ this.events.get(event).push(callback);
54
+ }
55
+
56
+ emit(event, ...args) {
57
+ const listeners = this.events.get(event);
58
+ if (listeners) {
59
+ listeners.forEach(callback => callback(...args));
60
+ }
61
+ }
62
+
63
+ // Simulate CMS sending a command
64
+ simulateCommand(command, data) {
65
+ this.emit(command, data);
66
+ }
67
+ }
68
+
69
+ return MockXmr;
70
+ }
71
+
72
+ /**
73
+ * Mock player instance with all required methods
74
+ *
75
+ * Usage:
76
+ * const mockPlayer = createMockPlayer();
77
+ * const wrapper = new XmrWrapper(config, mockPlayer);
78
+ */
79
+ export function createMockPlayer() {
80
+ return {
81
+ collect: vi.fn(() => Promise.resolve()),
82
+ captureScreenshot: vi.fn(() => Promise.resolve()),
83
+ changeLayout: vi.fn(() => Promise.resolve()),
84
+ overlayLayout: vi.fn(() => Promise.resolve()),
85
+ revertToSchedule: vi.fn(() => Promise.resolve()),
86
+ purgeAll: vi.fn(() => Promise.resolve()),
87
+ executeCommand: vi.fn(() => Promise.resolve()),
88
+ triggerWebhook: vi.fn(),
89
+ refreshDataConnectors: vi.fn(),
90
+ reportGeoLocation: vi.fn(() => Promise.resolve()),
91
+ updateStatus: vi.fn()
92
+ };
93
+ }
94
+
95
+ /**
96
+ * Mock config for XMR tests
97
+ */
98
+ export function createMockConfig(overrides = {}) {
99
+ return {
100
+ cmsAddress: 'https://test.cms.com',
101
+ hardwareKey: 'test-hw-key',
102
+ serverKey: 'test-server-key',
103
+ xmrChannel: 'test-channel',
104
+ ...overrides
105
+ };
106
+ }
107
+
108
+ /**
109
+ * Wait for condition to be true
110
+ *
111
+ * Usage:
112
+ * await waitFor(() => wrapper.isConnected(), 5000);
113
+ */
114
+ export async function waitFor(condition, timeout = 5000) {
115
+ const start = Date.now();
116
+ while (!condition()) {
117
+ if (Date.now() - start > timeout) {
118
+ throw new Error('waitFor timeout');
119
+ }
120
+ await new Promise(resolve => setTimeout(resolve, 50));
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Wait for a specific time (for testing timing-dependent logic)
126
+ *
127
+ * Usage:
128
+ * await wait(100); // Wait 100ms
129
+ */
130
+ export async function wait(ms) {
131
+ return new Promise(resolve => setTimeout(resolve, ms));
132
+ }