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,588 @@
|
|
|
1
|
+
import { describe, it, mock, beforeEach } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { createBrowser, createPageSession } from '../cdp.js';
|
|
4
|
+
import { ErrorTypes } from '../utils.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Mock factory for CDPConnection
|
|
8
|
+
*/
|
|
9
|
+
function createMockConnection() {
|
|
10
|
+
const listeners = new Map();
|
|
11
|
+
return {
|
|
12
|
+
connect: mock.fn(async () => {}),
|
|
13
|
+
close: mock.fn(async () => {}),
|
|
14
|
+
send: mock.fn(async () => ({})),
|
|
15
|
+
sendToSession: mock.fn(async () => ({})),
|
|
16
|
+
on: mock.fn((event, callback) => {
|
|
17
|
+
if (!listeners.has(event)) {
|
|
18
|
+
listeners.set(event, new Set());
|
|
19
|
+
}
|
|
20
|
+
listeners.get(event).add(callback);
|
|
21
|
+
}),
|
|
22
|
+
off: mock.fn((event, callback) => {
|
|
23
|
+
const set = listeners.get(event);
|
|
24
|
+
if (set) set.delete(callback);
|
|
25
|
+
}),
|
|
26
|
+
isConnected: mock.fn(() => true),
|
|
27
|
+
_listeners: listeners,
|
|
28
|
+
_emit: (event, data) => {
|
|
29
|
+
const set = listeners.get(event);
|
|
30
|
+
if (set) {
|
|
31
|
+
for (const cb of set) cb(data);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Mock factory for ChromeDiscovery
|
|
39
|
+
*/
|
|
40
|
+
function createMockDiscovery() {
|
|
41
|
+
return {
|
|
42
|
+
getVersion: mock.fn(async () => ({
|
|
43
|
+
browser: 'Chrome/120.0.0.0',
|
|
44
|
+
protocolVersion: '1.3',
|
|
45
|
+
webSocketDebuggerUrl: 'ws://localhost:9222/devtools/browser/abc123'
|
|
46
|
+
})),
|
|
47
|
+
getTargets: mock.fn(async () => []),
|
|
48
|
+
getPages: mock.fn(async () => []),
|
|
49
|
+
isAvailable: mock.fn(async () => true)
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Mock factory for TargetManager
|
|
55
|
+
*/
|
|
56
|
+
function createMockTargetManager() {
|
|
57
|
+
return {
|
|
58
|
+
enableDiscovery: mock.fn(async () => {}),
|
|
59
|
+
getTargets: mock.fn(async () => []),
|
|
60
|
+
getPages: mock.fn(async () => [
|
|
61
|
+
{ targetId: 'target-1', type: 'page', title: 'Test Page', url: 'https://example.com' },
|
|
62
|
+
{ targetId: 'target-2', type: 'page', title: 'Another Page', url: 'https://test.com' }
|
|
63
|
+
]),
|
|
64
|
+
createTarget: mock.fn(async (url) => 'new-target-id'),
|
|
65
|
+
closeTarget: mock.fn(async () => true),
|
|
66
|
+
activateTarget: mock.fn(async () => {}),
|
|
67
|
+
getTargetInfo: mock.fn(async () => ({}))
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Mock factory for SessionRegistry
|
|
73
|
+
*/
|
|
74
|
+
function createMockSessionRegistry() {
|
|
75
|
+
return {
|
|
76
|
+
attach: mock.fn(async (targetId) => `session-for-${targetId}`),
|
|
77
|
+
detach: mock.fn(async () => {}),
|
|
78
|
+
detachByTarget: mock.fn(async () => {}),
|
|
79
|
+
detachAll: mock.fn(async () => {}),
|
|
80
|
+
getSessionForTarget: mock.fn(() => undefined),
|
|
81
|
+
isAttached: mock.fn(() => false)
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Mock BrowserClient for isolated testing
|
|
87
|
+
* This mirrors the createBrowser function behavior
|
|
88
|
+
*/
|
|
89
|
+
function createMockBrowserClient(options = {}) {
|
|
90
|
+
const host = options.host ?? 'localhost';
|
|
91
|
+
const port = options.port ?? 9222;
|
|
92
|
+
|
|
93
|
+
let discovery = options._discovery ?? null;
|
|
94
|
+
let connection = null;
|
|
95
|
+
let targetManager = null;
|
|
96
|
+
let sessionRegistry = null;
|
|
97
|
+
let connected = false;
|
|
98
|
+
|
|
99
|
+
function ensureConnected() {
|
|
100
|
+
if (!connected) {
|
|
101
|
+
throw new Error('BrowserClient not connected. Call connect() first.');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Test helper to inject mocks
|
|
106
|
+
function _injectMocks(disc, conn, targets, sessions) {
|
|
107
|
+
discovery = disc;
|
|
108
|
+
connection = conn;
|
|
109
|
+
targetManager = targets;
|
|
110
|
+
sessionRegistry = sessions;
|
|
111
|
+
connected = true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function connect() {
|
|
115
|
+
if (connected) return;
|
|
116
|
+
const version = await discovery.getVersion();
|
|
117
|
+
await connection.connect();
|
|
118
|
+
connected = true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function disconnect() {
|
|
122
|
+
if (!connected) return;
|
|
123
|
+
await sessionRegistry.detachAll();
|
|
124
|
+
await connection.close();
|
|
125
|
+
connected = false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function getPages() {
|
|
129
|
+
ensureConnected();
|
|
130
|
+
return targetManager.getPages();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function newPage(url = 'about:blank') {
|
|
134
|
+
ensureConnected();
|
|
135
|
+
const targetId = await targetManager.createTarget(url);
|
|
136
|
+
const sessionId = await sessionRegistry.attach(targetId);
|
|
137
|
+
return createPageSession(connection, sessionId, targetId);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function attachToPage(targetId) {
|
|
141
|
+
ensureConnected();
|
|
142
|
+
const sessionId = await sessionRegistry.attach(targetId);
|
|
143
|
+
return createPageSession(connection, sessionId, targetId);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function findPage(urlPattern) {
|
|
147
|
+
ensureConnected();
|
|
148
|
+
const pages = await getPages();
|
|
149
|
+
const regex = urlPattern instanceof RegExp ? urlPattern : new RegExp(urlPattern);
|
|
150
|
+
const target = pages.find(p => regex.test(p.url));
|
|
151
|
+
if (!target) return null;
|
|
152
|
+
return attachToPage(target.targetId);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function closePage(targetId) {
|
|
156
|
+
ensureConnected();
|
|
157
|
+
await sessionRegistry.detachByTarget(targetId);
|
|
158
|
+
await targetManager.closeTarget(targetId);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
connect,
|
|
163
|
+
disconnect,
|
|
164
|
+
getPages,
|
|
165
|
+
newPage,
|
|
166
|
+
attachToPage,
|
|
167
|
+
findPage,
|
|
168
|
+
closePage,
|
|
169
|
+
isConnected: () => connected,
|
|
170
|
+
_injectMocks,
|
|
171
|
+
get connection() { return connection; },
|
|
172
|
+
get targets() { return targetManager; },
|
|
173
|
+
get sessions() { return sessionRegistry; }
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
describe('PageSession (functional)', () => {
|
|
178
|
+
let mockConnection;
|
|
179
|
+
|
|
180
|
+
beforeEach(() => {
|
|
181
|
+
mockConnection = createMockConnection();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should store connection, sessionId, and targetId', () => {
|
|
185
|
+
const session = createPageSession(mockConnection, 'session-123', 'target-456');
|
|
186
|
+
|
|
187
|
+
assert.strictEqual(session.sessionId, 'session-123');
|
|
188
|
+
assert.strictEqual(session.targetId, 'target-456');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should send commands via sendToSession', async () => {
|
|
192
|
+
mockConnection.sendToSession = mock.fn(async () => ({ result: 'success' }));
|
|
193
|
+
const session = createPageSession(mockConnection, 'session-123', 'target-456');
|
|
194
|
+
|
|
195
|
+
const result = await session.send('Page.navigate', { url: 'https://example.com' });
|
|
196
|
+
|
|
197
|
+
assert.strictEqual(mockConnection.sendToSession.mock.calls.length, 1);
|
|
198
|
+
const [sessionId, method, params] = mockConnection.sendToSession.mock.calls[0].arguments;
|
|
199
|
+
assert.strictEqual(sessionId, 'session-123');
|
|
200
|
+
assert.strictEqual(method, 'Page.navigate');
|
|
201
|
+
assert.deepStrictEqual(params, { url: 'https://example.com' });
|
|
202
|
+
assert.deepStrictEqual(result, { result: 'success' });
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should send commands with default empty params', async () => {
|
|
206
|
+
mockConnection.sendToSession = mock.fn(async () => ({}));
|
|
207
|
+
const session = createPageSession(mockConnection, 'session-123', 'target-456');
|
|
208
|
+
|
|
209
|
+
await session.send('Page.reload');
|
|
210
|
+
|
|
211
|
+
const [, , params] = mockConnection.sendToSession.mock.calls[0].arguments;
|
|
212
|
+
assert.deepStrictEqual(params, {});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('should subscribe to session-scoped events', () => {
|
|
216
|
+
const session = createPageSession(mockConnection, 'session-123', 'target-456');
|
|
217
|
+
const callback = () => {};
|
|
218
|
+
const initialCallCount = mockConnection.on.mock.calls.length;
|
|
219
|
+
|
|
220
|
+
session.on('Page.loadEventFired', callback);
|
|
221
|
+
|
|
222
|
+
// Should have one more call after subscribing
|
|
223
|
+
assert.strictEqual(mockConnection.on.mock.calls.length, initialCallCount + 1);
|
|
224
|
+
const lastCall = mockConnection.on.mock.calls[mockConnection.on.mock.calls.length - 1];
|
|
225
|
+
const [eventName, cb] = lastCall.arguments;
|
|
226
|
+
assert.strictEqual(eventName, 'session-123:Page.loadEventFired');
|
|
227
|
+
assert.strictEqual(cb, callback);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should unsubscribe from session-scoped events', () => {
|
|
231
|
+
const session = createPageSession(mockConnection, 'session-123', 'target-456');
|
|
232
|
+
const callback = () => {};
|
|
233
|
+
|
|
234
|
+
session.off('Page.loadEventFired', callback);
|
|
235
|
+
|
|
236
|
+
assert.strictEqual(mockConnection.off.mock.calls.length, 1);
|
|
237
|
+
const [eventName, cb] = mockConnection.off.mock.calls[0].arguments;
|
|
238
|
+
assert.strictEqual(eventName, 'session-123:Page.loadEventFired');
|
|
239
|
+
assert.strictEqual(cb, callback);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe('BrowserClient (mock)', () => {
|
|
244
|
+
let client;
|
|
245
|
+
let mockDiscovery;
|
|
246
|
+
let mockConnection;
|
|
247
|
+
let mockTargetManager;
|
|
248
|
+
let mockSessionRegistry;
|
|
249
|
+
|
|
250
|
+
beforeEach(() => {
|
|
251
|
+
mockDiscovery = createMockDiscovery();
|
|
252
|
+
mockConnection = createMockConnection();
|
|
253
|
+
mockTargetManager = createMockTargetManager();
|
|
254
|
+
mockSessionRegistry = createMockSessionRegistry();
|
|
255
|
+
client = createMockBrowserClient();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe('constructor', () => {
|
|
259
|
+
it('should initialize as not connected', () => {
|
|
260
|
+
assert.strictEqual(client.isConnected(), false);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('should return null for connection before connect', () => {
|
|
264
|
+
assert.strictEqual(client.connection, null);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('should return null for targets before connect', () => {
|
|
268
|
+
assert.strictEqual(client.targets, null);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('should return null for sessions before connect', () => {
|
|
272
|
+
assert.strictEqual(client.sessions, null);
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
describe('ensureConnected checks', () => {
|
|
277
|
+
it('should throw if getPages called when not connected', async () => {
|
|
278
|
+
await assert.rejects(
|
|
279
|
+
async () => client.getPages(),
|
|
280
|
+
{ message: 'BrowserClient not connected. Call connect() first.' }
|
|
281
|
+
);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('should throw if newPage called when not connected', async () => {
|
|
285
|
+
await assert.rejects(
|
|
286
|
+
async () => client.newPage(),
|
|
287
|
+
{ message: 'BrowserClient not connected. Call connect() first.' }
|
|
288
|
+
);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('should throw if attachToPage called when not connected', async () => {
|
|
292
|
+
await assert.rejects(
|
|
293
|
+
async () => client.attachToPage('target-123'),
|
|
294
|
+
{ message: 'BrowserClient not connected. Call connect() first.' }
|
|
295
|
+
);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('should throw if findPage called when not connected', async () => {
|
|
299
|
+
await assert.rejects(
|
|
300
|
+
async () => client.findPage(/example/),
|
|
301
|
+
{ message: 'BrowserClient not connected. Call connect() first.' }
|
|
302
|
+
);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('should throw if closePage called when not connected', async () => {
|
|
306
|
+
await assert.rejects(
|
|
307
|
+
async () => client.closePage('target-123'),
|
|
308
|
+
{ message: 'BrowserClient not connected. Call connect() first.' }
|
|
309
|
+
);
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
describe('disconnect', () => {
|
|
314
|
+
it('should do nothing if not connected', async () => {
|
|
315
|
+
// Should not throw
|
|
316
|
+
await client.disconnect();
|
|
317
|
+
assert.strictEqual(client.isConnected(), false);
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
describe('BrowserClient (connected mock)', () => {
|
|
323
|
+
let client;
|
|
324
|
+
let mockDiscovery;
|
|
325
|
+
let mockConnection;
|
|
326
|
+
let mockTargetManager;
|
|
327
|
+
let mockSessionRegistry;
|
|
328
|
+
|
|
329
|
+
beforeEach(() => {
|
|
330
|
+
mockDiscovery = createMockDiscovery();
|
|
331
|
+
mockConnection = createMockConnection();
|
|
332
|
+
mockTargetManager = createMockTargetManager();
|
|
333
|
+
mockSessionRegistry = createMockSessionRegistry();
|
|
334
|
+
client = createMockBrowserClient();
|
|
335
|
+
client._injectMocks(mockDiscovery, mockConnection, mockTargetManager, mockSessionRegistry);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('should be connected after injecting mocks', () => {
|
|
339
|
+
assert.strictEqual(client.isConnected(), true);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('should expose connection via getter', () => {
|
|
343
|
+
assert.strictEqual(client.connection, mockConnection);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('should expose targetManager via targets getter', () => {
|
|
347
|
+
assert.strictEqual(client.targets, mockTargetManager);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('should expose sessionRegistry via sessions getter', () => {
|
|
351
|
+
assert.strictEqual(client.sessions, mockSessionRegistry);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
describe('getPages', () => {
|
|
355
|
+
it('should return pages from target manager', async () => {
|
|
356
|
+
const pages = await client.getPages();
|
|
357
|
+
|
|
358
|
+
assert.strictEqual(mockTargetManager.getPages.mock.calls.length, 1);
|
|
359
|
+
assert.strictEqual(pages.length, 2);
|
|
360
|
+
assert.strictEqual(pages[0].targetId, 'target-1');
|
|
361
|
+
assert.strictEqual(pages[1].targetId, 'target-2');
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
describe('newPage', () => {
|
|
366
|
+
it('should create target and attach session', async () => {
|
|
367
|
+
const session = await client.newPage('https://test.com');
|
|
368
|
+
|
|
369
|
+
assert.strictEqual(mockTargetManager.createTarget.mock.calls.length, 1);
|
|
370
|
+
assert.strictEqual(mockTargetManager.createTarget.mock.calls[0].arguments[0], 'https://test.com');
|
|
371
|
+
assert.strictEqual(mockSessionRegistry.attach.mock.calls.length, 1);
|
|
372
|
+
assert.strictEqual(mockSessionRegistry.attach.mock.calls[0].arguments[0], 'new-target-id');
|
|
373
|
+
|
|
374
|
+
assert.strictEqual(session.targetId, 'new-target-id');
|
|
375
|
+
assert.strictEqual(session.sessionId, 'session-for-new-target-id');
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('should use about:blank as default URL', async () => {
|
|
379
|
+
await client.newPage();
|
|
380
|
+
|
|
381
|
+
assert.strictEqual(mockTargetManager.createTarget.mock.calls[0].arguments[0], 'about:blank');
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
describe('attachToPage', () => {
|
|
386
|
+
it('should attach to existing target', async () => {
|
|
387
|
+
const session = await client.attachToPage('existing-target');
|
|
388
|
+
|
|
389
|
+
assert.strictEqual(mockSessionRegistry.attach.mock.calls.length, 1);
|
|
390
|
+
assert.strictEqual(mockSessionRegistry.attach.mock.calls[0].arguments[0], 'existing-target');
|
|
391
|
+
|
|
392
|
+
assert.strictEqual(session.targetId, 'existing-target');
|
|
393
|
+
assert.strictEqual(session.sessionId, 'session-for-existing-target');
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
describe('findPage', () => {
|
|
398
|
+
it('should find page by RegExp and attach', async () => {
|
|
399
|
+
const session = await client.findPage(/example\.com/);
|
|
400
|
+
|
|
401
|
+
assert.strictEqual(session.targetId, 'target-1');
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('should find page by string pattern and attach', async () => {
|
|
405
|
+
const session = await client.findPage('test\\.com');
|
|
406
|
+
|
|
407
|
+
assert.strictEqual(session.targetId, 'target-2');
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it('should return null if no page matches', async () => {
|
|
411
|
+
const session = await client.findPage(/notfound/);
|
|
412
|
+
|
|
413
|
+
assert.strictEqual(session, null);
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
describe('closePage', () => {
|
|
418
|
+
it('should detach session and close target', async () => {
|
|
419
|
+
await client.closePage('target-to-close');
|
|
420
|
+
|
|
421
|
+
assert.strictEqual(mockSessionRegistry.detachByTarget.mock.calls.length, 1);
|
|
422
|
+
assert.strictEqual(mockSessionRegistry.detachByTarget.mock.calls[0].arguments[0], 'target-to-close');
|
|
423
|
+
assert.strictEqual(mockTargetManager.closeTarget.mock.calls.length, 1);
|
|
424
|
+
assert.strictEqual(mockTargetManager.closeTarget.mock.calls[0].arguments[0], 'target-to-close');
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
describe('disconnect', () => {
|
|
429
|
+
it('should detach all sessions and close connection', async () => {
|
|
430
|
+
await client.disconnect();
|
|
431
|
+
|
|
432
|
+
assert.strictEqual(mockSessionRegistry.detachAll.mock.calls.length, 1);
|
|
433
|
+
assert.strictEqual(mockConnection.close.mock.calls.length, 1);
|
|
434
|
+
assert.strictEqual(client.isConnected(), false);
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
describe('PageSession event handling', () => {
|
|
440
|
+
it('should properly format session-scoped event names', () => {
|
|
441
|
+
const mockConn = createMockConnection();
|
|
442
|
+
const session = createPageSession(mockConn, 'my-session-id', 'target-1');
|
|
443
|
+
|
|
444
|
+
const handler = () => {};
|
|
445
|
+
session.on('Network.requestWillBeSent', handler);
|
|
446
|
+
|
|
447
|
+
// Verify the event was registered with session prefix
|
|
448
|
+
assert.ok(mockConn._listeners.has('my-session-id:Network.requestWillBeSent'));
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it('should properly unregister session-scoped events', () => {
|
|
452
|
+
const mockConn = createMockConnection();
|
|
453
|
+
const session = createPageSession(mockConn, 'my-session-id', 'target-1');
|
|
454
|
+
|
|
455
|
+
const handler = () => {};
|
|
456
|
+
session.on('Network.requestWillBeSent', handler);
|
|
457
|
+
session.off('Network.requestWillBeSent', handler);
|
|
458
|
+
|
|
459
|
+
const listeners = mockConn._listeners.get('my-session-id:Network.requestWillBeSent');
|
|
460
|
+
assert.strictEqual(listeners.size, 0);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it('should receive events through connection emit', () => {
|
|
464
|
+
const mockConn = createMockConnection();
|
|
465
|
+
const session = createPageSession(mockConn, 'my-session-id', 'target-1');
|
|
466
|
+
|
|
467
|
+
let receivedData = null;
|
|
468
|
+
const handler = (data) => { receivedData = data; };
|
|
469
|
+
session.on('Page.loadEventFired', handler);
|
|
470
|
+
|
|
471
|
+
// Simulate event emission
|
|
472
|
+
mockConn._emit('my-session-id:Page.loadEventFired', { timestamp: 12345 });
|
|
473
|
+
|
|
474
|
+
assert.deepStrictEqual(receivedData, { timestamp: 12345 });
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
describe('PageSession validity tracking', () => {
|
|
479
|
+
it('should start as valid', () => {
|
|
480
|
+
const mockConn = createMockConnection();
|
|
481
|
+
const session = createPageSession(mockConn, 'my-session-id', 'target-1');
|
|
482
|
+
|
|
483
|
+
assert.strictEqual(session.isValid(), true);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it('should become invalid when detach event received', () => {
|
|
487
|
+
const mockConn = createMockConnection();
|
|
488
|
+
const session = createPageSession(mockConn, 'my-session-id', 'target-1');
|
|
489
|
+
|
|
490
|
+
// Simulate detach event
|
|
491
|
+
mockConn._emit('Target.detachedFromTarget', { sessionId: 'my-session-id' });
|
|
492
|
+
|
|
493
|
+
assert.strictEqual(session.isValid(), false);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
it('should not be invalidated by detach events for other sessions', () => {
|
|
497
|
+
const mockConn = createMockConnection();
|
|
498
|
+
const session = createPageSession(mockConn, 'my-session-id', 'target-1');
|
|
499
|
+
|
|
500
|
+
// Simulate detach event for different session
|
|
501
|
+
mockConn._emit('Target.detachedFromTarget', { sessionId: 'other-session-id' });
|
|
502
|
+
|
|
503
|
+
assert.strictEqual(session.isValid(), true);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it('should throw when sending commands to invalid session', async () => {
|
|
507
|
+
const mockConn = createMockConnection();
|
|
508
|
+
mockConn.sendToSession = mock.fn(async () => ({}));
|
|
509
|
+
const session = createPageSession(mockConn, 'my-session-id', 'target-1');
|
|
510
|
+
|
|
511
|
+
// Invalidate the session
|
|
512
|
+
mockConn._emit('Target.detachedFromTarget', { sessionId: 'my-session-id' });
|
|
513
|
+
|
|
514
|
+
await assert.rejects(
|
|
515
|
+
async () => session.send('Page.navigate', { url: 'https://example.com' }),
|
|
516
|
+
{ message: /Session my-session-id is no longer valid/ }
|
|
517
|
+
);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it('should clean up detach listener when disposed', () => {
|
|
521
|
+
const mockConn = createMockConnection();
|
|
522
|
+
const session = createPageSession(mockConn, 'my-session-id', 'target-1');
|
|
523
|
+
|
|
524
|
+
// Get initial listener count
|
|
525
|
+
const initialListeners = mockConn._listeners.get('Target.detachedFromTarget')?.size || 0;
|
|
526
|
+
assert.ok(initialListeners > 0);
|
|
527
|
+
|
|
528
|
+
session.dispose();
|
|
529
|
+
|
|
530
|
+
const finalListeners = mockConn._listeners.get('Target.detachedFromTarget')?.size || 0;
|
|
531
|
+
assert.strictEqual(finalListeners, initialListeners - 1);
|
|
532
|
+
assert.strictEqual(session.isValid(), false);
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
describe('BrowserClient real (connection tests)', () => {
|
|
537
|
+
it('should use default connectTimeout of 30000ms', () => {
|
|
538
|
+
const client = createBrowser();
|
|
539
|
+
// We can't directly access private field, but we can verify behavior
|
|
540
|
+
assert.strictEqual(client.isConnected(), false);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it('should accept custom connectTimeout option', () => {
|
|
544
|
+
const client = createBrowser({ connectTimeout: 5000 });
|
|
545
|
+
assert.strictEqual(client.isConnected(), false);
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
it('should throw an error when connection fails', async () => {
|
|
549
|
+
// Use a non-existent port to trigger connection failure
|
|
550
|
+
const client = createBrowser({
|
|
551
|
+
host: 'localhost',
|
|
552
|
+
port: 59999, // Non-existent port
|
|
553
|
+
connectTimeout: 100
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
await assert.rejects(
|
|
557
|
+
async () => client.connect(),
|
|
558
|
+
(err) => {
|
|
559
|
+
// Should fail with some connection-related error
|
|
560
|
+
// Can be TimeoutError, fetch failed, connection refused, or Chrome not reachable
|
|
561
|
+
return err.name === ErrorTypes.TIMEOUT ||
|
|
562
|
+
err instanceof Error; // Any error is acceptable when connection fails
|
|
563
|
+
}
|
|
564
|
+
);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
it('should pass connectTimeout to ChromeDiscovery', async () => {
|
|
568
|
+
// Test that ChromeDiscovery receives the timeout
|
|
569
|
+
// When Chrome is not running, we should get an error quickly
|
|
570
|
+
const client = createBrowser({
|
|
571
|
+
host: 'localhost',
|
|
572
|
+
port: 59998, // Non-existent port
|
|
573
|
+
connectTimeout: 100
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
const startTime = Date.now();
|
|
577
|
+
try {
|
|
578
|
+
await client.connect();
|
|
579
|
+
} catch (err) {
|
|
580
|
+
// Expected to fail
|
|
581
|
+
}
|
|
582
|
+
const elapsed = Date.now() - startTime;
|
|
583
|
+
|
|
584
|
+
// Should fail quickly (within our timeout + some buffer)
|
|
585
|
+
assert.ok(elapsed < 5000, `Expected quick failure, took ${elapsed}ms`);
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
});
|