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.
@@ -0,0 +1,356 @@
1
+ import { describe, it, mock, beforeEach, afterEach } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { createScreenshotCapture } from '../capture.js';
4
+
5
+ describe('ScreenshotCapture', () => {
6
+ let screenshotCapture;
7
+ let mockCdp;
8
+
9
+ beforeEach(() => {
10
+ mockCdp = {
11
+ send: mock.fn()
12
+ };
13
+ screenshotCapture = createScreenshotCapture(mockCdp);
14
+ });
15
+
16
+ afterEach(() => {
17
+ mock.reset();
18
+ });
19
+
20
+ describe('captureViewport', () => {
21
+ it('should capture viewport screenshot with default options', async () => {
22
+ const base64Data = Buffer.from('test-image').toString('base64');
23
+ mockCdp.send.mock.mockImplementation(() => Promise.resolve({ data: base64Data }));
24
+
25
+ const result = await screenshotCapture.captureViewport();
26
+
27
+ assert.strictEqual(mockCdp.send.mock.calls.length, 1);
28
+ assert.deepStrictEqual(mockCdp.send.mock.calls[0].arguments, [
29
+ 'Page.captureScreenshot',
30
+ { format: 'png' }
31
+ ]);
32
+ assert.ok(Buffer.isBuffer(result));
33
+ assert.strictEqual(result.toString(), 'test-image');
34
+ });
35
+
36
+ it('should capture viewport screenshot with jpeg format and quality', async () => {
37
+ const base64Data = Buffer.from('jpeg-image').toString('base64');
38
+ mockCdp.send.mock.mockImplementation(() => Promise.resolve({ data: base64Data }));
39
+
40
+ const result = await screenshotCapture.captureViewport({ format: 'jpeg', quality: 80 });
41
+
42
+ assert.deepStrictEqual(mockCdp.send.mock.calls[0].arguments, [
43
+ 'Page.captureScreenshot',
44
+ { format: 'jpeg', quality: 80 }
45
+ ]);
46
+ assert.ok(Buffer.isBuffer(result));
47
+ });
48
+
49
+ it('should throw error for quality above 100', async () => {
50
+ await assert.rejects(
51
+ () => screenshotCapture.captureViewport({ format: 'jpeg', quality: 150 }),
52
+ { message: 'Quality must be a number between 0 and 100' }
53
+ );
54
+ });
55
+
56
+ it('should throw error for quality below 0', async () => {
57
+ await assert.rejects(
58
+ () => screenshotCapture.captureViewport({ format: 'jpeg', quality: -10 }),
59
+ { message: 'Quality must be a number between 0 and 100' }
60
+ );
61
+ });
62
+
63
+ it('should throw error when quality is set for png format', async () => {
64
+ await assert.rejects(
65
+ () => screenshotCapture.captureViewport({ format: 'png', quality: 80 }),
66
+ { message: 'Quality option is only supported for jpeg and webp formats, not png' }
67
+ );
68
+ });
69
+
70
+ it('should accept png without quality', async () => {
71
+ const base64Data = Buffer.from('png-image').toString('base64');
72
+ mockCdp.send.mock.mockImplementation(() => Promise.resolve({ data: base64Data }));
73
+
74
+ await screenshotCapture.captureViewport({ format: 'png' });
75
+
76
+ assert.strictEqual(mockCdp.send.mock.calls[0].arguments[1].quality, undefined);
77
+ });
78
+ });
79
+
80
+ describe('captureFullPage', () => {
81
+ it('should capture full page screenshot', async () => {
82
+ const base64Data = Buffer.from('full-page').toString('base64');
83
+ mockCdp.send.mock.mockImplementation((method) => {
84
+ if (method === 'Page.getLayoutMetrics') {
85
+ return Promise.resolve({
86
+ contentSize: { width: 1200, height: 3000 }
87
+ });
88
+ }
89
+ return Promise.resolve({ data: base64Data });
90
+ });
91
+
92
+ const result = await screenshotCapture.captureFullPage();
93
+
94
+ assert.strictEqual(mockCdp.send.mock.calls.length, 2);
95
+ assert.strictEqual(mockCdp.send.mock.calls[0].arguments[0], 'Page.getLayoutMetrics');
96
+ assert.deepStrictEqual(mockCdp.send.mock.calls[1].arguments[1].clip, {
97
+ x: 0,
98
+ y: 0,
99
+ width: 1200,
100
+ height: 3000,
101
+ scale: 1
102
+ });
103
+ assert.ok(Buffer.isBuffer(result));
104
+ });
105
+ });
106
+
107
+ describe('captureRegion', () => {
108
+ it('should capture specific region', async () => {
109
+ const base64Data = Buffer.from('region').toString('base64');
110
+ mockCdp.send.mock.mockImplementation(() => Promise.resolve({ data: base64Data }));
111
+
112
+ const region = { x: 100, y: 200, width: 300, height: 400 };
113
+ const result = await screenshotCapture.captureRegion(region);
114
+
115
+ assert.deepStrictEqual(mockCdp.send.mock.calls[0].arguments[1].clip, {
116
+ x: 100,
117
+ y: 200,
118
+ width: 300,
119
+ height: 400,
120
+ scale: 1
121
+ });
122
+ assert.ok(Buffer.isBuffer(result));
123
+ });
124
+
125
+ it('should capture region with custom scale', async () => {
126
+ const base64Data = Buffer.from('region').toString('base64');
127
+ mockCdp.send.mock.mockImplementation(() => Promise.resolve({ data: base64Data }));
128
+
129
+ const region = { x: 0, y: 0, width: 100, height: 100 };
130
+ await screenshotCapture.captureRegion(region, { scale: 2 });
131
+
132
+ assert.strictEqual(mockCdp.send.mock.calls[0].arguments[1].clip.scale, 2);
133
+ });
134
+ });
135
+
136
+ describe('captureElement', () => {
137
+ it('should capture element with bounding box', async () => {
138
+ const base64Data = Buffer.from('element').toString('base64');
139
+ mockCdp.send.mock.mockImplementation(() => Promise.resolve({ data: base64Data }));
140
+
141
+ const boundingBox = { x: 50, y: 100, width: 200, height: 150 };
142
+ await screenshotCapture.captureElement(boundingBox);
143
+
144
+ assert.deepStrictEqual(mockCdp.send.mock.calls[0].arguments[1].clip, {
145
+ x: 50,
146
+ y: 100,
147
+ width: 200,
148
+ height: 150,
149
+ scale: 1
150
+ });
151
+ });
152
+
153
+ it('should capture element with padding', async () => {
154
+ const base64Data = Buffer.from('element').toString('base64');
155
+ mockCdp.send.mock.mockImplementation(() => Promise.resolve({ data: base64Data }));
156
+
157
+ const boundingBox = { x: 50, y: 100, width: 200, height: 150 };
158
+ await screenshotCapture.captureElement(boundingBox, { padding: 10 });
159
+
160
+ assert.deepStrictEqual(mockCdp.send.mock.calls[0].arguments[1].clip, {
161
+ x: 40,
162
+ y: 90,
163
+ width: 220,
164
+ height: 170,
165
+ scale: 1
166
+ });
167
+ });
168
+
169
+ it('should not allow negative coordinates with padding', async () => {
170
+ const base64Data = Buffer.from('element').toString('base64');
171
+ mockCdp.send.mock.mockImplementation(() => Promise.resolve({ data: base64Data }));
172
+
173
+ const boundingBox = { x: 5, y: 5, width: 100, height: 100 };
174
+ await screenshotCapture.captureElement(boundingBox, { padding: 10 });
175
+
176
+ const clip = mockCdp.send.mock.calls[0].arguments[1].clip;
177
+ assert.strictEqual(clip.x, 0);
178
+ assert.strictEqual(clip.y, 0);
179
+ });
180
+ });
181
+
182
+ describe('format validation', () => {
183
+ it('should accept png format', async () => {
184
+ const base64Data = Buffer.from('png').toString('base64');
185
+ mockCdp.send.mock.mockImplementation(() => Promise.resolve({ data: base64Data }));
186
+
187
+ await screenshotCapture.captureViewport({ format: 'png' });
188
+
189
+ assert.strictEqual(mockCdp.send.mock.calls[0].arguments[1].format, 'png');
190
+ });
191
+
192
+ it('should accept jpeg format', async () => {
193
+ const base64Data = Buffer.from('jpeg').toString('base64');
194
+ mockCdp.send.mock.mockImplementation(() => Promise.resolve({ data: base64Data }));
195
+
196
+ await screenshotCapture.captureViewport({ format: 'jpeg' });
197
+
198
+ assert.strictEqual(mockCdp.send.mock.calls[0].arguments[1].format, 'jpeg');
199
+ });
200
+
201
+ it('should accept webp format', async () => {
202
+ const base64Data = Buffer.from('webp').toString('base64');
203
+ mockCdp.send.mock.mockImplementation(() => Promise.resolve({ data: base64Data }));
204
+
205
+ await screenshotCapture.captureViewport({ format: 'webp' });
206
+
207
+ assert.strictEqual(mockCdp.send.mock.calls[0].arguments[1].format, 'webp');
208
+ });
209
+
210
+ it('should throw error for invalid format', async () => {
211
+ await assert.rejects(
212
+ () => screenshotCapture.captureViewport({ format: 'gif' }),
213
+ { message: 'Invalid screenshot format "gif". Valid formats are: png, jpeg, webp' }
214
+ );
215
+ });
216
+
217
+ it('should throw error for invalid format in captureFullPage', async () => {
218
+ await assert.rejects(
219
+ () => screenshotCapture.captureFullPage({ format: 'bmp' }),
220
+ { message: 'Invalid screenshot format "bmp". Valid formats are: png, jpeg, webp' }
221
+ );
222
+ });
223
+
224
+ it('should throw error for invalid format in captureRegion', async () => {
225
+ await assert.rejects(
226
+ () => screenshotCapture.captureRegion({ x: 0, y: 0, width: 100, height: 100 }, { format: 'tiff' }),
227
+ { message: 'Invalid screenshot format "tiff". Valid formats are: png, jpeg, webp' }
228
+ );
229
+ });
230
+
231
+ it('should throw error for invalid format in captureElement', async () => {
232
+ await assert.rejects(
233
+ () => screenshotCapture.captureElement({ x: 0, y: 0, width: 100, height: 100 }, { format: 'svg' }),
234
+ { message: 'Invalid screenshot format "svg". Valid formats are: png, jpeg, webp' }
235
+ );
236
+ });
237
+
238
+ it('should accept quality for webp format', async () => {
239
+ const base64Data = Buffer.from('webp').toString('base64');
240
+ mockCdp.send.mock.mockImplementation(() => Promise.resolve({ data: base64Data }));
241
+
242
+ await screenshotCapture.captureViewport({ format: 'webp', quality: 75 });
243
+
244
+ assert.deepStrictEqual(mockCdp.send.mock.calls[0].arguments, [
245
+ 'Page.captureScreenshot',
246
+ { format: 'webp', quality: 75 }
247
+ ]);
248
+ });
249
+
250
+ it('should throw error for non-numeric quality', async () => {
251
+ await assert.rejects(
252
+ () => screenshotCapture.captureViewport({ format: 'jpeg', quality: 'high' }),
253
+ { message: 'Quality must be a number between 0 and 100' }
254
+ );
255
+ });
256
+
257
+ it('should accept quality of exactly 0', async () => {
258
+ const base64Data = Buffer.from('jpeg').toString('base64');
259
+ mockCdp.send.mock.mockImplementation(() => Promise.resolve({ data: base64Data }));
260
+
261
+ await screenshotCapture.captureViewport({ format: 'jpeg', quality: 0 });
262
+
263
+ assert.strictEqual(mockCdp.send.mock.calls[0].arguments[1].quality, 0);
264
+ });
265
+
266
+ it('should accept quality of exactly 100', async () => {
267
+ const base64Data = Buffer.from('jpeg').toString('base64');
268
+ mockCdp.send.mock.mockImplementation(() => Promise.resolve({ data: base64Data }));
269
+
270
+ await screenshotCapture.captureViewport({ format: 'jpeg', quality: 100 });
271
+
272
+ assert.strictEqual(mockCdp.send.mock.calls[0].arguments[1].quality, 100);
273
+ });
274
+
275
+ it('should validate format before making CDP call in captureFullPage', async () => {
276
+ // Ensure validation happens before getLayoutMetrics is called
277
+ await assert.rejects(
278
+ () => screenshotCapture.captureFullPage({ format: 'invalid' }),
279
+ /Invalid screenshot format/
280
+ );
281
+
282
+ // Verify no CDP calls were made
283
+ assert.strictEqual(mockCdp.send.mock.calls.length, 0);
284
+ });
285
+ });
286
+
287
+ describe('saveToFile', () => {
288
+ it('should return absolute path for saved file', async () => {
289
+ // Note: We can't easily mock fs/promises in ESM, so we just test the path logic
290
+ const buffer = Buffer.from('test-data');
291
+ const path = '/tmp/screenshot-test-' + Date.now() + '.png';
292
+
293
+ // This will actually write the file, but tests the functionality
294
+ const result = await screenshotCapture.saveToFile(buffer, path);
295
+
296
+ assert.ok(result.endsWith('.png'));
297
+ assert.ok(result.includes('screenshot-test-'));
298
+
299
+ // Cleanup
300
+ const fs = await import('fs/promises');
301
+ try {
302
+ await fs.unlink(result);
303
+ } catch (e) {
304
+ // Ignore cleanup errors
305
+ }
306
+ });
307
+ });
308
+
309
+ describe('captureToFile', () => {
310
+ it('should capture viewport and save to file', async () => {
311
+ const base64Data = Buffer.from('viewport').toString('base64');
312
+ mockCdp.send.mock.mockImplementation(() => Promise.resolve({ data: base64Data }));
313
+
314
+ const path = '/tmp/capture-test-' + Date.now() + '.png';
315
+ const result = await screenshotCapture.captureToFile(path);
316
+
317
+ assert.ok(result.endsWith('.png'));
318
+ assert.ok(result.includes('capture-test-'));
319
+
320
+ // Verify CDP was called correctly
321
+ assert.strictEqual(mockCdp.send.mock.calls[0].arguments[0], 'Page.captureScreenshot');
322
+
323
+ // Cleanup
324
+ const fs = await import('fs/promises');
325
+ try {
326
+ await fs.unlink(result);
327
+ } catch (e) {
328
+ // Ignore cleanup errors
329
+ }
330
+ });
331
+
332
+ it('should capture full page when option is set', async () => {
333
+ const base64Data = Buffer.from('fullpage').toString('base64');
334
+ mockCdp.send.mock.mockImplementation((method) => {
335
+ if (method === 'Page.getLayoutMetrics') {
336
+ return Promise.resolve({ contentSize: { width: 800, height: 2000 } });
337
+ }
338
+ return Promise.resolve({ data: base64Data });
339
+ });
340
+
341
+ const path = '/tmp/fullpage-test-' + Date.now() + '.png';
342
+ await screenshotCapture.captureToFile(path, { fullPage: true });
343
+
344
+ // Should call getLayoutMetrics first for full page
345
+ assert.strictEqual(mockCdp.send.mock.calls[0].arguments[0], 'Page.getLayoutMetrics');
346
+
347
+ // Cleanup
348
+ const fs = await import('fs/promises');
349
+ try {
350
+ await fs.unlink(path);
351
+ } catch (e) {
352
+ // Ignore cleanup errors
353
+ }
354
+ });
355
+ });
356
+ });
@@ -0,0 +1,257 @@
1
+ import { describe, it, mock, beforeEach, afterEach } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { createSessionRegistry } from '../cdp.js';
4
+
5
+ /**
6
+ * Mock CDPConnection for testing
7
+ */
8
+ function createMockConnection() {
9
+ const eventListeners = new Map();
10
+ let sessionCounter = 0;
11
+
12
+ return {
13
+ eventListeners,
14
+ sentCommands: [],
15
+
16
+ on(event, callback) {
17
+ if (!eventListeners.has(event)) {
18
+ eventListeners.set(event, new Set());
19
+ }
20
+ eventListeners.get(event).add(callback);
21
+ },
22
+
23
+ off(event, callback) {
24
+ const listeners = eventListeners.get(event);
25
+ if (listeners) {
26
+ listeners.delete(callback);
27
+ }
28
+ },
29
+
30
+ async send(method, params = {}) {
31
+ this.sentCommands.push({ method, params });
32
+
33
+ switch (method) {
34
+ case 'Target.attachToTarget':
35
+ sessionCounter++;
36
+ return { sessionId: `session-${sessionCounter}` };
37
+ case 'Target.detachFromTarget':
38
+ return {};
39
+ default:
40
+ throw new Error(`Unknown method: ${method}`);
41
+ }
42
+ },
43
+
44
+ // Helper to emit events for testing
45
+ emit(event, params) {
46
+ const listeners = eventListeners.get(event);
47
+ if (listeners) {
48
+ for (const callback of listeners) {
49
+ callback(params);
50
+ }
51
+ }
52
+ }
53
+ };
54
+ }
55
+
56
+ describe('SessionRegistry', () => {
57
+ let mockConnection;
58
+ let sessionRegistry;
59
+
60
+ beforeEach(() => {
61
+ mockConnection = createMockConnection();
62
+ sessionRegistry = createSessionRegistry(mockConnection);
63
+ });
64
+
65
+ describe('constructor', () => {
66
+ it('should create instance and register event listeners', () => {
67
+ assert.ok(sessionRegistry);
68
+ assert.ok(mockConnection.eventListeners.has('Target.attachedToTarget'));
69
+ assert.ok(mockConnection.eventListeners.has('Target.detachedFromTarget'));
70
+ assert.ok(mockConnection.eventListeners.has('Target.targetDestroyed'));
71
+ });
72
+ });
73
+
74
+ describe('attach', () => {
75
+ it('should attach to target and return sessionId', async () => {
76
+ const sessionId = await sessionRegistry.attach('target-1');
77
+
78
+ assert.strictEqual(sessionId, 'session-1');
79
+
80
+ const cmd = mockConnection.sentCommands.find(c => c.method === 'Target.attachToTarget');
81
+ assert.strictEqual(cmd.params.targetId, 'target-1');
82
+ assert.strictEqual(cmd.params.flatten, true);
83
+ });
84
+
85
+ it('should return existing sessionId if already attached', async () => {
86
+ const sessionId1 = await sessionRegistry.attach('target-1');
87
+ const sessionId2 = await sessionRegistry.attach('target-1');
88
+
89
+ assert.strictEqual(sessionId1, sessionId2);
90
+
91
+ const cmds = mockConnection.sentCommands.filter(c => c.method === 'Target.attachToTarget');
92
+ assert.strictEqual(cmds.length, 1);
93
+ });
94
+
95
+ it('should track session and target mappings', async () => {
96
+ await sessionRegistry.attach('target-1');
97
+
98
+ assert.strictEqual(sessionRegistry.getSessionForTarget('target-1'), 'session-1');
99
+ assert.strictEqual(sessionRegistry.getTargetForSession('session-1'), 'target-1');
100
+ });
101
+ });
102
+
103
+ describe('detach', () => {
104
+ it('should detach from session', async () => {
105
+ const sessionId = await sessionRegistry.attach('target-1');
106
+ await sessionRegistry.detach(sessionId);
107
+
108
+ const cmd = mockConnection.sentCommands.find(c => c.method === 'Target.detachFromTarget');
109
+ assert.strictEqual(cmd.params.sessionId, sessionId);
110
+ });
111
+
112
+ it('should clear session and target mappings', async () => {
113
+ const sessionId = await sessionRegistry.attach('target-1');
114
+ await sessionRegistry.detach(sessionId);
115
+
116
+ assert.strictEqual(sessionRegistry.getSessionForTarget('target-1'), undefined);
117
+ assert.strictEqual(sessionRegistry.getTargetForSession(sessionId), undefined);
118
+ });
119
+
120
+ it('should do nothing for unknown session', async () => {
121
+ await sessionRegistry.detach('unknown-session');
122
+
123
+ const cmds = mockConnection.sentCommands.filter(c => c.method === 'Target.detachFromTarget');
124
+ assert.strictEqual(cmds.length, 0);
125
+ });
126
+ });
127
+
128
+ describe('detachByTarget', () => {
129
+ it('should detach by targetId', async () => {
130
+ await sessionRegistry.attach('target-1');
131
+ await sessionRegistry.detachByTarget('target-1');
132
+
133
+ assert.strictEqual(sessionRegistry.isAttached('target-1'), false);
134
+ });
135
+
136
+ it('should do nothing for unknown target', async () => {
137
+ await sessionRegistry.detachByTarget('unknown-target');
138
+
139
+ const cmds = mockConnection.sentCommands.filter(c => c.method === 'Target.detachFromTarget');
140
+ assert.strictEqual(cmds.length, 0);
141
+ });
142
+ });
143
+
144
+ describe('isAttached', () => {
145
+ it('should return true for attached target', async () => {
146
+ await sessionRegistry.attach('target-1');
147
+
148
+ assert.strictEqual(sessionRegistry.isAttached('target-1'), true);
149
+ });
150
+
151
+ it('should return false for unattached target', () => {
152
+ assert.strictEqual(sessionRegistry.isAttached('target-1'), false);
153
+ });
154
+ });
155
+
156
+ describe('getAllSessions', () => {
157
+ it('should return all active sessions', async () => {
158
+ await sessionRegistry.attach('target-1');
159
+ await sessionRegistry.attach('target-2');
160
+
161
+ const sessions = sessionRegistry.getAllSessions();
162
+
163
+ assert.strictEqual(sessions.length, 2);
164
+ assert.ok(sessions.some(s => s.targetId === 'target-1'));
165
+ assert.ok(sessions.some(s => s.targetId === 'target-2'));
166
+ });
167
+
168
+ it('should return empty array when no sessions', () => {
169
+ const sessions = sessionRegistry.getAllSessions();
170
+ assert.strictEqual(sessions.length, 0);
171
+ });
172
+ });
173
+
174
+ describe('detachAll', () => {
175
+ it('should detach all sessions', async () => {
176
+ await sessionRegistry.attach('target-1');
177
+ await sessionRegistry.attach('target-2');
178
+
179
+ await sessionRegistry.detachAll();
180
+
181
+ assert.strictEqual(sessionRegistry.getAllSessions().length, 0);
182
+
183
+ const cmds = mockConnection.sentCommands.filter(c => c.method === 'Target.detachFromTarget');
184
+ assert.strictEqual(cmds.length, 2);
185
+ });
186
+ });
187
+
188
+ describe('event handling', () => {
189
+ it('should handle attachedToTarget event', () => {
190
+ mockConnection.emit('Target.attachedToTarget', {
191
+ sessionId: 'external-session',
192
+ targetInfo: { targetId: 'external-target' }
193
+ });
194
+
195
+ assert.strictEqual(sessionRegistry.getSessionForTarget('external-target'), 'external-session');
196
+ assert.strictEqual(sessionRegistry.getTargetForSession('external-session'), 'external-target');
197
+ });
198
+
199
+ it('should handle detachedFromTarget event', async () => {
200
+ const sessionId = await sessionRegistry.attach('target-1');
201
+
202
+ mockConnection.emit('Target.detachedFromTarget', { sessionId });
203
+
204
+ assert.strictEqual(sessionRegistry.isAttached('target-1'), false);
205
+ });
206
+
207
+ it('should handle targetDestroyed event (external tab close)', async () => {
208
+ await sessionRegistry.attach('target-1');
209
+ assert.strictEqual(sessionRegistry.isAttached('target-1'), true);
210
+
211
+ // Simulate Chrome closing the tab externally
212
+ mockConnection.emit('Target.targetDestroyed', { targetId: 'target-1' });
213
+
214
+ assert.strictEqual(sessionRegistry.isAttached('target-1'), false);
215
+ assert.strictEqual(sessionRegistry.getSessionForTarget('target-1'), undefined);
216
+ });
217
+
218
+ it('should ignore targetDestroyed for unknown targets', () => {
219
+ // Should not throw
220
+ mockConnection.emit('Target.targetDestroyed', { targetId: 'unknown-target' });
221
+ });
222
+ });
223
+
224
+ describe('concurrent attach handling', () => {
225
+ it('should return same session for concurrent attach calls to same target', async () => {
226
+ // Start two attach calls concurrently
227
+ const [sessionId1, sessionId2] = await Promise.all([
228
+ sessionRegistry.attach('target-1'),
229
+ sessionRegistry.attach('target-1')
230
+ ]);
231
+
232
+ assert.strictEqual(sessionId1, sessionId2);
233
+
234
+ // Should only have sent one attach command
235
+ const cmds = mockConnection.sentCommands.filter(c => c.method === 'Target.attachToTarget');
236
+ assert.strictEqual(cmds.length, 1);
237
+ });
238
+ });
239
+
240
+ describe('cleanup', () => {
241
+ it('should detach all sessions and remove event listeners', async () => {
242
+ await sessionRegistry.attach('target-1');
243
+ await sessionRegistry.attach('target-2');
244
+
245
+ await sessionRegistry.cleanup();
246
+
247
+ assert.strictEqual(sessionRegistry.getAllSessions().length, 0);
248
+
249
+ // Event listeners should be removed
250
+ const attachedListeners = mockConnection.eventListeners.get('Target.attachedToTarget');
251
+ assert.strictEqual(attachedListeners?.size || 0, 0);
252
+
253
+ const destroyedListeners = mockConnection.eventListeners.get('Target.targetDestroyed');
254
+ assert.strictEqual(destroyedListeners?.size || 0, 0);
255
+ });
256
+ });
257
+ });