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,181 @@
1
+ import { describe, it, mock, beforeEach, afterEach } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { createDiscovery } from '../cdp.js';
4
+
5
+ describe('ChromeDiscovery', () => {
6
+ let originalFetch;
7
+ let mockFetch;
8
+
9
+ beforeEach(() => {
10
+ originalFetch = globalThis.fetch;
11
+ mockFetch = mock.fn();
12
+ globalThis.fetch = mockFetch;
13
+ });
14
+
15
+ afterEach(() => {
16
+ globalThis.fetch = originalFetch;
17
+ });
18
+
19
+ describe('createDiscovery', () => {
20
+ it('should create discovery with default host and port', () => {
21
+ const discovery = createDiscovery();
22
+ assert.ok(discovery);
23
+ assert.ok(typeof discovery.getVersion === 'function');
24
+ });
25
+
26
+ it('should accept custom host and port', () => {
27
+ const discovery = createDiscovery('192.168.1.100', 9333);
28
+ assert.ok(discovery);
29
+ });
30
+ });
31
+
32
+ describe('getVersion', () => {
33
+ it('should return browser version info', async () => {
34
+ const mockResponse = {
35
+ Browser: 'Chrome/120.0.0.0',
36
+ 'Protocol-Version': '1.3',
37
+ webSocketDebuggerUrl: 'ws://localhost:9222/devtools/browser/abc'
38
+ };
39
+
40
+ mockFetch.mock.mockImplementation(() => Promise.resolve({
41
+ ok: true,
42
+ json: () => Promise.resolve(mockResponse)
43
+ }));
44
+
45
+ const discovery = createDiscovery();
46
+ const result = await discovery.getVersion();
47
+
48
+ assert.strictEqual(result.browser, 'Chrome/120.0.0.0');
49
+ assert.strictEqual(result.protocolVersion, '1.3');
50
+ assert.strictEqual(result.webSocketDebuggerUrl, 'ws://localhost:9222/devtools/browser/abc');
51
+ });
52
+
53
+ it('should throw when Chrome not reachable', async () => {
54
+ mockFetch.mock.mockImplementation(() => Promise.resolve({
55
+ ok: false,
56
+ status: 404
57
+ }));
58
+
59
+ const discovery = createDiscovery();
60
+
61
+ await assert.rejects(
62
+ () => discovery.getVersion(),
63
+ /Chrome not reachable/
64
+ );
65
+ });
66
+ });
67
+
68
+ describe('getTargets', () => {
69
+ it('should return all targets', async () => {
70
+ const mockTargets = [
71
+ { id: '1', type: 'page', title: 'Tab 1', url: 'https://example.com' },
72
+ { id: '2', type: 'service_worker', title: 'SW', url: 'https://example.com/sw.js' }
73
+ ];
74
+
75
+ mockFetch.mock.mockImplementation(() => Promise.resolve({
76
+ ok: true,
77
+ json: () => Promise.resolve(mockTargets)
78
+ }));
79
+
80
+ const discovery = createDiscovery();
81
+ const result = await discovery.getTargets();
82
+
83
+ assert.strictEqual(result.length, 2);
84
+ assert.strictEqual(result[0].id, '1');
85
+ assert.strictEqual(result[1].type, 'service_worker');
86
+ });
87
+
88
+ it('should throw on failure', async () => {
89
+ mockFetch.mock.mockImplementation(() => Promise.resolve({
90
+ ok: false,
91
+ status: 500
92
+ }));
93
+
94
+ const discovery = createDiscovery();
95
+
96
+ await assert.rejects(
97
+ () => discovery.getTargets(),
98
+ /Failed to get targets: 500/
99
+ );
100
+ });
101
+ });
102
+
103
+ describe('getPages', () => {
104
+ it('should return only page targets', async () => {
105
+ const mockTargets = [
106
+ { id: '1', type: 'page', title: 'Tab 1', url: 'https://example.com' },
107
+ { id: '2', type: 'service_worker', title: 'SW', url: 'https://example.com/sw.js' },
108
+ { id: '3', type: 'page', title: 'Tab 2', url: 'https://test.com' }
109
+ ];
110
+
111
+ mockFetch.mock.mockImplementation(() => Promise.resolve({
112
+ ok: true,
113
+ json: () => Promise.resolve(mockTargets)
114
+ }));
115
+
116
+ const discovery = createDiscovery();
117
+ const result = await discovery.getPages();
118
+
119
+ assert.strictEqual(result.length, 2);
120
+ assert.ok(result.every(t => t.type === 'page'));
121
+ });
122
+ });
123
+
124
+ describe('findPageByUrl', () => {
125
+ const mockTargets = [
126
+ { id: '1', type: 'page', title: 'Example', url: 'https://example.com/path' },
127
+ { id: '2', type: 'page', title: 'Test', url: 'https://test.com' }
128
+ ];
129
+
130
+ beforeEach(() => {
131
+ mockFetch.mock.mockImplementation(() => Promise.resolve({
132
+ ok: true,
133
+ json: () => Promise.resolve(mockTargets)
134
+ }));
135
+ });
136
+
137
+ it('should find page by string pattern', async () => {
138
+ const discovery = createDiscovery();
139
+ const result = await discovery.findPageByUrl('example.com');
140
+
141
+ assert.strictEqual(result.id, '1');
142
+ });
143
+
144
+ it('should find page by regex pattern', async () => {
145
+ const discovery = createDiscovery();
146
+ const result = await discovery.findPageByUrl(/test\.com$/);
147
+
148
+ assert.strictEqual(result.id, '2');
149
+ });
150
+
151
+ it('should return null when no match', async () => {
152
+ const discovery = createDiscovery();
153
+ const result = await discovery.findPageByUrl('nonexistent.com');
154
+
155
+ assert.strictEqual(result, null);
156
+ });
157
+ });
158
+
159
+ describe('isAvailable', () => {
160
+ it('should return true when Chrome is running', async () => {
161
+ mockFetch.mock.mockImplementation(() => Promise.resolve({
162
+ ok: true,
163
+ json: () => Promise.resolve({ Browser: 'Chrome' })
164
+ }));
165
+
166
+ const discovery = createDiscovery();
167
+ const result = await discovery.isAvailable();
168
+
169
+ assert.strictEqual(result, true);
170
+ });
171
+
172
+ it('should return false when Chrome is not running', async () => {
173
+ mockFetch.mock.mockImplementation(() => Promise.reject(new Error('Connection refused')));
174
+
175
+ const discovery = createDiscovery();
176
+ const result = await discovery.isAvailable();
177
+
178
+ assert.strictEqual(result, false);
179
+ });
180
+ });
181
+ });
@@ -0,0 +1,302 @@
1
+ import { describe, it, mock, beforeEach, afterEach } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { createConsoleCapture } from '../capture.js';
4
+
5
+ describe('ConsoleCapture', () => {
6
+ let consoleCapture;
7
+ let mockCdp;
8
+ let eventHandlers;
9
+
10
+ beforeEach(() => {
11
+ eventHandlers = {};
12
+ mockCdp = {
13
+ send: mock.fn(() => Promise.resolve()),
14
+ on: mock.fn((event, handler) => {
15
+ eventHandlers[event] = handler;
16
+ }),
17
+ off: mock.fn((event, handler) => {
18
+ if (eventHandlers[event] === handler) {
19
+ delete eventHandlers[event];
20
+ }
21
+ })
22
+ };
23
+ consoleCapture = createConsoleCapture(mockCdp);
24
+ });
25
+
26
+ afterEach(() => {
27
+ mock.reset();
28
+ });
29
+
30
+ describe('factory function', () => {
31
+ it('should create instance via factory function', () => {
32
+ const capture = createConsoleCapture(mockCdp);
33
+ assert.ok(capture);
34
+ assert.strictEqual(typeof capture.startCapture, 'function');
35
+ assert.strictEqual(typeof capture.stopCapture, 'function');
36
+ assert.strictEqual(typeof capture.getMessages, 'function');
37
+ });
38
+ });
39
+
40
+ describe('startCapture', () => {
41
+ it('should enable Runtime domain only (not Console)', async () => {
42
+ await consoleCapture.startCapture();
43
+
44
+ const sendCalls = mockCdp.send.mock.calls.map(c => c.arguments[0]);
45
+ assert.ok(sendCalls.includes('Runtime.enable'));
46
+ assert.ok(!sendCalls.includes('Console.enable'), 'Console.enable should not be called');
47
+ });
48
+
49
+ it('should register only Runtime event handlers', async () => {
50
+ await consoleCapture.startCapture();
51
+
52
+ assert.strictEqual(mockCdp.on.mock.calls.length, 2);
53
+ assert.ok(!eventHandlers['Console.messageAdded'], 'Console.messageAdded should not be registered');
54
+ assert.ok(eventHandlers['Runtime.consoleAPICalled']);
55
+ assert.ok(eventHandlers['Runtime.exceptionThrown']);
56
+ });
57
+
58
+ it('should not start capturing twice', async () => {
59
+ await consoleCapture.startCapture();
60
+ await consoleCapture.startCapture();
61
+
62
+ assert.strictEqual(mockCdp.send.mock.calls.length, 1);
63
+ });
64
+ });
65
+
66
+ describe('stopCapture', () => {
67
+ it('should disable Runtime domain only (not Console)', async () => {
68
+ await consoleCapture.startCapture();
69
+ await consoleCapture.stopCapture();
70
+
71
+ const sendCalls = mockCdp.send.mock.calls.map(c => c.arguments[0]);
72
+ assert.ok(sendCalls.includes('Runtime.disable'));
73
+ assert.ok(!sendCalls.includes('Console.disable'), 'Console.disable should not be called');
74
+ });
75
+
76
+ it('should unregister event handlers', async () => {
77
+ await consoleCapture.startCapture();
78
+ await consoleCapture.stopCapture();
79
+
80
+ assert.strictEqual(mockCdp.off.mock.calls.length, 2);
81
+ });
82
+
83
+ it('should do nothing if not capturing', async () => {
84
+ await consoleCapture.stopCapture();
85
+
86
+ assert.strictEqual(mockCdp.send.mock.calls.length, 0);
87
+ });
88
+ });
89
+
90
+ describe('message capture', () => {
91
+ it('should capture Runtime.consoleAPICalled events with type "console"', async () => {
92
+ await consoleCapture.startCapture();
93
+
94
+ eventHandlers['Runtime.consoleAPICalled']({
95
+ type: 'log',
96
+ args: [{ value: 'Hello' }, { value: 'World' }],
97
+ timestamp: 12345
98
+ });
99
+
100
+ const messages = consoleCapture.getMessages();
101
+ assert.strictEqual(messages.length, 1);
102
+ assert.strictEqual(messages[0].type, 'console');
103
+ assert.strictEqual(messages[0].level, 'log');
104
+ assert.strictEqual(messages[0].text, 'Hello World');
105
+ });
106
+
107
+ it('should not produce duplicate messages (only one per console call)', async () => {
108
+ await consoleCapture.startCapture();
109
+
110
+ // Only consoleAPICalled should be registered - no Console.messageAdded
111
+ eventHandlers['Runtime.consoleAPICalled']({
112
+ type: 'log',
113
+ args: [{ value: 'Test message' }],
114
+ timestamp: 12345
115
+ });
116
+
117
+ const messages = consoleCapture.getMessages();
118
+ assert.strictEqual(messages.length, 1, 'Should have exactly one message per console call');
119
+ });
120
+
121
+ it('should capture Runtime.exceptionThrown events', async () => {
122
+ await consoleCapture.startCapture();
123
+
124
+ eventHandlers['Runtime.exceptionThrown']({
125
+ timestamp: 12345,
126
+ exceptionDetails: {
127
+ text: 'Uncaught ReferenceError: foo is not defined',
128
+ url: 'http://test.com/app.js',
129
+ lineNumber: 100,
130
+ columnNumber: 5,
131
+ exception: {
132
+ description: 'ReferenceError: foo is not defined'
133
+ }
134
+ }
135
+ });
136
+
137
+ const messages = consoleCapture.getMessages();
138
+ assert.strictEqual(messages.length, 1);
139
+ assert.strictEqual(messages[0].type, 'exception');
140
+ assert.strictEqual(messages[0].level, 'error');
141
+ });
142
+ });
143
+
144
+ describe('getMessagesByLevel', () => {
145
+ it('should filter messages by single level', async () => {
146
+ await consoleCapture.startCapture();
147
+
148
+ eventHandlers['Runtime.consoleAPICalled']({ type: 'log', args: [{ value: 'Log' }], timestamp: 1 });
149
+ eventHandlers['Runtime.consoleAPICalled']({ type: 'error', args: [{ value: 'Error' }], timestamp: 2 });
150
+ eventHandlers['Runtime.consoleAPICalled']({ type: 'warn', args: [{ value: 'Warning' }], timestamp: 3 });
151
+
152
+ const errors = consoleCapture.getMessagesByLevel('error');
153
+ assert.strictEqual(errors.length, 1);
154
+ assert.strictEqual(errors[0].text, 'Error');
155
+ });
156
+
157
+ it('should filter messages by multiple levels', async () => {
158
+ await consoleCapture.startCapture();
159
+
160
+ eventHandlers['Runtime.consoleAPICalled']({ type: 'log', args: [{ value: 'Log' }], timestamp: 1 });
161
+ eventHandlers['Runtime.consoleAPICalled']({ type: 'error', args: [{ value: 'Error' }], timestamp: 2 });
162
+ eventHandlers['Runtime.consoleAPICalled']({ type: 'warn', args: [{ value: 'Warning' }], timestamp: 3 });
163
+
164
+ const filtered = consoleCapture.getMessagesByLevel(['error', 'warning']);
165
+ assert.strictEqual(filtered.length, 2);
166
+ });
167
+ });
168
+
169
+ describe('getErrors', () => {
170
+ it('should return only errors and exceptions', async () => {
171
+ await consoleCapture.startCapture();
172
+
173
+ eventHandlers['Runtime.consoleAPICalled']({ type: 'log', args: [{ value: 'Log' }], timestamp: 1 });
174
+ eventHandlers['Runtime.consoleAPICalled']({ type: 'error', args: [{ value: 'Error' }], timestamp: 2 });
175
+ eventHandlers['Runtime.exceptionThrown']({
176
+ timestamp: 3,
177
+ exceptionDetails: { text: 'Exception' }
178
+ });
179
+
180
+ const errors = consoleCapture.getErrors();
181
+ assert.strictEqual(errors.length, 2);
182
+ });
183
+ });
184
+
185
+ describe('getWarnings', () => {
186
+ it('should return only warnings', async () => {
187
+ await consoleCapture.startCapture();
188
+
189
+ eventHandlers['Runtime.consoleAPICalled']({ type: 'warn', args: [{ value: 'Warning 1' }], timestamp: 1 });
190
+ eventHandlers['Runtime.consoleAPICalled']({ type: 'log', args: [{ value: 'Log' }], timestamp: 2 });
191
+ eventHandlers['Runtime.consoleAPICalled']({ type: 'warning', args: [{ value: 'Warning 2' }], timestamp: 3 });
192
+
193
+ const warnings = consoleCapture.getWarnings();
194
+ assert.strictEqual(warnings.length, 2);
195
+ });
196
+ });
197
+
198
+ describe('hasErrors', () => {
199
+ it('should return false when no errors', async () => {
200
+ await consoleCapture.startCapture();
201
+
202
+ eventHandlers['Runtime.consoleAPICalled']({ type: 'log', args: [{ value: 'Log' }], timestamp: 1 });
203
+
204
+ assert.strictEqual(consoleCapture.hasErrors(), false);
205
+ });
206
+
207
+ it('should return true when errors exist', async () => {
208
+ await consoleCapture.startCapture();
209
+
210
+ eventHandlers['Runtime.consoleAPICalled']({ type: 'error', args: [{ value: 'Error' }], timestamp: 1 });
211
+
212
+ assert.strictEqual(consoleCapture.hasErrors(), true);
213
+ });
214
+ });
215
+
216
+ describe('clear', () => {
217
+ it('should clear all messages', async () => {
218
+ await consoleCapture.startCapture();
219
+
220
+ eventHandlers['Runtime.consoleAPICalled']({ type: 'log', args: [{ value: 'Log' }], timestamp: 1 });
221
+ assert.strictEqual(consoleCapture.getMessages().length, 1);
222
+
223
+ consoleCapture.clear();
224
+ assert.strictEqual(consoleCapture.getMessages().length, 0);
225
+ });
226
+ });
227
+
228
+ describe('clearBrowserConsole', () => {
229
+ it('should send Console.clearMessages', async () => {
230
+ await consoleCapture.clearBrowserConsole();
231
+
232
+ assert.strictEqual(mockCdp.send.mock.calls.length, 1);
233
+ assert.strictEqual(mockCdp.send.mock.calls[0].arguments[0], 'Console.clearMessages');
234
+ });
235
+ });
236
+
237
+ describe('arg formatting', () => {
238
+ it('should format different arg types', async () => {
239
+ await consoleCapture.startCapture();
240
+
241
+ eventHandlers['Runtime.consoleAPICalled']({
242
+ type: 'log',
243
+ args: [
244
+ { value: 'string' },
245
+ { description: '[Object]' },
246
+ { unserializableValue: 'Infinity' },
247
+ { type: 'function' }
248
+ ],
249
+ timestamp: 1
250
+ });
251
+
252
+ const messages = consoleCapture.getMessages();
253
+ assert.strictEqual(messages[0].text, 'string [Object] Infinity [function]');
254
+ });
255
+ });
256
+
257
+ describe('console type mapping', () => {
258
+ it('should map console types to levels', async () => {
259
+ await consoleCapture.startCapture();
260
+
261
+ const types = ['log', 'debug', 'info', 'error', 'warn', 'dir', 'table', 'trace', 'assert'];
262
+
263
+ for (const type of types) {
264
+ eventHandlers['Runtime.consoleAPICalled']({
265
+ type,
266
+ args: [{ value: type }],
267
+ timestamp: Date.now()
268
+ });
269
+ }
270
+
271
+ const messages = consoleCapture.getMessages();
272
+ // log: log, dir, table, trace = 4
273
+ assert.strictEqual(messages.filter(m => m.level === 'log').length, 4);
274
+ assert.strictEqual(messages.filter(m => m.level === 'debug').length, 1);
275
+ assert.strictEqual(messages.filter(m => m.level === 'info').length, 1);
276
+ // error: error, assert = 2
277
+ assert.strictEqual(messages.filter(m => m.level === 'error').length, 2);
278
+ // warning: warn = 1
279
+ assert.strictEqual(messages.filter(m => m.level === 'warning').length, 1);
280
+ });
281
+ });
282
+
283
+ describe('maxMessages limit', () => {
284
+ it('should respect maxMessages option', async () => {
285
+ const limitedCapture = createConsoleCapture(mockCdp, { maxMessages: 3 });
286
+ await limitedCapture.startCapture();
287
+
288
+ for (let i = 0; i < 5; i++) {
289
+ eventHandlers['Runtime.consoleAPICalled']({
290
+ type: 'log',
291
+ args: [{ value: `Message ${i}` }],
292
+ timestamp: i
293
+ });
294
+ }
295
+
296
+ const messages = limitedCapture.getMessages();
297
+ assert.strictEqual(messages.length, 3);
298
+ assert.strictEqual(messages[0].text, 'Message 2');
299
+ assert.strictEqual(messages[2].text, 'Message 4');
300
+ });
301
+ });
302
+ });