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.
- package/README.md +80 -35
- package/SKILL.md +151 -239
- package/install.js +1 -0
- package/package.json +1 -1
- package/src/aria/index.js +8 -0
- package/src/aria/output-processor.js +173 -0
- package/src/aria/role-query.js +1229 -0
- package/src/aria/snapshot.js +459 -0
- package/src/aria.js +237 -43
- package/src/cdp/browser.js +22 -4
- package/src/cdp-skill.js +245 -69
- package/src/dom/click-executor.js +240 -76
- package/src/dom/element-locator.js +34 -25
- package/src/dom/fill-executor.js +55 -27
- package/src/page/dialog-handler.js +119 -0
- package/src/page/page-controller.js +190 -3
- package/src/runner/context-helpers.js +33 -55
- package/src/runner/execute-dynamic.js +8 -7
- package/src/runner/execute-form.js +11 -11
- package/src/runner/execute-input.js +2 -2
- package/src/runner/execute-interaction.js +99 -120
- package/src/runner/execute-navigation.js +11 -26
- package/src/runner/execute-query.js +8 -5
- package/src/runner/step-executors.js +225 -84
- package/src/runner/step-registry.js +1064 -0
- package/src/runner/step-validator.js +16 -754
- package/src/tests/Aria.test.js +1025 -0
- package/src/tests/ContextHelpers.test.js +39 -28
- package/src/tests/ExecuteBrowser.test.js +572 -0
- package/src/tests/ExecuteDynamic.test.js +2 -457
- package/src/tests/ExecuteForm.test.js +700 -0
- package/src/tests/ExecuteInput.test.js +540 -0
- package/src/tests/ExecuteInteraction.test.js +319 -0
- package/src/tests/ExecuteQuery.test.js +820 -0
- package/src/tests/FillExecutor.test.js +2 -2
- package/src/tests/StepValidator.test.js +222 -76
- package/src/tests/TestRunner.test.js +36 -25
- package/src/tests/integration.test.js +2 -1
- package/src/types.js +9 -9
- package/src/utils/backoff.js +118 -0
- package/src/utils/cdp-helpers.js +130 -0
- package/src/utils/devices.js +140 -0
- package/src/utils/errors.js +242 -0
- package/src/utils/index.js +65 -0
- package/src/utils/temp.js +75 -0
- package/src/utils/validators.js +433 -0
- 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:
|
|
12
|
+
* DEPENDENCIES:
|
|
13
|
+
* - ./step-registry.js: getAllStepTypes, getVisualActions
|
|
13
14
|
*/
|
|
14
15
|
|
|
15
|
-
|
|
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
|
-
//
|
|
26
|
-
|
|
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('
|
|
97
|
-
if (actions.includes('
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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;
|
|
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
|
-
|
|
323
|
-
if (contextId
|
|
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
|
-
|
|
434
|
-
|
|
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
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
cancelable: true,
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
clientY:
|
|
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
|
-
|
|
519
|
+
return { success: false, error: e.message };
|
|
504
520
|
}
|
|
505
521
|
}
|
|
506
522
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
175
|
-
|
|
176
|
-
|
|
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
|
}
|