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
@@ -6,30 +6,18 @@
6
6
  * - buildActionContext(action, params, context) → string - Describes what action was taken
7
7
  * - buildCommandContext(steps) → string - Summarizes multi-step commands
8
8
  * - captureFailureContext(deps) → Object - Gathers debug info on failure
9
- * - STEP_TYPES - Array of valid step type names
10
- * - VISUAL_ACTIONS - Actions that trigger auto-screenshot
9
+ * - STEP_TYPES - Array of valid step type names (from registry)
10
+ * - VISUAL_ACTIONS - Actions that trigger auto-screenshot (from registry)
11
11
  *
12
- * DEPENDENCIES: None (pure functions)
12
+ * DEPENDENCIES:
13
+ * - ./step-registry.js: getAllStepTypes, getVisualActions
13
14
  */
14
15
 
15
- export const STEP_TYPES = [
16
- 'goto', 'wait', 'click', 'fill', 'fillForm', 'press', 'query', 'queryAll',
17
- 'inspect', 'scroll', 'console', 'pdf', 'eval', 'snapshot', 'snapshotSearch',
18
- 'hover', 'viewport', 'cookies', 'back', 'forward', 'waitForNavigation', 'listTabs',
19
- 'closeTab', 'openTab', 'type', 'select', 'selectOption', 'validate', 'submit',
20
- 'assert', 'switchToFrame', 'switchToMainFrame', 'listFrames', 'drag', 'formState',
21
- 'extract', 'getDom', 'getBox', 'fillActive', 'refAt', 'elementsAt', 'elementsNear',
22
- 'reload', 'pageFunction', 'poll', 'pipeline', 'writeSiteProfile', 'readSiteProfile'
23
- ];
16
+ import { getAllStepTypes, getVisualActions } from './step-registry.js';
24
17
 
25
- // Visual actions that trigger auto-screenshot
26
- // Actions that should capture a screenshot - anything that interacts with or queries the visible page
27
- export const VISUAL_ACTIONS = [
28
- 'goto', 'reload', 'click', 'fill', 'fillForm', 'type', 'hover', 'press', 'scroll', 'wait', // interactions
29
- 'snapshot', 'snapshotSearch', 'query', 'queryAll', 'inspect', 'eval', 'extract', 'formState', // queries
30
- 'drag', 'select', 'selectOption', 'validate', 'submit', 'assert', // other page interactions
31
- 'openTab' // navigation actions - behave like goto for auto-snapshot
32
- ];
18
+ // Re-export from registry for backwards compatibility
19
+ export const STEP_TYPES = getAllStepTypes();
20
+ export const VISUAL_ACTIONS = getVisualActions();
33
21
 
