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
|
@@ -133,10 +133,11 @@ export async function executeGetDom(pageController, params) {
|
|
|
133
133
|
};
|
|
134
134
|
})()`;
|
|
135
135
|
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
136
|
+
const evalArgs = { expression, returnByValue: true };
|
|
137
|
+
const contextId = pageController.getFrameContext();
|
|
138
|
+
if (contextId) evalArgs.contextId = contextId;
|
|
139
|
+
|
|
140
|
+
const result = await session.send('Runtime.evaluate', evalArgs);
|
|
140
141
|
|
|
141
142
|
if (result.exceptionDetails) {
|
|
142
143
|
throw new Error(`getDom error: ${result.exceptionDetails.text}`);
|
|
@@ -746,7 +747,9 @@ export async function executeQuery(elementLocator, params) {
|
|
|
746
747
|
*/
|
|
747
748
|
|
|
748
749
|
export async function executeRoleQuery(elementLocator, params) {
|
|
749
|
-
const roleQueryExecutor = createRoleQueryExecutor(elementLocator.session, elementLocator
|
|
750
|
+
const roleQueryExecutor = createRoleQueryExecutor(elementLocator.session, elementLocator, {
|
|
751
|
+
getFrameContext: elementLocator.getFrameContext
|
|
752
|
+
});
|
|
750
753
|
return roleQueryExecutor.execute(params);
|
|
751
754
|
}
|
|
752
755
|
|
|
@@ -49,12 +49,40 @@ import { executeWait, executeWaitForNavigation, executeScroll } from './execute-
|
|
|
49
49
|
import { executeClick, executeHover, executeDrag } from './execute-interaction.js';
|
|
50
50
|
import { executeFillActive, executeSelectOption } 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
54
|
import { executePdf, executeEval, executeCookies, executeListTabs, executeCloseTab, executeConsole, formatCommandConsole } from './execute-browser.js';
|
|
54
|
-
|
|
55
|
+
// executeEval kept for internal dispatch from unified pageFunction
|
|
56
|
+
import { executePageFunction, executePoll, executeWriteSiteProfile, executeReadSiteProfile, loadSiteProfile } from './execute-dynamic.js';
|
|
55
57
|
|
|
56
58
|
const keyValidator = createKeyValidator();
|
|
57
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Detect if a string looks like a function expression (vs a bare expression).
|
|
62
|
+
* Functions start with (, function keyword, async keyword, or match arrow function patterns.
|
|
63
|
+
* Must use word boundaries to avoid matching identifiers like "asyncStorage" or "functionName".
|
|
64
|
+
*/
|
|
65
|
+
function isFunctionExpression(str) {
|
|
66
|
+
const trimmed = str.trim();
|
|
67
|
+
// Parenthesized expression / IIFE / arrow with parens: (...)
|
|
68
|
+
if (trimmed.startsWith('(')) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
// function keyword followed by space, paren, or * (generator)
|
|
72
|
+
if (/^function[\s*(]/.test(trimmed)) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
// async keyword followed by space or paren (async function, async () =>)
|
|
76
|
+
if (/^async[\s(]/.test(trimmed)) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
// Arrow function: identifier => ...
|
|
80
|
+
if (/^\w+\s*=>/.test(trimmed)) {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
58
86
|
/**
|
|
59
87
|
* Execute a single test step
|
|
60
88
|
* @param {Object} deps - Dependencies
|
|
@@ -64,7 +92,7 @@ const keyValidator = createKeyValidator();
|
|
|
64
92
|
*/
|
|
65
93
|
export async function executeStep(deps, step, options = {}) {
|
|
66
94
|
const { pageController, elementLocator, inputEmulator } = deps;
|
|
67
|
-
|
|
95
|
+
let stepTimeout = options.stepTimeout || 30000;
|
|
68
96
|
const isOptional = step.optional === true;
|
|
69
97
|
|
|
70
98
|
const stepResult = {
|
|
@@ -101,6 +129,9 @@ export async function executeStep(deps, step, options = {}) {
|
|
|
101
129
|
const gotoOptions = typeof step.goto === 'object' ? step.goto : {};
|
|
102
130
|
await pageController.navigate(url, gotoOptions);
|
|
103
131
|
|
|
132
|
+
// Wait for network to settle after navigation (best-effort, never throws)
|
|
133
|
+
await pageController.waitForNetworkSettle();
|
|
134
|
+
|
|
104
135
|
// Site profile: load existing or signal that none exists
|
|
105
136
|
try {
|
|
106
137
|
const currentUrl = await pageController.evaluateInFrame('window.location.href', { returnByValue: true });
|
|
@@ -122,15 +153,27 @@ export async function executeStep(deps, step, options = {}) {
|
|
|
122
153
|
stepResult.action = 'reload';
|
|
123
154
|
const reloadOptions = step.reload === true ? {} : step.reload;
|
|
124
155
|
await pageController.reload(reloadOptions);
|
|
156
|
+
await pageController.waitForNetworkSettle();
|
|
157
|
+
} else if (step.sleep !== undefined) {
|
|
158
|
+
stepResult.action = 'sleep';
|
|
159
|
+
await sleep(step.sleep);
|
|
125
160
|
} else if (step.wait !== undefined) {
|
|
126
161
|
stepResult.action = 'wait';
|
|
127
|
-
|
|
128
|
-
await sleep(step.wait);
|
|
129
|
-
} else {
|
|
130
|
-
await executeWait(elementLocator, step.wait);
|
|
131
|
-
}
|
|
162
|
+
await executeWait(elementLocator, step.wait);
|
|
132
163
|
} else if (step.click !== undefined) {
|
|
133
164
|
stepResult.action = 'click';
|
|
165
|
+
|
|
166
|
+
// Capture tabs before click for new-tab detection
|
|
167
|
+
let tabsBefore = null;
|
|
168
|
+
try {
|
|
169
|
+
if (deps.browser) {
|
|
170
|
+
const pages = await deps.browser.getPages();
|
|
171
|
+
tabsBefore = new Set(pages.map(p => p.targetId));
|
|
172
|
+
}
|
|
173
|
+
} catch {
|
|
174
|
+
// Detection failure is non-fatal
|
|
175
|
+
}
|
|
176
|
+
|
|
134
177
|
const clickResult = await executeClick(elementLocator, inputEmulator, deps.ariaSnapshot, step.click);
|
|
135
178
|
if (clickResult) {
|
|
136
179
|
if (clickResult.method) {
|
|
@@ -154,29 +197,75 @@ export async function executeStep(deps, step, options = {}) {
|
|
|
154
197
|
stepResult.warning = 'CDP click was intercepted, used JavaScript click fallback';
|
|
155
198
|
}
|
|
156
199
|
}
|
|
200
|
+
|
|
201
|
+
// Detect new tabs opened by the click
|
|
202
|
+
try {
|
|
203
|
+
if (tabsBefore && deps.browser) {
|
|
204
|
+
await sleep(200);
|
|
205
|
+
const pagesAfter = await deps.browser.getPages();
|
|
206
|
+
const newTabs = pagesAfter
|
|
207
|
+
.filter(p => !tabsBefore.has(p.targetId))
|
|
208
|
+
.map(p => ({ targetId: p.targetId, url: p.url, title: p.title }));
|
|
209
|
+
if (newTabs.length > 0) {
|
|
210
|
+
// Register new tabs in the tab registry so agents can switch to them
|
|
211
|
+
if (deps.registerNewTab) {
|
|
212
|
+
for (const tab of newTabs) {
|
|
213
|
+
tab.alias = deps.registerNewTab(tab.targetId);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
stepResult.output = stepResult.output || {};
|
|
217
|
+
stepResult.output.newTabs = newTabs;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
} catch {
|
|
221
|
+
// Detection failure is non-fatal
|
|
222
|
+
}
|
|
157
223
|
} else if (step.fill !== undefined) {
|
|
158
224
|
stepResult.action = 'fill';
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
225
|
+
const params = step.fill;
|
|
226
|
+
|
|
227
|
+
if (typeof params === 'string') {
|
|
228
|
+
// Shape 1: focused mode — type into active element
|
|
229
|
+
stepResult.output = await executeFillActive(pageController, inputEmulator, params);
|
|
230
|
+
stepResult.output.mode = 'focused';
|
|
231
|
+
} else if (params && typeof params === 'object') {
|
|
232
|
+
const hasTargeting = params.selector || params.ref || params.label;
|
|
233
|
+
const hasFields = params.fields && typeof params.fields === 'object';
|
|
234
|
+
|
|
235
|
+
if (hasTargeting) {
|
|
236
|
+
// Shape 2: single field with targeting
|
|
237
|
+
const fillExecutor = createFillExecutor(
|
|
238
|
+
elementLocator.session,
|
|
239
|
+
elementLocator,
|
|
240
|
+
inputEmulator,
|
|
241
|
+
deps.ariaSnapshot,
|
|
242
|
+
{ getFrameContext: pageController.getFrameContext }
|
|
243
|
+
);
|
|
244
|
+
const urlBeforeFill = await getCurrentUrl(elementLocator.session);
|
|
245
|
+
await fillExecutor.execute(params);
|
|
246
|
+
const urlAfterFill = await getCurrentUrl(elementLocator.session);
|
|
247
|
+
stepResult.output = { mode: 'single' };
|
|
248
|
+
if (urlAfterFill !== urlBeforeFill) {
|
|
249
|
+
stepResult.output.navigated = true;
|
|
250
|
+
stepResult.output.newUrl = urlAfterFill;
|
|
251
|
+
}
|
|
252
|
+
} else if (params.value !== undefined && !hasFields) {
|
|
253
|
+
// Shape 3: focused with options
|
|
254
|
+
stepResult.output = await executeFillActive(pageController, inputEmulator, params);
|
|
255
|
+
stepResult.output.mode = 'focused';
|
|
256
|
+
} else {
|
|
257
|
+
// Shape 4 ({fields: ...}) or Shape 5 (plain mapping)
|
|
258
|
+
const fillExecutor = createFillExecutor(
|
|
259
|
+
elementLocator.session,
|
|
260
|
+
elementLocator,
|
|
261
|
+
inputEmulator,
|
|
262
|
+
deps.ariaSnapshot,
|
|
263
|
+
{ getFrameContext: pageController.getFrameContext }
|
|
264
|
+
);
|
|
265
|
+
stepResult.output = await fillExecutor.executeBatch(params);
|
|
266
|
+
stepResult.output.mode = 'batch';
|
|
267
|
+
}
|
|
170
268
|
}
|
|
171
|
-
} else if (step.fillForm !== undefined) {
|
|
172
|
-
stepResult.action = 'fillForm';
|
|
173
|
-
const fillExecutor = createFillExecutor(
|
|
174
|
-
elementLocator.session,
|
|
175
|
-
elementLocator,
|
|
176
|
-
inputEmulator,
|
|
177
|
-
deps.ariaSnapshot
|
|
178
|
-
);
|
|
179
|
-
stepResult.output = await fillExecutor.executeBatch(step.fillForm);
|
|
180
269
|
} else if (step.press !== undefined) {
|
|
181
270
|
stepResult.action = 'press';
|
|
182
271
|
const keyValidation = keyValidator.validate(step.press);
|
|
@@ -203,11 +292,10 @@ export async function executeStep(deps, step, options = {}) {
|
|
|
203
292
|
} else if (step.pdf !== undefined) {
|
|
204
293
|
stepResult.action = 'pdf';
|
|
205
294
|
stepResult.output = await executePdf(deps.pdfCapture, elementLocator, step.pdf);
|
|
206
|
-
} else if (step.eval !== undefined) {
|
|
207
|
-
stepResult.action = 'eval';
|
|
208
|
-
stepResult.output = await executeEval(pageController, step.eval);
|
|
209
295
|
} else if (step.snapshot !== undefined) {
|
|
210
296
|
stepResult.action = 'snapshot';
|
|
297
|
+
// Brief network settle before capturing — catches async content loading
|
|
298
|
+
await pageController.waitForNetworkSettle({ timeout: 1500, idleTime: 200 });
|
|
211
299
|
stepResult.output = await executeSnapshot(deps.ariaSnapshot, step.snapshot, { tabAlias: options.tabAlias, inlineLimit: options.inlineLimit });
|
|
212
300
|
} else if (step.snapshotSearch !== undefined) {
|
|
213
301
|
stepResult.action = 'snapshotSearch';
|
|
@@ -229,11 +317,13 @@ export async function executeStep(deps, step, options = {}) {
|
|
|
229
317
|
const backOptions = step.back === true ? {} : step.back;
|
|
230
318
|
const entry = await pageController.goBack(backOptions);
|
|
231
319
|
stepResult.output = entry ? { url: entry.url, title: entry.title } : { noHistory: true };
|
|
320
|
+
if (entry) await pageController.waitForNetworkSettle();
|
|
232
321
|
} else if (step.forward !== undefined) {
|
|
233
322
|
stepResult.action = 'forward';
|
|
234
323
|
const forwardOptions = step.forward === true ? {} : step.forward;
|
|
235
324
|
const entry = await pageController.goForward(forwardOptions);
|
|
236
325
|
stepResult.output = entry ? { url: entry.url, title: entry.title } : { noHistory: true };
|
|
326
|
+
if (entry) await pageController.waitForNetworkSettle();
|
|
237
327
|
} else if (step.waitForNavigation !== undefined) {
|
|
238
328
|
stepResult.action = 'waitForNavigation';
|
|
239
329
|
await executeWaitForNavigation(pageController, step.waitForNavigation);
|
|
@@ -243,16 +333,21 @@ export async function executeStep(deps, step, options = {}) {
|
|
|
243
333
|
} else if (step.closeTab !== undefined) {
|
|
244
334
|
stepResult.action = 'closeTab';
|
|
245
335
|
stepResult.output = await executeCloseTab(deps.browser, step.closeTab);
|
|
246
|
-
} else if (step.
|
|
247
|
-
stepResult.action = '
|
|
248
|
-
if (step.
|
|
249
|
-
if (step.
|
|
250
|
-
|
|
336
|
+
} else if (step.newTab !== undefined) {
|
|
337
|
+
stepResult.action = 'newTab';
|
|
338
|
+
if (step._newTabHandled) {
|
|
339
|
+
if (step._newTabUrl) {
|
|
340
|
+
const navOptions = {};
|
|
341
|
+
if (step._newTabTimeout) {
|
|
342
|
+
navOptions.timeout = step._newTabTimeout;
|
|
343
|
+
}
|
|
344
|
+
await pageController.navigate(step._newTabUrl, navOptions);
|
|
345
|
+
await pageController.waitForNetworkSettle();
|
|
251
346
|
|
|
252
347
|
// Site profile check (same as goto)
|
|
253
348
|
try {
|
|
254
349
|
const currentUrl = await pageController.evaluateInFrame('window.location.href', { returnByValue: true });
|
|
255
|
-
const resolvedUrl = currentUrl.result?.value || step.
|
|
350
|
+
const resolvedUrl = currentUrl.result?.value || step._newTabUrl;
|
|
256
351
|
const domain = new URL(resolvedUrl).hostname.replace(/^www\./, '');
|
|
257
352
|
|
|
258
353
|
const existingProfile = await loadSiteProfile(domain);
|
|
@@ -267,29 +362,18 @@ export async function executeStep(deps, step, options = {}) {
|
|
|
267
362
|
// Profile errors are non-fatal
|
|
268
363
|
}
|
|
269
364
|
}
|
|
270
|
-
stepResult.output = { tab: step.
|
|
365
|
+
stepResult.output = { tab: step._newTabAlias };
|
|
271
366
|
} else {
|
|
272
367
|
throw new Error('openTab must be the first step when no targetId is provided');
|
|
273
368
|
}
|
|
274
|
-
} else if (step.
|
|
275
|
-
stepResult.action = '
|
|
369
|
+
} else if (step.selectText !== undefined) {
|
|
370
|
+
stepResult.action = 'selectText';
|
|
276
371
|
const keyboardExecutor = createKeyboardExecutor(
|
|
277
372
|
elementLocator.session,
|
|
278
373
|
elementLocator,
|
|
279
374
|
inputEmulator
|
|
280
375
|
);
|
|
281
|
-
stepResult.output = await keyboardExecutor.
|
|
282
|
-
} else if (step.select !== undefined) {
|
|
283
|
-
stepResult.action = 'select';
|
|
284
|
-
const keyboardExecutor = createKeyboardExecutor(
|
|
285
|
-
elementLocator.session,
|
|
286
|
-
elementLocator,
|
|
287
|
-
inputEmulator
|
|
288
|
-
);
|
|
289
|
-
stepResult.output = await keyboardExecutor.executeSelect(step.select);
|
|
290
|
-
} else if (step.validate !== undefined) {
|
|
291
|
-
stepResult.action = 'validate';
|
|
292
|
-
stepResult.output = await executeValidate(elementLocator, step.validate);
|
|
376
|
+
stepResult.output = await keyboardExecutor.executeSelect(step.selectText);
|
|
293
377
|
} else if (step.submit !== undefined) {
|
|
294
378
|
stepResult.action = 'submit';
|
|
295
379
|
stepResult.output = await executeSubmit(elementLocator, step.submit);
|
|
@@ -299,26 +383,44 @@ export async function executeStep(deps, step, options = {}) {
|
|
|
299
383
|
} else if (step.queryAll !== undefined) {
|
|
300
384
|
stepResult.action = 'queryAll';
|
|
301
385
|
stepResult.output = await executeQueryAll(elementLocator, step.queryAll);
|
|
302
|
-
} else if (step.
|
|
303
|
-
stepResult.action = '
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
386
|
+
} else if (step.frame !== undefined) {
|
|
387
|
+
stepResult.action = 'frame';
|
|
388
|
+
const frameParams = step.frame;
|
|
389
|
+
if (frameParams === 'top') {
|
|
390
|
+
stepResult.output = await pageController.switchToMainFrame();
|
|
391
|
+
} else if (typeof frameParams === 'object' && frameParams.list) {
|
|
392
|
+
stepResult.output = await pageController.getFrameTree();
|
|
393
|
+
} else {
|
|
394
|
+
// string selector, number index, or {name: "foo"} — all go to switchToFrame
|
|
395
|
+
stepResult.output = await pageController.switchToFrame(frameParams);
|
|
396
|
+
}
|
|
311
397
|
} else if (step.drag !== undefined) {
|
|
312
398
|
stepResult.action = 'drag';
|
|
313
399
|
stepResult.output = await executeDrag(elementLocator, inputEmulator, pageController, deps.ariaSnapshot, step.drag);
|
|
314
|
-
} else if (step.
|
|
315
|
-
stepResult.action = '
|
|
316
|
-
const
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
400
|
+
} else if (step.get !== undefined) {
|
|
401
|
+
stepResult.action = 'get';
|
|
402
|
+
const getParams = step.get;
|
|
403
|
+
const mode = typeof getParams === 'object' ? getParams.mode : null;
|
|
404
|
+
|
|
405
|
+
if (mode === 'html') {
|
|
406
|
+
// HTML extraction mode → use getDom
|
|
407
|
+
stepResult.output = await executeGetDom(pageController, getParams);
|
|
408
|
+
stepResult.output.mode = 'html';
|
|
409
|
+
} else if (mode === 'box') {
|
|
410
|
+
// Bounding box mode → use getBox (requires ref format)
|
|
411
|
+
stepResult.output = await executeGetBox(deps.ariaSnapshot, getParams.ref || getParams.selector);
|
|
412
|
+
stepResult.output.mode = 'box';
|
|
413
|
+
} else if (mode === 'value') {
|
|
414
|
+
// Form value extraction → use formState
|
|
415
|
+
const formValidator = createFormValidator(elementLocator.session, elementLocator);
|
|
416
|
+
const selector = typeof getParams === 'string' ? getParams : getParams.selector;
|
|
417
|
+
stepResult.output = await formValidator.getFormState(selector);
|
|
418
|
+
stepResult.output.mode = 'value';
|
|
419
|
+
} else {
|
|
420
|
+
// Default: text/attributes extraction → use extract
|
|
421
|
+
stepResult.output = await executeExtract(deps, getParams);
|
|
422
|
+
stepResult.output.mode = mode || 'text';
|
|
423
|
+
}
|
|
322
424
|
} else if (step.selectOption !== undefined) {
|
|
323
425
|
stepResult.action = 'selectOption';
|
|
324
426
|
stepResult.output = await executeSelectOption(elementLocator, step.selectOption);
|
|
@@ -328,33 +430,63 @@ export async function executeStep(deps, step, options = {}) {
|
|
|
328
430
|
} else if (step.getBox !== undefined) {
|
|
329
431
|
stepResult.action = 'getBox';
|
|
330
432
|
stepResult.output = await executeGetBox(deps.ariaSnapshot, step.getBox);
|
|
331
|
-
} else if (step.fillActive !== undefined) {
|
|
332
|
-
stepResult.action = 'fillActive';
|
|
333
|
-
stepResult.output = await executeFillActive(pageController, inputEmulator, step.fillActive);
|
|
334
|
-
} else if (step.refAt !== undefined) {
|
|
335
|
-
stepResult.action = 'refAt';
|
|
336
|
-
stepResult.output = await executeRefAt(pageController.session, step.refAt);
|
|
337
433
|
} else if (step.elementsAt !== undefined) {
|
|
338
434
|
stepResult.action = 'elementsAt';
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
435
|
+
const eaParams = step.elementsAt;
|
|
436
|
+
if (Array.isArray(eaParams)) {
|
|
437
|
+
// Batch mode (array of coordinates)
|
|
438
|
+
stepResult.output = await executeElementsAt(pageController.session, eaParams);
|
|
439
|
+
} else if (eaParams && typeof eaParams === 'object' && eaParams.radius !== undefined) {
|
|
440
|
+
// Nearby mode (has radius)
|
|
441
|
+
stepResult.output = await executeElementsNear(pageController.session, eaParams);
|
|
442
|
+
} else {
|
|
443
|
+
// Single point mode (was refAt)
|
|
444
|
+
stepResult.output = await executeRefAt(pageController.session, eaParams);
|
|
445
|
+
}
|
|
343
446
|
} else if (step.pageFunction !== undefined) {
|
|
344
447
|
stepResult.action = 'pageFunction';
|
|
345
|
-
|
|
448
|
+
const pfParams = step.pageFunction;
|
|
449
|
+
// Check if this is a bare expression (not a function)
|
|
450
|
+
const pfStr = typeof pfParams === 'string' ? pfParams : (pfParams?.fn || pfParams?.expression);
|
|
451
|
+
const isBareExpression = pfStr && !isFunctionExpression(pfStr);
|
|
452
|
+
if (isBareExpression) {
|
|
453
|
+
// Use eval-style wrapping for bare expressions
|
|
454
|
+
const evalParams = typeof pfParams === 'string'
|
|
455
|
+
? pfStr
|
|
456
|
+
: { expression: pfStr, await: pfParams?.await, serialize: pfParams?.serialize, timeout: pfParams?.timeout };
|
|
457
|
+
stepResult.output = await executeEval(pageController, evalParams);
|
|
458
|
+
} else {
|
|
459
|
+
// If expression key provided, remap to fn
|
|
460
|
+
if (typeof pfParams === 'object' && pfParams.expression && !pfParams.fn) {
|
|
461
|
+
stepResult.output = await executePageFunction(pageController, { ...pfParams, fn: pfParams.expression });
|
|
462
|
+
} else {
|
|
463
|
+
stepResult.output = await executePageFunction(pageController, pfParams);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
346
466
|
} else if (step.poll !== undefined) {
|
|
347
467
|
stepResult.action = 'poll';
|
|
348
468
|
stepResult.output = await executePoll(pageController, step.poll);
|
|
349
|
-
} else if (step.pipeline !== undefined) {
|
|
350
|
-
stepResult.action = 'pipeline';
|
|
351
|
-
stepResult.output = await executePipeline(pageController, step.pipeline);
|
|
352
469
|
} else if (step.writeSiteProfile !== undefined) {
|
|
353
470
|
stepResult.action = 'writeSiteProfile';
|
|
354
471
|
stepResult.output = await executeWriteSiteProfile(step.writeSiteProfile);
|
|
355
472
|
} else if (step.readSiteProfile !== undefined) {
|
|
356
473
|
stepResult.action = 'readSiteProfile';
|
|
357
474
|
stepResult.output = await executeReadSiteProfile(step.readSiteProfile);
|
|
475
|
+
} else if (step.switchTab !== undefined) {
|
|
476
|
+
stepResult.action = 'switchTab';
|
|
477
|
+
if (step._switchTabHandled) {
|
|
478
|
+
stepResult.output = { tab: step._switchTabAlias, connected: true };
|
|
479
|
+
} else {
|
|
480
|
+
throw new Error('switchTab must be the first step when no tab is specified');
|
|
481
|
+
}
|
|
482
|
+
} else if (step.getUrl !== undefined) {
|
|
483
|
+
stepResult.action = 'getUrl';
|
|
484
|
+
const urlResult = await pageController.evaluateInFrame('window.location.href', { returnByValue: true });
|
|
485
|
+
stepResult.output = { url: urlResult.result?.value };
|
|
486
|
+
} else if (step.getTitle !== undefined) {
|
|
487
|
+
stepResult.action = 'getTitle';
|
|
488
|
+
const titleResult = await pageController.evaluateInFrame('document.title', { returnByValue: true });
|
|
489
|
+
stepResult.output = { title: titleResult.result?.value };
|
|
358
490
|
}
|
|
359
491
|
|
|
360
492
|
// Process hooks on action steps (settledWhen, observe)
|
|
@@ -367,7 +499,11 @@ export async function executeStep(deps, step, options = {}) {
|
|
|
367
499
|
timeout: stepTimeout || 30000
|
|
368
500
|
});
|
|
369
501
|
if (!settledResult.resolved) {
|
|
370
|
-
|
|
502
|
+
const lastValStr = settledResult.lastValue !== undefined
|
|
503
|
+
? ` (last value: ${JSON.stringify(settledResult.lastValue).substring(0, 200)})`
|
|
504
|
+
: '';
|
|
505
|
+
stepResult.warning = (stepResult.warning || '') +
|
|
506
|
+
`settledWhen timed out after ${settledResult.elapsed || 'unknown'}ms${lastValStr}`;
|
|
371
507
|
}
|
|
372
508
|
}
|
|
373
509
|
|
|
@@ -380,6 +516,11 @@ export async function executeStep(deps, step, options = {}) {
|
|
|
380
516
|
const definedAction = STEP_TYPES.find(type => step[type] !== undefined);
|
|
381
517
|
const stepParams = definedAction ? step[definedAction] : null;
|
|
382
518
|
|
|
519
|
+
// Step-level timeout overrides the default step timeout
|
|
520
|
+
if (stepParams && typeof stepParams === 'object' && typeof stepParams.timeout === 'number' && stepParams.timeout > 0) {
|
|
521
|
+
stepTimeout = stepParams.timeout;
|
|
522
|
+
}
|
|
523
|
+
|
|
383
524
|
let timeoutId;
|
|
384
525
|
try {
|
|
385
526
|
const stepPromise = executeStepInternal();
|