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
@@ -30,13 +30,27 @@ const MAX_TIMEOUT = TIMEOUTS.MAX;
30
30
  * @param {Object} session - CDP session
31
31
  * @param {Object} [options] - Configuration options
32
32
  * @param {number} [options.timeout=30000] - Default timeout in ms
33
+ * @param {Function} [options.getFrameContext] - Returns contextId when in a non-main frame
33
34
  * @returns {Object} Element locator interface
34
35
  */
35
36
  export function createElementLocator(session, options = {}) {
36
37
  if (!session) throw new Error('CDP session is required');
37
38
 
39
+ const getFrameContext = options.getFrameContext || null;
38
40
  let defaultTimeout = options.timeout || 30000;
39
41
 
42
+ /**
43
+ * Build Runtime.evaluate params, injecting contextId when in an iframe.
44
+ */
45
+ function evalParams(expression, returnByValue = false) {
46
+ const params = { expression, returnByValue };
47
+ if (getFrameContext) {
48
+ const contextId = getFrameContext();
49
+ if (contextId) params.contextId = contextId;
50
+ }
51
+ return params;
52
+ }
53
+
40
54
  function validateTimeout(timeout) {
41
55
  if (typeof timeout !== 'number' || !Number.isFinite(timeout)) return defaultTimeout;
42
56
  if (timeout < 0) return 0;
@@ -59,10 +73,9 @@ export function createElementLocator(session, options = {}) {
59
73
 
60
74
  let result;
61
75
  try {
62
- result = await session.send('Runtime.evaluate', {
63
- expression: `document.querySelector(${JSON.stringify(selector)})`,
64
- returnByValue: false
65
- });
76
+ result = await session.send('Runtime.evaluate',
77
+ evalParams(`document.querySelector(${JSON.stringify(selector)})`, false)
78
+ );
66
79
  } catch (error) {
67
80
  throw connectionError(error.message, 'Runtime.evaluate (querySelector)');
68
81
  }
@@ -89,10 +102,9 @@ export function createElementLocator(session, options = {}) {
89
102
 
90
103
  let result;
91
104
  try {
92
- result = await session.send('Runtime.evaluate', {
93
- expression: `Array.from(document.querySelectorAll(${JSON.stringify(selector)}))`,
94
- returnByValue: false
95
- });
105
+ result = await session.send('Runtime.evaluate',
106
+ evalParams(`Array.from(document.querySelectorAll(${JSON.stringify(selector)}))`, false)
107
+ );
96
108
  } catch (error) {
97
109
  throw connectionError(error.message, 'Runtime.evaluate (querySelectorAll)');
98
110
  }
@@ -175,10 +187,9 @@ export function createElementLocator(session, options = {}) {
175
187
  while (Date.now() - startTime < validatedTimeout) {
176
188
  let result;
177
189
  try {
178
- result = await session.send('Runtime.evaluate', {
179
- expression: checkExpr,
180
- returnByValue: true
181
- });
190
+ result = await session.send('Runtime.evaluate',
191
+ evalParams(checkExpr, true)
192
+ );
182
193
  } catch (error) {
183
194
  throw connectionError(error.message, 'Runtime.evaluate (waitForText)');
184
195
  }
@@ -278,10 +289,9 @@ export function createElementLocator(session, options = {}) {
278
289
 
279
290
  let result;
280
291
  try {
281
- result = await session.send('Runtime.evaluate', {
282
- expression,
283
- returnByValue: false
284
- });
292
+ result = await session.send('Runtime.evaluate',
293
+ evalParams(expression, false)
294
+ );
285
295
  } catch (error) {
286
296
  throw connectionError(error.message, 'Runtime.evaluate (queryByRole)');
287
297
  }
@@ -403,10 +413,9 @@ export function createElementLocator(session, options = {}) {
403
413
 
404
414
  let result;
405
415
  try {
406
- result = await session.send('Runtime.evaluate', {
407
- expression,
408
- returnByValue: false
409
- });
416
+ result = await session.send('Runtime.evaluate',
417
+ evalParams(expression, false)
418
+ );
410
419
  } catch (error) {
411
420
  throw connectionError(error.message, 'Runtime.evaluate (findElementByText)');
412
421
  }
@@ -514,10 +523,9 @@ export function createElementLocator(session, options = {}) {
514
523
 
515
524
  let result;
516
525
  try {
517
- result = await session.send('Runtime.evaluate', {
518
- expression,
519
- returnByValue: false
520
- });
526
+ result = await session.send('Runtime.evaluate',
527
+ evalParams(expression, false)
528
+ );
521
529
  } catch (error) {
522
530
  throw connectionError(error.message, 'Runtime.evaluate (findElementByTextWithinSelector)');
523
531
  }
@@ -582,6 +590,7 @@ export function createElementLocator(session, options = {}) {
582
590
  waitForElementByText,
583
591
  getBoundingBox,
584
592
  getDefaultTimeout: () => defaultTimeout,
585
- setDefaultTimeout: (timeout) => { defaultTimeout = validateTimeout(timeout); }
593
+ setDefaultTimeout: (timeout) => { defaultTimeout = validateTimeout(timeout); },
594
+ get getFrameContext() { return getFrameContext; }
586
595
  };
587
596
  }
@@ -16,6 +16,7 @@
16
16
  import { createActionabilityChecker } from './actionability.js';
17
17
  import { createElementValidator } from './element-validator.js';
18
18
  import { createReactInputFiller } from './react-filler.js';
19
+ import { createLazyResolver } from './LazyResolver.js';
19
20
  import {
20
21
  sleep,
21
22
  elementNotFoundError,
@@ -31,50 +32,95 @@ import {
31
32
  * @param {Object} elementLocator - Element locator instance
32
33
  * @param {Object} inputEmulator - Input emulator instance
33
34
  * @param {Object} [ariaSnapshot] - Optional ARIA snapshot instance
35
+ * @param {Object} [options] - Configuration options
36
+ * @param {Function} [options.getFrameContext] - Returns contextId when in a non-main frame
34
37
  * @returns {Object} Fill executor interface
35
38
  */
36
- export function createFillExecutor(session, elementLocator, inputEmulator, ariaSnapshot = null) {
39
+ export function createFillExecutor(session, elementLocator, inputEmulator, ariaSnapshot = null, options = {}) {
37
40
  if (!session) throw new Error('CDP session is required');
38
41
  if (!elementLocator) throw new Error('Element locator is required');
39
42
  if (!inputEmulator) throw new Error('Input emulator is required');
40
43
 
44
+ const getFrameContext = options.getFrameContext || null;
41
45
  const actionabilityChecker = createActionabilityChecker(session);
42
46
  const elementValidator = createElementValidator(session);
43
47
  const reactInputFiller = createReactInputFiller(session);
48
+ const lazyResolver = createLazyResolver(session, { getFrameContext });
44
49
 
45
- async function fillByRef(ref, value, opts = {}) {
46
- const { clear = true, react = false } = opts;
47
-
48
- if (!ariaSnapshot) {
49
- throw new Error('ariaSnapshot is required for ref-based fills');
50
+ /**
51
+ * Build Runtime.evaluate params, injecting contextId when in an iframe.
52
+ */
53
+ function evalParams(expression, returnByValue = false) {
54
+ const params = { expression, returnByValue };
55
+ if (getFrameContext) {
56
+ const contextId = getFrameContext();
57
+ if (contextId) params.contextId = contextId;
50
58
  }
59
+ return params;
60
+ }
51
61
 
52
- const refInfo = await ariaSnapshot.getElementByRef(ref);
53
- if (!refInfo) {
54
- throw elementNotFoundError(`ref:${ref}`, 0);
62
+ /**
63
+ * Select all and fill with value, handling the empty-string case.
64
+ * When value is "" and clear is true, presses Delete after selectAll
65
+ * to actually remove the selected content (insertText("") is a no-op).
66
+ */
67
+ async function selectAndFill(value, clear) {
68
+ if (clear) {
69
+ await inputEmulator.selectAll();
55
70
  }
56
-
57
- if (refInfo.stale) {
58
- throw new Error(`Element ref:${ref} is no longer attached to the DOM. Page content may have changed. Run 'snapshot' again to get fresh refs.`);
71
+ if (value === '' && clear) {
72
+ // insertText("") is a no-op in CDP — press Delete to remove selected text
73
+ await inputEmulator.press('Delete');
74
+ // Dispatch input/change events so frameworks (React, Vue, etc.) react to the clear
75
+ await session.send('Runtime.evaluate', evalParams(`
76
+ (function() {
77
+ const el = document.activeElement;
78
+ if (el) {
79
+ el.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
80
+ el.dispatchEvent(new Event('change', { bubbles: true, cancelable: true }));
81
+ }
82
+ })()
83
+ `, true));
84
+ } else {
85
+ await inputEmulator.insertText(String(value));
59
86
  }
87
+ }
60
88
 
61
- if (refInfo.isVisible === false) {
62
- throw new Error(`Element ref:${ref} exists but is not visible. It may be hidden or have zero dimensions.`);
89
+ async function fillByRef(ref, value, opts = {}) {
90
+ const { clear = true, react = false } = opts;
91
+
92
+ // LAZY RESOLUTION: Always resolve ref from metadata, never rely on cached element
93
+ // This eliminates stale element errors entirely
94
+ const resolved = await lazyResolver.resolveRef(ref);
95
+ if (!resolved) {
96
+ throw elementNotFoundError(`ref:${ref}`, 0);
63
97
  }
64
98
 
65
- const elementResult = await session.send('Runtime.evaluate', {
66
- expression: `(function() {
67
- const el = window.__ariaRefs && window.__ariaRefs.get(${JSON.stringify(ref)});
68
- return el;
69
- })()`,
70
- returnByValue: false
99
+ const objectId = resolved.objectId;
100
+
101
+ // Get visibility info using the resolved element
102
+ const visibilityResult = await session.send('Runtime.callFunctionOn', {
103
+ objectId,
104
+ functionDeclaration: `function() {
105
+ const style = window.getComputedStyle(this);
106
+ const rect = this.getBoundingClientRect();
107
+ return {
108
+ isVisible: style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0' && rect.width > 0 && rect.height > 0,
109
+ box: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }
110
+ };
111
+ }`,
112
+ returnByValue: true
71
113
  });
72
114
 
73
- if (!elementResult.result.objectId) {
74
- throw elementNotFoundError(`ref:${ref}`, 0);
75
- }
115
+ const refInfo = {
116
+ box: visibilityResult.result?.value?.box || resolved.box,
117
+ isVisible: visibilityResult.result?.value?.isVisible ?? true
118
+ };
76
119
 
77
- const objectId = elementResult.result.objectId;
120
+ if (refInfo.isVisible === false) {
121
+ await releaseObject(session, objectId);
122
+ throw new Error(`Element ref:${ref} exists but is not visible. It may be hidden or have zero dimensions.`);
123
+ }
78
124
 
79
125
  const editableCheck = await elementValidator.isEditable(objectId);
80
126
  if (!editableCheck.editable) {
@@ -106,11 +152,7 @@ export function createFillExecutor(session, elementLocator, inputEmulator, ariaS
106
152
  functionDeclaration: `function() { this.focus(); }`
107
153
  });
108
154
 
109
- if (clear) {
110
- await inputEmulator.selectAll();
111
- }
112
-
113
- await inputEmulator.insertText(String(value));
155
+ await selectAndFill(value, clear);
114
156
 
115
157
  return { filled: true, ref, method: 'insertText' };
116
158
  } finally {
@@ -153,11 +195,7 @@ export function createFillExecutor(session, elementLocator, inputEmulator, ariaS
153
195
  functionDeclaration: `function() { this.focus(); }`
154
196
  });
155
197
 
156
- if (clear) {
157
- await inputEmulator.selectAll();
158
- }
159
-
160
- await inputEmulator.insertText(String(value));
198
+ await selectAndFill(value, clear);
161
199
 
162
200
  return { filled: true, selector, method: 'insertText' };
163
201
  } catch (e) {
@@ -277,10 +315,9 @@ export function createFillExecutor(session, elementLocator, inputEmulator, ariaS
277
315
 
278
316
  let result;
279
317
  try {
280
- result = await session.send('Runtime.evaluate', {
281
- expression,
282
- returnByValue: false
283
- });
318
+ result = await session.send('Runtime.evaluate',
319
+ evalParams(expression, false)
320
+ );
284
321
  } catch (error) {
285
322
  throw connectionError(error.message, 'Runtime.evaluate (findInputByLabel)');
286
323
  }
@@ -383,11 +420,7 @@ export function createFillExecutor(session, elementLocator, inputEmulator, ariaS
383
420
  functionDeclaration: `function() { this.focus(); }`
384
421
  });
385
422
 
386
- if (clear) {
387
- await inputEmulator.selectAll();
388
- }
389
-
390
- await inputEmulator.insertText(String(value));
423
+ await selectAndFill(value, clear);
391
424
 
392
425
  return { filled: true, label, method: 'insertText', foundBy: foundMethod };
393
426
  } catch (e) {
@@ -405,9 +438,9 @@ export function createFillExecutor(session, elementLocator, inputEmulator, ariaS
405
438
  throw new Error('Fill requires value');
406
439
  }
407
440
 
408
- // Detect if selector looks like a versioned ref (s{N}e{M})
409
- // This allows {"fill": {"selector": "s1e1", "value": "..."}} to work like {"fill": {"ref": "s1e1", "value": "..."}}
410
- if (!ref && selector && /^s\d+e\d+$/.test(selector)) {
441
+ // Detect if selector looks like a versioned ref (f{frameId}s{N}e{M})
442
+ // This allows {"fill": {"selector": "f0s1e1", "value": "..."}} to work like {"fill": {"ref": "f0s1e1", "value": "..."}}
443
+ if (!ref && selector && /^f(\d+|\[[^\]]+\])s\d+e\d+$/.test(selector)) {
411
444
  ref = selector;
412
445
  }
413
446
 
@@ -430,7 +463,7 @@ export function createFillExecutor(session, elementLocator, inputEmulator, ariaS
430
463
 
431
464
  async function executeBatch(params) {
432
465
  if (!params || typeof params !== 'object') {
433
- throw new Error('fillForm requires an object mapping selectors to values');
466
+ throw new Error('fill batch requires an object mapping selectors to values');
434
467
  }
435
468
 
436
469
  // Support both formats:
@@ -450,7 +483,7 @@ export function createFillExecutor(session, elementLocator, inputEmulator, ariaS
450
483
 
451
484
  const entries = Object.entries(fields);
452
485
  if (entries.length === 0) {
453
- throw new Error('fillForm requires at least one field');
486
+ throw new Error('fill batch requires at least one field');
454
487
  }
455
488
 
456
489
  const results = [];
@@ -458,8 +491,8 @@ export function createFillExecutor(session, elementLocator, inputEmulator, ariaS
458
491
 
459
492
  for (const [selector, value] of entries) {
460
493
  try {
461
- // Match versioned ref format s{N}e{M}
462
- const isRef = /^s\d+e\d+$/.test(selector);
494
+ // Match versioned ref format f{frameId}s{N}e{M}
495
+ const isRef = /^f(\d+|\[[^\]]+\])s\d+e\d+$/.test(selector);
463
496
 
464
497
  if (isRef) {
465
498
  await fillByRef(selector, value, { clear: true, react: useReact });
package/src/dom/index.js CHANGED
@@ -76,6 +76,9 @@ export { createKeyboardExecutor } from './keyboard-executor.js';
76
76
  // Wait executor (waiting operations)
77
77
  export { createWaitExecutor } from './wait-executor.js';
78
78
 
79
+ // Lazy resolver (stateless element resolution)
80
+ export { createLazyResolver } from './LazyResolver.js';
81
+
79
82
  // ============================================================================
80
83
  // Convenience Functions
81
84
  // ============================================================================
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Dialog Handler Module
3
+ * Handles JavaScript alerts, confirms, and prompts
4
+ *
5
+ * PUBLIC EXPORTS:
6
+ * - createDialogHandler(session) - Factory for dialog handler
7
+ *
8
+ * @module cdp-skill/page/dialog-handler
9
+ */
10
+
11
+ /**
12
+ * Create a dialog handler for JavaScript dialogs
13
+ * @param {import('../types.js').CDPSession} session - CDP session
14
+ * @returns {Object} Dialog handler interface
15
+ */
16
+ export function createDialogHandler(session) {
17
+ let dialogCallback = null;
18
+ let boundHandler = null;
19
+ const responseQueue = [];
20
+
21
+ function onDialogOpening(params) {
22
+ const { type, message, defaultPrompt } = params;
23
+
24
+ // Default behavior: accept all dialogs
25
+ let accept = true;
26
+ let promptText = undefined;
27
+
28
+ // Check if there's a queued response
29
+ if (responseQueue.length > 0) {
30
+ const queued = responseQueue.shift();
31
+ accept = queued.accept !== false;
32
+ promptText = queued.promptText;
33
+ } else if (dialogCallback) {
34
+ // If custom callback is set, use it
35
+ const result = dialogCallback({ type, message, defaultPrompt });
36
+ accept = result.accept !== false;
37
+ promptText = result.promptText;
38
+ } else {
39
+ // Auto-accept with reasonable defaults for prompts
40
+ if (type === 'prompt') {
41
+ // Use defaultPrompt if available
42
+ // Otherwise, for test automation purposes, use a reasonable default
43
+ if (defaultPrompt !== undefined && defaultPrompt.length > 0) {
44
+ promptText = defaultPrompt;
45
+ } else if (message && message.toLowerCase().includes('prompt')) {
46
+ // For prompt dialogs asking for input, use a test value
47
+ promptText = 'Hello CDP';
48
+ } else {
49
+ promptText = '';
50
+ }
51
+ }
52
+ }
53
+
54
+ // Handle the dialog
55
+ session.send('Page.handleJavaScriptDialog', {
56
+ accept,
57
+ promptText
58
+ }).catch(err => {
59
+ // Ignore errors - dialog may have been already handled
60
+ });
61
+ }
62
+
63
+ /**
64
+ * Enable dialog handling
65
+ * @param {Function} [callback] - Optional callback to customize dialog handling
66
+ * @returns {Promise<void>}
67
+ */
68
+ async function enable(callback = null) {
69
+ dialogCallback = callback;
70
+
71
+ if (!boundHandler) {
72
+ boundHandler = onDialogOpening;
73
+ session.on('Page.javascriptDialogOpening', boundHandler);
74
+ }
75
+
76
+ // Enable page domain if not already enabled
77
+ try {
78
+ await session.send('Page.enable');
79
+ } catch (err) {
80
+ // Ignore if already enabled
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Disable dialog handling
86
+ * @returns {Promise<void>}
87
+ */
88
+ async function disable() {
89
+ if (boundHandler) {
90
+ session.off('Page.javascriptDialogOpening', boundHandler);
91
+ boundHandler = null;
92
+ }
93
+ dialogCallback = null;
94
+ }
95
+
96
+ /**
97
+ * Set a custom dialog handler
98
+ * @param {Function} callback - Callback({type, message, defaultPrompt}) => {accept, promptText}
99
+ */
100
+ function setHandler(callback) {
101
+ dialogCallback = callback;
102
+ }
103
+
104
+ /**
105
+ * Queue a response for the next dialog
106
+ * @param {boolean} accept - Whether to accept the dialog
107
+ * @param {string} [promptText] - Text to enter for prompts
108
+ */
109
+ function queueResponse(accept, promptText) {
110
+ responseQueue.push({ accept, promptText });
111
+ }
112
+
113
+ return {
114
+ enable,
115
+ disable,
116
+ setHandler,
117
+ queueResponse
118
+ };
119
+ }