cdp-skill 1.0.8 → 1.0.15

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.
Files changed (51) hide show
  1. package/README.md +80 -35
  2. package/SKILL.md +157 -241
  3. package/install.js +1 -0
  4. package/package.json +1 -1
  5. package/src/aria/index.js +8 -0
  6. package/src/aria/output-processor.js +173 -0
  7. package/src/aria/role-query.js +1229 -0
  8. package/src/aria/snapshot.js +459 -0
  9. package/src/aria.js +251 -50
  10. package/src/cdp/browser.js +22 -4
  11. package/src/cdp-skill.js +246 -69
  12. package/src/dom/LazyResolver.js +634 -0
  13. package/src/dom/click-executor.js +366 -94
  14. package/src/dom/element-locator.js +34 -25
  15. package/src/dom/fill-executor.js +83 -50
  16. package/src/dom/index.js +3 -0
  17. package/src/page/dialog-handler.js +119 -0
  18. package/src/page/page-controller.js +236 -3
  19. package/src/runner/context-helpers.js +33 -55
  20. package/src/runner/execute-dynamic.js +8 -7
  21. package/src/runner/execute-form.js +11 -11
  22. package/src/runner/execute-input.js +2 -2
  23. package/src/runner/execute-interaction.js +105 -126
  24. package/src/runner/execute-navigation.js +14 -29
  25. package/src/runner/execute-query.js +17 -11
  26. package/src/runner/step-executors.js +225 -84
  27. package/src/runner/step-registry.js +1064 -0
  28. package/src/runner/step-validator.js +16 -754
  29. package/src/tests/Aria.test.js +1025 -0
  30. package/src/tests/ClickExecutor.test.js +170 -50
  31. package/src/tests/ContextHelpers.test.js +41 -30
  32. package/src/tests/ExecuteBrowser.test.js +572 -0
  33. package/src/tests/ExecuteDynamic.test.js +2 -457
  34. package/src/tests/ExecuteForm.test.js +700 -0
  35. package/src/tests/ExecuteInput.test.js +540 -0
  36. package/src/tests/ExecuteInteraction.test.js +319 -0
  37. package/src/tests/ExecuteQuery.test.js +820 -0
  38. package/src/tests/FillExecutor.test.js +89 -37
  39. package/src/tests/LazyResolver.test.js +383 -0
  40. package/src/tests/StepValidator.test.js +224 -78
  41. package/src/tests/TestRunner.test.js +38 -27
  42. package/src/tests/integration.test.js +2 -1
  43. package/src/types.js +9 -9
  44. package/src/utils/backoff.js +118 -0
  45. package/src/utils/cdp-helpers.js +130 -0
  46. package/src/utils/devices.js +140 -0
  47. package/src/utils/errors.js +242 -0
  48. package/src/utils/index.js +65 -0
  49. package/src/utils/temp.js +75 -0
  50. package/src/utils/validators.js +433 -0
  51. package/src/utils.js +14 -1142
