cdp-skill 1.0.8 → 1.0.14

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 (47) hide show
  1. package/README.md +80 -35
  2. package/SKILL.md +151 -239
  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 +237 -43
  10. package/src/cdp/browser.js +22 -4
  11. package/src/cdp-skill.js +245 -69
  12. package/src/dom/click-executor.js +240 -76
  13. package/src/dom/element-locator.js +34 -25
  14. package/src/dom/fill-executor.js +55 -27
  15. package/src/page/dialog-handler.js +119 -0
  16. package/src/page/page-controller.js +190 -3
  17. package/src/runner/context-helpers.js +33 -55
  18. package/src/runner/execute-dynamic.js +8 -7
  19. package/src/runner/execute-form.js +11 -11
  20. package/src/runner/execute-input.js +2 -2
  21. package/src/runner/execute-interaction.js +99 -120
  22. package/src/runner/execute-navigation.js +11 -26
  23. package/src/runner/execute-query.js +8 -5
  24. package/src/runner/step-executors.js +225 -84
  25. package/src/runner/step-registry.js +1064 -0
  26. package/src/runner/step-validator.js +16 -754
  27. package/src/tests/Aria.test.js +1025 -0
  28. package/src/tests/ContextHelpers.test.js +39 -28
  29. package/src/tests/ExecuteBrowser.test.js +572 -0
  30. package/src/tests/ExecuteDynamic.test.js +2 -457
  31. package/src/tests/ExecuteForm.test.js +700 -0
  32. package/src/tests/ExecuteInput.test.js +540 -0
  33. package/src/tests/ExecuteInteraction.test.js +319 -0
  34. package/src/tests/ExecuteQuery.test.js +820 -0
  35. package/src/tests/FillExecutor.test.js +2 -2
  36. package/src/tests/StepValidator.test.js +222 -76
  37. package/src/tests/TestRunner.test.js +36 -25
  38. package/src/tests/integration.test.js +2 -1
  39. package/src/types.js +9 -9
  40. package/src/utils/backoff.js +118 -0
  41. package/src/utils/cdp-helpers.js +130 -0
  42. package/src/utils/devices.js +140 -0
  43. package/src/utils/errors.js +242 -0
  44. package/src/utils/index.js +65 -0
  45. package/src/utils/temp.js +75 -0
  46. package/src/utils/validators.js +433 -0
  47. package/src/utils.js +14 -1142
@@ -7,8 +7,6 @@ import os from 'os';
7
7
  import {
8
8
  executePageFunction,
9
9
  executePoll,
10
- compilePipeline,
11
- executePipeline,
12
10
  executeWriteSiteProfile,
13
11
  loadSiteProfile
14
12
  } from '../runner/execute-dynamic.js';
@@ -285,441 +283,6 @@ describe('executePoll', () => {
285
283
  });
286
284
  });
287
285
 
