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,406 @@
|
|
|
1
|
+
import { describe, it, mock, beforeEach, afterEach } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import {
|
|
4
|
+
waitForCondition,
|
|
5
|
+
waitForFunction,
|
|
6
|
+
waitForNetworkIdle,
|
|
7
|
+
waitForDocumentReady,
|
|
8
|
+
waitForSelector
|
|
9
|
+
} from '../page.js';
|
|
10
|
+
import { ErrorTypes } from '../utils.js';
|
|
11
|
+
|
|
12
|
+
describe('WaitStrategy (functional)', () => {
|
|
13
|
+
let mockClient;
|
|
14
|
+
let eventHandlers;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
eventHandlers = {};
|
|
18
|
+
mockClient = {
|
|
19
|
+
send: mock.fn(),
|
|
20
|
+
on: mock.fn((event, handler) => {
|
|
21
|
+
if (!eventHandlers[event]) {
|
|
22
|
+
eventHandlers[event] = [];
|
|
23
|
+
}
|
|
24
|
+
eventHandlers[event].push(handler);
|
|
25
|
+
}),
|
|
26
|
+
off: mock.fn((event, handler) => {
|
|
27
|
+
if (eventHandlers[event]) {
|
|
28
|
+
eventHandlers[event] = eventHandlers[event].filter(h => h !== handler);
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const emitEvent = (event, data) => {
|
|
35
|
+
if (eventHandlers[event]) {
|
|
36
|
+
eventHandlers[event].forEach(handler => handler(data));
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
describe('waitForCondition', () => {
|
|
41
|
+
it('should resolve immediately if condition is true', async () => {
|
|
42
|
+
let callCount = 0;
|
|
43
|
+
const checkFn = async () => {
|
|
44
|
+
callCount++;
|
|
45
|
+
return true;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
await waitForCondition(checkFn);
|
|
49
|
+
assert.strictEqual(callCount, 1);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should poll until condition is true', async () => {
|
|
53
|
+
let callCount = 0;
|
|
54
|
+
const checkFn = async () => {
|
|
55
|
+
callCount++;
|
|
56
|
+
return callCount >= 3;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
await waitForCondition(checkFn, { pollInterval: 10 });
|
|
60
|
+
assert.strictEqual(callCount, 3);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should throw TimeoutError when condition not met', async () => {
|
|
64
|
+
const checkFn = async () => false;
|
|
65
|
+
|
|
66
|
+
await assert.rejects(
|
|
67
|
+
waitForCondition(checkFn, {
|
|
68
|
+
timeout: 50,
|
|
69
|
+
pollInterval: 10,
|
|
70
|
+
message: 'Custom message'
|
|
71
|
+
}),
|
|
72
|
+
(err) => {
|
|
73
|
+
assert.strictEqual(err.name, ErrorTypes.TIMEOUT);
|
|
74
|
+
assert.ok(err.message.includes('Custom message'));
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should respect custom timeout', async () => {
|
|
81
|
+
const start = Date.now();
|
|
82
|
+
const checkFn = async () => false;
|
|
83
|
+
|
|
84
|
+
await assert.rejects(
|
|
85
|
+
waitForCondition(checkFn, { timeout: 100, pollInterval: 10 }),
|
|
86
|
+
(err) => err.name === ErrorTypes.TIMEOUT
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const elapsed = Date.now() - start;
|
|
90
|
+
assert.ok(elapsed >= 100 && elapsed < 200);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('waitForFunction', () => {
|
|
95
|
+
it('should resolve when expression returns truthy', async () => {
|
|
96
|
+
mockClient.send.mock.mockImplementation(async () => ({
|
|
97
|
+
result: { value: 'truthy-value' }
|
|
98
|
+
}));
|
|
99
|
+
|
|
100
|
+
const result = await waitForFunction(mockClient, 'someExpression()');
|
|
101
|
+
assert.strictEqual(result, 'truthy-value');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should poll until expression is truthy', async () => {
|
|
105
|
+
let callCount = 0;
|
|
106
|
+
mockClient.send.mock.mockImplementation(async () => {
|
|
107
|
+
callCount++;
|
|
108
|
+
return {
|
|
109
|
+
result: { value: callCount >= 3 ? 'found' : null }
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const result = await waitForFunction(mockClient, 'someExpression()', {
|
|
114
|
+
pollInterval: 10
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
assert.strictEqual(result, 'found');
|
|
118
|
+
assert.strictEqual(callCount, 3);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should continue polling when expression throws', async () => {
|
|
122
|
+
let callCount = 0;
|
|
123
|
+
mockClient.send.mock.mockImplementation(async () => {
|
|
124
|
+
callCount++;
|
|
125
|
+
if (callCount < 3) {
|
|
126
|
+
return { exceptionDetails: { text: 'Error' } };
|
|
127
|
+
}
|
|
128
|
+
return { result: { value: 'success' } };
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const result = await waitForFunction(mockClient, 'someExpression()', {
|
|
132
|
+
pollInterval: 10
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
assert.strictEqual(result, 'success');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should throw TimeoutError when expression never truthy', async () => {
|
|
139
|
+
mockClient.send.mock.mockImplementation(async () => ({
|
|
140
|
+
result: { value: null }
|
|
141
|
+
}));
|
|
142
|
+
|
|
143
|
+
await assert.rejects(
|
|
144
|
+
waitForFunction(mockClient, 'falsyExpression()', {
|
|
145
|
+
timeout: 50,
|
|
146
|
+
pollInterval: 10
|
|
147
|
+
}),
|
|
148
|
+
(err) => {
|
|
149
|
+
assert.strictEqual(err.name, ErrorTypes.TIMEOUT);
|
|
150
|
+
assert.ok(err.message.includes('falsyExpression()'));
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should use awaitPromise for async expressions', async () => {
|
|
157
|
+
mockClient.send.mock.mockImplementation(async (method, params) => {
|
|
158
|
+
assert.strictEqual(params.awaitPromise, true);
|
|
159
|
+
return { result: { value: true } };
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
await waitForFunction(mockClient, 'asyncFn()');
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('waitForNetworkIdle', () => {
|
|
167
|
+
it('should resolve when no requests are pending', async () => {
|
|
168
|
+
const idlePromise = waitForNetworkIdle(mockClient, { idleTime: 50 });
|
|
169
|
+
|
|
170
|
+
// Simulate immediate idle (no requests)
|
|
171
|
+
await idlePromise;
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should wait for pending requests to finish', async () => {
|
|
175
|
+
const idlePromise = waitForNetworkIdle(mockClient, {
|
|
176
|
+
idleTime: 50,
|
|
177
|
+
timeout: 2000
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Simulate a request
|
|
181
|
+
setTimeout(() => {
|
|
182
|
+
emitEvent('Network.requestWillBeSent', { requestId: 'req-1' });
|
|
183
|
+
}, 10);
|
|
184
|
+
|
|
185
|
+
setTimeout(() => {
|
|
186
|
+
emitEvent('Network.loadingFinished', { requestId: 'req-1' });
|
|
187
|
+
}, 30);
|
|
188
|
+
|
|
189
|
+
await idlePromise;
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should reset idle timer when new request starts', async () => {
|
|
193
|
+
const startTime = Date.now();
|
|
194
|
+
const idlePromise = waitForNetworkIdle(mockClient, {
|
|
195
|
+
idleTime: 100,
|
|
196
|
+
timeout: 2000
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Start first request
|
|
200
|
+
setTimeout(() => {
|
|
201
|
+
emitEvent('Network.requestWillBeSent', { requestId: 'req-1' });
|
|
202
|
+
}, 10);
|
|
203
|
+
|
|
204
|
+
// Finish first request
|
|
205
|
+
setTimeout(() => {
|
|
206
|
+
emitEvent('Network.loadingFinished', { requestId: 'req-1' });
|
|
207
|
+
}, 30);
|
|
208
|
+
|
|
209
|
+
// Start second request just before idle would fire
|
|
210
|
+
setTimeout(() => {
|
|
211
|
+
emitEvent('Network.requestWillBeSent', { requestId: 'req-2' });
|
|
212
|
+
}, 80);
|
|
213
|
+
|
|
214
|
+
// Finish second request
|
|
215
|
+
setTimeout(() => {
|
|
216
|
+
emitEvent('Network.loadingFinished', { requestId: 'req-2' });
|
|
217
|
+
}, 100);
|
|
218
|
+
|
|
219
|
+
await idlePromise;
|
|
220
|
+
const elapsed = Date.now() - startTime;
|
|
221
|
+
// Should take longer due to second request resetting idle timer
|
|
222
|
+
assert.ok(elapsed >= 200);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('should handle loadingFailed as request finished', async () => {
|
|
226
|
+
const idlePromise = waitForNetworkIdle(mockClient, {
|
|
227
|
+
idleTime: 50,
|
|
228
|
+
timeout: 2000
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
setTimeout(() => {
|
|
232
|
+
emitEvent('Network.requestWillBeSent', { requestId: 'req-1' });
|
|
233
|
+
}, 10);
|
|
234
|
+
|
|
235
|
+
setTimeout(() => {
|
|
236
|
+
emitEvent('Network.loadingFailed', { requestId: 'req-1' });
|
|
237
|
+
}, 30);
|
|
238
|
+
|
|
239
|
+
await idlePromise;
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('should throw TimeoutError if network never idle', async () => {
|
|
243
|
+
const idlePromise = waitForNetworkIdle(mockClient, {
|
|
244
|
+
idleTime: 50,
|
|
245
|
+
timeout: 100
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Keep making requests
|
|
249
|
+
let reqCount = 0;
|
|
250
|
+
const interval = setInterval(() => {
|
|
251
|
+
emitEvent('Network.requestWillBeSent', { requestId: `req-${reqCount++}` });
|
|
252
|
+
}, 20);
|
|
253
|
+
|
|
254
|
+
await assert.rejects(idlePromise, (err) => err.name === ErrorTypes.TIMEOUT);
|
|
255
|
+
clearInterval(interval);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('should cleanup event listeners on resolve', async () => {
|
|
259
|
+
const initialOffCount = mockClient.off.mock.calls.length;
|
|
260
|
+
|
|
261
|
+
await waitForNetworkIdle(mockClient, { idleTime: 10 });
|
|
262
|
+
|
|
263
|
+
const finalOffCount = mockClient.off.mock.calls.length;
|
|
264
|
+
assert.ok(finalOffCount > initialOffCount);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('should cleanup event listeners on timeout', async () => {
|
|
268
|
+
const initialOffCount = mockClient.off.mock.calls.length;
|
|
269
|
+
|
|
270
|
+
// Keep making requests to trigger timeout
|
|
271
|
+
const interval = setInterval(() => {
|
|
272
|
+
emitEvent('Network.requestWillBeSent', { requestId: `req-${Date.now()}` });
|
|
273
|
+
}, 10);
|
|
274
|
+
|
|
275
|
+
await assert.rejects(
|
|
276
|
+
waitForNetworkIdle(mockClient, { idleTime: 50, timeout: 50 }),
|
|
277
|
+
(err) => err.name === ErrorTypes.TIMEOUT
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
clearInterval(interval);
|
|
281
|
+
|
|
282
|
+
const finalOffCount = mockClient.off.mock.calls.length;
|
|
283
|
+
assert.ok(finalOffCount > initialOffCount);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
describe('waitForDocumentReady', () => {
|
|
288
|
+
it('should resolve when document is complete', async () => {
|
|
289
|
+
mockClient.send.mock.mockImplementation(async () => ({
|
|
290
|
+
result: { value: 'complete' }
|
|
291
|
+
}));
|
|
292
|
+
|
|
293
|
+
const state = await waitForDocumentReady(mockClient, 'complete');
|
|
294
|
+
assert.strictEqual(state, 'complete');
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('should resolve when document reaches target state', async () => {
|
|
298
|
+
mockClient.send.mock.mockImplementation(async () => ({
|
|
299
|
+
result: { value: 'interactive' }
|
|
300
|
+
}));
|
|
301
|
+
|
|
302
|
+
const state = await waitForDocumentReady(mockClient, 'interactive');
|
|
303
|
+
assert.strictEqual(state, 'interactive');
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('should resolve when document exceeds target state', async () => {
|
|
307
|
+
mockClient.send.mock.mockImplementation(async () => ({
|
|
308
|
+
result: { value: 'complete' }
|
|
309
|
+
}));
|
|
310
|
+
|
|
311
|
+
const state = await waitForDocumentReady(mockClient, 'loading');
|
|
312
|
+
assert.strictEqual(state, 'complete');
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('should poll until target state reached', async () => {
|
|
316
|
+
let callCount = 0;
|
|
317
|
+
const states = ['loading', 'loading', 'interactive', 'complete'];
|
|
318
|
+
|
|
319
|
+
mockClient.send.mock.mockImplementation(async () => ({
|
|
320
|
+
result: { value: states[Math.min(callCount++, states.length - 1)] }
|
|
321
|
+
}));
|
|
322
|
+
|
|
323
|
+
const state = await waitForDocumentReady(mockClient, 'complete', {
|
|
324
|
+
pollInterval: 10
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
assert.strictEqual(state, 'complete');
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('should throw on invalid target state', async () => {
|
|
331
|
+
await assert.rejects(
|
|
332
|
+
waitForDocumentReady(mockClient, 'invalid'),
|
|
333
|
+
/Invalid target state/
|
|
334
|
+
);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('should throw TimeoutError when state not reached', async () => {
|
|
338
|
+
mockClient.send.mock.mockImplementation(async () => ({
|
|
339
|
+
result: { value: 'loading' }
|
|
340
|
+
}));
|
|
341
|
+
|
|
342
|
+
await assert.rejects(
|
|
343
|
+
waitForDocumentReady(mockClient, 'complete', { timeout: 50, pollInterval: 10 }),
|
|
344
|
+
(err) => err.name === ErrorTypes.TIMEOUT
|
|
345
|
+
);
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
describe('waitForSelector', () => {
|
|
350
|
+
it('should resolve when selector found', async () => {
|
|
351
|
+
mockClient.send.mock.mockImplementation(async () => ({
|
|
352
|
+
result: { value: true }
|
|
353
|
+
}));
|
|
354
|
+
|
|
355
|
+
await waitForSelector(mockClient, '#my-element');
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('should poll until selector found', async () => {
|
|
359
|
+
let callCount = 0;
|
|
360
|
+
mockClient.send.mock.mockImplementation(async () => ({
|
|
361
|
+
result: { value: callCount++ >= 2 }
|
|
362
|
+
}));
|
|
363
|
+
|
|
364
|
+
await waitForSelector(mockClient, '.my-class', { pollInterval: 10 });
|
|
365
|
+
assert.ok(callCount >= 3);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('should check visibility when visible option is true', async () => {
|
|
369
|
+
let expression = '';
|
|
370
|
+
mockClient.send.mock.mockImplementation(async (method, params) => {
|
|
371
|
+
expression = params.expression;
|
|
372
|
+
return { result: { value: true } };
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
await waitForSelector(mockClient, '#visible-el', { visible: true });
|
|
376
|
+
|
|
377
|
+
assert.ok(expression.includes('getComputedStyle'));
|
|
378
|
+
assert.ok(expression.includes('display'));
|
|
379
|
+
assert.ok(expression.includes('visibility'));
|
|
380
|
+
assert.ok(expression.includes('opacity'));
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('should escape single quotes in selector', async () => {
|
|
384
|
+
let expression = '';
|
|
385
|
+
mockClient.send.mock.mockImplementation(async (method, params) => {
|
|
386
|
+
expression = params.expression;
|
|
387
|
+
return { result: { value: true } };
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
await waitForSelector(mockClient, "[data-test='value']");
|
|
391
|
+
|
|
392
|
+
assert.ok(expression.includes("\\'"));
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it('should throw TimeoutError when selector not found', async () => {
|
|
396
|
+
mockClient.send.mock.mockImplementation(async () => ({
|
|
397
|
+
result: { value: false }
|
|
398
|
+
}));
|
|
399
|
+
|
|
400
|
+
await assert.rejects(
|
|
401
|
+
waitForSelector(mockClient, '#nonexistent', { timeout: 50, pollInterval: 10 }),
|
|
402
|
+
(err) => err.name === ErrorTypes.TIMEOUT
|
|
403
|
+
);
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
});
|