cdp-skill 1.0.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/SKILL.md +543 -0
- package/install.js +92 -0
- package/package.json +47 -0
- package/src/aria.js +1302 -0
- package/src/capture.js +1359 -0
- package/src/cdp.js +905 -0
- package/src/cli.js +244 -0
- package/src/dom.js +3525 -0
- package/src/index.js +155 -0
- package/src/page.js +1720 -0
- package/src/runner.js +2111 -0
- package/src/tests/BrowserClient.test.js +588 -0
- package/src/tests/CDPConnection.test.js +598 -0
- package/src/tests/ChromeDiscovery.test.js +181 -0
- package/src/tests/ConsoleCapture.test.js +302 -0
- package/src/tests/ElementHandle.test.js +586 -0
- package/src/tests/ElementLocator.test.js +586 -0
- package/src/tests/ErrorAggregator.test.js +327 -0
- package/src/tests/InputEmulator.test.js +641 -0
- package/src/tests/NetworkErrorCapture.test.js +458 -0
- package/src/tests/PageController.test.js +822 -0
- package/src/tests/ScreenshotCapture.test.js +356 -0
- package/src/tests/SessionRegistry.test.js +257 -0
- package/src/tests/TargetManager.test.js +274 -0
- package/src/tests/TestRunner.test.js +1529 -0
- package/src/tests/WaitStrategy.test.js +406 -0
- package/src/tests/integration.test.js +431 -0
- package/src/utils.js +1034 -0
- package/uninstall.js +44 -0
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
import { describe, it, mock, beforeEach, afterEach } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Mock WebSocket class for testing
|
|
6
|
+
*/
|
|
7
|
+
class MockWebSocket {
|
|
8
|
+
constructor(url) {
|
|
9
|
+
this.url = url;
|
|
10
|
+
this.listeners = new Map();
|
|
11
|
+
this.sentMessages = [];
|
|
12
|
+
this.readyState = 0; // CONNECTING
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
on(event, callback) {
|
|
16
|
+
if (!this.listeners.has(event)) {
|
|
17
|
+
this.listeners.set(event, []);
|
|
18
|
+
}
|
|
19
|
+
this.listeners.get(event).push(callback);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
removeAllListeners() {
|
|
23
|
+
this.listeners.clear();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
send(message) {
|
|
27
|
+
this.sentMessages.push(JSON.parse(message));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
close() {
|
|
31
|
+
this.readyState = 3; // CLOSED
|
|
32
|
+
this.emit('close');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
emit(event, data) {
|
|
36
|
+
const callbacks = this.listeners.get(event) || [];
|
|
37
|
+
callbacks.forEach(cb => cb(data));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Simulate successful connection
|
|
41
|
+
simulateOpen() {
|
|
42
|
+
this.readyState = 1; // OPEN
|
|
43
|
+
this.emit('open');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Simulate receiving a message
|
|
47
|
+
simulateMessage(data) {
|
|
48
|
+
this.emit('message', Buffer.from(JSON.stringify(data)));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Simulate error
|
|
52
|
+
simulateError(error) {
|
|
53
|
+
this.emit('error', error);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// We need to test CDPConnection behavior without importing it (to avoid ws dependency)
|
|
58
|
+
// So we'll test the logic patterns that CDPConnection uses
|
|
59
|
+
|
|
60
|
+
describe('CDPConnection', () => {
|
|
61
|
+
describe('message ID generation', () => {
|
|
62
|
+
it('should increment message IDs', () => {
|
|
63
|
+
let messageId = 0;
|
|
64
|
+
const ids = [];
|
|
65
|
+
for (let i = 0; i < 5; i++) {
|
|
66
|
+
ids.push(++messageId);
|
|
67
|
+
}
|
|
68
|
+
assert.deepStrictEqual(ids, [1, 2, 3, 4, 5]);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('event listener management', () => {
|
|
73
|
+
it('should add and remove event listeners', () => {
|
|
74
|
+
const eventListeners = new Map();
|
|
75
|
+
const handler = () => {};
|
|
76
|
+
|
|
77
|
+
// Add listener
|
|
78
|
+
if (!eventListeners.has('Page.loadEventFired')) {
|
|
79
|
+
eventListeners.set('Page.loadEventFired', new Set());
|
|
80
|
+
}
|
|
81
|
+
eventListeners.get('Page.loadEventFired').add(handler);
|
|
82
|
+
assert.strictEqual(eventListeners.get('Page.loadEventFired').size, 1);
|
|
83
|
+
|
|
84
|
+
// Remove listener
|
|
85
|
+
eventListeners.get('Page.loadEventFired').delete(handler);
|
|
86
|
+
assert.strictEqual(eventListeners.get('Page.loadEventFired').size, 0);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should support multiple listeners for same event', () => {
|
|
90
|
+
const eventListeners = new Map();
|
|
91
|
+
const handler1 = () => {};
|
|
92
|
+
const handler2 = () => {};
|
|
93
|
+
|
|
94
|
+
eventListeners.set('Page.loadEventFired', new Set());
|
|
95
|
+
eventListeners.get('Page.loadEventFired').add(handler1);
|
|
96
|
+
eventListeners.get('Page.loadEventFired').add(handler2);
|
|
97
|
+
|
|
98
|
+
assert.strictEqual(eventListeners.get('Page.loadEventFired').size, 2);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should remove specific listener only', () => {
|
|
102
|
+
const eventListeners = new Map();
|
|
103
|
+
const handler1 = () => {};
|
|
104
|
+
const handler2 = () => {};
|
|
105
|
+
|
|
106
|
+
eventListeners.set('Page.loadEventFired', new Set());
|
|
107
|
+
eventListeners.get('Page.loadEventFired').add(handler1);
|
|
108
|
+
eventListeners.get('Page.loadEventFired').add(handler2);
|
|
109
|
+
|
|
110
|
+
eventListeners.get('Page.loadEventFired').delete(handler1);
|
|
111
|
+
|
|
112
|
+
assert.strictEqual(eventListeners.get('Page.loadEventFired').size, 1);
|
|
113
|
+
assert.ok(eventListeners.get('Page.loadEventFired').has(handler2));
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('removeAllListeners', () => {
|
|
118
|
+
it('should remove all listeners for specific event', () => {
|
|
119
|
+
const eventListeners = new Map();
|
|
120
|
+
const handler = () => {};
|
|
121
|
+
|
|
122
|
+
eventListeners.set('Page.loadEventFired', new Set([handler]));
|
|
123
|
+
eventListeners.set('Network.requestWillBeSent', new Set([handler]));
|
|
124
|
+
|
|
125
|
+
eventListeners.delete('Page.loadEventFired');
|
|
126
|
+
|
|
127
|
+
assert.strictEqual(eventListeners.has('Page.loadEventFired'), false);
|
|
128
|
+
assert.ok(eventListeners.has('Network.requestWillBeSent'));
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should remove all listeners when clearing', () => {
|
|
132
|
+
const eventListeners = new Map();
|
|
133
|
+
const handler = () => {};
|
|
134
|
+
|
|
135
|
+
eventListeners.set('Page.loadEventFired', new Set([handler]));
|
|
136
|
+
eventListeners.set('Network.requestWillBeSent', new Set([handler]));
|
|
137
|
+
|
|
138
|
+
eventListeners.clear();
|
|
139
|
+
|
|
140
|
+
assert.strictEqual(eventListeners.size, 0);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('message handling', () => {
|
|
145
|
+
it('should parse JSON messages', () => {
|
|
146
|
+
const data = Buffer.from(JSON.stringify({ id: 1, result: { value: 'test' } }));
|
|
147
|
+
const message = JSON.parse(data.toString());
|
|
148
|
+
assert.strictEqual(message.id, 1);
|
|
149
|
+
assert.deepStrictEqual(message.result, { value: 'test' });
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should identify command responses by id', () => {
|
|
153
|
+
const message = { id: 1, result: { value: 'test' } };
|
|
154
|
+
assert.ok(message.id !== undefined);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should identify events by method', () => {
|
|
158
|
+
const message = { method: 'Page.loadEventFired', params: { timestamp: 12345 } };
|
|
159
|
+
assert.ok(message.method !== undefined);
|
|
160
|
+
assert.strictEqual(message.id, undefined);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should handle error responses', () => {
|
|
164
|
+
const message = { id: 1, error: { code: -32000, message: 'Target not found' } };
|
|
165
|
+
assert.ok(message.error);
|
|
166
|
+
assert.strictEqual(message.error.message, 'Target not found');
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe('session-scoped events', () => {
|
|
171
|
+
it('should route events with sessionId', () => {
|
|
172
|
+
const eventListeners = new Map();
|
|
173
|
+
const events = [];
|
|
174
|
+
|
|
175
|
+
// Register session-scoped listener
|
|
176
|
+
const sessionKey = 'session123:Page.loadEventFired';
|
|
177
|
+
eventListeners.set(sessionKey, new Set([(params, sessionId) => {
|
|
178
|
+
events.push({ params, sessionId });
|
|
179
|
+
}]));
|
|
180
|
+
|
|
181
|
+
// Simulate event dispatch
|
|
182
|
+
const message = {
|
|
183
|
+
method: 'Page.loadEventFired',
|
|
184
|
+
sessionId: 'session123',
|
|
185
|
+
params: { timestamp: 12345 }
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const sessionEventKey = `${message.sessionId}:${message.method}`;
|
|
189
|
+
const listeners = eventListeners.get(sessionEventKey);
|
|
190
|
+
if (listeners) {
|
|
191
|
+
for (const callback of listeners) {
|
|
192
|
+
callback(message.params, message.sessionId);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
assert.strictEqual(events.length, 1);
|
|
197
|
+
assert.strictEqual(events[0].sessionId, 'session123');
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('pending commands', () => {
|
|
202
|
+
it('should track pending commands by id', () => {
|
|
203
|
+
const pendingCommands = new Map();
|
|
204
|
+
|
|
205
|
+
const id = 1;
|
|
206
|
+
const resolve = (result) => result;
|
|
207
|
+
const reject = (error) => { throw error; };
|
|
208
|
+
const timer = setTimeout(() => {}, 30000);
|
|
209
|
+
|
|
210
|
+
pendingCommands.set(id, { resolve, reject, timer });
|
|
211
|
+
|
|
212
|
+
assert.ok(pendingCommands.has(id));
|
|
213
|
+
clearTimeout(timer);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should clean up on response', () => {
|
|
217
|
+
const pendingCommands = new Map();
|
|
218
|
+
const timer = setTimeout(() => {}, 30000);
|
|
219
|
+
pendingCommands.set(1, { resolve: () => {}, reject: () => {}, timer });
|
|
220
|
+
|
|
221
|
+
// Simulate response handling
|
|
222
|
+
const pending = pendingCommands.get(1);
|
|
223
|
+
clearTimeout(pending.timer);
|
|
224
|
+
pendingCommands.delete(1);
|
|
225
|
+
|
|
226
|
+
assert.strictEqual(pendingCommands.size, 0);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('should reject all pending on disconnect', () => {
|
|
230
|
+
const pendingCommands = new Map();
|
|
231
|
+
const rejected = [];
|
|
232
|
+
|
|
233
|
+
for (let i = 1; i <= 3; i++) {
|
|
234
|
+
const timer = setTimeout(() => {}, 30000);
|
|
235
|
+
pendingCommands.set(i, {
|
|
236
|
+
resolve: () => {},
|
|
237
|
+
reject: (err) => rejected.push(err),
|
|
238
|
+
timer
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Simulate connection close
|
|
243
|
+
for (const [id, pending] of pendingCommands) {
|
|
244
|
+
clearTimeout(pending.timer);
|
|
245
|
+
pending.reject(new Error('Connection closed'));
|
|
246
|
+
}
|
|
247
|
+
pendingCommands.clear();
|
|
248
|
+
|
|
249
|
+
assert.strictEqual(rejected.length, 3);
|
|
250
|
+
assert.strictEqual(pendingCommands.size, 0);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
describe('exponential backoff calculation', () => {
|
|
255
|
+
it('should calculate correct delays for each attempt', () => {
|
|
256
|
+
const retryDelay = 1000;
|
|
257
|
+
const maxRetryDelay = 30000;
|
|
258
|
+
|
|
259
|
+
const calculateBackoff = (attempt) => {
|
|
260
|
+
const delay = retryDelay * Math.pow(2, attempt);
|
|
261
|
+
return Math.min(delay, maxRetryDelay);
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// attempt 0: 1000 * 2^0 = 1000ms
|
|
265
|
+
assert.strictEqual(calculateBackoff(0), 1000);
|
|
266
|
+
// attempt 1: 1000 * 2^1 = 2000ms
|
|
267
|
+
assert.strictEqual(calculateBackoff(1), 2000);
|
|
268
|
+
// attempt 2: 1000 * 2^2 = 4000ms
|
|
269
|
+
assert.strictEqual(calculateBackoff(2), 4000);
|
|
270
|
+
// attempt 3: 1000 * 2^3 = 8000ms
|
|
271
|
+
assert.strictEqual(calculateBackoff(3), 8000);
|
|
272
|
+
// attempt 4: 1000 * 2^4 = 16000ms
|
|
273
|
+
assert.strictEqual(calculateBackoff(4), 16000);
|
|
274
|
+
// attempt 5: 1000 * 2^5 = 32000ms, but capped at 30000
|
|
275
|
+
assert.strictEqual(calculateBackoff(5), 30000);
|
|
276
|
+
// attempt 6: still capped at 30000
|
|
277
|
+
assert.strictEqual(calculateBackoff(6), 30000);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('should respect custom retry delay', () => {
|
|
281
|
+
const retryDelay = 500;
|
|
282
|
+
const maxRetryDelay = 30000;
|
|
283
|
+
|
|
284
|
+
const calculateBackoff = (attempt) => {
|
|
285
|
+
const delay = retryDelay * Math.pow(2, attempt);
|
|
286
|
+
return Math.min(delay, maxRetryDelay);
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
assert.strictEqual(calculateBackoff(0), 500);
|
|
290
|
+
assert.strictEqual(calculateBackoff(1), 1000);
|
|
291
|
+
assert.strictEqual(calculateBackoff(2), 2000);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('should respect custom max retry delay', () => {
|
|
295
|
+
const retryDelay = 1000;
|
|
296
|
+
const maxRetryDelay = 5000;
|
|
297
|
+
|
|
298
|
+
const calculateBackoff = (attempt) => {
|
|
299
|
+
const delay = retryDelay * Math.pow(2, attempt);
|
|
300
|
+
return Math.min(delay, maxRetryDelay);
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
assert.strictEqual(calculateBackoff(0), 1000);
|
|
304
|
+
assert.strictEqual(calculateBackoff(1), 2000);
|
|
305
|
+
assert.strictEqual(calculateBackoff(2), 4000);
|
|
306
|
+
assert.strictEqual(calculateBackoff(3), 5000); // capped
|
|
307
|
+
assert.strictEqual(calculateBackoff(4), 5000); // still capped
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
describe('reconnection logic', () => {
|
|
312
|
+
it('should track reconnection state', () => {
|
|
313
|
+
let reconnecting = false;
|
|
314
|
+
let retryAttempt = 0;
|
|
315
|
+
const maxRetries = 5;
|
|
316
|
+
|
|
317
|
+
// Simulate starting reconnection
|
|
318
|
+
reconnecting = true;
|
|
319
|
+
retryAttempt = 0;
|
|
320
|
+
|
|
321
|
+
assert.strictEqual(reconnecting, true);
|
|
322
|
+
assert.strictEqual(retryAttempt, 0);
|
|
323
|
+
|
|
324
|
+
// Simulate retry attempts
|
|
325
|
+
while (retryAttempt < maxRetries) {
|
|
326
|
+
retryAttempt++;
|
|
327
|
+
if (retryAttempt === 3) {
|
|
328
|
+
// Simulate successful reconnection
|
|
329
|
+
reconnecting = false;
|
|
330
|
+
retryAttempt = 0;
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
assert.strictEqual(reconnecting, false);
|
|
336
|
+
assert.strictEqual(retryAttempt, 0);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('should stop reconnection when intentional close is triggered', () => {
|
|
340
|
+
let reconnecting = true;
|
|
341
|
+
let intentionalClose = false;
|
|
342
|
+
const attempts = [];
|
|
343
|
+
|
|
344
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
345
|
+
if (intentionalClose) break;
|
|
346
|
+
|
|
347
|
+
attempts.push(attempt);
|
|
348
|
+
|
|
349
|
+
if (attempt === 2) {
|
|
350
|
+
intentionalClose = true;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
reconnecting = false;
|
|
355
|
+
|
|
356
|
+
assert.deepStrictEqual(attempts, [0, 1, 2]);
|
|
357
|
+
assert.strictEqual(reconnecting, false);
|
|
358
|
+
assert.strictEqual(intentionalClose, true);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('should emit reconnecting event with attempt and delay info', () => {
|
|
362
|
+
const events = [];
|
|
363
|
+
const retryDelay = 1000;
|
|
364
|
+
const maxRetryDelay = 30000;
|
|
365
|
+
|
|
366
|
+
const emit = (event, data) => events.push({ event, data });
|
|
367
|
+
|
|
368
|
+
const calculateBackoff = (attempt) => {
|
|
369
|
+
const delay = retryDelay * Math.pow(2, attempt);
|
|
370
|
+
return Math.min(delay, maxRetryDelay);
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
// Simulate 3 reconnection attempts
|
|
374
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
375
|
+
const delay = calculateBackoff(attempt);
|
|
376
|
+
emit('reconnecting', { attempt: attempt + 1, delay });
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
assert.strictEqual(events.length, 3);
|
|
380
|
+
assert.deepStrictEqual(events[0], { event: 'reconnecting', data: { attempt: 1, delay: 1000 } });
|
|
381
|
+
assert.deepStrictEqual(events[1], { event: 'reconnecting', data: { attempt: 2, delay: 2000 } });
|
|
382
|
+
assert.deepStrictEqual(events[2], { event: 'reconnecting', data: { attempt: 3, delay: 4000 } });
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('should emit reconnected event on successful reconnection', () => {
|
|
386
|
+
const events = [];
|
|
387
|
+
const emit = (event, data) => events.push({ event, data });
|
|
388
|
+
|
|
389
|
+
// Simulate successful reconnection
|
|
390
|
+
emit('reconnected', {});
|
|
391
|
+
|
|
392
|
+
assert.strictEqual(events.length, 1);
|
|
393
|
+
assert.deepStrictEqual(events[0], { event: 'reconnected', data: {} });
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('should call onClose callback after max retries exceeded', () => {
|
|
397
|
+
let closeReason = null;
|
|
398
|
+
const onCloseCallback = (reason) => { closeReason = reason; };
|
|
399
|
+
|
|
400
|
+
const maxRetries = 3;
|
|
401
|
+
let retryAttempt = 0;
|
|
402
|
+
let reconnecting = true;
|
|
403
|
+
const intentionalClose = false;
|
|
404
|
+
|
|
405
|
+
// Simulate exhausting all retries
|
|
406
|
+
while (retryAttempt < maxRetries) {
|
|
407
|
+
retryAttempt++;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
reconnecting = false;
|
|
411
|
+
if (!intentionalClose && onCloseCallback) {
|
|
412
|
+
onCloseCallback('Connection closed unexpectedly after max retries');
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
assert.strictEqual(closeReason, 'Connection closed unexpectedly after max retries');
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
describe('connection options', () => {
|
|
420
|
+
it('should use default options when not provided', () => {
|
|
421
|
+
const defaults = {
|
|
422
|
+
maxRetries: 5,
|
|
423
|
+
retryDelay: 1000,
|
|
424
|
+
maxRetryDelay: 30000,
|
|
425
|
+
autoReconnect: false
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
const options = {};
|
|
429
|
+
const maxRetries = options.maxRetries ?? 5;
|
|
430
|
+
const retryDelay = options.retryDelay ?? 1000;
|
|
431
|
+
const maxRetryDelay = options.maxRetryDelay ?? 30000;
|
|
432
|
+
const autoReconnect = options.autoReconnect ?? false;
|
|
433
|
+
|
|
434
|
+
assert.strictEqual(maxRetries, defaults.maxRetries);
|
|
435
|
+
assert.strictEqual(retryDelay, defaults.retryDelay);
|
|
436
|
+
assert.strictEqual(maxRetryDelay, defaults.maxRetryDelay);
|
|
437
|
+
assert.strictEqual(autoReconnect, defaults.autoReconnect);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it('should allow custom options', () => {
|
|
441
|
+
const options = {
|
|
442
|
+
maxRetries: 10,
|
|
443
|
+
retryDelay: 500,
|
|
444
|
+
maxRetryDelay: 15000,
|
|
445
|
+
autoReconnect: true
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
const maxRetries = options.maxRetries ?? 5;
|
|
449
|
+
const retryDelay = options.retryDelay ?? 1000;
|
|
450
|
+
const maxRetryDelay = options.maxRetryDelay ?? 30000;
|
|
451
|
+
const autoReconnect = options.autoReconnect ?? false;
|
|
452
|
+
|
|
453
|
+
assert.strictEqual(maxRetries, 10);
|
|
454
|
+
assert.strictEqual(retryDelay, 500);
|
|
455
|
+
assert.strictEqual(maxRetryDelay, 15000);
|
|
456
|
+
assert.strictEqual(autoReconnect, true);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('should handle partial options', () => {
|
|
460
|
+
const options = {
|
|
461
|
+
maxRetries: 3,
|
|
462
|
+
autoReconnect: true
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
const maxRetries = options.maxRetries ?? 5;
|
|
466
|
+
const retryDelay = options.retryDelay ?? 1000;
|
|
467
|
+
const maxRetryDelay = options.maxRetryDelay ?? 30000;
|
|
468
|
+
const autoReconnect = options.autoReconnect ?? false;
|
|
469
|
+
|
|
470
|
+
assert.strictEqual(maxRetries, 3);
|
|
471
|
+
assert.strictEqual(retryDelay, 1000); // default
|
|
472
|
+
assert.strictEqual(maxRetryDelay, 30000); // default
|
|
473
|
+
assert.strictEqual(autoReconnect, true);
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
describe('autoReconnect behavior', () => {
|
|
478
|
+
it('should not attempt reconnection when autoReconnect is false', () => {
|
|
479
|
+
let reconnectAttempted = false;
|
|
480
|
+
const autoReconnect = false;
|
|
481
|
+
const wasConnected = true;
|
|
482
|
+
const intentionalClose = false;
|
|
483
|
+
|
|
484
|
+
if (wasConnected && !intentionalClose && autoReconnect) {
|
|
485
|
+
reconnectAttempted = true;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
assert.strictEqual(reconnectAttempted, false);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it('should attempt reconnection when autoReconnect is true', () => {
|
|
492
|
+
let reconnectAttempted = false;
|
|
493
|
+
const autoReconnect = true;
|
|
494
|
+
const wasConnected = true;
|
|
495
|
+
const intentionalClose = false;
|
|
496
|
+
|
|
497
|
+
if (wasConnected && !intentionalClose && autoReconnect) {
|
|
498
|
+
reconnectAttempted = true;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
assert.strictEqual(reconnectAttempted, true);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
it('should not attempt reconnection on intentional close', () => {
|
|
505
|
+
let reconnectAttempted = false;
|
|
506
|
+
const autoReconnect = true;
|
|
507
|
+
const wasConnected = true;
|
|
508
|
+
const intentionalClose = true;
|
|
509
|
+
|
|
510
|
+
if (wasConnected && !intentionalClose && autoReconnect) {
|
|
511
|
+
reconnectAttempted = true;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
assert.strictEqual(reconnectAttempted, false);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it('should not attempt reconnection if never connected', () => {
|
|
518
|
+
let reconnectAttempted = false;
|
|
519
|
+
const autoReconnect = true;
|
|
520
|
+
const wasConnected = false;
|
|
521
|
+
const intentionalClose = false;
|
|
522
|
+
|
|
523
|
+
if (wasConnected && !intentionalClose && autoReconnect) {
|
|
524
|
+
reconnectAttempted = true;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
assert.strictEqual(reconnectAttempted, false);
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
describe('MockWebSocket', () => {
|
|
532
|
+
let ws;
|
|
533
|
+
|
|
534
|
+
beforeEach(() => {
|
|
535
|
+
ws = new MockWebSocket('ws://localhost:9222/devtools/browser/abc');
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it('should store URL', () => {
|
|
539
|
+
assert.strictEqual(ws.url, 'ws://localhost:9222/devtools/browser/abc');
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it('should register event listeners', () => {
|
|
543
|
+
const handler = () => {};
|
|
544
|
+
ws.on('open', handler);
|
|
545
|
+
assert.strictEqual(ws.listeners.get('open').length, 1);
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
it('should emit events to listeners', () => {
|
|
549
|
+
let called = false;
|
|
550
|
+
ws.on('open', () => { called = true; });
|
|
551
|
+
ws.emit('open');
|
|
552
|
+
assert.strictEqual(called, true);
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it('should track sent messages', () => {
|
|
556
|
+
ws.send(JSON.stringify({ id: 1, method: 'Page.navigate', params: { url: 'https://example.com' } }));
|
|
557
|
+
assert.strictEqual(ws.sentMessages.length, 1);
|
|
558
|
+
assert.strictEqual(ws.sentMessages[0].method, 'Page.navigate');
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
it('should simulate message receiving', () => {
|
|
562
|
+
const messages = [];
|
|
563
|
+
ws.on('message', (data) => {
|
|
564
|
+
messages.push(JSON.parse(data.toString()));
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
ws.simulateMessage({ id: 1, result: { frameId: 'abc' } });
|
|
568
|
+
|
|
569
|
+
assert.strictEqual(messages.length, 1);
|
|
570
|
+
assert.strictEqual(messages[0].id, 1);
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
it('should simulate errors', () => {
|
|
574
|
+
const errors = [];
|
|
575
|
+
ws.on('error', (err) => errors.push(err));
|
|
576
|
+
|
|
577
|
+
ws.simulateError(new Error('Connection refused'));
|
|
578
|
+
|
|
579
|
+
assert.strictEqual(errors.length, 1);
|
|
580
|
+
assert.strictEqual(errors[0].message, 'Connection refused');
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
it('should emit close on close()', () => {
|
|
584
|
+
let closed = false;
|
|
585
|
+
ws.on('close', () => { closed = true; });
|
|
586
|
+
ws.close();
|
|
587
|
+
assert.strictEqual(closed, true);
|
|
588
|
+
assert.strictEqual(ws.readyState, 3);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
it('should clear all listeners on removeAllListeners()', () => {
|
|
592
|
+
ws.on('open', () => {});
|
|
593
|
+
ws.on('message', () => {});
|
|
594
|
+
ws.removeAllListeners();
|
|
595
|
+
assert.strictEqual(ws.listeners.size, 0);
|
|
596
|
+
});
|
|
597
|
+
});
|
|
598
|
+
});
|