288
- // ---------------------------------------------------------------------------
289
- // Tests: compilePipeline
290
- // ---------------------------------------------------------------------------
291
-
292
- describe('compilePipeline', () => {
293
- it('should generate valid JS for find+fill', () => {
294
- const js = compilePipeline([{ find: '#name', fill: 'John' }]);
295
- assert.ok(js.includes('document.querySelector'));
296
- assert.ok(js.includes('#name'));
297
- assert.ok(js.includes('John'));
298
- assert.ok(js.includes('nativeSetter'));
299
- assert.ok(js.includes("dispatchEvent(new Event('input'"));
300
- });
301
-
302
- it('should generate valid JS for find+click', () => {
303
- const js = compilePipeline([{ find: '#btn', click: true }]);
304
- assert.ok(js.includes('document.querySelector'));
305
- assert.ok(js.includes('#btn'));
306
- assert.ok(js.includes('.click()'));
307
- });
308
-
309
- it('should generate valid JS for find+type', () => {
310
- const js = compilePipeline([{ find: '#search', type: 'hello' }]);
311
- assert.ok(js.includes('document.querySelector'));
312
- assert.ok(js.includes('#search'));
313
- assert.ok(js.includes('focus'));
314
- assert.ok(js.includes('KeyboardEvent'));
315
- assert.ok(js.includes('hello'));
316
- });
317
-
318
- it('should generate valid JS for find+check', () => {
319
- const js = compilePipeline([{ find: '#agree', check: true }]);
320
- assert.ok(js.includes('document.querySelector'));
321
- assert.ok(js.includes('#agree'));
322
- assert.ok(js.includes('checked'));
323
- assert.ok(js.includes('true'));
324
- });
325
-
326
- it('should generate valid JS for find+select', () => {
327
- const js = compilePipeline([{ find: '#color', select: 'red' }]);
328
- assert.ok(js.includes('document.querySelector'));
329
- assert.ok(js.includes('#color'));
330
- assert.ok(js.includes('"red"'));
331
- });
332
-
333
- it('should generate valid JS for waitFor', () => {
334
- const js = compilePipeline([{ waitFor: '() => document.querySelector("#loaded")' }]);
335
- assert.ok(js.includes('new Promise'));
336
- assert.ok(js.includes('setInterval'));
337
- assert.ok(js.includes('#loaded'));
338
- });
339
-
340
- it('should use custom timeout for waitFor', () => {
341
- const js = compilePipeline([{ waitFor: '() => true', timeout: 5000 }]);
342
- assert.ok(js.includes('5000'));
343
- });
344
-
345
- it('should generate valid JS for sleep', () => {
346
- const js = compilePipeline([{ sleep: 200 }]);
347
- assert.ok(js.includes('setTimeout'));
348
- assert.ok(js.includes('200'));
349
- });
350
-
351
- it('should generate valid JS for return', () => {
352
- const js = compilePipeline([{ return: '() => document.title' }]);
353
- assert.ok(js.includes('document.title'));
354
- assert.ok(js.includes('value:val'));
355
- });
356
-
357
- it('should throw on unrecognized micro-op', () => {
358
- assert.throws(
359
- () => compilePipeline([{ unknown: true }]),
360
- { message: /unrecognized micro-op/ }
361
- );
362
- });
363
-
364
- it('should combine multiple ops in sequence', () => {
365
- const js = compilePipeline([
366
- { find: '#user', fill: 'Alice' },
367
- { find: '#pass', fill: 'secret' },
368
- { find: '#submit', click: true }
369
- ]);
370
- assert.ok(js.includes('#user'));
371
- assert.ok(js.includes('Alice'));
372
- assert.ok(js.includes('#pass'));
373
- assert.ok(js.includes('secret'));
374
- assert.ok(js.includes('#submit'));
375
- assert.ok(js.includes('.click()'));
376
- assert.ok(js.includes('async function'));
377
- assert.ok(js.includes('completed:true'));
378
- });
379
-
380
- it('should include error handling in generated code', () => {
381
- const js = compilePipeline([{ find: '#el', click: true }]);
382
- assert.ok(js.includes('catch'));
383
- assert.ok(js.includes('failedAt'));
384
- assert.ok(js.includes('completed:false'));
385
- });
386
-
387
- it('should track step index in error info', () => {
388
- const js = compilePipeline([
389
- { find: '#a', fill: 'x' },
390
- { find: '#b', fill: 'y' }
391
- ]);
392
- assert.ok(js.includes('step:0'));
393
- assert.ok(js.includes('step:1'));
394
- });
395
- });
396
-
397
- // ---------------------------------------------------------------------------
398
- // Tests: executePipeline
399
- // ---------------------------------------------------------------------------
400
-
401
- describe('executePipeline', () => {
402
- afterEach(() => { mock.reset(); });
403
-
404
- it('should execute array form', async () => {
405
- const pc = createMockPageController({ completed: true, steps: 2, results: [{ok:true},{ok:true}] });
406
- const result = await executePipeline(pc, [
407
- { find: '#a', fill: 'x' },
408
- { find: '#b', fill: 'y' }
409
- ]);
410
-
411
- assert.strictEqual(result.completed, true);
412
- assert.strictEqual(result.steps, 2);
413
- assert.strictEqual(pc.evaluateInFrame.mock.calls.length, 1);
414
- // Should use awaitPromise: true
415
- assert.strictEqual(pc.evaluateInFrame.mock.calls[0].arguments[1].awaitPromise, true);
416
- });
417
-
418
- it('should execute object form with steps and timeout', async () => {
419
- const pc = createMockPageController({ completed: true, steps: 1, results: [{ok:true}] });
420
- const result = await executePipeline(pc, {
421
- steps: [{ find: '#x', click: true }],
422
- timeout: 5000
423
- });
424
-
425
- assert.strictEqual(result.completed, true);
426
- });
427
-
428
- it('should throw on empty array', async () => {
429
- const pc = createMockPageController(null);
430
- await assert.rejects(
431
- () => executePipeline(pc, []),
432
- { message: /non-empty array/ }
433
- );
434
- });
435
-
436
- it('should throw on non-array params without steps', async () => {
437
- const pc = createMockPageController(null);
438
- await assert.rejects(
439
- () => executePipeline(pc, { timeout: 1000 }),
440
- { message: /non-empty array/ }
441
- );
442
- });
443
-
444
- it('should throw on browser exception', async () => {
445
- const pc = createMockPageController(undefined, { exception: 'TypeError: el is null' });
446
- await assert.rejects(
447
- () => executePipeline(pc, [{ find: '#missing', click: true }]),
448
- { message: /pipeline error/ }
449
- );
450
- });
451
-
452
- it('should timeout when evaluation hangs', async () => {
453
- const pc = {
454
- evaluateInFrame: mock.fn(() => new Promise(() => { /* never resolves */ }))
455
- };
456
- await assert.rejects(
457
- () => executePipeline(pc, { steps: [{ find: '#x', click: true }], timeout: 50 }),
458
- { message: /timed out after 50ms/ }
459
- );
460
- });
461
-
462
- it('should return result value from browser', async () => {
463
- const pipelineResult = { completed: false, failedAt: 0, error: 'not found: #x', results: [] };
464
- const pc = createMockPageController(pipelineResult);
465
- const result = await executePipeline(pc, [{ find: '#x', click: true }]);
466
-
467
- assert.strictEqual(result.completed, false);
468
- assert.strictEqual(result.failedAt, 0);
469
- });
470
- });
471
-
472
- // ---------------------------------------------------------------------------
473
- // Tests: executeWriteSiteProfile and loadSiteProfile
474
- // ---------------------------------------------------------------------------
475
-
476
- describe('executeWriteSiteProfile', () => {
477
- const testDomain = `test-dyn-${Date.now()}.example.com`;
478
-
479
- afterEach(async () => {
480
- await cleanupTestProfiles([testDomain]);
481
- });
482
-
483
- it('should write a profile file', async () => {
484
- const result = await executeWriteSiteProfile({
485
- domain: testDomain,
486
- content: '# Test profile\nContent here.'
487
- });
488
-
489
- assert.strictEqual(result.written, true);
490
- assert.ok(result.path);
491
- assert.strictEqual(result.domain, testDomain);
492
-
493
- // Verify file was written
494
- const content = await fs.readFile(result.path, 'utf8');
495
- assert.strictEqual(content, '# Test profile\nContent here.');
496
- });
497
-
498
- it('should require domain', async () => {
499
- await assert.rejects(
500
- () => executeWriteSiteProfile({ content: 'test' }),
501
- { message: /requires domain and content/ }
502
- );
503
- });
504
-
505
- it('should require content', async () => {
506
- await assert.rejects(
507
- () => executeWriteSiteProfile({ domain: 'example.com' }),
508
- { message: /requires domain and content/ }
509
- );
510
- });
511
-
512
- it('should throw on missing params', async () => {
513
- await assert.rejects(
514
- () => executeWriteSiteProfile(null),
515
- { message: /requires domain and content/ }
516
- );
517
- });
518
- });
519
-
520
- describe('loadSiteProfile', () => {
521
- const testDomain = `test-load-${Date.now()}.example.com`;
522
-
523
- afterEach(async () => {
524
- await cleanupTestProfiles([testDomain]);
525
- });
526
-
527
- it('should return null when no profile exists', async () => {
528
- const result = await loadSiteProfile('nonexistent-domain-xyz-12345.com');
529
- assert.strictEqual(result, null);
530
- });
531
-
532
- it('should return content when profile exists', async () => {
533
- // First write a profile
534
- await executeWriteSiteProfile({
535
- domain: testDomain,
536
- content: '# Loaded content'
537
- });
538
-
539
- const result = await loadSiteProfile(testDomain);
540
- assert.strictEqual(result, '# Loaded content');
541
- });
542
- });
543
-
544
-
545
-
546
-
547
- // ---------------------------------------------------------------------------
548
- // Tests: StepValidator - Dynamic step types
549
- // ---------------------------------------------------------------------------
550
-
551
- describe('StepValidator - pageFunction validation', () => {
552
- it('should accept valid string form', () => {
553
- const errors = validateStepInternal({ pageFunction: '() => document.title' });
554
- assert.strictEqual(errors.length, 0);
555
- });
556
-
557
- it('should accept valid object form', () => {
558
- const errors = validateStepInternal({ pageFunction: { fn: '() => 42', refs: true, timeout: 5000 } });
559
- assert.strictEqual(errors.length, 0);
560
- });
561
-
562
- it('should reject empty string', () => {
563
- const errors = validateStepInternal({ pageFunction: '' });
564
- assert.ok(errors.some(e => e.includes('non-empty function string')));
565
- });
566
-
567
- it('should reject missing fn in object form', () => {
568
- const errors = validateStepInternal({ pageFunction: { timeout: 1000 } });
569
- assert.ok(errors.some(e => e.includes('non-empty fn string')));
570
- });
571
-
572
- it('should reject invalid refs type', () => {
573
- const errors = validateStepInternal({ pageFunction: { fn: '() => 1', refs: 'yes' } });
574
- assert.ok(errors.some(e => e.includes('refs must be a boolean')));
575
- });
576
-
577
- it('should reject invalid timeout', () => {
578
- const errors = validateStepInternal({ pageFunction: { fn: '() => 1', timeout: -100 } });
579
- assert.ok(errors.some(e => e.includes('timeout must be a non-negative')));
580
- });
581
-
582
- it('should reject non-string non-object form', () => {
583
- const errors = validateStepInternal({ pageFunction: 42 });
584
- assert.ok(errors.some(e => e.includes('function string or params object')));
585
- });
586
- });
587
-
588
- describe('StepValidator - poll validation', () => {
589
- it('should accept valid string form', () => {
590
- const errors = validateStepInternal({ poll: '() => document.readyState === "complete"' });
591
- assert.strictEqual(errors.length, 0);
592
- });
593
-
594
- it('should accept valid object form', () => {
595
- const errors = validateStepInternal({ poll: { fn: '() => true', interval: 200, timeout: 10000 } });
596
- assert.strictEqual(errors.length, 0);
597
- });
598
-
599
- it('should reject empty string', () => {
600
- const errors = validateStepInternal({ poll: '' });
601
- assert.ok(errors.some(e => e.includes('non-empty function string')));
602
- });
603
-
604
- it('should reject missing fn', () => {
605
- const errors = validateStepInternal({ poll: { interval: 100 } });
606
- assert.ok(errors.some(e => e.includes('non-empty fn string')));
607
- });
608
-
609
- it('should reject invalid interval', () => {
610
- const errors = validateStepInternal({ poll: { fn: '() => true', interval: -50 } });
611
- assert.ok(errors.some(e => e.includes('interval must be a non-negative')));
612
- });
613
-
614
- it('should reject invalid timeout', () => {
615
- const errors = validateStepInternal({ poll: { fn: '() => true', timeout: 'long' } });
616
- assert.ok(errors.some(e => e.includes('timeout must be a non-negative')));
617
- });
618
-
619
- it('should reject non-string non-object form', () => {
620
- const errors = validateStepInternal({ poll: 123 });
621
- assert.ok(errors.some(e => e.includes('function string or params object')));
622
- });
623
- });
624
-
625
- describe('StepValidator - pipeline validation', () => {
626
- it('should accept valid array of micro-ops', () => {
627
- const errors = validateStepInternal({
628
- pipeline: [
629
- { find: '#name', fill: 'John' },
630
- { find: '#submit', click: true }
631
- ]
632
- });
633
- assert.strictEqual(errors.length, 0);
634
- });
635
-
636
- it('should accept valid object form with steps', () => {
637
- const errors = validateStepInternal({
638
- pipeline: {
639
- steps: [{ find: '#btn', click: true }],
640
- timeout: 5000
641
- }
642
- });
643
- assert.strictEqual(errors.length, 0);
644
- });
645
-
646
- it('should reject empty array', () => {
647
- const errors = validateStepInternal({ pipeline: [] });
648
- assert.ok(errors.some(e => e.includes('non-empty array')));
649
- });
650
-
651
- it('should reject invalid micro-ops without find/waitFor/sleep/return', () => {
652
- const errors = validateStepInternal({ pipeline: [{ something: true }] });
653
- assert.ok(errors.some(e => e.includes('unrecognized micro-op')));
654
- });
655
-
656
- it('should reject find without action', () => {
657
- const errors = validateStepInternal({ pipeline: [{ find: '#el' }] });
658
- assert.ok(errors.some(e => e.includes('find requires an action')));
659
- });
660
-
661
- it('should accept find with fill action', () => {
662
- const errors = validateStepInternal({ pipeline: [{ find: '#el', fill: 'val' }] });
663
- assert.strictEqual(errors.length, 0);
664
- });
665
-
666
- it('should accept find with click action', () => {
667
- const errors = validateStepInternal({ pipeline: [{ find: '#el', click: true }] });
668
- assert.strictEqual(errors.length, 0);
669
- });
670
-
671
- it('should accept find with type action', () => {
672
- const errors = validateStepInternal({ pipeline: [{ find: '#el', type: 'text' }] });
673
- assert.strictEqual(errors.length, 0);
674
- });
675
-
676
- it('should accept find with check action', () => {
677
- const errors = validateStepInternal({ pipeline: [{ find: '#el', check: true }] });
678
- assert.strictEqual(errors.length, 0);
679
- });
680
-
681
- it('should accept find with select action', () => {
682
- const errors = validateStepInternal({ pipeline: [{ find: '#el', select: 'opt' }] });
683
- assert.strictEqual(errors.length, 0);
684
- });
685
-
686
- it('should accept waitFor micro-op', () => {
687
- const errors = validateStepInternal({ pipeline: [{ waitFor: '() => true' }] });
688
- assert.strictEqual(errors.length, 0);
689
- });
690
-
691
- it('should accept sleep micro-op', () => {
692
- const errors = validateStepInternal({ pipeline: [{ sleep: 500 }] });
693
- assert.strictEqual(errors.length, 0);
694
- });
695
-
696
- it('should accept return micro-op', () => {
697
- const errors = validateStepInternal({ pipeline: [{ return: '() => document.title' }] });
698
- assert.strictEqual(errors.length, 0);
699
- });
700
-
701
- it('should reject invalid timeout on object form', () => {
702
- const errors = validateStepInternal({
703
- pipeline: {
704
- steps: [{ find: '#el', click: true }],
705
- timeout: -1
706
- }
707
- });
708
- assert.ok(errors.some(e => e.includes('timeout must be a non-negative')));
709
- });
710
-
711
- it('should report errors for multiple invalid ops', () => {
712
- const errors = validateStepInternal({
713
- pipeline: [
714
- { find: '#a' }, // missing action
715
- { bad: true }, // unrecognized
716
- { find: '#b', fill: 'ok' } // valid
717
- ]
718
- });
719
- assert.ok(errors.length >= 2);
720
- });
721
- });
722
-
723
286
  describe('StepValidator - writeSiteProfile validation', () => {
724
287
  it('should accept valid params', () => {
725
288
  const errors = validateStepInternal({
@@ -815,9 +378,6 @@ describe('STEP_TYPES includes dynamic steps', () => {
815
378
  assert.ok(STEP_TYPES.includes('poll'));
816
379
  });
817
380
 
818
- it('should include pipeline', () => {
819
- assert.ok(STEP_TYPES.includes('pipeline'));
820
- });
821
381
 
822
382
  it('should include writeSiteProfile', () => {
823
383
  assert.ok(STEP_TYPES.includes('writeSiteProfile'));
@@ -888,20 +448,6 @@ describe('step-executors dispatch for dynamic steps', () => {
888
448
  assert.ok(result.output.resolved);
889
449
  });
890
450
 
891
- it('should dispatch pipeline step', async () => {
892
- mockDeps.pageController.evaluateInFrame = mock.fn(() => Promise.resolve({
893
- result: { value: { completed: true, steps: 1, results: [{ok:true}] } },
894
- exceptionDetails: undefined
895
- }));
896
-
897
- const result = await executeStep(mockDeps, {
898
- pipeline: [{ find: '#btn', click: true }]
899
- });
900
-
901
- assert.strictEqual(result.action, 'pipeline');
902
- assert.strictEqual(result.status, 'ok');
903
- assert.ok(result.output.completed);
904
- });
905
451
 
906
452
  it('should dispatch writeSiteProfile step', async () => {
907
453
  const uniqueDomain = `dispatch-test-${Date.now()}.example.com`;
@@ -1105,6 +651,7 @@ describe('step-executors goto profile integration', () => {
1105
651
  });
1106
652
  }),
1107
653
  navigate: mock.fn(() => Promise.resolve()),
654
+ waitForNetworkSettle: mock.fn(() => Promise.resolve({ settled: true, pendingCount: 0 })),
1108
655
  getUrl: mock.fn(() => Promise.resolve(`https://${testDomain}/page`)),
1109
656
  session: { send: mockSessionSend }
1110
657
  },
@@ -1160,7 +707,6 @@ describe('validateSteps for dynamic steps', () => {
1160
707
  { goto: 'https://example.com' },
1161
708
  { pageFunction: '() => document.title' },
1162
709
  { poll: { fn: '() => true', timeout: 5000 } },
1163
- { pipeline: [{ find: '#btn', click: true }] },
1164
710
  { writeSiteProfile: { domain: 'example.com', content: '# test' } }
1165
711
  ]);
1166
712
  assert.strictEqual(result.valid, true);
@@ -1171,10 +717,9 @@ describe('validateSteps for dynamic steps', () => {
1171
717
  const result = validateSteps([
1172
718
  { pageFunction: '' },
1173
719
  { poll: { interval: 100 } },
1174
- { pipeline: [] },
1175
720
  { writeSiteProfile: { domain: 'x' } }
1176
721
  ]);
1177
722
  assert.strictEqual(result.valid, false);
1178
- assert.strictEqual(result.errors.length, 4);
723
+ assert.strictEqual(result.errors.length, 3);
1179
724
  });
1180
725
  });