cdp-skill 1.0.16 → 1.0.17
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 +4 -4
- package/SKILL.md +276 -170
- package/package.json +9 -8
- package/{src → scripts}/aria/index.js +1 -1
- package/scripts/aria/role-query.js +295 -0
- package/{src → scripts}/aria.js +11 -5
- package/{src → scripts}/capture/console-capture.js +11 -9
- package/{src → scripts}/capture/screenshot-capture.js +8 -9
- package/{src → scripts}/cdp/connection.js +30 -6
- package/{src → scripts}/cdp-skill.js +7 -6
- package/{src → scripts}/diff.js +7 -6
- package/{src → scripts}/dom/LazyResolver.js +23 -12
- package/{src → scripts}/dom/actionability.js +39 -22
- package/{src → scripts}/dom/click-executor.js +90 -53
- package/{src → scripts}/dom/element-locator.js +4 -4
- package/{src → scripts}/dom/fill-executor.js +8 -4
- package/{src → scripts}/dom/input-emulator.js +47 -9
- package/{src → scripts}/dom/react-filler.js +11 -3
- package/{src → scripts}/dom/wait-executor.js +10 -2
- package/{src → scripts}/page/dialog-handler.js +7 -3
- package/{src → scripts}/page/dom-stability.js +17 -10
- package/{src → scripts}/page/page-controller.js +41 -34
- package/{src → scripts}/runner/context-helpers.js +7 -0
- package/{src → scripts}/runner/execute-browser.js +3 -118
- package/{src → scripts}/runner/execute-dynamic.js +46 -11
- package/{src → scripts}/runner/execute-form.js +6 -4
- package/{src → scripts}/runner/execute-input.js +127 -100
- package/{src → scripts}/runner/execute-interaction.js +31 -46
- package/{src → scripts}/runner/execute-navigation.js +14 -12
- package/{src → scripts}/runner/step-executors.js +28 -9
- package/{src → scripts}/runner/step-registry.js +57 -8
- package/{src → scripts}/runner/step-validator.js +13 -3
- package/{src → scripts}/tests/ExecuteInput.test.js +58 -188
- package/src/aria/role-query.js +0 -1229
- package/src/aria/snapshot.js +0 -459
- /package/{src → scripts}/aria/output-processor.js +0 -0
- /package/{src → scripts}/capture/debug-capture.js +0 -0
- /package/{src → scripts}/capture/error-aggregator.js +0 -0
- /package/{src → scripts}/capture/eval-serializer.js +0 -0
- /package/{src → scripts}/capture/index.js +0 -0
- /package/{src → scripts}/capture/network-capture.js +0 -0
- /package/{src → scripts}/capture/pdf-capture.js +0 -0
- /package/{src → scripts}/cdp/browser.js +0 -0
- /package/{src → scripts}/cdp/discovery.js +0 -0
- /package/{src → scripts}/cdp/index.js +0 -0
- /package/{src → scripts}/cdp/target-and-session.js +0 -0
- /package/{src → scripts}/constants.js +0 -0
- /package/{src → scripts}/dom/element-handle.js +0 -0
- /package/{src → scripts}/dom/element-validator.js +0 -0
- /package/{src → scripts}/dom/index.js +0 -0
- /package/{src → scripts}/dom/keyboard-executor.js +0 -0
- /package/{src → scripts}/dom/quad-helpers.js +0 -0
- /package/{src → scripts}/index.js +0 -0
- /package/{src → scripts}/page/cookie-manager.js +0 -0
- /package/{src → scripts}/page/index.js +0 -0
- /package/{src → scripts}/page/wait-utilities.js +0 -0
- /package/{src → scripts}/page/web-storage-manager.js +0 -0
- /package/{src → scripts}/runner/execute-query.js +0 -0
- /package/{src → scripts}/runner/index.js +0 -0
- /package/{src → scripts}/tests/Actionability.test.js +0 -0
- /package/{src → scripts}/tests/Aria.test.js +0 -0
- /package/{src → scripts}/tests/BrowserClient.test.js +0 -0
- /package/{src → scripts}/tests/CDPConnection.test.js +0 -0
- /package/{src → scripts}/tests/ChromeDiscovery.test.js +0 -0
- /package/{src → scripts}/tests/ClickExecutor.test.js +0 -0
- /package/{src → scripts}/tests/ConsoleCapture.test.js +0 -0
- /package/{src → scripts}/tests/ContextHelpers.test.js +0 -0
- /package/{src → scripts}/tests/CookieManager.test.js +0 -0
- /package/{src → scripts}/tests/DebugCapture.test.js +0 -0
- /package/{src → scripts}/tests/ElementHandle.test.js +0 -0
- /package/{src → scripts}/tests/ElementLocator.test.js +0 -0
- /package/{src → scripts}/tests/ErrorAggregator.test.js +0 -0
- /package/{src → scripts}/tests/EvalSerializer.test.js +0 -0
- /package/{src → scripts}/tests/ExecuteBrowser.test.js +0 -0
- /package/{src → scripts}/tests/ExecuteDynamic.test.js +0 -0
- /package/{src → scripts}/tests/ExecuteForm.test.js +0 -0
- /package/{src → scripts}/tests/ExecuteInteraction.test.js +0 -0
- /package/{src → scripts}/tests/ExecuteQuery.test.js +0 -0
- /package/{src → scripts}/tests/FillExecutor.test.js +0 -0
- /package/{src → scripts}/tests/InputEmulator.test.js +0 -0
- /package/{src → scripts}/tests/KeyboardExecutor.test.js +0 -0
- /package/{src → scripts}/tests/LazyResolver.test.js +0 -0
- /package/{src → scripts}/tests/NetworkErrorCapture.test.js +0 -0
- /package/{src → scripts}/tests/PageController.test.js +0 -0
- /package/{src → scripts}/tests/PdfCapture.test.js +0 -0
- /package/{src → scripts}/tests/ScreenshotCapture.test.js +0 -0
- /package/{src → scripts}/tests/SessionRegistry.test.js +0 -0
- /package/{src → scripts}/tests/StepValidator.test.js +0 -0
- /package/{src → scripts}/tests/TargetManager.test.js +0 -0
- /package/{src → scripts}/tests/TestRunner.test.js +0 -0
- /package/{src → scripts}/tests/WaitStrategy.test.js +0 -0
- /package/{src → scripts}/tests/WaitUtilities.test.js +0 -0
- /package/{src → scripts}/tests/WebStorageManager.test.js +0 -0
- /package/{src → scripts}/tests/integration.test.js +0 -0
- /package/{src → scripts}/types.js +0 -0
- /package/{src → scripts}/utils/backoff.js +0 -0
- /package/{src → scripts}/utils/cdp-helpers.js +0 -0
- /package/{src → scripts}/utils/devices.js +0 -0
- /package/{src → scripts}/utils/errors.js +0 -0
- /package/{src → scripts}/utils/index.js +0 -0
- /package/{src → scripts}/utils/temp.js +0 -0
- /package/{src → scripts}/utils/validators.js +0 -0
- /package/{src → scripts}/utils.js +0 -0
|
@@ -3,105 +3,14 @@
|
|
|
3
3
|
* Fill and select step executors
|
|
4
4
|
*
|
|
5
5
|
* EXPORTS:
|
|
6
|
-
* - executeFill(elementLocator, inputEmulator, params) → Promise<void>
|
|
7
6
|
* - executeFillActive(pageController, inputEmulator, params) → Promise<Object>
|
|
8
7
|
* - executeSelectOption(elementLocator, params) → Promise<Object>
|
|
9
8
|
*
|
|
10
9
|
* DEPENDENCIES:
|
|
11
|
-
* - ../
|
|
12
|
-
* - ../utils.js: elementNotFoundError, elementNotEditableError, resetInputState
|
|
10
|
+
* - ../utils.js: elementNotFoundError
|
|
13
11
|
*/
|
|
14
12
|
|
|
15
|
-
import {
|
|
16
|
-
import { elementNotFoundError, elementNotEditableError, resetInputState } from '../utils.js';
|
|
17
|
-
|
|
18
|
-
export async function executeFill(elementLocator, inputEmulator, params) {
|
|
19
|
-
const { selector, value, react } = params;
|
|
20
|
-
|
|
21
|
-
if (!selector || value === undefined) {
|
|
22
|
-
throw new Error('Fill requires selector and value');
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const element = await elementLocator.findElement(selector);
|
|
26
|
-
if (!element) {
|
|
27
|
-
throw elementNotFoundError(selector, 0);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Validate element is editable before attempting fill
|
|
31
|
-
const validator = createElementValidator(elementLocator.session);
|
|
32
|
-
const editableCheck = await validator.isEditable(element._handle.objectId);
|
|
33
|
-
if (!editableCheck.editable) {
|
|
34
|
-
await element._handle.dispose();
|
|
35
|
-
throw elementNotEditableError(selector, editableCheck.reason);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Try fast path first - scroll to center with short stability check
|
|
39
|
-
let actionable;
|
|
40
|
-
try {
|
|
41
|
-
await element._handle.scrollIntoView({ block: 'center' });
|
|
42
|
-
// Use short stability timeout - most elements stabilize quickly
|
|
43
|
-
await element._handle.waitForStability({ frames: 2, timeout: 300 });
|
|
44
|
-
actionable = await element._handle.isActionable();
|
|
45
|
-
} catch (e) {
|
|
46
|
-
// Stability check failed, check actionability anyway
|
|
47
|
-
actionable = await element._handle.isActionable();
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// If not actionable, try alternative scroll strategies
|
|
51
|
-
if (!actionable.actionable) {
|
|
52
|
-
let lastError = new Error(`Element not actionable: ${actionable.reason}`);
|
|
53
|
-
|
|
54
|
-
for (const strategy of ['end', 'start', 'nearest']) {
|
|
55
|
-
try {
|
|
56
|
-
await element._handle.scrollIntoView({ block: strategy });
|
|
57
|
-
await element._handle.waitForStability({ frames: 2, timeout: 500 });
|
|
58
|
-
actionable = await element._handle.isActionable();
|
|
59
|
-
|
|
60
|
-
if (actionable.actionable) break;
|
|
61
|
-
lastError = new Error(`Element not actionable: ${actionable.reason}`);
|
|
62
|
-
} catch (e) {
|
|
63
|
-
lastError = e;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (!actionable.actionable) {
|
|
68
|
-
await element._handle.dispose();
|
|
69
|
-
await resetInputState(elementLocator.session);
|
|
70
|
-
throw lastError;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
// Use React-specific fill approach if react option is set
|
|
76
|
-
if (react) {
|
|
77
|
-
const reactFiller = createReactInputFiller(elementLocator.session);
|
|
78
|
-
await reactFiller.fillByObjectId(element._handle.objectId, value);
|
|
79
|
-
return; // Success
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Standard fill approach using keyboard events
|
|
83
|
-
const box = await element._handle.getBoundingBox();
|
|
84
|
-
const x = box.x + box.width / 2;
|
|
85
|
-
const y = box.y + box.height / 2;
|
|
86
|
-
|
|
87
|
-
// Click to focus
|
|
88
|
-
await inputEmulator.click(x, y);
|
|
89
|
-
|
|
90
|
-
// Focus element directly - more reliable than relying on click
|
|
91
|
-
await element._handle.focus();
|
|
92
|
-
|
|
93
|
-
if (params.clear !== false) {
|
|
94
|
-
await inputEmulator.selectAll();
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
await inputEmulator.type(String(value));
|
|
98
|
-
} catch (e) {
|
|
99
|
-
await resetInputState(elementLocator.session);
|
|
100
|
-
throw e;
|
|
101
|
-
} finally {
|
|
102
|
-
await element._handle.dispose();
|
|
103
|
-
}
|
|
104
|
-
}
|
|
13
|
+
import { elementNotFoundError } from '../utils.js';
|
|
105
14
|
|
|
106
15
|
/**
|
|
107
16
|
* Execute a selectOption step - select option in dropdown
|
|
@@ -226,15 +135,12 @@ export async function executeSelectOption(elementLocator, params) {
|
|
|
226
135
|
*/
|
|
227
136
|
|
|
228
137
|
export async function executeFillActive(pageController, inputEmulator, params) {
|
|
229
|
-
const session = pageController.session;
|
|
230
|
-
|
|
231
138
|
// Parse params
|
|
232
139
|
const value = typeof params === 'string' ? params : (params && params.value);
|
|
233
140
|
const clear = typeof params === 'object' && params !== null ? params.clear !== false : true;
|
|
234
141
|
|
|
235
142
|
// Check if there's an active element and if it's editable
|
|
236
|
-
const checkResult = await
|
|
237
|
-
expression: `(function() {
|
|
143
|
+
const checkResult = await pageController.evaluateInFrame(`(function() {
|
|
238
144
|
const el = document.activeElement;
|
|
239
145
|
if (!el || el === document.body || el === document.documentElement) {
|
|
240
146
|
return { error: 'No element is focused' };
|
|
@@ -271,9 +177,8 @@ export async function executeFillActive(pageController, inputEmulator, params) {
|
|
|
271
177
|
selector: selector,
|
|
272
178
|
valueBefore: el.value || ''
|
|
273
179
|
};
|
|
274
|
-
})()
|
|
275
|
-
|
|
276
|
-
});
|
|
180
|
+
})()`);
|
|
181
|
+
|
|
277
182
|
|
|
278
183
|
if (checkResult.exceptionDetails) {
|
|
279
184
|
throw new Error(`fillActive error: ${checkResult.exceptionDetails.text}`);
|
|
@@ -302,6 +207,128 @@ export async function executeFillActive(pageController, inputEmulator, params) {
|
|
|
302
207
|
};
|
|
303
208
|
}
|
|
304
209
|
|
|
210
|
+
/**
|
|
211
|
+
* Execute an upload step - set files on a file input element via CDP
|
|
212
|
+
* Uses DOM.setFileInputFiles to set file paths on <input type="file"> elements
|
|
213
|
+
*
|
|
214
|
+
* Shapes:
|
|
215
|
+
* {"upload": "/path/to/file.txt"} — single file, auto-find file input
|
|
216
|
+
* {"upload": ["/path/a.txt", "/path/b.png"]} — multiple files, auto-find file input
|
|
217
|
+
* {"upload": {"selector": "#file-input", "file": "a.txt"}} — targeted single file
|
|
218
|
+
* {"upload": {"selector": "#file-input", "files": ["a.txt", "b.png"]}} — targeted multiple files
|
|
219
|
+
* {"upload": {"ref": "f0s1e3", "files": ["a.txt"]}} — ref-targeted
|
|
220
|
+
*/
|
|
221
|
+
export async function executeUpload(elementLocator, pageController, params) {
|
|
222
|
+
const session = elementLocator.session;
|
|
223
|
+
|
|
224
|
+
// Normalize params into { files, selector?, ref? }
|
|
225
|
+
let files, selector, ref;
|
|
226
|
+
if (typeof params === 'string') {
|
|
227
|
+
files = [params];
|
|
228
|
+
} else if (Array.isArray(params)) {
|
|
229
|
+
files = params;
|
|
230
|
+
} else {
|
|
231
|
+
selector = params.selector;
|
|
232
|
+
ref = params.ref;
|
|
233
|
+
files = params.files || (params.file ? [params.file] : []);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Find the file input element
|
|
237
|
+
let objectId;
|
|
238
|
+
try {
|
|
239
|
+
if (ref) {
|
|
240
|
+
// Resolve ref to objectId via aria refs
|
|
241
|
+
const evalParams = {
|
|
242
|
+
expression: `window.__ariaRefs && window.__ariaRefs.get(${JSON.stringify(ref)})`,
|
|
243
|
+
returnByValue: false
|
|
244
|
+
};
|
|
245
|
+
const contextId = pageController.getFrameContext();
|
|
246
|
+
if (contextId) evalParams.contextId = contextId;
|
|
247
|
+
const result = await session.send('Runtime.evaluate', evalParams);
|
|
248
|
+
if (!result.result.objectId) {
|
|
249
|
+
throw new Error(`Element ref ${ref} not found — run 'snapshot' to get fresh refs`);
|
|
250
|
+
}
|
|
251
|
+
objectId = result.result.objectId;
|
|
252
|
+
} else if (selector) {
|
|
253
|
+
// Find by CSS selector
|
|
254
|
+
const evalParams = {
|
|
255
|
+
expression: `document.querySelector(${JSON.stringify(selector)})`,
|
|
256
|
+
returnByValue: false
|
|
257
|
+
};
|
|
258
|
+
const contextId = pageController.getFrameContext();
|
|
259
|
+
if (contextId) evalParams.contextId = contextId;
|
|
260
|
+
const result = await session.send('Runtime.evaluate', evalParams);
|
|
261
|
+
if (!result.result.objectId) {
|
|
262
|
+
throw elementNotFoundError(selector, 0);
|
|
263
|
+
}
|
|
264
|
+
objectId = result.result.objectId;
|
|
265
|
+
} else {
|
|
266
|
+
// Auto-find: look for a file input on the page
|
|
267
|
+
const evalParams = {
|
|
268
|
+
expression: `document.querySelector('input[type="file"]')`,
|
|
269
|
+
returnByValue: false
|
|
270
|
+
};
|
|
271
|
+
const contextId = pageController.getFrameContext();
|
|
272
|
+
if (contextId) evalParams.contextId = contextId;
|
|
273
|
+
const result = await session.send('Runtime.evaluate', evalParams);
|
|
274
|
+
if (!result.result.objectId) {
|
|
275
|
+
throw new Error('No file input (input[type="file"]) found on the page');
|
|
276
|
+
}
|
|
277
|
+
objectId = result.result.objectId;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Verify it's actually a file input
|
|
281
|
+
const typeCheck = await session.send('Runtime.callFunctionOn', {
|
|
282
|
+
objectId,
|
|
283
|
+
functionDeclaration: `function() {
|
|
284
|
+
return {
|
|
285
|
+
tagName: this.tagName,
|
|
286
|
+
type: this.type,
|
|
287
|
+
accept: this.accept || '',
|
|
288
|
+
multiple: this.hasAttribute('multiple')
|
|
289
|
+
};
|
|
290
|
+
}`,
|
|
291
|
+
returnByValue: true
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const info = typeCheck.result.value;
|
|
295
|
+
if (!info || info.tagName !== 'INPUT' || info.type !== 'file') {
|
|
296
|
+
throw new Error(`Target element is ${info?.tagName || 'unknown'} type="${info?.type || 'unknown'}" — expected input[type="file"]`);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (files.length > 1 && !info.multiple) {
|
|
300
|
+
throw new Error(`File input does not have 'multiple' attribute but ${files.length} files were provided`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Set files via CDP
|
|
304
|
+
await session.send('DOM.setFileInputFiles', {
|
|
305
|
+
objectId,
|
|
306
|
+
files
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// Dispatch change event so frameworks detect the file selection
|
|
310
|
+
await session.send('Runtime.callFunctionOn', {
|
|
311
|
+
objectId,
|
|
312
|
+
functionDeclaration: `function() {
|
|
313
|
+
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
314
|
+
this.dispatchEvent(new Event('input', { bubbles: true }));
|
|
315
|
+
}`
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
uploaded: true,
|
|
320
|
+
files,
|
|
321
|
+
accept: info.accept,
|
|
322
|
+
multiple: info.multiple,
|
|
323
|
+
target: selector || ref || 'input[type="file"]'
|
|
324
|
+
};
|
|
325
|
+
} finally {
|
|
326
|
+
if (objectId) {
|
|
327
|
+
try { await session.send('Runtime.releaseObject', { objectId }); } catch {}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
305
332
|
/**
|
|
306
333
|
* Execute a refAt step - get or create a ref for the element at given coordinates
|
|
307
334
|
* Uses document.elementFromPoint to find the element, then assigns/retrieves a ref
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* EXPORTS:
|
|
6
6
|
* - executeClick(elementLocator, inputEmulator, ariaSnapshot, params) → Promise<Object>
|
|
7
|
+
* - clickWithVerification: removed (stale, superseded by click-executor.js pointerdown version)
|
|
7
8
|
* - executeHover(elementLocator, inputEmulator, ariaSnapshot, params) → Promise<Object>
|
|
8
9
|
* - executeDrag(elementLocator, inputEmulator, pageController, ariaSnapshot, params) → Promise<Object>
|
|
9
10
|
*
|
|
@@ -15,8 +16,6 @@
|
|
|
15
16
|
import { createClickExecutor, createActionabilityChecker } from '../dom/index.js';
|
|
16
17
|
import { elementNotFoundError, sleep, resetInputState, releaseObject } from '../utils.js';
|
|
17
18
|
|
|
18
|
-
const SCROLL_STRATEGIES = ['center', 'end', 'start', 'nearest'];
|
|
19
|
-
|
|
20
19
|
export async function executeClick(elementLocator, inputEmulator, ariaSnapshot, params) {
|
|
21
20
|
// Delegate to ClickExecutor for improved click handling with JS fallback
|
|
22
21
|
const clickExecutor = createClickExecutor(
|
|
@@ -28,42 +27,6 @@ export async function executeClick(elementLocator, inputEmulator, ariaSnapshot,
|
|
|
28
27
|
return clickExecutor.execute(params);
|
|
29
28
|
}
|
|
30
29
|
|
|
31
|
-
export async function clickWithVerification(elementLocator, inputEmulator, x, y, targetObjectId) {
|
|
32
|
-
const session = elementLocator.session;
|
|
33
|
-
|
|
34
|
-
// Setup event listener on target before clicking
|
|
35
|
-
await session.send('Runtime.callFunctionOn', {
|
|
36
|
-
objectId: targetObjectId,
|
|
37
|
-
functionDeclaration: `function() {
|
|
38
|
-
this.__clickReceived = false;
|
|
39
|
-
this.__clickHandler = () => { this.__clickReceived = true; };
|
|
40
|
-
this.addEventListener('click', this.__clickHandler, { once: true });
|
|
41
|
-
}`
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
// Perform click
|
|
45
|
-
await inputEmulator.click(x, y);
|
|
46
|
-
await sleep(50);
|
|
47
|
-
|
|
48
|
-
// Check if target received the click
|
|
49
|
-
const verifyResult = await session.send('Runtime.callFunctionOn', {
|
|
50
|
-
objectId: targetObjectId,
|
|
51
|
-
functionDeclaration: `function() {
|
|
52
|
-
this.removeEventListener('click', this.__clickHandler);
|
|
53
|
-
const received = this.__clickReceived;
|
|
54
|
-
delete this.__clickReceived;
|
|
55
|
-
delete this.__clickHandler;
|
|
56
|
-
return received;
|
|
57
|
-
}`,
|
|
58
|
-
returnByValue: true
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
return {
|
|
62
|
-
clicked: true,
|
|
63
|
-
targetReceived: verifyResult.result.value === true
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
30
|
/**
|
|
68
31
|
* Execute a hover step - moves mouse over an element to trigger hover events
|
|
69
32
|
* Uses Playwright-style auto-waiting for element to be visible and stable
|
|
@@ -187,7 +150,9 @@ export async function executeHover(elementLocator, inputEmulator, ariaSnapshot,
|
|
|
187
150
|
|
|
188
151
|
// Use Playwright-style auto-waiting for element to be actionable
|
|
189
152
|
// Hover requires: visible, stable
|
|
190
|
-
const actionabilityChecker = createActionabilityChecker(elementLocator.session
|
|
153
|
+
const actionabilityChecker = createActionabilityChecker(elementLocator.session, {
|
|
154
|
+
getFrameContext: elementLocator.getFrameContext || null
|
|
155
|
+
});
|
|
191
156
|
const waitResult = await actionabilityChecker.waitForActionable(selector, 'hover', {
|
|
192
157
|
timeout,
|
|
193
158
|
force
|
|
@@ -428,7 +393,7 @@ export async function executeDrag(elementLocator, inputEmulator, pageController,
|
|
|
428
393
|
const sourceSelector = getSelectorExpression(source);
|
|
429
394
|
const targetSelector = getSelectorExpression(target);
|
|
430
395
|
|
|
431
|
-
const
|
|
396
|
+
const dragEvalParams = {
|
|
432
397
|
expression: `
|
|
433
398
|
(function() {
|
|
434
399
|
const sourceEl = ${sourceSelector || 'null'};
|
|
@@ -523,21 +488,41 @@ export async function executeDrag(elementLocator, inputEmulator, pageController,
|
|
|
523
488
|
if (method === 'mouse') return doMouseDrag();
|
|
524
489
|
if (method === 'html5') return doHtml5Drag();
|
|
525
490
|
|
|
526
|
-
// auto:
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
491
|
+
// auto: detect HTML5 DnD indicators before choosing method
|
|
492
|
+
const needsHtml5 = sourceEl && targetEl && (
|
|
493
|
+
sourceEl.draggable ||
|
|
494
|
+
sourceEl.hasAttribute('ondragstart') ||
|
|
495
|
+
sourceEl.hasAttribute('ondrag') ||
|
|
496
|
+
targetEl.hasAttribute('ondrop') ||
|
|
497
|
+
targetEl.hasAttribute('ondragover')
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
if (needsHtml5) {
|
|
501
|
+
// Try HTML5 DnD first for elements that clearly use it
|
|
531
502
|
const html5Result = doHtml5Drag();
|
|
532
503
|
if (html5Result.success) return html5Result;
|
|
504
|
+
// Fall back to mouse if HTML5 failed
|
|
505
|
+
return doMouseDrag();
|
|
533
506
|
}
|
|
534
507
|
|
|
508
|
+
// No HTML5 indicators — use mouse events, fall back to HTML5
|
|
509
|
+
const mouseResult = doMouseDrag();
|
|
510
|
+
if (!mouseResult.success && sourceEl && targetEl) {
|
|
511
|
+
const html5Result = doHtml5Drag();
|
|
512
|
+
if (html5Result.success) return html5Result;
|
|
513
|
+
}
|
|
535
514
|
return mouseResult;
|
|
536
515
|
})()
|
|
537
516
|
`,
|
|
538
517
|
returnByValue: true,
|
|
539
518
|
awaitPromise: false
|
|
540
|
-
}
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
// Add frame context if in an iframe
|
|
522
|
+
const contextId = pageController.getFrameContext();
|
|
523
|
+
if (contextId) dragEvalParams.contextId = contextId;
|
|
524
|
+
|
|
525
|
+
const jsDragResult = await session.send('Runtime.evaluate', dragEvalParams);
|
|
541
526
|
|
|
542
527
|
const dragResult = jsDragResult.result?.value;
|
|
543
528
|
|
|
@@ -31,11 +31,10 @@ export async function executeWait(elementLocator, params) {
|
|
|
31
31
|
* @returns {Promise<void>}
|
|
32
32
|
*/
|
|
33
33
|
export async function executeWaitForNavigation(pageController, params) {
|
|
34
|
-
const options = params ===
|
|
34
|
+
const options = (params && typeof params === 'object') ? params : {};
|
|
35
35
|
const timeout = options.timeout || 30000;
|
|
36
36
|
const waitUntil = options.waitUntil || 'load';
|
|
37
37
|
|
|
38
|
-
const session = pageController.session;
|
|
39
38
|
const startTime = Date.now();
|
|
40
39
|
|
|
41
40
|
// Poll for page ready state
|
|
@@ -47,11 +46,8 @@ export async function executeWaitForNavigation(pageController, params) {
|
|
|
47
46
|
}
|
|
48
47
|
|
|
49
48
|
try {
|
|
50
|
-
const result = await
|
|
51
|
-
|
|
52
|
-
returnByValue: true
|
|
53
|
-
});
|
|
54
|
-
const readyState = result.result.value;
|
|
49
|
+
const result = await pageController.evaluateInFrame('document.readyState');
|
|
50
|
+
const readyState = result.result?.value;
|
|
55
51
|
|
|
56
52
|
if (waitUntil === 'commit') {
|
|
57
53
|
resolve();
|
|
@@ -130,8 +126,11 @@ export async function executeScroll(elementLocator, inputEmulator, pageControlle
|
|
|
130
126
|
if (!el) {
|
|
131
127
|
throw elementNotFoundError(params, 0);
|
|
132
128
|
}
|
|
133
|
-
|
|
134
|
-
|
|
129
|
+
try {
|
|
130
|
+
await el.scrollIntoView();
|
|
131
|
+
} finally {
|
|
132
|
+
await el.dispose();
|
|
133
|
+
}
|
|
135
134
|
}
|
|
136
135
|
}
|
|
137
136
|
} else if (params && typeof params === 'object') {
|
|
@@ -145,8 +144,11 @@ export async function executeScroll(elementLocator, inputEmulator, pageControlle
|
|
|
145
144
|
if (!el) {
|
|
146
145
|
throw elementNotFoundError(params.selector, 0);
|
|
147
146
|
}
|
|
148
|
-
|
|
149
|
-
|
|
147
|
+
try {
|
|
148
|
+
await el.scrollIntoView();
|
|
149
|
+
} finally {
|
|
150
|
+
await el.dispose();
|
|
151
|
+
}
|
|
150
152
|
} else if (params.deltaY !== undefined || params.deltaX !== undefined) {
|
|
151
153
|
// Scroll by delta using JavaScript (more reliable than CDP mouse wheel events)
|
|
152
154
|
await pageController.evaluateInFrame(`window.scrollBy(${params.deltaX || 0}, ${params.deltaY || 0})`);
|
|
@@ -161,5 +163,5 @@ export async function executeScroll(elementLocator, inputEmulator, pageControlle
|
|
|
161
163
|
'({ scrollX: window.scrollX, scrollY: window.scrollY })'
|
|
162
164
|
);
|
|
163
165
|
|
|
164
|
-
return posResult.result
|
|
166
|
+
return posResult.result?.value ?? { scrollX: 0, scrollY: 0 };
|
|
165
167
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* SUBMODULES:
|
|
10
10
|
* - ./execute-navigation.js: executeWait, executeWaitForNavigation, executeScroll
|
|
11
11
|
* - ./execute-interaction.js: executeClick, executeHover, executeDrag
|
|
12
|
-
* - ./execute-input.js:
|
|
12
|
+
* - ./execute-input.js: executeFillActive, executeSelectOption
|
|
13
13
|
* - ./execute-query.js: executeSnapshot, executeQuery, executeQueryAll, executeInspect, etc.
|
|
14
14
|
* - ./execute-form.js: executeValidate, executeSubmit, executeFormState, executeExtract, executeAssert
|
|
15
15
|
* - ./execute-browser.js: executePdf, executeEval, executeCookies, executeConsole, etc.
|
|
@@ -47,10 +47,10 @@ import { validateSteps } from './step-validator.js';
|
|
|
47
47
|
// Import domain executors
|
|
48
48
|
import { executeWait, executeWaitForNavigation, executeScroll } from './execute-navigation.js';
|
|
49
49
|
import { executeClick, executeHover, executeDrag } from './execute-interaction.js';
|
|
50
|
-
import { executeFillActive, executeSelectOption } from './execute-input.js';
|
|
50
|
+
import { executeFillActive, executeSelectOption, executeUpload } from './execute-input.js';
|
|
51
51
|
import { executeSnapshot, executeSnapshotSearch, executeQuery, executeQueryAll, executeInspect, executeGetDom, executeGetBox, executeRefAt, executeElementsAt, executeElementsNear } from './execute-query.js';
|
|
52
52
|
// executeRefAt, executeElementsNear kept for internal dispatch from unified elementsAt
|
|
53
|
-
import { executeSubmit, executeExtract } from './execute-form.js';
|
|
53
|
+
import { executeSubmit, executeExtract, executeAssert } from './execute-form.js';
|
|
54
54
|
import { executePdf, executeEval, executeCookies, executeListTabs, executeCloseTab, executeConsole, formatCommandConsole } from './execute-browser.js';
|
|
55
55
|
// executeEval kept for internal dispatch from unified pageFunction
|
|
56
56
|
import { executePageFunction, executePoll, executeWriteSiteProfile, executeReadSiteProfile, loadSiteProfile } from './execute-dynamic.js';
|
|
@@ -115,7 +115,7 @@ export async function executeStep(deps, step, options = {}) {
|
|
|
115
115
|
if (actionValue && typeof actionValue === 'object' && actionValue.readyWhen) {
|
|
116
116
|
const readyResult = await executePoll(pageController, {
|
|
117
117
|
fn: actionValue.readyWhen,
|
|
118
|
-
timeout:
|
|
118
|
+
timeout: stepTimeout || 30000
|
|
119
119
|
});
|
|
120
120
|
if (!readyResult.resolved) {
|
|
121
121
|
throw new Error(`readyWhen did not resolve within timeout`);
|
|
@@ -397,6 +397,9 @@ export async function executeStep(deps, step, options = {}) {
|
|
|
397
397
|
} else if (step.drag !== undefined) {
|
|
398
398
|
stepResult.action = 'drag';
|
|
399
399
|
stepResult.output = await executeDrag(elementLocator, inputEmulator, pageController, deps.ariaSnapshot, step.drag);
|
|
400
|
+
} else if (step.upload !== undefined) {
|
|
401
|
+
stepResult.action = 'upload';
|
|
402
|
+
stepResult.output = await executeUpload(elementLocator, pageController, step.upload);
|
|
400
403
|
} else if (step.get !== undefined) {
|
|
401
404
|
stepResult.action = 'get';
|
|
402
405
|
const getParams = step.get;
|
|
@@ -416,10 +419,29 @@ export async function executeStep(deps, step, options = {}) {
|
|
|
416
419
|
const selector = typeof getParams === 'string' ? getParams : getParams.selector;
|
|
417
420
|
stepResult.output = await formValidator.getFormState(selector);
|
|
418
421
|
stepResult.output.mode = 'value';
|
|
422
|
+
} else if (mode === 'attributes') {
|
|
423
|
+
// Attributes extraction — return all HTML attributes of the element
|
|
424
|
+
const selector = getParams.selector || getParams.ref;
|
|
425
|
+
const attrResult = await pageController.evaluateInFrame(`
|
|
426
|
+
(function() {
|
|
427
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
428
|
+
if (!el) return { error: 'Element not found: ' + ${JSON.stringify(selector)} };
|
|
429
|
+
const attrs = {};
|
|
430
|
+
for (const attr of el.attributes) {
|
|
431
|
+
attrs[attr.name] = attr.value;
|
|
432
|
+
}
|
|
433
|
+
return { attributes: attrs };
|
|
434
|
+
})()
|
|
435
|
+
`, { returnByValue: true });
|
|
436
|
+
const data = attrResult.result?.value;
|
|
437
|
+
if (data?.error) {
|
|
438
|
+
throw new Error(data.error);
|
|
439
|
+
}
|
|
440
|
+
stepResult.output = { attributes: data?.attributes || {}, mode: 'attributes' };
|
|
419
441
|
} else {
|
|
420
|
-
// Default: text
|
|
442
|
+
// Default: text extraction (with auto table/list detection)
|
|
421
443
|
stepResult.output = await executeExtract(deps, getParams);
|
|
422
|
-
stepResult.output.mode =
|
|
444
|
+
stepResult.output.mode = 'text';
|
|
423
445
|
}
|
|
424
446
|
} else if (step.selectOption !== undefined) {
|
|
425
447
|
stepResult.action = 'selectOption';
|
|
@@ -564,9 +586,6 @@ export async function executeStep(deps, step, options = {}) {
|
|
|
564
586
|
return stepResult;
|
|
565
587
|
}
|
|
566
588
|
|
|
567
|
-
// Import executeAssert from execute-form.js
|
|
568
|
-
import { executeAssert } from './execute-form.js';
|
|
569
|
-
|
|
570
589
|
/**
|
|
571
590
|
* Run an array of test steps
|
|
572
591
|
* @param {Object} deps - Dependencies
|
|
@@ -57,7 +57,8 @@ export const STEP_TYPES = {
|
|
|
57
57
|
READ_SITE_PROFILE: 'readSiteProfile',
|
|
58
58
|
SWITCH_TAB: 'switchTab',
|
|
59
59
|
GET_URL: 'getUrl',
|
|
60
|
-
GET_TITLE: 'getTitle'
|
|
60
|
+
GET_TITLE: 'getTitle',
|
|
61
|
+
UPLOAD: 'upload'
|
|
61
62
|
};
|
|
62
63
|
|
|
63
64
|
/**
|
|
@@ -152,7 +153,7 @@ export const STEP_CONFIG = {
|
|
|
152
153
|
errors.push('wait hidden must be a boolean');
|
|
153
154
|
}
|
|
154
155
|
} else {
|
|
155
|
-
errors.push('wait requires a
|
|
156
|
+
errors.push('wait requires a selector string or params object with selector/text/textRegex/urlContains');
|
|
156
157
|
}
|
|
157
158
|
return errors;
|
|
158
159
|
},
|
|
@@ -309,7 +310,8 @@ export const STEP_CONFIG = {
|
|
|
309
310
|
if (!params || typeof params !== 'object') {
|
|
310
311
|
errors.push('queryAll requires an object mapping names to selectors');
|
|
311
312
|
} else {
|
|
312
|
-
const
|
|
313
|
+
const hookKeys = new Set(['readyWhen', 'settledWhen', 'observe', 'timeout', 'optional']);
|
|
314
|
+
const entries = Object.entries(params).filter(([name]) => !hookKeys.has(name));
|
|
313
315
|
if (entries.length === 0) {
|
|
314
316
|
errors.push('queryAll requires at least one query');
|
|
315
317
|
}
|
|
@@ -341,13 +343,14 @@ export const STEP_CONFIG = {
|
|
|
341
343
|
validate: (params) => {
|
|
342
344
|
const errors = [];
|
|
343
345
|
if (typeof params === 'string') {
|
|
344
|
-
if (
|
|
345
|
-
errors.push(
|
|
346
|
+
if (params.length === 0) {
|
|
347
|
+
errors.push('scroll requires a non-empty string');
|
|
346
348
|
}
|
|
349
|
+
// Accepts direction ("top","bottom","up","down"), CSS selector, or ref string
|
|
347
350
|
} else if (params && typeof params === 'object') {
|
|
348
|
-
// selector, x, y, deltaX, deltaY are all valid
|
|
351
|
+
// selector, ref, x, y, deltaX, deltaY are all valid
|
|
349
352
|
} else {
|
|
350
|
-
errors.push('scroll requires direction string or params object');
|
|
353
|
+
errors.push('scroll requires a direction/selector string or params object');
|
|
351
354
|
}
|
|
352
355
|
return errors;
|
|
353
356
|
},
|
|
@@ -358,7 +361,7 @@ export const STEP_CONFIG = {
|
|
|
358
361
|
[STEP_TYPES.CONSOLE]: {
|
|
359
362
|
validate: (params) => {
|
|
360
363
|
const errors = [];
|
|
361
|
-
if (params !== true && params !== false && typeof params !== 'object') {
|
|
364
|
+
if (params !== true && params !== false && (typeof params !== 'object' || params === null)) {
|
|
362
365
|
errors.push('console requires true, false, or params object');
|
|
363
366
|
}
|
|
364
367
|
return errors;
|
|
@@ -808,6 +811,10 @@ export const STEP_CONFIG = {
|
|
|
808
811
|
} else if (typeof params === 'object' && params !== null) {
|
|
809
812
|
if (!params.refs && !params.ref) {
|
|
810
813
|
errors.push('getBox requires ref or refs');
|
|
814
|
+
} else if (params.ref && typeof params.ref !== 'string') {
|
|
815
|
+
errors.push('getBox ref must be a string');
|
|
816
|
+
} else if (params.refs && !Array.isArray(params.refs)) {
|
|
817
|
+
errors.push('getBox refs must be an array');
|
|
811
818
|
}
|
|
812
819
|
} else {
|
|
813
820
|
errors.push('getBox requires a ref string, array of refs, or options object');
|
|
@@ -1001,6 +1008,48 @@ export const STEP_CONFIG = {
|
|
|
1001
1008
|
},
|
|
1002
1009
|
isVisual: false,
|
|
1003
1010
|
hooks: []
|
|
1011
|
+
},
|
|
1012
|
+
|
|
1013
|
+
[STEP_TYPES.UPLOAD]: {
|
|
1014
|
+
validate: (params) => {
|
|
1015
|
+
const errors = [];
|
|
1016
|
+
if (typeof params === 'string') {
|
|
1017
|
+
// Shape 1: single file path
|
|
1018
|
+
if (params.length === 0) {
|
|
1019
|
+
errors.push('upload requires a non-empty file path');
|
|
1020
|
+
}
|
|
1021
|
+
} else if (Array.isArray(params)) {
|
|
1022
|
+
// Shape 2: array of file paths
|
|
1023
|
+
if (params.length === 0) {
|
|
1024
|
+
errors.push('upload requires at least one file path');
|
|
1025
|
+
}
|
|
1026
|
+
for (const p of params) {
|
|
1027
|
+
if (typeof p !== 'string' || p.length === 0) {
|
|
1028
|
+
errors.push('upload file paths must be non-empty strings');
|
|
1029
|
+
break;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
} else if (params && typeof params === 'object') {
|
|
1033
|
+
// Shape 3: object with selector/ref + files
|
|
1034
|
+
if (!params.selector && !params.ref) {
|
|
1035
|
+
errors.push('upload object requires selector or ref to target a file input');
|
|
1036
|
+
}
|
|
1037
|
+
if (!params.files && !params.file) {
|
|
1038
|
+
errors.push('upload object requires files (array) or file (string)');
|
|
1039
|
+
}
|
|
1040
|
+
if (params.files && !Array.isArray(params.files)) {
|
|
1041
|
+
errors.push('upload files must be an array of file path strings');
|
|
1042
|
+
}
|
|
1043
|
+
if (params.file && typeof params.file !== 'string') {
|
|
1044
|
+
errors.push('upload file must be a string');
|
|
1045
|
+
}
|
|
1046
|
+
} else {
|
|
1047
|
+
errors.push('upload requires a file path string, array of paths, or object with selector/ref and files');
|
|
1048
|
+
}
|
|
1049
|
+
return errors;
|
|
1050
|
+
},
|
|
1051
|
+
isVisual: true,
|
|
1052
|
+
hooks: ['readyWhen', 'settledWhen', 'observe']
|
|
1004
1053
|
}
|
|
1005
1054
|
};
|
|
1006
1055
|
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* - ./step-registry.js: getAllStepTypes, getStepConfig, validateHooks
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { getAllStepTypes, getStepConfig, validateHooks } from './step-registry.js';
|
|
13
|
+
import { getAllStepTypes, getStepConfig, validateHooks, stepSupportsHooks } from './step-registry.js';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Validate a single step definition
|
|
@@ -53,8 +53,18 @@ export function validateStepInternal(step) {
|
|
|
53
53
|
errors.push(...stepErrors);
|
|
54
54
|
|
|
55
55
|
// Validate hooks on action steps (readyWhen, settledWhen, observe)
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
if (stepSupportsHooks(action)) {
|
|
57
|
+
const hookErrors = validateHooks(params);
|
|
58
|
+
errors.push(...hookErrors);
|
|
59
|
+
} else if (params && typeof params === 'object' && params !== null) {
|
|
60
|
+
// Reject hook keys on steps that don't support them
|
|
61
|
+
const hookKeys = ['readyWhen', 'settledWhen', 'observe'];
|
|
62
|
+
for (const key of hookKeys) {
|
|
63
|
+
if (params[key] !== undefined) {
|
|
64
|
+
errors.push(`${action} does not support the '${key}' hook`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
58
68
|
|
|
59
69
|
return errors;
|
|
60
70
|
}
|