@xiboplayer/xmr 0.6.4 → 0.6.6
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 +2 -2
- package/src/test-utils.js +1 -1
- package/src/xmr-wrapper.js +23 -87
- package/src/xmr-wrapper.test.js +31 -135
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiboplayer/xmr",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.6",
|
|
4
4
|
"description": "XMR WebSocket client for real-time Xibo CMS commands",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xibosignage/xibo-communication-framework": "^0.0.6",
|
|
13
|
-
"@xiboplayer/utils": "0.6.
|
|
13
|
+
"@xiboplayer/utils": "0.6.6"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
16
|
"vitest": "^2.0.0"
|
package/src/test-utils.js
CHANGED
|
@@ -89,7 +89,7 @@ export function createMockPlayer() {
|
|
|
89
89
|
refreshDataConnectors: vi.fn(),
|
|
90
90
|
reportGeoLocation: vi.fn(() => Promise.resolve()),
|
|
91
91
|
requestGeoLocation: vi.fn(() => Promise.resolve({ latitude: 41.3851, longitude: 2.1734 })),
|
|
92
|
-
|
|
92
|
+
emit: vi.fn()
|
|
93
93
|
};
|
|
94
94
|
}
|
|
95
95
|
|
package/src/xmr-wrapper.js
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
* Integrates the official @xibosignage/xibo-communication-framework
|
|
5
5
|
* to enable real-time push commands from CMS via WebSocket.
|
|
6
6
|
*
|
|
7
|
+
* Connection lifecycle is delegated to the framework, which has a
|
|
8
|
+
* built-in 60s health-check interval that reconnects automatically.
|
|
9
|
+
* This wrapper only routes events to player callbacks.
|
|
10
|
+
*
|
|
7
11
|
* Supported commands:
|
|
8
12
|
* - collectNow: Trigger immediate XMDS collection cycle
|
|
9
13
|
* - screenShot/screenshot: Capture and upload screenshot
|
|
@@ -35,61 +39,40 @@ export class XmrWrapper {
|
|
|
35
39
|
this.player = player;
|
|
36
40
|
this.xmr = null;
|
|
37
41
|
this.connected = false;
|
|
38
|
-
this.reconnectAttempts = 0;
|
|
39
|
-
this.maxReconnectAttempts = 10;
|
|
40
|
-
this.reconnectDelay = 5000; // 5 seconds
|
|
41
|
-
this.lastXmrUrl = null;
|
|
42
|
-
this.lastCmsKey = null;
|
|
43
|
-
this.reconnectTimer = null;
|
|
44
|
-
this.intentionalShutdown = false;
|
|
45
42
|
}
|
|
46
43
|
|
|
47
44
|
/**
|
|
48
|
-
* Initialize and start XMR connection
|
|
45
|
+
* Initialize and start XMR connection.
|
|
46
|
+
*
|
|
47
|
+
* Creates a single Xmr instance and lets the framework manage
|
|
48
|
+
* reconnection via its internal 60s health-check timer.
|
|
49
|
+
* Calling start() again on an already-running instance is safe —
|
|
50
|
+
* the framework skips if already connected to the same URL.
|
|
51
|
+
*
|
|
49
52
|
* @param {string} xmrUrl - WebSocket URL (ws:// or wss://)
|
|
50
53
|
* @param {string} cmsKey - CMS authentication key
|
|
51
54
|
* @returns {Promise<boolean>} Success status
|
|
52
55
|
*/
|
|
53
56
|
async start(xmrUrl, cmsKey) {
|
|
54
57
|
try {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
// Clear intentional shutdown flag (we're starting again)
|
|
58
|
-
this.intentionalShutdown = false;
|
|
59
|
-
|
|
60
|
-
// Save connection details for reconnection
|
|
61
|
-
this.lastXmrUrl = xmrUrl;
|
|
62
|
-
this.lastCmsKey = cmsKey;
|
|
63
|
-
|
|
64
|
-
// Cancel any pending reconnect attempts
|
|
65
|
-
if (this.reconnectTimer) {
|
|
66
|
-
clearTimeout(this.reconnectTimer);
|
|
67
|
-
this.reconnectTimer = null;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Create XMR instance with channel ID (or reuse if already exists)
|
|
58
|
+
// Reuse existing instance — the framework handles reconnection.
|
|
59
|
+
// Only create a new instance on first call or after stop().
|
|
71
60
|
if (!this.xmr) {
|
|
61
|
+
log.info('Initializing connection to:', xmrUrl);
|
|
72
62
|
const channel = this.config.xmrChannel || `player-${this.config.hardwareKey}`;
|
|
73
63
|
this.xmr = new Xmr(channel);
|
|
74
|
-
// Setup event handlers before connecting (only once)
|
|
75
64
|
this.setupEventHandlers();
|
|
65
|
+
await this.xmr.init();
|
|
76
66
|
}
|
|
77
67
|
|
|
78
|
-
// Initialize and connect
|
|
79
|
-
await this.xmr.init();
|
|
80
68
|
await this.xmr.start(xmrUrl, cmsKey);
|
|
81
|
-
|
|
82
69
|
this.connected = true;
|
|
83
|
-
this.reconnectAttempts = 0;
|
|
84
70
|
log.info('Connected successfully');
|
|
85
71
|
|
|
86
72
|
return true;
|
|
87
73
|
} catch (error) {
|
|
88
74
|
log.warn('Failed to start:', error.message);
|
|
89
|
-
log.info('
|
|
90
|
-
|
|
91
|
-
// Schedule reconnection attempt
|
|
92
|
-
this.scheduleReconnect(xmrUrl, cmsKey);
|
|
75
|
+
log.info('Framework will retry automatically every 60s');
|
|
93
76
|
|
|
94
77
|
return false;
|
|
95
78
|
}
|
|
@@ -105,21 +88,13 @@ export class XmrWrapper {
|
|
|
105
88
|
this.xmr.on('connected', () => {
|
|
106
89
|
log.info('WebSocket connected');
|
|
107
90
|
this.connected = true;
|
|
108
|
-
this.
|
|
109
|
-
this.player.updateStatus?.('XMR connected');
|
|
91
|
+
this.player.emit?.('xmr-status', { connected: true });
|
|
110
92
|
});
|
|
111
93
|
|
|
112
94
|
this.xmr.on('disconnected', () => {
|
|
113
|
-
log.warn('WebSocket disconnected');
|
|
95
|
+
log.warn('WebSocket disconnected (framework will reconnect)');
|
|
114
96
|
this.connected = false;
|
|
115
|
-
this.player.
|
|
116
|
-
|
|
117
|
-
// Attempt to reconnect if we have the connection details
|
|
118
|
-
// BUT not if this was an intentional shutdown
|
|
119
|
-
if (this.lastXmrUrl && this.lastCmsKey && !this.intentionalShutdown) {
|
|
120
|
-
log.info('Connection lost, scheduling reconnection...');
|
|
121
|
-
this.scheduleReconnect(this.lastXmrUrl, this.lastCmsKey);
|
|
122
|
-
}
|
|
97
|
+
this.player.emit?.('xmr-status', { connected: false });
|
|
123
98
|
});
|
|
124
99
|
|
|
125
100
|
this.xmr.on('error', (error) => {
|
|
@@ -151,8 +126,6 @@ export class XmrWrapper {
|
|
|
151
126
|
// CMS command: License Check (no-op for Linux clients)
|
|
152
127
|
this.xmr.on('licenceCheck', () => {
|
|
153
128
|
log.debug('Received licenceCheck (no-op for Linux client)');
|
|
154
|
-
// Linux clients always report valid license
|
|
155
|
-
// No action needed - clientType: "linux" bypasses commercial license
|
|
156
129
|
});
|
|
157
130
|
|
|
158
131
|
// CMS command: Change Layout
|
|
@@ -220,7 +193,6 @@ export class XmrWrapper {
|
|
|
220
193
|
const commandCode = data?.commandCode || data;
|
|
221
194
|
log.info('Received commandAction command:', commandCode);
|
|
222
195
|
try {
|
|
223
|
-
// Use local commands from RegisterDisplay (stored on player), not XMR payload commands
|
|
224
196
|
const localCommands = this.player.displayCommands || data?.commands;
|
|
225
197
|
await this.player.executeCommand(commandCode, localCommands);
|
|
226
198
|
log.debug('commandAction completed successfully');
|
|
@@ -279,7 +251,6 @@ export class XmrWrapper {
|
|
|
279
251
|
this.xmr.on('criteriaUpdate', async (data) => {
|
|
280
252
|
log.info('Received criteriaUpdate command:', data);
|
|
281
253
|
try {
|
|
282
|
-
// Trigger immediate collection to get updated display criteria
|
|
283
254
|
await this.player.collect();
|
|
284
255
|
log.debug('criteriaUpdate completed successfully');
|
|
285
256
|
} catch (error) {
|
|
@@ -296,7 +267,6 @@ export class XmrWrapper {
|
|
|
296
267
|
const hasCoordinates = data && data.latitude != null && data.longitude != null;
|
|
297
268
|
|
|
298
269
|
if (hasCoordinates) {
|
|
299
|
-
// CMS is pushing coordinates to us
|
|
300
270
|
if (this.player.reportGeoLocation) {
|
|
301
271
|
this.player.reportGeoLocation(data);
|
|
302
272
|
log.debug('currentGeoLocation: coordinates applied');
|
|
@@ -304,7 +274,6 @@ export class XmrWrapper {
|
|
|
304
274
|
log.warn('Geo location reporting not implemented in player');
|
|
305
275
|
}
|
|
306
276
|
} else {
|
|
307
|
-
// CMS is asking us to report our location via browser API
|
|
308
277
|
if (this.player.requestGeoLocation) {
|
|
309
278
|
await this.player.requestGeoLocation();
|
|
310
279
|
log.debug('currentGeoLocation: browser location requested');
|
|
@@ -319,50 +288,17 @@ export class XmrWrapper {
|
|
|
319
288
|
}
|
|
320
289
|
|
|
321
290
|
/**
|
|
322
|
-
*
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
326
|
-
log.warn('Max reconnection attempts reached, giving up');
|
|
327
|
-
log.info('Will retry on next collection cycle');
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// Cancel any existing reconnect timer
|
|
332
|
-
if (this.reconnectTimer) {
|
|
333
|
-
clearTimeout(this.reconnectTimer);
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
this.reconnectAttempts++;
|
|
337
|
-
const delay = this.reconnectDelay * this.reconnectAttempts;
|
|
338
|
-
|
|
339
|
-
log.debug(`Scheduling reconnect attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay}ms`);
|
|
340
|
-
|
|
341
|
-
this.reconnectTimer = setTimeout(() => {
|
|
342
|
-
log.debug('Attempting to reconnect...');
|
|
343
|
-
this.reconnectTimer = null;
|
|
344
|
-
this.start(xmrUrl, cmsKey);
|
|
345
|
-
}, delay);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
/**
|
|
349
|
-
* Stop XMR connection
|
|
291
|
+
* Stop XMR connection and clean up the framework instance.
|
|
292
|
+
* The framework's internal 60s timer is cleared when the instance
|
|
293
|
+
* is discarded, so no reconnection will occur after stop().
|
|
350
294
|
*/
|
|
351
295
|
async stop() {
|
|
352
|
-
// Mark as intentional shutdown to prevent reconnection
|
|
353
|
-
this.intentionalShutdown = true;
|
|
354
|
-
|
|
355
|
-
// Cancel any pending reconnect attempts
|
|
356
|
-
if (this.reconnectTimer) {
|
|
357
|
-
clearTimeout(this.reconnectTimer);
|
|
358
|
-
this.reconnectTimer = null;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
296
|
if (!this.xmr) return;
|
|
362
297
|
|
|
363
298
|
try {
|
|
364
299
|
await this.xmr.stop();
|
|
365
300
|
this.connected = false;
|
|
301
|
+
this.xmr = null;
|
|
366
302
|
log.info('Stopped');
|
|
367
303
|
} catch (error) {
|
|
368
304
|
log.error('Error stopping:', error);
|
package/src/xmr-wrapper.test.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* XmrWrapper Tests
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Tests connection lifecycle, all CMS commands, and error handling.
|
|
5
|
+
* Reconnection is delegated to the framework's built-in 60s health-check.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
@@ -84,13 +84,6 @@ describe('XmrWrapper', () => {
|
|
|
84
84
|
expect(wrapper.connected).toBe(false);
|
|
85
85
|
expect(wrapper.xmr).toBeNull();
|
|
86
86
|
});
|
|
87
|
-
|
|
88
|
-
it('should initialize reconnection properties', () => {
|
|
89
|
-
expect(wrapper.reconnectAttempts).toBe(0);
|
|
90
|
-
expect(wrapper.maxReconnectAttempts).toBe(10);
|
|
91
|
-
expect(wrapper.reconnectDelay).toBe(5000);
|
|
92
|
-
expect(wrapper.reconnectTimer).toBeNull();
|
|
93
|
-
});
|
|
94
87
|
});
|
|
95
88
|
|
|
96
89
|
describe('start(xmrUrl, cmsKey)', () => {
|
|
@@ -102,27 +95,27 @@ describe('XmrWrapper', () => {
|
|
|
102
95
|
expect(wrapper.xmr).toBeDefined();
|
|
103
96
|
expect(wrapper.xmr.init).toHaveBeenCalled();
|
|
104
97
|
expect(wrapper.xmr.start).toHaveBeenCalledWith('wss://test.xmr.com', 'cms-key-123');
|
|
105
|
-
expect(wrapper.reconnectAttempts).toBe(0);
|
|
106
98
|
});
|
|
107
99
|
|
|
108
|
-
it('should
|
|
100
|
+
it('should reuse existing xmr instance on subsequent start() calls', async () => {
|
|
101
|
+
await wrapper.start('wss://test.xmr.com', 'cms-key-123');
|
|
102
|
+
const firstInstance = wrapper.xmr;
|
|
103
|
+
|
|
109
104
|
await wrapper.start('wss://test.xmr.com', 'cms-key-123');
|
|
110
105
|
|
|
111
|
-
expect(wrapper.
|
|
112
|
-
expect(
|
|
106
|
+
expect(wrapper.xmr).toBe(firstInstance);
|
|
107
|
+
expect(firstInstance.init).toHaveBeenCalledTimes(1);
|
|
108
|
+
expect(firstInstance.start).toHaveBeenCalledTimes(2);
|
|
113
109
|
});
|
|
114
110
|
|
|
115
|
-
it('should
|
|
111
|
+
it('should create new instance after stop()', async () => {
|
|
116
112
|
await wrapper.start('wss://test.xmr.com', 'cms-key-123');
|
|
117
113
|
const firstInstance = wrapper.xmr;
|
|
118
114
|
|
|
119
|
-
|
|
120
|
-
wrapper.connected = false;
|
|
121
|
-
|
|
115
|
+
await wrapper.stop();
|
|
122
116
|
await wrapper.start('wss://test.xmr.com', 'cms-key-123');
|
|
123
|
-
const secondInstance = wrapper.xmr;
|
|
124
117
|
|
|
125
|
-
expect(
|
|
118
|
+
expect(wrapper.xmr).not.toBe(firstInstance);
|
|
126
119
|
});
|
|
127
120
|
|
|
128
121
|
it('should use custom xmrChannel if provided', async () => {
|
|
@@ -144,40 +137,18 @@ describe('XmrWrapper', () => {
|
|
|
144
137
|
});
|
|
145
138
|
|
|
146
139
|
it('should handle connection failure gracefully', async () => {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const result = await newWrapper.start('wss://test.xmr.com', 'cms-key-123');
|
|
156
|
-
expect(result).toBe(false);
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it('should schedule reconnect on failure', async () => {
|
|
161
|
-
const newWrapper = new XmrWrapper(mockConfig, mockPlayer);
|
|
162
|
-
await newWrapper.start('wss://test.xmr.com', 'cms-key-123');
|
|
140
|
+
// Hook setupEventHandlers to patch the freshly-created Xmr instance
|
|
141
|
+
// (init is an instance property, so prototype patching doesn't work)
|
|
142
|
+
const origSetup = wrapper.setupEventHandlers.bind(wrapper);
|
|
143
|
+
wrapper.setupEventHandlers = function() {
|
|
144
|
+
origSetup.call(this);
|
|
145
|
+
this.xmr.init = vi.fn(() => Promise.reject(new Error('Connection failed')));
|
|
146
|
+
};
|
|
163
147
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
newWrapper.xmr.start = vi.fn(() => Promise.reject(new Error('Connection failed')));
|
|
167
|
-
newWrapper.connected = false;
|
|
168
|
-
|
|
169
|
-
await newWrapper.start('wss://test.xmr.com', 'cms-key-123');
|
|
170
|
-
expect(newWrapper.reconnectTimer).toBeDefined();
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it('should cancel pending reconnect timer on new start', async () => {
|
|
175
|
-
wrapper.reconnectTimer = setTimeout(() => {}, 5000);
|
|
176
|
-
const timerId = wrapper.reconnectTimer;
|
|
177
|
-
|
|
178
|
-
await wrapper.start('wss://test.xmr.com', 'cms-key-123');
|
|
148
|
+
const result = await wrapper.start('wss://test.xmr.com', 'cms-key-123');
|
|
149
|
+
expect(result).toBe(false);
|
|
179
150
|
|
|
180
|
-
|
|
151
|
+
wrapper.setupEventHandlers = origSetup;
|
|
181
152
|
});
|
|
182
153
|
});
|
|
183
154
|
|
|
@@ -194,8 +165,7 @@ describe('XmrWrapper', () => {
|
|
|
194
165
|
xmrInstance.simulateCommand('connected');
|
|
195
166
|
|
|
196
167
|
expect(wrapper.connected).toBe(true);
|
|
197
|
-
expect(
|
|
198
|
-
expect(mockPlayer.updateStatus).toHaveBeenCalledWith('XMR connected');
|
|
168
|
+
expect(mockPlayer.emit).toHaveBeenCalledWith('xmr-status', { connected: true });
|
|
199
169
|
});
|
|
200
170
|
|
|
201
171
|
it('should handle disconnected event', () => {
|
|
@@ -204,15 +174,7 @@ describe('XmrWrapper', () => {
|
|
|
204
174
|
xmrInstance.simulateCommand('disconnected');
|
|
205
175
|
|
|
206
176
|
expect(wrapper.connected).toBe(false);
|
|
207
|
-
expect(mockPlayer.
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it('should schedule reconnect on disconnect', () => {
|
|
211
|
-
wrapper.connected = true;
|
|
212
|
-
|
|
213
|
-
xmrInstance.simulateCommand('disconnected');
|
|
214
|
-
|
|
215
|
-
expect(wrapper.reconnectTimer).toBeDefined();
|
|
177
|
+
expect(mockPlayer.emit).toHaveBeenCalledWith('xmr-status', { connected: false });
|
|
216
178
|
});
|
|
217
179
|
|
|
218
180
|
it('should handle error event', () => {
|
|
@@ -591,69 +553,17 @@ describe('XmrWrapper', () => {
|
|
|
591
553
|
});
|
|
592
554
|
});
|
|
593
555
|
|
|
594
|
-
describe('Reconnection Logic', () => {
|
|
595
|
-
it('should schedule reconnect with exponential backoff', async () => {
|
|
596
|
-
const newWrapper = new XmrWrapper(mockConfig, mockPlayer);
|
|
597
|
-
await newWrapper.start('wss://test.xmr.com', 'cms-key-123');
|
|
598
|
-
|
|
599
|
-
// Make subsequent starts fail
|
|
600
|
-
if (newWrapper.xmr) {
|
|
601
|
-
newWrapper.xmr.start = vi.fn(() => Promise.reject(new Error('Connection failed')));
|
|
602
|
-
newWrapper.connected = false;
|
|
603
|
-
|
|
604
|
-
// First reconnect attempt
|
|
605
|
-
await newWrapper.start('wss://test.xmr.com', 'cms-key-123');
|
|
606
|
-
expect(newWrapper.reconnectAttempts).toBe(1);
|
|
607
|
-
|
|
608
|
-
// Second reconnect attempt (should have longer delay)
|
|
609
|
-
await newWrapper.start('wss://test.xmr.com', 'cms-key-123');
|
|
610
|
-
expect(newWrapper.reconnectAttempts).toBe(2);
|
|
611
|
-
}
|
|
612
|
-
});
|
|
613
|
-
|
|
614
|
-
it('should stop reconnecting after max attempts', async () => {
|
|
615
|
-
wrapper.reconnectAttempts = wrapper.maxReconnectAttempts;
|
|
616
|
-
|
|
617
|
-
wrapper.scheduleReconnect('wss://test.xmr.com', 'cms-key-123');
|
|
618
|
-
|
|
619
|
-
expect(wrapper.reconnectTimer).toBeNull();
|
|
620
|
-
});
|
|
621
|
-
|
|
622
|
-
it('should cancel existing timer before scheduling new reconnect', () => {
|
|
623
|
-
wrapper.reconnectTimer = setTimeout(() => {}, 5000);
|
|
624
|
-
const firstTimer = wrapper.reconnectTimer;
|
|
625
|
-
|
|
626
|
-
wrapper.scheduleReconnect('wss://test.xmr.com', 'cms-key-123');
|
|
627
|
-
|
|
628
|
-
expect(wrapper.reconnectTimer).not.toBe(firstTimer);
|
|
629
|
-
});
|
|
630
|
-
|
|
631
|
-
it('should reset reconnect attempts on successful connection', async () => {
|
|
632
|
-
wrapper.reconnectAttempts = 5;
|
|
633
|
-
|
|
634
|
-
await wrapper.start('wss://test.xmr.com', 'cms-key-123');
|
|
635
|
-
|
|
636
|
-
expect(wrapper.reconnectAttempts).toBe(0);
|
|
637
|
-
});
|
|
638
|
-
});
|
|
639
|
-
|
|
640
556
|
describe('stop()', () => {
|
|
641
|
-
it('should stop XMR connection', async () => {
|
|
557
|
+
it('should stop XMR connection and null out instance', async () => {
|
|
642
558
|
await wrapper.start('wss://test.xmr.com', 'cms-key-123');
|
|
559
|
+
const xmrRef = wrapper.xmr;
|
|
643
560
|
wrapper.connected = true;
|
|
644
561
|
|
|
645
562
|
await wrapper.stop();
|
|
646
563
|
|
|
647
|
-
expect(
|
|
564
|
+
expect(xmrRef.stop).toHaveBeenCalled();
|
|
648
565
|
expect(wrapper.connected).toBe(false);
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
it('should cancel pending reconnect timer', async () => {
|
|
652
|
-
wrapper.reconnectTimer = setTimeout(() => {}, 5000);
|
|
653
|
-
|
|
654
|
-
await wrapper.stop();
|
|
655
|
-
|
|
656
|
-
expect(wrapper.reconnectTimer).toBeNull();
|
|
566
|
+
expect(wrapper.xmr).toBeNull();
|
|
657
567
|
});
|
|
658
568
|
|
|
659
569
|
it('should handle stop when not started', async () => {
|
|
@@ -662,7 +572,7 @@ describe('XmrWrapper', () => {
|
|
|
662
572
|
|
|
663
573
|
it('should handle stop errors gracefully', async () => {
|
|
664
574
|
await wrapper.start('wss://test.xmr.com', 'cms-key-123');
|
|
665
|
-
wrapper.xmr.stop.
|
|
575
|
+
wrapper.xmr.stop = vi.fn(() => Promise.reject(new Error('Stop failed')));
|
|
666
576
|
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
667
577
|
|
|
668
578
|
await wrapper.stop();
|
|
@@ -773,25 +683,11 @@ describe('XmrWrapper', () => {
|
|
|
773
683
|
});
|
|
774
684
|
|
|
775
685
|
describe('Memory Management', () => {
|
|
776
|
-
it('should
|
|
777
|
-
wrapper.reconnectTimer = setTimeout(() => {}, 5000);
|
|
778
|
-
|
|
779
|
-
await wrapper.stop();
|
|
780
|
-
|
|
781
|
-
expect(wrapper.reconnectTimer).toBeNull();
|
|
782
|
-
});
|
|
783
|
-
|
|
784
|
-
it('should allow garbage collection after stop', async () => {
|
|
686
|
+
it('should null out xmr instance on stop', async () => {
|
|
785
687
|
await wrapper.start('wss://test.xmr.com', 'cms-key-123');
|
|
786
688
|
await wrapper.stop();
|
|
787
689
|
|
|
788
|
-
|
|
789
|
-
// but stop() should cancel it
|
|
790
|
-
// Allow up to a small window for async cleanup
|
|
791
|
-
await vi.runAllTimersAsync();
|
|
792
|
-
|
|
793
|
-
// After all timers run and stop completes, timer should be null
|
|
794
|
-
expect(wrapper.reconnectTimer).toBeNull();
|
|
690
|
+
expect(wrapper.xmr).toBeNull();
|
|
795
691
|
});
|
|
796
692
|
});
|
|
797
693
|
});
|