34
22
  /**
35
23
  * Build action context string for diff summary
@@ -59,8 +47,19 @@ export function buildActionContext(action, params, context) {
59
47
  case 'hover': {
60
48
  if (typeof params === 'string') return `Hovered over ${params}`;
61
49
  if (params?.selector) return `Hovered over ${params.selector}`;
50
+ if (params?.ref) return `Hovered over [ref=${params.ref}]`;
51
+ if (params?.text) return `Hovered over "${params.text}"`;
52
+ if (typeof params?.x === 'number' && typeof params?.y === 'number') return `Hovered over (${params.x}, ${params.y})`;
62
53
  return 'Hovered over element';
63
54
  }
55
+ case 'frame': {
56
+ if (params === 'top') return 'Switched to main frame';
57
+ if (typeof params === 'string') return `Switched to frame ${params}`;
58
+ if (typeof params === 'number') return `Switched to frame index ${params}`;
59
+ if (params?.list) return 'Listed frames';
60
+ if (params?.name) return `Switched to frame "${params.name}"`;
61
+ return 'Frame operation';
62
+ }
64
63
  case 'fill':
65
64
  case 'type': {
66
65
  if (params?.selector) return `Typed in ${params.selector}`;
@@ -93,8 +92,8 @@ export function buildCommandContext(steps) {
93
92
  if (actions.includes('hover')) return 'Hovered';
94
93
  if (actions.includes('fill') || actions.includes('type')) return 'Typed';
95
94
  if (actions.includes('press')) return 'Pressed key';
96
- if (actions.includes('goto') || actions.includes('openTab')) return 'Navigated';
97
- if (actions.includes('select')) return 'Selected';
95
+ if (actions.includes('goto') || actions.includes('newTab')) return 'Navigated';
96
+ if (actions.includes('selectText')) return 'Selected';
98
97
  if (actions.includes('drag')) return 'Dragged';
99
98
 
100
99
  // Default: list the actions
@@ -120,10 +119,7 @@ export async function captureFailureContext(deps, options = {}) {
120
119
 
121
120
  try {
122
121
  // Get page title
123
- const titleResult = await pageController.session.send('Runtime.evaluate', {
124
- expression: 'document.title',
125
- returnByValue: true
126
- });
122
+ const titleResult = await pageController.evaluateInFrame('document.title');
127
123
  context.title = titleResult.result.value || '';
128
124
  } catch {
129
125
  context.title = null;
@@ -131,10 +127,7 @@ export async function captureFailureContext(deps, options = {}) {
131
127
 
132
128
  try {
133
129
  // Get current URL
134
- const urlResult = await pageController.session.send('Runtime.evaluate', {
135
- expression: 'window.location.href',
136
- returnByValue: true
137
- });
130
+ const urlResult = await pageController.evaluateInFrame('window.location.href');
138
131
  context.url = urlResult.result.value || '';
139
132
  } catch {
140
133
  context.url = null;
@@ -142,14 +135,11 @@ export async function captureFailureContext(deps, options = {}) {
142
135
 
143
136
  try {
144
137
  // Get scroll position
145
- const scrollResult = await pageController.session.send('Runtime.evaluate', {
146
- expression: `({
138
+ const scrollResult = await pageController.evaluateInFrame(`({
147
139
  x: window.scrollX || document.documentElement.scrollLeft,
148
140
  y: window.scrollY || document.documentElement.scrollTop,
149
141
  maxY: Math.max(document.body.scrollHeight, document.documentElement.scrollHeight) - window.innerHeight
150
- })`,
151
- returnByValue: true
152
- });
142
+ })`);
153
143
  const scroll = scrollResult.result.value;
154
144
  context.scrollPosition = {
155
145
  x: scroll.x,
@@ -163,8 +153,7 @@ export async function captureFailureContext(deps, options = {}) {
163
153
 
164
154
  try {
165
155
  // Get visible buttons with refs (limit 8)
166
- const buttonsResult = await pageController.session.send('Runtime.evaluate', {
167
- expression: `
156
+ const buttonsResult = await pageController.evaluateInFrame(`
168
157
  (function() {
169
158
  const buttons = Array.from(document.querySelectorAll('button, input[type="button"], input[type="submit"], [role="button"]'));
170
159
  return buttons
@@ -193,9 +182,7 @@ export async function captureFailureContext(deps, options = {}) {
193
182
  return { text, selector, ref };
194
183
  });
195
184
  })()
196
- `,
197
- returnByValue: true
198
- });
185
+ `);
199
186
  context.visibleButtons = buttonsResult.result.value || [];
200
187
  } catch {
201
188
  context.visibleButtons = [];
@@ -203,8 +190,7 @@ export async function captureFailureContext(deps, options = {}) {
203
190
 
204
191
  try {
205
192
  // Get visible links (limit 5)
206
- const linksResult = await pageController.session.send('Runtime.evaluate', {
207
- expression: `
193
+ const linksResult = await pageController.evaluateInFrame(`
208
194
  (function() {
209
195
  const links = Array.from(document.querySelectorAll('a[href]'));
210
196
  return links
@@ -219,9 +205,7 @@ export async function captureFailureContext(deps, options = {}) {
219
205
  href: a.href ? a.href.substring(0, 100) : ''
220
206
  }));
221
207
  })()
222
- `,
223
- returnByValue: true
224
- });
208
+ `);
225
209
  context.visibleLinks = linksResult.result.value || [];
226
210
  } catch {
227
211
  context.visibleLinks = [];
@@ -229,8 +213,7 @@ export async function captureFailureContext(deps, options = {}) {
229
213
 
230
214
  try {
231
215
  // Get any visible error messages or alerts
232
- const errorsResult = await pageController.session.send('Runtime.evaluate', {
233
- expression: `
216
+ const errorsResult = await pageController.evaluateInFrame(`
234
217
  (function() {
235
218
  const errorSelectors = [
236
219
  '.error', '.alert', '.warning', '.message',
@@ -252,9 +235,7 @@ export async function captureFailureContext(deps, options = {}) {
252
235
  }
253
236
  return errors.slice(0, 3);
254
237
  })()
255
- `,
256
- returnByValue: true
257
- });
238
+ `);
258
239
  context.visibleErrors = errorsResult.result.value || [];
259
240
  } catch {
260
241
  context.visibleErrors = [];
@@ -264,8 +245,7 @@ export async function captureFailureContext(deps, options = {}) {
264
245
  if (failedSelector || failedText) {
265
246
  try {
266
247
  const searchTerm = failedText || failedSelector;
267
- const nearMatchesResult = await pageController.session.send('Runtime.evaluate', {
268
- expression: `
248
+ const nearMatchesResult = await pageController.evaluateInFrame(`
269
249
  (function() {
270
250
  const searchTerm = ${JSON.stringify(searchTerm)}.toLowerCase();
271
251
  const candidates = [];
@@ -322,9 +302,7 @@ export async function captureFailureContext(deps, options = {}) {
322
302
  candidates.sort((a, b) => b.score - a.score);
323
303
  return candidates.slice(0, 5);
324
304
  })()
325
- `,
326
- returnByValue: true
327
- });
305
+ `);
328
306
  context.nearMatches = nearMatchesResult.result.value || [];
329
307
  } catch {
330
308
  context.nearMatches = [];
@@ -53,7 +53,7 @@ function processSerializedResult(raw) {
53
53
  * @returns {Promise<Object>} serialized return value
54
54
  */
