cdp-skill 1.0.7 → 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 +198 -1344
- 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 +268 -68
- 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 +34 -143
- 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 +256 -95
- package/src/runner/step-registry.js +1064 -0
- package/src/runner/step-validator.js +16 -740
- 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 +34 -736
- 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
|
@@ -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,37 +129,51 @@ 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
|
|
|
104
|
-
//
|
|
132
|
+
// Wait for network to settle after navigation (best-effort, never throws)
|
|
133
|
+
await pageController.waitForNetworkSettle();
|
|
134
|
+
|
|
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 });
|
|
107
138
|
const resolvedUrl = currentUrl.result?.value || url;
|
|
108
139
|
const domain = new URL(resolvedUrl).hostname.replace(/^www\./, '');
|
|
109
140
|
|
|
110
|
-
const
|
|
111
|
-
if (
|
|
112
|
-
stepResult.
|
|
141
|
+
const existingProfile = await loadSiteProfile(domain);
|
|
142
|
+
if (existingProfile) {
|
|
143
|
+
stepResult.siteProfile = existingProfile;
|
|
113
144
|
} else {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
145
|
+
stepResult.profileAvailable = false;
|
|
146
|
+
stepResult.profileDomain = domain;
|
|
147
|
+
stepResult.hint = `Unknown site: ${domain}. No site profile exists. Create one with writeSiteProfile after exploring the site (snapshot, pageFunction, snapshotSearch). This speeds up all future visits.`;
|
|
118
148
|
}
|
|
119
149
|
} catch {
|
|
120
|
-
//
|
|
150
|
+
// Profile errors are non-fatal
|
|
121
151
|
}
|
|
122
152
|
} else if (step.reload !== undefined) {
|
|
123
153
|
stepResult.action = 'reload';
|
|
124
154
|
const reloadOptions = step.reload === true ? {} : step.reload;
|
|
125
155
|
await pageController.reload(reloadOptions);
|
|
156
|
+
await pageController.waitForNetworkSettle();
|
|
157
|
+
} else if (step.sleep !== undefined) {
|
|
158
|
+
stepResult.action = 'sleep';
|
|
159
|
+
await sleep(step.sleep);
|
|
126
160
|
} else if (step.wait !== undefined) {
|
|
127
161
|
stepResult.action = 'wait';
|
|
128
|
-
|
|
129
|
-
await sleep(step.wait);
|
|
130
|
-
} else {
|
|
131
|
-
await executeWait(elementLocator, step.wait);
|
|
132
|
-
}
|
|
162
|
+
await executeWait(elementLocator, step.wait);
|
|
133
163
|
} else if (step.click !== undefined) {
|
|
134
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
|
+
|
|
135
177
|
const clickResult = await executeClick(elementLocator, inputEmulator, deps.ariaSnapshot, step.click);
|
|
136
178
|
if (clickResult) {
|
|
137
179
|
if (clickResult.method) {
|
|
@@ -155,29 +197,75 @@ export async function executeStep(deps, step, options = {}) {
|
|
|
155
197
|
stepResult.warning = 'CDP click was intercepted, used JavaScript click fallback';
|
|
156
198
|
}
|
|
157
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
|
+
}
|
|
158
223
|
} else if (step.fill !== undefined) {
|
|
159
224
|
stepResult.action = 'fill';
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
+
}
|
|
171
268
|
}
|
|
172
|
-
} else if (step.fillForm !== undefined) {
|
|
173
|
-
stepResult.action = 'fillForm';
|
|
174
|
-
const fillExecutor = createFillExecutor(
|
|
175
|
-
elementLocator.session,
|
|
176
|
-
elementLocator,
|
|
177
|
-
inputEmulator,
|
|
178
|
-
deps.ariaSnapshot
|
|
179
|
-
);
|
|
180
|
-
stepResult.output = await fillExecutor.executeBatch(step.fillForm);
|
|
181
269
|
} else if (step.press !== undefined) {
|
|
182
270
|
stepResult.action = 'press';
|
|
183
271
|
const keyValidation = keyValidator.validate(step.press);
|
|
@@ -204,11 +292,10 @@ export async function executeStep(deps, step, options = {}) {
|
|
|
204
292
|
} else if (step.pdf !== undefined) {
|
|
205
293
|
stepResult.action = 'pdf';
|
|
206
294
|
stepResult.output = await executePdf(deps.pdfCapture, elementLocator, step.pdf);
|
|
207
|
-
} else if (step.eval !== undefined) {
|
|
208
|
-
stepResult.action = 'eval';
|
|
209
|
-
stepResult.output = await executeEval(pageController, step.eval);
|
|
210
295
|
} else if (step.snapshot !== undefined) {
|
|
211
296
|
stepResult.action = 'snapshot';
|
|
297
|
+
// Brief network settle before capturing — catches async content loading
|
|
298
|
+
await pageController.waitForNetworkSettle({ timeout: 1500, idleTime: 200 });
|
|
212
299
|
stepResult.output = await executeSnapshot(deps.ariaSnapshot, step.snapshot, { tabAlias: options.tabAlias, inlineLimit: options.inlineLimit });
|
|
213
300
|
} else if (step.snapshotSearch !== undefined) {
|
|
214
301
|
stepResult.action = 'snapshotSearch';
|
|
@@ -230,11 +317,13 @@ export async function executeStep(deps, step, options = {}) {
|
|
|
230
317
|
const backOptions = step.back === true ? {} : step.back;
|
|
231
318
|
const entry = await pageController.goBack(backOptions);
|
|
232
319
|
stepResult.output = entry ? { url: entry.url, title: entry.title } : { noHistory: true };
|
|
320
|
+
if (entry) await pageController.waitForNetworkSettle();
|
|
233
321
|
} else if (step.forward !== undefined) {
|
|
234
322
|
stepResult.action = 'forward';
|
|
235
323
|
const forwardOptions = step.forward === true ? {} : step.forward;
|
|
236
324
|
const entry = await pageController.goForward(forwardOptions);
|
|
237
325
|
stepResult.output = entry ? { url: entry.url, title: entry.title } : { noHistory: true };
|
|
326
|
+
if (entry) await pageController.waitForNetworkSettle();
|
|
238
327
|
} else if (step.waitForNavigation !== undefined) {
|
|
239
328
|
stepResult.action = 'waitForNavigation';
|
|
240
329
|
await executeWaitForNavigation(pageController, step.waitForNavigation);
|
|
@@ -244,35 +333,47 @@ export async function executeStep(deps, step, options = {}) {
|
|
|
244
333
|
} else if (step.closeTab !== undefined) {
|
|
245
334
|
stepResult.action = 'closeTab';
|
|
246
335
|
stepResult.output = await executeCloseTab(deps.browser, step.closeTab);
|
|
247
|
-
} else if (step.
|
|
248
|
-
stepResult.action = '
|
|
249
|
-
if (step.
|
|
250
|
-
if (step.
|
|
251
|
-
|
|
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();
|
|
346
|
+
|
|
347
|
+
// Site profile check (same as goto)
|
|
348
|
+
try {
|
|
349
|
+
const currentUrl = await pageController.evaluateInFrame('window.location.href', { returnByValue: true });
|
|
350
|
+
const resolvedUrl = currentUrl.result?.value || step._newTabUrl;
|
|
351
|
+
const domain = new URL(resolvedUrl).hostname.replace(/^www\./, '');
|
|
352
|
+
|
|
353
|
+
const existingProfile = await loadSiteProfile(domain);
|
|
354
|
+
if (existingProfile) {
|
|
355
|
+
stepResult.siteProfile = existingProfile;
|
|
356
|
+
} else {
|
|
357
|
+
stepResult.profileAvailable = false;
|
|
358
|
+
stepResult.profileDomain = domain;
|
|
359
|
+
stepResult.hint = `Unknown site: ${domain}. No site profile exists. Create one with writeSiteProfile after exploring the site (snapshot, pageFunction, snapshotSearch). This speeds up all future visits.`;
|
|
360
|
+
}
|
|
361
|
+
} catch {
|
|
362
|
+
// Profile errors are non-fatal
|
|
363
|
+
}
|
|
252
364
|
}
|
|
253
|
-
stepResult.output = { tab: step.
|
|
365
|
+
stepResult.output = { tab: step._newTabAlias };
|
|
254
366
|
} else {
|
|
255
367
|
throw new Error('openTab must be the first step when no targetId is provided');
|
|
256
368
|
}
|
|
257
|
-
} else if (step.
|
|
258
|
-
stepResult.action = '
|
|
369
|
+
} else if (step.selectText !== undefined) {
|
|
370
|
+
stepResult.action = 'selectText';
|
|
259
371
|
const keyboardExecutor = createKeyboardExecutor(
|
|
260
372
|
elementLocator.session,
|
|
261
373
|
elementLocator,
|
|
262
374
|
inputEmulator
|
|
263
375
|
);
|
|
264
|
-
stepResult.output = await keyboardExecutor.
|
|
265
|
-
} else if (step.select !== undefined) {
|
|
266
|
-
stepResult.action = 'select';
|
|
267
|
-
const keyboardExecutor = createKeyboardExecutor(
|
|
268
|
-
elementLocator.session,
|
|
269
|
-
elementLocator,
|
|
270
|
-
inputEmulator
|
|
271
|
-
);
|
|
272
|
-
stepResult.output = await keyboardExecutor.executeSelect(step.select);
|
|
273
|
-
} else if (step.validate !== undefined) {
|
|
274
|
-
stepResult.action = 'validate';
|
|
275
|
-
stepResult.output = await executeValidate(elementLocator, step.validate);
|
|
376
|
+
stepResult.output = await keyboardExecutor.executeSelect(step.selectText);
|
|
276
377
|
} else if (step.submit !== undefined) {
|
|
277
378
|
stepResult.action = 'submit';
|
|
278
379
|
stepResult.output = await executeSubmit(elementLocator, step.submit);
|
|
@@ -282,26 +383,44 @@ export async function executeStep(deps, step, options = {}) {
|
|
|
282
383
|
} else if (step.queryAll !== undefined) {
|
|
283
384
|
stepResult.action = 'queryAll';
|
|
284
385
|
stepResult.output = await executeQueryAll(elementLocator, step.queryAll);
|
|
285
|
-
} else if (step.
|
|
286
|
-
stepResult.action = '
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
+
}
|
|
294
397
|
} else if (step.drag !== undefined) {
|
|
295
398
|
stepResult.action = 'drag';
|
|
296
399
|
stepResult.output = await executeDrag(elementLocator, inputEmulator, pageController, deps.ariaSnapshot, step.drag);
|
|
297
|
-
} else if (step.
|
|
298
|
-
stepResult.action = '
|
|
299
|
-
const
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
+
}
|
|
305
424
|
} else if (step.selectOption !== undefined) {
|
|
306
425
|
stepResult.action = 'selectOption';
|
|
307
426
|
stepResult.output = await executeSelectOption(elementLocator, step.selectOption);
|
|
@@ -311,30 +430,63 @@ export async function executeStep(deps, step, options = {}) {
|
|
|
311
430
|
} else if (step.getBox !== undefined) {
|
|
312
431
|
stepResult.action = 'getBox';
|
|
313
432
|
stepResult.output = await executeGetBox(deps.ariaSnapshot, step.getBox);
|
|
314
|
-
} else if (step.fillActive !== undefined) {
|
|
315
|
-
stepResult.action = 'fillActive';
|
|
316
|
-
stepResult.output = await executeFillActive(pageController, inputEmulator, step.fillActive);
|
|
317
|
-
} else if (step.refAt !== undefined) {
|
|
318
|
-
stepResult.action = 'refAt';
|
|
319
|
-
stepResult.output = await executeRefAt(pageController.session, step.refAt);
|
|
320
433
|
} else if (step.elementsAt !== undefined) {
|
|
321
434
|
stepResult.action = 'elementsAt';
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
+
}
|
|
326
446
|
} else if (step.pageFunction !== undefined) {
|
|
327
447
|
stepResult.action = 'pageFunction';
|
|
328
|
-
|
|
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
|
+
}
|
|
329
466
|
} else if (step.poll !== undefined) {
|
|
330
467
|
stepResult.action = 'poll';
|
|
331
468
|
stepResult.output = await executePoll(pageController, step.poll);
|
|
332
|
-
} else if (step.
|
|
333
|
-
stepResult.action = '
|
|
334
|
-
stepResult.output = await
|
|
335
|
-
} else if (step.
|
|
336
|
-
stepResult.action = '
|
|
337
|
-
stepResult.output = await
|
|
469
|
+
} else if (step.writeSiteProfile !== undefined) {
|
|
470
|
+
stepResult.action = 'writeSiteProfile';
|
|
471
|
+
stepResult.output = await executeWriteSiteProfile(step.writeSiteProfile);
|
|
472
|
+
} else if (step.readSiteProfile !== undefined) {
|
|
473
|
+
stepResult.action = 'readSiteProfile';
|
|
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 };
|
|
338
490
|
}
|
|
339
491
|
|
|
340
492
|
// Process hooks on action steps (settledWhen, observe)
|
|
@@ -347,7 +499,11 @@ export async function executeStep(deps, step, options = {}) {
|
|
|
347
499
|
timeout: stepTimeout || 30000
|
|
348
500
|
});
|
|
349
501
|
if (!settledResult.resolved) {
|
|
350
|
-
|
|
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}`;
|
|
351
507
|
}
|
|
352
508
|
}
|
|
353
509
|
|
|
@@ -360,6 +516,11 @@ export async function executeStep(deps, step, options = {}) {
|
|
|
360
516
|
const definedAction = STEP_TYPES.find(type => step[type] !== undefined);
|
|
361
517
|
const stepParams = definedAction ? step[definedAction] : null;
|
|
362
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
|
+
|
|
363
524
|
let timeoutId;
|
|
364
525
|
try {
|
|
365
526
|
const stepPromise = executeStepInternal();
|