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,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
|
+
});
|