@@ -0,0 +1,572 @@
1
+ import { describe, it, mock, beforeEach, afterEach } from 'node:test';
2
+ import assert from 'node:assert';
3
+
4
+ import {
5
+ executePdf,
6
+ executeEval,
7
+ executeCookies,
8
+ executeListTabs,
9
+ executeCloseTab,
10
+ executeConsole,
11
+ parseExpiration,
12
+ formatStackTrace,
13
+ formatCommandConsole
14
+ } from '../runner/execute-browser.js';
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Helpers
18
+ // ---------------------------------------------------------------------------
19
+
20
+ function createMockPdfCapture(opts = {}) {
21
+ return {
22
+ saveToFile: mock.fn((path, options) => {
23
+ if (opts.saveError) throw new Error(opts.saveError);
24
+ return Promise.resolve({ path, size: 12345 });
25
+ })
26
+ };
27
+ }
28
+
29
+ function createMockPageController(opts = {}) {
30
+ return {
31
+ session: {
32
+ send: mock.fn(() => Promise.resolve({}))
33
+ },
34
+ evaluateInFrame: mock.fn((expression, options) => {
35
+ if (opts.exception) {
36
+ return Promise.resolve({
37
+ result: { value: undefined },
38
+ exceptionDetails: {
39
+ exception: { description: opts.exception },
40
+ text: opts.exception
41
+ }
42
+ });
43
+ }
44
+ if (opts.syntaxError) {
45
+ return Promise.resolve({
46
+ result: { value: undefined },
47
+ exceptionDetails: {
48
+ text: 'SyntaxError: Unexpected token'
49
+ }
50
+ });
51
+ }
52
+ if (opts.timeout) {
53
+ return new Promise(() => {}); // Never resolves
54
+ }
55
+ return Promise.resolve({
56
+ result: {
57
+ value: opts.evalResult || { success: true },
58
+ type: 'object'
59
+ }
60
+ });
61
+ }),
62
+ getFrameContext: mock.fn(() => opts.contextId || null)
63
+ };
64
+ }
65
+
66
+ function createMockCookieManager(opts = {}) {
67
+ return {
68
+ getCookies: mock.fn((urls) => {
69
+ if (opts.getError) throw new Error(opts.getError);
70
+ return Promise.resolve(opts.cookies || [
71
+ { name: 'session', value: 'abc123', domain: 'example.com' }
72
+ ]);
73
+ }),
74
+ setCookies: mock.fn((cookies) => {
75
+ if (opts.setError) throw new Error(opts.setError);
76
+ return Promise.resolve({ count: cookies.length });
77
+ }),
78
+ clearCookies: mock.fn((urls, options) => {
79
+ if (opts.clearError) throw new Error(opts.clearError);
80
+ return Promise.resolve({ count: opts.clearCount || 5 });
81
+ }),
82
+ deleteCookies: mock.fn((names, options) => {
83
+ if (opts.deleteError) throw new Error(opts.deleteError);
84
+ return Promise.resolve({ count: Array.isArray(names) ? names.length : 1 });
85
+ })
86
+ };
87
+ }
88
+
89
+ function createMockBrowser(opts = {}) {
90
+ return {
91
+ getPages: mock.fn(() => {
92
+ if (opts.getError) throw new Error(opts.getError);
93
+ return Promise.resolve(opts.pages || [
94
+ { targetId: 't1', url: 'https://example.com', title: 'Example' }
95
+ ]);
96
+ }),
97
+ closePage: mock.fn((targetId) => {
98
+ if (opts.closeError) throw new Error(opts.closeError);
99
+ return Promise.resolve();
100
+ })
101
+ };
102
+ }
103
+
104
+ function createMockConsoleCapture(opts = {}) {
105
+ return {
106
+ getMessages: mock.fn(() => opts.messages || []),
107
+ getMessagesByLevel: mock.fn((level) => opts.messagesByLevel || []),
108
+ getMessagesByType: mock.fn((type) => opts.messagesByType || []),
109
+ clear: mock.fn(() => {}),
110
+ getMessageCount: mock.fn(() => opts.messageCount || 0)
111
+ };
112
+ }
113
+
114
+ // ---------------------------------------------------------------------------
115
+ // Tests: executePdf
116
+ // ---------------------------------------------------------------------------
117
+
118
+ describe('executePdf', () => {
119
+ afterEach(() => { mock.reset(); });
120
+
121
+ it('should throw if pdfCapture not available', async () => {
122
+ await assert.rejects(
123
+ executePdf(null, {}, 'test.pdf'),
124
+ { message: 'PDF capture not available' }
125
+ );
126
+ });
127
+
128
+ it('should save PDF with string path param', async () => {
129
+ const pdfCapture = createMockPdfCapture();
130
+ const result = await executePdf(pdfCapture, {}, 'report.pdf');
131
+
132
+ assert.strictEqual(pdfCapture.saveToFile.mock.calls.length, 1);
133
+ assert.ok(result.path);
134
+ assert.strictEqual(result.size, 12345);
135
+ });
136
+
137
+ it('should save PDF with object params', async () => {
138
+ const pdfCapture = createMockPdfCapture();
139
+ const result = await executePdf(pdfCapture, {}, {
140
+ path: 'report.pdf',
141
+ landscape: true
142
+ });
143
+
144
+ assert.ok(result.path);
145
+ assert.strictEqual(pdfCapture.saveToFile.mock.calls.length, 1);
146
+ });
147
+
148
+ it('should propagate saveToFile errors', async () => {
149
+ const pdfCapture = createMockPdfCapture({ saveError: 'Write failed' });
150
+ await assert.rejects(
151
+ executePdf(pdfCapture, {}, 'test.pdf'),
152
+ { message: 'Write failed' }
153
+ );
154
+ });
155
+ });
156
+
157
+ // ---------------------------------------------------------------------------
158
+ // Tests: executeEval
159
+ // ---------------------------------------------------------------------------
160
+
161
+ describe('executeEval', () => {
162
+ afterEach(() => { mock.reset(); });
163
+
164
+ it('should throw if expression is missing', async () => {
165
+ const pc = createMockPageController();
166
+ await assert.rejects(
167
+ executeEval(pc, {}),
168
+ { message: /requires a non-empty expression/i }
169
+ );
170
+ });
171
+
172
+ it('should execute string expression', async () => {
173
+ const pc = createMockPageController({ evalResult: { value: 42 } });
174
+ const result = await executeEval(pc, 'document.title');
175
+
176
+ assert.strictEqual(pc.evaluateInFrame.mock.calls.length, 1);
177
+ assert.ok(result.value);
178
+ });
179
+
180
+ it('should execute object params with expression', async () => {
181
+ const pc = createMockPageController({ evalResult: 123 });
182
+ const result = await executeEval(pc, { expression: '1 + 2' });
183
+
184
+ assert.ok(result);
185
+ });
186
+
187
+ it('should support await option', async () => {
188
+ const pc = createMockPageController();
189
+ await executeEval(pc, { expression: 'Promise.resolve(42)', await: true });
190
+
191
+ const call = pc.evaluateInFrame.mock.calls[0];
192
+ assert.strictEqual(call.arguments[1].awaitPromise, true);
193
+ });
194
+
195
+ it('should detect unbalanced quotes', async () => {
196
+ const pc = createMockPageController();
197
+ await assert.rejects(
198
+ executeEval(pc, 'document.querySelector("test)'),
199
+ { message: /unbalanced quotes/i }
200
+ );
201
+ });
202
+
203
+ it('should detect unbalanced braces', async () => {
204
+ const pc = createMockPageController();
205
+ await assert.rejects(
206
+ executeEval(pc, '{ foo: "bar" '),
207
+ { message: /unbalanced braces/i }
208
+ );
209
+ });
210
+
211
+ it('should detect unbalanced parentheses', async () => {
212
+ const pc = createMockPageController();
213
+ await assert.rejects(
214
+ executeEval(pc, 'Math.max(1, 2 '),
215
+ { message: /unbalanced parentheses/i }
216
+ );
217
+ });
218
+
219
+ it('should handle syntax errors with context', async () => {
220
+ const pc = createMockPageController({ syntaxError: true });
221
+ await assert.rejects(
222
+ executeEval(pc, 'invalid syntax here'),
223
+ { message: /syntax error/i }
224
+ );
225
+ });
226
+
227
+ it('should handle evaluation exceptions', async () => {
228
+ const pc = createMockPageController({ exception: 'ReferenceError: x is not defined' });
229
+ await assert.rejects(
230
+ executeEval(pc, 'x'),
231
+ { message: /Eval error/i }
232
+ );
233
+ });
234
+
235
+ it('should support timeout option', async () => {
236
+ const pc = createMockPageController({ timeout: true });
237
+ await assert.rejects(
238
+ executeEval(pc, { expression: 'new Promise(() => {})', timeout: 100 }),
239
+ { message: /timed out/i }
240
+ );
241
+ });
242
+
243
+ it('should support serialize option (default true)', async () => {
244
+ const pc = createMockPageController();
245
+ await executeEval(pc, { expression: 'document.title' });
246
+
247
+ // Serialize wraps expression
248
+ const call = pc.evaluateInFrame.mock.calls[0];
249
+ assert.ok(call.arguments[0].includes('document.title'));
250
+ });
251
+
252
+ it('should support serialize: false', async () => {
253
+ const pc = createMockPageController();
254
+ await executeEval(pc, { expression: 'document.title', serialize: false });
255
+
256
+ const call = pc.evaluateInFrame.mock.calls[0];
257
+ assert.strictEqual(call.arguments[0], 'document.title');
258
+ });
259
+ });
260
+
261
+ // ---------------------------------------------------------------------------
262
+ // Tests: parseExpiration
263
+ // ---------------------------------------------------------------------------
264
+
265
+ describe('parseExpiration', () => {
266
+ it('should return number as-is', () => {
267
+ assert.strictEqual(parseExpiration(12345), 12345);
268
+ });
269
+
270
+ it('should parse minutes', () => {
271
+ const result = parseExpiration('5m');
272
+ assert.ok(result > Date.now() / 1000);
273
+ });
274
+
275
+ it('should parse hours', () => {
276
+ const result = parseExpiration('2h');
277
+ assert.ok(result > Date.now() / 1000);
278
+ });
279
+
280
+ it('should parse days', () => {
281
+ const result = parseExpiration('7d');
282
+ assert.ok(result > Date.now() / 1000);
283
+ });
284
+
285
+ it('should parse weeks', () => {
286
+ const result = parseExpiration('2w');
287
+ assert.ok(result > Date.now() / 1000);
288
+ });
289
+
290
+ it('should parse years', () => {
291
+ const result = parseExpiration('1y');
292
+ assert.ok(result > Date.now() / 1000);
293
+ });
294
+
295
+ it('should parse number strings', () => {
296
+ const result = parseExpiration('9999');
297
+ assert.strictEqual(result, 9999);
298
+ });
299
+
300
+ it('should return undefined for invalid formats', () => {
301
+ assert.strictEqual(parseExpiration('invalid'), undefined);
302
+ // '5x' parses as number 5 (parseInt extracts leading digits)
303
+ assert.strictEqual(parseExpiration('5x'), 5);
304
+ assert.strictEqual(parseExpiration(true), undefined);
305
+ });
306
+ });
307
+
308
+ // ---------------------------------------------------------------------------
309
+ // Tests: executeCookies
310
+ // ---------------------------------------------------------------------------
311
+
312
+ describe('executeCookies', () => {
313
+ afterEach(() => { mock.reset(); });
314
+
315
+ it('should throw if cookieManager not available', async () => {
316
+ const pc = createMockPageController();
317
+ await assert.rejects(
318
+ executeCookies(null, pc, { get: true }),
319
+ { message: 'Cookie manager not available' }
320
+ );
321
+ });
322
+
323
+ it('should get cookies for current page', async () => {
324
+ const manager = createMockCookieManager();
325
+ const pc = createMockPageController();
326
+
327
+ const result = await executeCookies(manager, pc, { get: true });
328
+
329
+ assert.strictEqual(result.action, 'get');
330
+ assert.ok(Array.isArray(result.cookies));
331
+ });
332
+
333
+ it('should get cookies with name filter', async () => {
334
+ const manager = createMockCookieManager({
335
+ cookies: [
336
+ { name: 'session', value: 'abc' },
337
+ { name: 'tracking', value: 'xyz' }
338
+ ]
339
+ });
340
+ const pc = createMockPageController();
341
+
342
+ const result = await executeCookies(manager, pc, { get: true, name: 'session' });
343
+
344
+ assert.strictEqual(result.cookies.length, 1);
345
+ assert.strictEqual(result.cookies[0].name, 'session');
346
+ });
347
+
348
+ it('should set cookies', async () => {
349
+ const manager = createMockCookieManager();
350
+ const pc = createMockPageController();
351
+
352
+ const result = await executeCookies(manager, pc, {
353
+ set: [{ name: 'test', value: '123', domain: 'example.com' }]
354
+ });
355
+
356
+ assert.strictEqual(result.action, 'set');
357
+ assert.strictEqual(result.count, 1);
358
+ });
359
+
360
+ it('should parse human-readable expiration in cookies', async () => {
361
+ const manager = createMockCookieManager();
362
+ const pc = createMockPageController();
363
+
364
+ await executeCookies(manager, pc, {
365
+ set: [{ name: 'test', value: '123', expires: '7d' }]
366
+ });
367
+
368
+ const call = manager.setCookies.mock.calls[0];
369
+ const cookie = call.arguments[0][0];
370
+ assert.ok(typeof cookie.expires === 'number');
371
+ });
372
+
373
+ it('should clear cookies', async () => {
374
+ const manager = createMockCookieManager({ clearCount: 10 });
375
+ const pc = createMockPageController();
376
+
377
+ const result = await executeCookies(manager, pc, { clear: [] });
378
+
379
+ assert.strictEqual(result.action, 'clear');
380
+ assert.strictEqual(result.count, 10);
381
+ });
382
+
383
+ it('should delete cookies by name', async () => {
384
+ const manager = createMockCookieManager();
385
+ const pc = createMockPageController();
386
+
387
+ const result = await executeCookies(manager, pc, {
388
+ delete: ['session', 'tracking']
389
+ });
390
+
391
+ assert.strictEqual(result.action, 'delete');
392
+ assert.strictEqual(result.count, 2);
393
+ });
394
+
395
+ it('should throw for invalid action', async () => {
396
+ const manager = createMockCookieManager();
397
+ const pc = createMockPageController();
398
+
399
+ await assert.rejects(
400
+ executeCookies(manager, pc, {}),
401
+ { message: /requires action/i }
402
+ );
403
+ });
404
+ });
405
+
406
+ // ---------------------------------------------------------------------------
407
+ // Tests: executeListTabs
408
+ // ---------------------------------------------------------------------------
409
+
410
+ describe('executeListTabs', () => {
411
+ afterEach(() => { mock.reset(); });
412
+
413
+ it('should throw if browser not available', async () => {
414
+ // executeListTabs checks browser synchronously
415
+ try {
416
+ await executeListTabs(null);
417
+ assert.fail('Should have thrown');
418
+ } catch (e) {
419
+ assert.ok(e.message.includes('Browser not available'));
420
+ }
421
+ });
422
+
423
+ it('should list all tabs', async () => {
424
+ const browser = createMockBrowser({
425
+ pages: [
426
+ { targetId: 't1', url: 'https://a.com', title: 'A' },
427
+ { targetId: 't2', url: 'https://b.com', title: 'B' }
428
+ ]
429
+ });
430
+
431
+ const result = await executeListTabs(browser);
432
+
433
+ assert.strictEqual(result.count, 2);
434
+ assert.strictEqual(result.tabs.length, 2);
435
+ assert.strictEqual(result.tabs[0].targetId, 't1');
436
+ assert.strictEqual(result.tabs[1].targetId, 't2');
437
+ });
438
+
439
+ it('should return empty tabs list', async () => {
440
+ const browser = createMockBrowser({ pages: [] });
441
+ const result = await executeListTabs(browser);
442
+
443
+ assert.strictEqual(result.count, 0);
444
+ assert.strictEqual(result.tabs.length, 0);
445
+ });
446
+ });
447
+
448
+ // ---------------------------------------------------------------------------
449
+ // Tests: executeCloseTab
450
+ // ---------------------------------------------------------------------------
451
+
452
+ describe('executeCloseTab', () => {
453
+ afterEach(() => { mock.reset(); });
454
+
455
+ it('should throw if browser not available', async () => {
456
+ // executeCloseTab checks browser synchronously
457
+ try {
458
+ await executeCloseTab(null, 't1');
459
+ assert.fail('Should have thrown');
460
+ } catch (e) {
461
+ assert.ok(e.message.includes('Browser not available'));
462
+ }
463
+ });
464
+
465
+ it('should close tab by targetId', async () => {
466
+ const browser = createMockBrowser();
467
+ const result = await executeCloseTab(browser, 't1');
468
+
469
+ assert.strictEqual(result.closed, 't1');
470
+ assert.strictEqual(browser.closePage.mock.calls.length, 1);
471
+ assert.strictEqual(browser.closePage.mock.calls[0].arguments[0], 't1');
472
+ });
473
+
474
+ it('should propagate close errors', async () => {
475
+ const browser = createMockBrowser({ closeError: 'Tab not found' });
476
+ await assert.rejects(
477
+ executeCloseTab(browser, 't999'),
478
+ { message: 'Tab not found' }
479
+ );
480
+ });
481
+ });
482
+
483
+ // ---------------------------------------------------------------------------
484
+ // Tests: formatStackTrace
485
+ // ---------------------------------------------------------------------------
486
+
487
+ describe('formatStackTrace', () => {
488
+ it('should return null for empty stack', () => {
489
+ assert.strictEqual(formatStackTrace(null), null);
490
+ assert.strictEqual(formatStackTrace({}), null);
491
+ });
492
+
493
+ it('should format stack frames', () => {
494
+ const stack = {
495
+ callFrames: [
496
+ { functionName: 'foo', url: 'https://example.com/app.js', lineNumber: 10, columnNumber: 5 },
497
+ { functionName: '', url: 'https://example.com/app.js', lineNumber: 20, columnNumber: 10 }
498
+ ]
499
+ };
500
+
501
+ const result = formatStackTrace(stack);
502
+
503
+ assert.strictEqual(result.length, 2);
504
+ assert.strictEqual(result[0].functionName, 'foo');
505
+ assert.strictEqual(result[0].lineNumber, 10);
506
+ assert.strictEqual(result[1].functionName, '(anonymous)');
507
+ });
508
+ });
509
+
510
+ // ---------------------------------------------------------------------------
511
+ // Tests: executeConsole
512
+ // ---------------------------------------------------------------------------
513
+
514
+ describe('executeConsole', () => {
515
+ afterEach(() => { mock.reset(); });
516
+
517
+ it('should return error if consoleCapture not available', async () => {
518
+ const result = await executeConsole(null, {});
519
+
520
+ assert.ok(result.error);
521
+ assert.strictEqual(result.messages.length, 0);
522
+ });
523
+
524
+ it('should get console messages', async () => {
525
+ const capture = createMockConsoleCapture({
526
+ messages: [
527
+ { level: 'log', text: 'Hello', timestamp: 1000 },
528
+ { level: 'error', text: 'Error!', timestamp: 2000 }
529
+ ]
530
+ });
531
+
532
+ const result = await executeConsole(capture, {});
533
+
534
+ assert.strictEqual(capture.getMessages.mock.calls.length, 1);
535
+ });
536
+
537
+ it('should filter by level', async () => {
538
+ const capture = createMockConsoleCapture({
539
+ messagesByLevel: [
540
+ { level: 'error', text: 'Error 1' }
541
+ ]
542
+ });
543
+
544
+ await executeConsole(capture, { level: 'error' });
545
+
546
+ assert.strictEqual(capture.getMessagesByLevel.mock.calls.length, 1);
547
+ assert.strictEqual(capture.getMessagesByLevel.mock.calls[0].arguments[0], 'error');
548
+ });
549
+
550
+ it('should filter by type', async () => {
551
+ const capture = createMockConsoleCapture({
552
+ messagesByType: [
553
+ { type: 'exception', text: 'Exception!' }
554
+ ]
555
+ });
556
+
557
+ await executeConsole(capture, { type: 'exception' });
558
+
559
+ assert.strictEqual(capture.getMessagesByType.mock.calls.length, 1);
560
+ });
561
+
562
+ it('should apply limit', async () => {
563
+ const capture = createMockConsoleCapture({
564
+ messages: Array(100).fill().map((_, i) => ({ level: 'log', text: `Msg ${i}` }))
565
+ });
566
+
567
+ await executeConsole(capture, { limit: 10 });
568
+
569
+ // Limit is applied by slicing messages
570
+ assert.ok(capture.getMessages.mock.calls.length > 0);
571
+ });
572
+ });