55
55
  export async function executePageFunction(pageController, params) {
56
- const fn = typeof params === 'string' ? params : params.fn;
56
+ const fn = typeof params === 'string' ? params : (params.fn || params.expression);
57
57
  const useRefs = typeof params === 'object' && params.refs === true;
58
58
  const timeout = typeof params === 'object' && typeof params.timeout === 'number'
59
59
  ? params.timeout : null;
@@ -150,11 +150,11 @@ export async function executePoll(pageController, params) {
150
150
  const rawVal = result.result.value;
151
151
  const isTruthy = rawVal !== null && rawVal !== undefined &&
152
152
  rawVal !== false && rawVal !== 0 && rawVal !== '' &&
153
- !(typeof rawVal === 'object' && rawVal.type === 'null') &&
154
- !(typeof rawVal === 'object' && rawVal.type === 'undefined') &&
155
- !(typeof rawVal === 'object' && rawVal.type === 'boolean' && rawVal.value === false) &&
156
- !(typeof rawVal === 'object' && rawVal.type === 'number' && rawVal.value === 0) &&
157
- !(typeof rawVal === 'object' && rawVal.type === 'string' && rawVal.value === '');
153
+ !(typeof rawVal === 'object' && rawVal !== null && rawVal.type === 'null') &&
154
+ !(typeof rawVal === 'object' && rawVal !== null && rawVal.type === 'undefined') &&
155
+ !(typeof rawVal === 'object' && rawVal !== null && rawVal.type === 'boolean' && rawVal.value === false) &&
156
+ !(typeof rawVal === 'object' && rawVal !== null && rawVal.type === 'number' && rawVal.value === 0) &&
157
+ !(typeof rawVal === 'object' && rawVal !== null && rawVal.type === 'string' && rawVal.value === '');
158
158
 
159
159
  if (isTruthy) {
160
160
  return { resolved: true, value: processed, elapsed: Date.now() - start };
@@ -393,7 +393,8 @@ export async function loadSiteProfile(domain) {
393
393
  */
394
394
  export async function executeWriteSiteProfile(params) {
395
395
  if (!params || !params.domain || !params.content) {
396
- throw new Error('writeSiteProfile requires domain and content');
396
+ const providedKeys = params ? Object.keys(params).join(', ') : 'none';
397
+ throw new Error(`writeSiteProfile requires domain and content (got keys: ${providedKeys})`);
397
398
  }
398
399
 
399
400
  const clean = sanitizeDomain(params.domain);
@@ -47,8 +47,7 @@ export async function executeExtract(deps, params) {
47
47
  throw new Error('extract requires a selector');
48
48
  }
49
49
 
50
- const result = await session.send('Runtime.evaluate', {
51
- expression: `
50
+ const extractExpr = `
52
51
  (function() {
53
52
  const selector = ${JSON.stringify(selector)};
54
53
  const typeHint = ${JSON.stringify(type)};
@@ -160,11 +159,15 @@ export async function executeExtract(deps, params) {
160
159
  };
161
160
  }
162
161
 
163
- return { error: 'Could not detect data type. Use type: "table" or "list" option.', detectedType };
162
+ // Fallback: extract text content when element is not a table or list
163
+ const text = (el.textContent || '').trim();
164
+ return { type: 'text', text, tagName };
164
165
  })()
165
- `,
166
- returnByValue: true
167
- });
166
+ `;
167
+ const extractArgs = { expression: extractExpr, returnByValue: true };
168
+ const contextId = pageController.getFrameContext();
169
+ if (contextId) extractArgs.contextId = contextId;
170
+ const result = await session.send('Runtime.evaluate', extractArgs);
168
171
 
169
172
  if (result.exceptionDetails) {
170
173
  throw new Error('Extract error: ' + result.exceptionDetails.text);
@@ -254,15 +257,12 @@ export async function executeAssert(pageController, elementLocator, params) {
254
257
 
255
258
  try {
256
259
  // Get the text content of the target element
257
- const textResult = await pageController.session.send('Runtime.evaluate', {
258
- expression: `
260
+ const textResult = await pageController.evaluateInFrame(`
259
261
  (function() {
260
262
  const el = document.querySelector(${JSON.stringify(selector)});
261
263
  return el ? el.textContent : null;
262
264
  })()
263
- `,
264
- returnByValue: true
265
- });
265
+ `);
266
266
 
267
267
  const actualText = textResult.result.value;
268
268
  textAssertion.found = actualText !== null;
@@ -229,8 +229,8 @@ export async function executeFillActive(pageController, inputEmulator, params) {
229
229
  const session = pageController.session;
230
230
 
231
231
  // Parse params
232
- const value = typeof params === 'string' ? params : params.value;
233
- const clear = typeof params === 'object' ? params.clear !== false : true;
232
+ const value = typeof params === 'string' ? params : (params && params.value);
233
+ const clear = typeof params === 'object' && params !== null ? params.clear !== false : true;
234
234
 
235
235
  // Check if there's an active element and if it's editable
236
236
  const checkResult = await session.send('Runtime.evaluate', {
@@ -70,8 +70,9 @@ export async function clickWithVerification(elementLocator, inputEmulator, x, y,
70
70
  * Feature 13: Supports captureResult to detect new visible elements after hover
71
71
  */
72
72
  export async function executeHover(elementLocator, inputEmulator, ariaSnapshot, params) {
73
- const selector = typeof params === 'string' ? params : params.selector;
73
+ const selector = typeof params === 'string' ? params : (params.selector || null);
74
74
  let ref = typeof params === 'object' ? params.ref : null;
75
+ const text = typeof params === 'object' ? params.text : null;
75
76
  const duration = typeof params === 'object' ? (params.duration || 0) : 0;
76
77
 
77
78
  // Detect if string selector looks like a ref (e.g., "s1e1", "s2e12")
@@ -80,9 +81,35 @@ export async function executeHover(elementLocator, inputEmulator, ariaSnapshot,
80
81
  ref = selector;
81
82
  }
82
83
  const force = typeof params === 'object' && params.force === true;
83
- const timeout = typeof params === 'object' ? (params.timeout || 10000) : 10000; // Reduced from 30s to 10s
84
+ const timeout = typeof params === 'object' ? (params.timeout || 10000) : 10000;
84
85
  const captureResult = typeof params === 'object' && params.captureResult === true;
85
86
 
87
+ // Handle coordinate-based hover
88
+ if (typeof params === 'object' && typeof params.x === 'number' && typeof params.y === 'number' && !ref && !selector && !text) {
89
+ await inputEmulator.hover(params.x, params.y, { duration });
90
+ if (captureResult) {
91
+ await sleep(100);
92
+ return await captureHoverResult(elementLocator.session, []);
93
+ }
94
+ return { hovered: true };
95
+ }
96
+
97
+ // Handle text-based hover
98
+ if (text && ariaSnapshot) {
99
+ const refInfo = await ariaSnapshot.findByText(text);
100
+ if (!refInfo) {
101
+ throw elementNotFoundError(`text:${text}`, 0);
102
+ }
103
+ const x = refInfo.box.x + refInfo.box.width / 2;
104
+ const y = refInfo.box.y + refInfo.box.height / 2;
105
+ await inputEmulator.hover(x, y, { duration });
106
+ if (captureResult) {
107
+ await sleep(100);
108
+ return await captureHoverResult(elementLocator.session, []);
109
+ }
110
+ return { hovered: true };
111
+ }
112
+
86
113
  const session = elementLocator.session;
87
114
  let visibleElementsBefore = [];
88
115
 
@@ -285,7 +312,7 @@ export async function captureHoverResult(session, visibleBefore) {
285
312
  * @returns {Promise<Object>} Drag result
286
313
  */
287
314
  export async function executeDrag(elementLocator, inputEmulator, pageController, ariaSnapshot, params) {
288
- const { source, target, steps = 10, delay = 0 } = params;
315
+ const { source, target, steps = 10, delay = 0, method = 'auto' } = params;
289
316
  const session = elementLocator.session;
290
317
 
291
318
  // Helper to get element bounding box by ref
@@ -305,8 +332,6 @@ export async function executeDrag(elementLocator, inputEmulator, pageController,
305
332
 
306
333
  // Helper to get element bounding box in current frame context
307
334
  async function getElementBox(selector) {
308
- // Use page controller's frame context if available
309
- const contextId = pageController.currentExecutionContextId;
310
335
  const evalParams = {
311
336
  expression: `
312
337
  (function() {
@@ -319,10 +344,8 @@ export async function executeDrag(elementLocator, inputEmulator, pageController,
319
344
  returnByValue: true
320
345
  };
321
346
 
322
- // Add context ID if we're in a non-main frame
323
- if (contextId && pageController.currentFrameId !== pageController.mainFrameId) {
324
- evalParams.contextId = contextId;
325
- }
347
+ const contextId = pageController.getFrameContext();
348
+ if (contextId) evalParams.contextId = contextId;
326
349
 
327
350
  const result = await session.send('Runtime.evaluate', evalParams);
328
351
  if (result.exceptionDetails) {
@@ -415,10 +438,10 @@ export async function executeDrag(elementLocator, inputEmulator, pageController,
415
438
  const targetX = ${targetX};
416
439
  const targetY = ${targetY};
417
440
  const steps = ${steps};
441
+ const method = ${JSON.stringify(method)};
418
442
 
419
443
  // Check if source is an input[type=range] (slider)
420
444
  if (sourceEl && sourceEl.tagName === 'INPUT' && sourceEl.type === 'range') {
421
- // For range inputs, calculate the value based on target position
422
445
  const rect = sourceEl.getBoundingClientRect();
423
446
  const percent = Math.max(0, Math.min(1, (targetX - rect.left) / rect.width));
424
447
  const min = parseFloat(sourceEl.min) || 0;
@@ -430,130 +453,86 @@ export async function executeDrag(elementLocator, inputEmulator, pageController,
430
453
  return { success: true, method: 'range-input', value: newValue };
431
454
  }
432
455
 
433
- // Try HTML5 Drag and Drop API first
434
- if (sourceEl && targetEl) {
456
+ function doMouseDrag() {
457
+ const sourceElAtPoint = sourceEl || document.elementFromPoint(sourceX, sourceY);
458
+ if (!sourceElAtPoint) {
459
+ return { success: false, error: 'No element at source coordinates' };
460
+ }
461
+
462
+ sourceElAtPoint.dispatchEvent(new MouseEvent('mousedown', {
463
+ bubbles: true, cancelable: true,
464
+ clientX: sourceX, clientY: sourceY, button: 0, buttons: 1
465
+ }));
466
+
467
+ const deltaX = (targetX - sourceX) / steps;
468
+ const deltaY = (targetY - sourceY) / steps;
469
+
470
+ for (let i = 1; i <= steps; i++) {
471
+ const currentX = sourceX + deltaX * i;
472
+ const currentY = sourceY + deltaY * i;
473
+ const elAtPoint = document.elementFromPoint(currentX, currentY) || sourceElAtPoint;
474
+ elAtPoint.dispatchEvent(new MouseEvent('mousemove', {
475
+ bubbles: true, cancelable: true,
476
+ clientX: currentX, clientY: currentY, button: 0, buttons: 1
477
+ }));
478
+ }
479
+
480
+ const targetElAtPoint = document.elementFromPoint(targetX, targetY) || sourceElAtPoint;
481
+ targetElAtPoint.dispatchEvent(new MouseEvent('mouseup', {
482
+ bubbles: true, cancelable: true,
483
+ clientX: targetX, clientY: targetY, button: 0, buttons: 0
484
+ }));
485
+
486
+ return { success: true, method: 'mouse-events' };
487
+ }
488
+
489
+ function doHtml5Drag() {
490
+ if (!sourceEl || !targetEl) {
491
+ return { success: false, error: 'HTML5 DnD requires both source and target elements' };
492
+ }
435
493
  try {
436
- // Create DataTransfer object
437
494
  const dataTransfer = new DataTransfer();
438
495
  dataTransfer.effectAllowed = 'all';
439
496
  dataTransfer.dropEffect = 'move';
440
497
 
441
- // Dispatch dragstart on source
442
- const dragStartEvent = new DragEvent('dragstart', {
443
- bubbles: true,
444
- cancelable: true,
445
- dataTransfer: dataTransfer,
446
- clientX: sourceX,
447
- clientY: sourceY
448
- });
449
- sourceEl.dispatchEvent(dragStartEvent);
450
-
451
- // Dispatch drag on source
452
- const dragEvent = new DragEvent('drag', {
453
- bubbles: true,
454
- cancelable: true,
455
- dataTransfer: dataTransfer,
456
- clientX: sourceX,
457
- clientY: sourceY
458
- });
459
- sourceEl.dispatchEvent(dragEvent);
460
-
461
- // Dispatch dragenter on target
462
- const dragEnterEvent = new DragEvent('dragenter', {
463
- bubbles: true,
464
- cancelable: true,
465
- dataTransfer: dataTransfer,
466
- clientX: targetX,
467
- clientY: targetY
468
- });
469
- targetEl.dispatchEvent(dragEnterEvent);
470
-
471
- // Dispatch dragover on target
472
- const dragOverEvent = new DragEvent('dragover', {
473
- bubbles: true,
474
- cancelable: true,
475
- dataTransfer: dataTransfer,
476
- clientX: targetX,
477
- clientY: targetY
478
- });
479
- targetEl.dispatchEvent(dragOverEvent);
480
-
481
- // Dispatch drop on target
482
- const dropEvent = new DragEvent('drop', {
483
- bubbles: true,
484
- cancelable: true,
485
- dataTransfer: dataTransfer,
486
- clientX: targetX,
487
- clientY: targetY
488
- });
489
- targetEl.dispatchEvent(dropEvent);
490
-
491
- // Dispatch dragend on source
492
- const dragEndEvent = new DragEvent('dragend', {
493
- bubbles: true,
494
- cancelable: true,
495
- dataTransfer: dataTransfer,
496
- clientX: targetX,
497
- clientY: targetY
498
- });
499
- sourceEl.dispatchEvent(dragEndEvent);
498
+ sourceEl.dispatchEvent(new DragEvent('dragstart', {
499
+ bubbles: true, cancelable: true, dataTransfer, clientX: sourceX, clientY: sourceY
500
+ }));
501
+ sourceEl.dispatchEvent(new DragEvent('drag', {
502
+ bubbles: true, cancelable: true, dataTransfer, clientX: sourceX, clientY: sourceY
503
+ }));
504
+ targetEl.dispatchEvent(new DragEvent('dragenter', {
505
+ bubbles: true, cancelable: true, dataTransfer, clientX: targetX, clientY: targetY
506
+ }));
507
+ targetEl.dispatchEvent(new DragEvent('dragover', {
508
+ bubbles: true, cancelable: true, dataTransfer, clientX: targetX, clientY: targetY
509
+ }));
510
+ targetEl.dispatchEvent(new DragEvent('drop', {
511
+ bubbles: true, cancelable: true, dataTransfer, clientX: targetX, clientY: targetY
512
+ }));
513
+ sourceEl.dispatchEvent(new DragEvent('dragend', {
514
+ bubbles: true, cancelable: true, dataTransfer, clientX: targetX, clientY: targetY
515
+ }));
500
516
 
501
517
  return { success: true, method: 'html5-dnd' };
502
518
  } catch (e) {
503
- // Fall through to mouse events
519
+ return { success: false, error: e.message };
504
520
  }
505
521
  }
506
522
 
507
- // Fallback: Mouse event simulation for non-DnD dragging (e.g., sortable lists, custom drag)
508
- const sourceElAtPoint = sourceEl || document.elementFromPoint(sourceX, sourceY);
509
- if (!sourceElAtPoint) {
510
- return { success: false, error: 'No element at source coordinates' };
511
- }
523
+ if (method === 'mouse') return doMouseDrag();
524
+ if (method === 'html5') return doHtml5Drag();
525
+
526
+ // auto: try mouse first (works for jQuery UI, sortable lists), then HTML5 DnD
527
+ const mouseResult = doMouseDrag();
528
+ if (mouseResult.success) return mouseResult;
512
529
 
513
- // Dispatch mouse events
514
- const mouseDown = new MouseEvent('mousedown', {
515
- bubbles: true,
516
- cancelable: true,
517
- clientX: sourceX,
518
- clientY: sourceY,
519
- button: 0,
520
- buttons: 1
521
- });
522
- sourceElAtPoint.dispatchEvent(mouseDown);
523
-
524
- // Move in steps
525
- const deltaX = (targetX - sourceX) / steps;
526
- const deltaY = (targetY - sourceY) / steps;
527
-
528
- for (let i = 1; i <= steps; i++) {
529
- const currentX = sourceX + deltaX * i;
530
- const currentY = sourceY + deltaY * i;
531
- const elAtPoint = document.elementFromPoint(currentX, currentY) || sourceElAtPoint;
532
-
533
- const mouseMove = new MouseEvent('mousemove', {
534
- bubbles: true,
535
- cancelable: true,
536
- clientX: currentX,
537
- clientY: currentY,
538
- button: 0,
539
- buttons: 1
540
- });
541
- elAtPoint.dispatchEvent(mouseMove);
530
+ if (sourceEl && targetEl) {
531
+ const html5Result = doHtml5Drag();
532
+ if (html5Result.success) return html5Result;
542
533
  }
543
534
 
544
- // Release at target
545
- const targetElAtPoint = document.elementFromPoint(targetX, targetY) || sourceElAtPoint;
546
- const mouseUp = new MouseEvent('mouseup', {
547
- bubbles: true,
548
- cancelable: true,
549
- clientX: targetX,
550
- clientY: targetY,
551
- button: 0,
552
- buttons: 0
553
- });
554
- targetElAtPoint.dispatchEvent(mouseUp);
555
-
556
- return { success: true, method: 'mouse-events' };
535
+ return mouseResult;
557
536
  })()
558
537
  `,
559
538
  returnByValue: true,
@@ -95,40 +95,30 @@ export async function executeScroll(elementLocator, inputEmulator, pageControlle
95
95
  throw new Error(`Element ref:${ref} is no longer attached to the DOM. Run 'snapshot' again to get fresh refs.`);
96
96
  }
97
97
  // Scroll to element using its coordinates
98
- await pageController.session.send('Runtime.evaluate', {
99
- expression: `
98
+ await pageController.evaluateInFrame(`
100
99
  (function() {
101
100
  const el = window.__ariaRefs && window.__ariaRefs.get(${JSON.stringify(ref)});
102
101
  if (el && el.scrollIntoView) {
103
102
  el.scrollIntoView({ block: 'center', behavior: 'smooth' });
104
103
  }
105
104
  })()
106
- `
107
- });
105
+ `);
108
106
  }
109
107
 
110
108
  if (typeof params === 'string') {
111
109
  // Direction-based scroll
112
110
  switch (params) {
113
111
  case 'top':
114
- await pageController.session.send('Runtime.evaluate', {
115
- expression: 'window.scrollTo(0, 0)'
116
- });
112
+ await pageController.evaluateInFrame('window.scrollTo(0, 0)');
117
113
  break;
118
114
  case 'bottom':
119
- await pageController.session.send('Runtime.evaluate', {
120
- expression: 'window.scrollTo(0, document.body.scrollHeight)'
121
- });
115
+ await pageController.evaluateInFrame('window.scrollTo(0, document.body.scrollHeight)');
122
116
  break;
123
117
  case 'up':
124
- await pageController.session.send('Runtime.evaluate', {
125
- expression: 'window.scrollBy(0, -300)'
126
- });
118
+ await pageController.evaluateInFrame('window.scrollBy(0, -300)');
127
119
  break;
128
120
  case 'down':
129
- await pageController.session.send('Runtime.evaluate', {
130
- expression: 'window.scrollBy(0, 300)'
131
- });
121
+ await pageController.evaluateInFrame('window.scrollBy(0, 300)');
132
122
  break;
133
123
  default:
134
124
  // Check if it looks like a ref (e.g., "s1e1", "s2e12")
@@ -159,22 +149,17 @@ export async function executeScroll(elementLocator, inputEmulator, pageControlle
159
149
  await el.dispose();
160
150
  } else if (params.deltaY !== undefined || params.deltaX !== undefined) {
161
151
  // Scroll by delta using JavaScript (more reliable than CDP mouse wheel events)
162
- await pageController.session.send('Runtime.evaluate', {
163
- expression: `window.scrollBy(${params.deltaX || 0}, ${params.deltaY || 0})`
164
- });
152
+ await pageController.evaluateInFrame(`window.scrollBy(${params.deltaX || 0}, ${params.deltaY || 0})`);
165
153
  } else if (params.y !== undefined) {
166
154
  // Scroll to position
167
- await pageController.session.send('Runtime.evaluate', {
168
- expression: `window.scrollTo(${params.x || 0}, ${params.y})`
169
- });
155
+ await pageController.evaluateInFrame(`window.scrollTo(${params.x || 0}, ${params.y})`);
170
156
  }
171
157
  }
172
158
 
173
159
  // Return current scroll position
174
- const posResult = await pageController.session.send('Runtime.evaluate', {
175
- expression: '({ scrollX: window.scrollX, scrollY: window.scrollY })',
176
- returnByValue: true
177
- });
160
+ const posResult = await pageController.evaluateInFrame(
161
+ '({ scrollX: window.scrollX, scrollY: window.scrollY })'
162
+ );
178
163
 
179
164
  return posResult.result.value;
180
165
  }