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,458 @@
1
+ import { describe, it, mock, beforeEach, afterEach } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { createNetworkCapture } from '../capture.js';
4
+
5
+ describe('NetworkErrorCapture', () => {
6
+ let networkCapture;
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
+ networkCapture = createNetworkCapture(mockCdp);
24
+ });
25
+
26
+ afterEach(() => {
27
+ mock.reset();
28
+ });
29
+
30
+ describe('startCapture', () => {
31
+ it('should enable Network domain', async () => {
32
+ await networkCapture.startCapture();
33
+
34
+ assert.strictEqual(mockCdp.send.mock.calls.length, 1);
35
+ assert.strictEqual(mockCdp.send.mock.calls[0].arguments[0], 'Network.enable');
36
+ });
37
+
38
+ it('should register event handlers', async () => {
39
+ await networkCapture.startCapture();
40
+
41
+ assert.strictEqual(mockCdp.on.mock.calls.length, 4);
42
+ assert.ok(eventHandlers['Network.requestWillBeSent']);
43
+ assert.ok(eventHandlers['Network.loadingFailed']);
44
+ assert.ok(eventHandlers['Network.responseReceived']);
45
+ assert.ok(eventHandlers['Network.loadingFinished']);
46
+ });
47
+
48
+ it('should not start capturing twice', async () => {
49
+ await networkCapture.startCapture();
50
+ await networkCapture.startCapture();
51
+
52
+ assert.strictEqual(mockCdp.send.mock.calls.length, 1);
53
+ });
54
+ });
55
+
56
+ describe('stopCapture', () => {
57
+ it('should disable Network domain', async () => {
58
+ await networkCapture.startCapture();
59
+ await networkCapture.stopCapture();
60
+
61
+ assert.ok(mockCdp.send.mock.calls.some(c => c.arguments[0] === 'Network.disable'));
62
+ });
63
+
64
+ it('should unregister event handlers', async () => {
65
+ await networkCapture.startCapture();
66
+ await networkCapture.stopCapture();
67
+
68
+ assert.strictEqual(mockCdp.off.mock.calls.length, 4);
69
+ });
70
+
71
+ it('should do nothing if not capturing', async () => {
72
+ await networkCapture.stopCapture();
73
+
74
+ assert.strictEqual(mockCdp.send.mock.calls.length, 0);
75
+ });
76
+ });
77
+
78
+ describe('network failures', () => {
79
+ it('should capture loading failures', async () => {
80
+ await networkCapture.startCapture();
81
+
82
+ eventHandlers['Network.requestWillBeSent']({
83
+ requestId: 'req-1',
84
+ request: { url: 'http://test.com/api', method: 'GET' },
85
+ timestamp: 1000,
86
+ type: 'XHR'
87
+ });
88
+
89
+ eventHandlers['Network.loadingFailed']({
90
+ requestId: 'req-1',
91
+ type: 'XHR',
92
+ errorText: 'net::ERR_CONNECTION_REFUSED',
93
+ timestamp: 1001
94
+ });
95
+
96
+ const failures = networkCapture.getNetworkFailures();
97
+ assert.strictEqual(failures.length, 1);
98
+ assert.strictEqual(failures[0].type, 'network-failure');
99
+ assert.strictEqual(failures[0].url, 'http://test.com/api');
100
+ assert.strictEqual(failures[0].method, 'GET');
101
+ assert.strictEqual(failures[0].errorText, 'net::ERR_CONNECTION_REFUSED');
102
+ });
103
+
104
+ it('should handle unknown request in loading failure', async () => {
105
+ await networkCapture.startCapture();
106
+
107
+ eventHandlers['Network.loadingFailed']({
108
+ requestId: 'unknown-req',
109
+ type: 'Script',
110
+ errorText: 'net::ERR_NAME_NOT_RESOLVED',
111
+ timestamp: 1000
112
+ });
113
+
114
+ const failures = networkCapture.getNetworkFailures();
115
+ assert.strictEqual(failures.length, 1);
116
+ assert.strictEqual(failures[0].url, 'unknown');
117
+ assert.strictEqual(failures[0].method, 'unknown');
118
+ });
119
+
120
+ it('should capture canceled requests', async () => {
121
+ await networkCapture.startCapture();
122
+
123
+ eventHandlers['Network.requestWillBeSent']({
124
+ requestId: 'req-1',
125
+ request: { url: 'http://test.com/slow', method: 'GET' },
126
+ timestamp: 1000,
127
+ type: 'XHR'
128
+ });
129
+
130
+ eventHandlers['Network.loadingFailed']({
131
+ requestId: 'req-1',
132
+ type: 'XHR',
133
+ errorText: 'net::ERR_ABORTED',
134
+ canceled: true,
135
+ timestamp: 1001
136
+ });
137
+
138
+ const failures = networkCapture.getNetworkFailures();
139
+ assert.strictEqual(failures[0].canceled, true);
140
+ });
141
+ });
142
+
143
+ describe('HTTP errors', () => {
144
+ it('should capture 4xx errors', async () => {
145
+ await networkCapture.startCapture();
146
+
147
+ eventHandlers['Network.requestWillBeSent']({
148
+ requestId: 'req-1',
149
+ request: { url: 'http://test.com/notfound', method: 'GET' },
150
+ timestamp: 1000,
151
+ type: 'Document'
152
+ });
153
+
154
+ eventHandlers['Network.responseReceived']({
155
+ requestId: 'req-1',
156
+ response: {
157
+ url: 'http://test.com/notfound',
158
+ status: 404,
159
+ statusText: 'Not Found',
160
+ mimeType: 'text/html'
161
+ },
162
+ type: 'Document',
163
+ timestamp: 1001
164
+ });
165
+
166
+ const httpErrors = networkCapture.getHttpErrors();
167
+ assert.strictEqual(httpErrors.length, 1);
168
+ assert.strictEqual(httpErrors[0].type, 'http-error');
169
+ assert.strictEqual(httpErrors[0].status, 404);
170
+ assert.strictEqual(httpErrors[0].statusText, 'Not Found');
171
+ });
172
+
173
+ it('should capture 5xx errors', async () => {
174
+ await networkCapture.startCapture();
175
+
176
+ eventHandlers['Network.requestWillBeSent']({
177
+ requestId: 'req-1',
178
+ request: { url: 'http://test.com/api', method: 'POST' },
179
+ timestamp: 1000,
180
+ type: 'XHR'
181
+ });
182
+
183
+ eventHandlers['Network.responseReceived']({
184
+ requestId: 'req-1',
185
+ response: {
186
+ url: 'http://test.com/api',
187
+ status: 500,
188
+ statusText: 'Internal Server Error',
189
+ mimeType: 'application/json'
190
+ },
191
+ type: 'XHR',
192
+ timestamp: 1001
193
+ });
194
+
195
+ const httpErrors = networkCapture.getHttpErrors();
196
+ assert.strictEqual(httpErrors.length, 1);
197
+ assert.strictEqual(httpErrors[0].status, 500);
198
+ });
199
+
200
+ it('should not capture successful responses', async () => {
201
+ await networkCapture.startCapture();
202
+
203
+ eventHandlers['Network.requestWillBeSent']({
204
+ requestId: 'req-1',
205
+ request: { url: 'http://test.com/ok', method: 'GET' },
206
+ timestamp: 1000,
207
+ type: 'Document'
208
+ });
209
+
210
+ eventHandlers['Network.responseReceived']({
211
+ requestId: 'req-1',
212
+ response: {
213
+ url: 'http://test.com/ok',
214
+ status: 200,
215
+ statusText: 'OK',
216
+ mimeType: 'text/html'
217
+ },
218
+ type: 'Document',
219
+ timestamp: 1001
220
+ });
221
+
222
+ const httpErrors = networkCapture.getHttpErrors();
223
+ assert.strictEqual(httpErrors.length, 0);
224
+ });
225
+
226
+ it('should not capture HTTP errors when disabled', async () => {
227
+ await networkCapture.startCapture({ captureHttpErrors: false });
228
+
229
+ eventHandlers['Network.responseReceived']({
230
+ requestId: 'req-1',
231
+ response: {
232
+ url: 'http://test.com/notfound',
233
+ status: 404,
234
+ statusText: 'Not Found',
235
+ mimeType: 'text/html'
236
+ },
237
+ type: 'Document',
238
+ timestamp: 1001
239
+ });
240
+
241
+ const httpErrors = networkCapture.getHttpErrors();
242
+ assert.strictEqual(httpErrors.length, 0);
243
+ });
244
+
245
+ it('should ignore specified status codes', async () => {
246
+ await networkCapture.startCapture({ ignoreStatusCodes: [404] });
247
+
248
+ eventHandlers['Network.responseReceived']({
249
+ requestId: 'req-1',
250
+ response: {
251
+ url: 'http://test.com/notfound',
252
+ status: 404,
253
+ statusText: 'Not Found',
254
+ mimeType: 'text/html'
255
+ },
256
+ type: 'Document',
257
+ timestamp: 1000
258
+ });
259
+
260
+ eventHandlers['Network.responseReceived']({
261
+ requestId: 'req-2',
262
+ response: {
263
+ url: 'http://test.com/error',
264
+ status: 500,
265
+ statusText: 'Internal Server Error',
266
+ mimeType: 'text/html'
267
+ },
268
+ type: 'Document',
269
+ timestamp: 1001
270
+ });
271
+
272
+ const httpErrors = networkCapture.getHttpErrors();
273
+ assert.strictEqual(httpErrors.length, 1);
274
+ assert.strictEqual(httpErrors[0].status, 500);
275
+ });
276
+ });
277
+
278
+ describe('getAllErrors', () => {
279
+ it('should combine and sort all errors', async () => {
280
+ await networkCapture.startCapture();
281
+
282
+ eventHandlers['Network.responseReceived']({
283
+ requestId: 'req-1',
284
+ response: { url: 'http://test.com/a', status: 404, statusText: 'Not Found', mimeType: 'text/html' },
285
+ type: 'Document',
286
+ timestamp: 3000
287
+ });
288
+
289
+ eventHandlers['Network.loadingFailed']({
290
+ requestId: 'req-2',
291
+ type: 'Script',
292
+ errorText: 'Failed',
293
+ timestamp: 1000
294
+ });
295
+
296
+ eventHandlers['Network.responseReceived']({
297
+ requestId: 'req-3',
298
+ response: { url: 'http://test.com/b', status: 500, statusText: 'Error', mimeType: 'text/html' },
299
+ type: 'Document',
300
+ timestamp: 2000
301
+ });
302
+
303
+ const allErrors = networkCapture.getAllErrors();
304
+ assert.strictEqual(allErrors.length, 3);
305
+ assert.strictEqual(allErrors[0].timestamp, 1000);
306
+ assert.strictEqual(allErrors[1].timestamp, 2000);
307
+ assert.strictEqual(allErrors[2].timestamp, 3000);
308
+ });
309
+ });
310
+
311
+ describe('hasErrors', () => {
312
+ it('should return false when no errors', async () => {
313
+ await networkCapture.startCapture();
314
+ assert.strictEqual(networkCapture.hasErrors(), false);
315
+ });
316
+
317
+ it('should return true when network failures exist', async () => {
318
+ await networkCapture.startCapture();
319
+
320
+ eventHandlers['Network.loadingFailed']({
321
+ requestId: 'req-1',
322
+ type: 'Script',
323
+ errorText: 'Failed',
324
+ timestamp: 1000
325
+ });
326
+
327
+ assert.strictEqual(networkCapture.hasErrors(), true);
328
+ });
329
+
330
+ it('should return true when HTTP errors exist', async () => {
331
+ await networkCapture.startCapture();
332
+
333
+ eventHandlers['Network.responseReceived']({
334
+ requestId: 'req-1',
335
+ response: { url: 'http://test.com/a', status: 404, statusText: 'Not Found', mimeType: 'text/html' },
336
+ type: 'Document',
337
+ timestamp: 1000
338
+ });
339
+
340
+ assert.strictEqual(networkCapture.hasErrors(), true);
341
+ });
342
+ });
343
+
344
+ describe('getErrorsByType', () => {
345
+ it('should filter errors by resource type', async () => {
346
+ await networkCapture.startCapture();
347
+
348
+ eventHandlers['Network.loadingFailed']({
349
+ requestId: 'req-1',
350
+ type: 'Script',
351
+ errorText: 'Script failed',
352
+ timestamp: 1000
353
+ });
354
+
355
+ eventHandlers['Network.loadingFailed']({
356
+ requestId: 'req-2',
357
+ type: 'Document',
358
+ errorText: 'Document failed',
359
+ timestamp: 1001
360
+ });
361
+
362
+ const scriptErrors = networkCapture.getErrorsByType('Script');
363
+ assert.strictEqual(scriptErrors.length, 1);
364
+ assert.strictEqual(scriptErrors[0].resourceType, 'Script');
365
+ });
366
+
367
+ it('should filter by multiple types', async () => {
368
+ await networkCapture.startCapture();
369
+
370
+ eventHandlers['Network.loadingFailed']({
371
+ requestId: 'req-1',
372
+ type: 'Script',
373
+ errorText: 'Failed',
374
+ timestamp: 1000
375
+ });
376
+
377
+ eventHandlers['Network.loadingFailed']({
378
+ requestId: 'req-2',
379
+ type: 'Stylesheet',
380
+ errorText: 'Failed',
381
+ timestamp: 1001
382
+ });
383
+
384
+ eventHandlers['Network.loadingFailed']({
385
+ requestId: 'req-3',
386
+ type: 'Image',
387
+ errorText: 'Failed',
388
+ timestamp: 1002
389
+ });
390
+
391
+ const filtered = networkCapture.getErrorsByType(['Script', 'Stylesheet']);
392
+ assert.strictEqual(filtered.length, 2);
393
+ });
394
+ });
395
+
396
+ describe('clear', () => {
397
+ it('should clear all errors and requests', async () => {
398
+ await networkCapture.startCapture();
399
+
400
+ eventHandlers['Network.requestWillBeSent']({
401
+ requestId: 'req-1',
402
+ request: { url: 'http://test.com/api', method: 'GET' },
403
+ timestamp: 1000,
404
+ type: 'XHR'
405
+ });
406
+
407
+ eventHandlers['Network.loadingFailed']({
408
+ requestId: 'req-1',
409
+ type: 'XHR',
410
+ errorText: 'Failed',
411
+ timestamp: 1001
412
+ });
413
+
414
+ eventHandlers['Network.responseReceived']({
415
+ requestId: 'req-2',
416
+ response: { url: 'http://test.com/error', status: 500, statusText: 'Error', mimeType: 'text/html' },
417
+ type: 'Document',
418
+ timestamp: 1002
419
+ });
420
+
421
+ assert.strictEqual(networkCapture.getNetworkFailures().length, 1);
422
+ assert.strictEqual(networkCapture.getHttpErrors().length, 1);
423
+
424
+ networkCapture.clear();
425
+
426
+ assert.strictEqual(networkCapture.getNetworkFailures().length, 0);
427
+ assert.strictEqual(networkCapture.getHttpErrors().length, 0);
428
+ });
429
+ });
430
+
431
+ describe('request cleanup', () => {
432
+ it('should clean up requests on loading finished', async () => {
433
+ await networkCapture.startCapture();
434
+
435
+ eventHandlers['Network.requestWillBeSent']({
436
+ requestId: 'req-1',
437
+ request: { url: 'http://test.com/api', method: 'GET' },
438
+ timestamp: 1000,
439
+ type: 'XHR'
440
+ });
441
+
442
+ eventHandlers['Network.loadingFinished']({
443
+ requestId: 'req-1',
444
+ timestamp: 1001
445
+ });
446
+
447
+ eventHandlers['Network.loadingFailed']({
448
+ requestId: 'req-1',
449
+ type: 'XHR',
450
+ errorText: 'Failed',
451
+ timestamp: 1002
452
+ });
453
+
454
+ const failures = networkCapture.getNetworkFailures();
455
+ assert.strictEqual(failures[0].url, 'unknown');
456
+ });
457
+ });
458
+ });