cdp-skill 1.0.8 → 1.0.15
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 +157 -241
- 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 +251 -50
- package/src/cdp/browser.js +22 -4
- package/src/cdp-skill.js +246 -69
- package/src/dom/LazyResolver.js +634 -0
- package/src/dom/click-executor.js +366 -94
- package/src/dom/element-locator.js +34 -25
- package/src/dom/fill-executor.js +83 -50
- package/src/dom/index.js +3 -0
- package/src/page/dialog-handler.js +119 -0
- package/src/page/page-controller.js +236 -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 +105 -126
- package/src/runner/execute-navigation.js +14 -29
- package/src/runner/execute-query.js +17 -11
- 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/ClickExecutor.test.js +170 -50
- package/src/tests/ContextHelpers.test.js +41 -30
- 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 +89 -37
- package/src/tests/LazyResolver.test.js +383 -0
- package/src/tests/StepValidator.test.js +224 -78
- package/src/tests/TestRunner.test.js +38 -27
- 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
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
} from '../utils.js';
|
|
22
22
|
|
|
23
23
|
import { TIMEOUTS } from '../constants.js';
|
|
24
|
+
import { createDialogHandler } from './dialog-handler.js';
|
|
24
25
|
|
|
25
26
|
const MAX_TIMEOUT = TIMEOUTS.MAX;
|
|
26
27
|
|
|
@@ -39,9 +40,13 @@ export const WaitCondition = Object.freeze({
|
|
|
39
40
|
/**
|
|
40
41
|
* Create a page controller for navigation and lifecycle events
|
|
41
42
|
* @param {import('../types.js').CDPSession} cdpClient - CDP client with send/on/off methods
|
|
43
|
+
* @param {Object} [options] - Options
|
|
44
|
+
* @param {function(Object): void} [options.onFrameChanged] - Called when frame context changes (for persistence)
|
|
45
|
+
* @param {function(): Object|null} [options.getSavedFrameState] - Returns saved frame state (for restoration)
|
|
42
46
|
* @returns {Object} Page controller interface
|
|
43
47
|
*/
|
|
44
|
-
export function createPageController(cdpClient) {
|
|
48
|
+
export function createPageController(cdpClient, options = {}) {
|
|
49
|
+
const { onFrameChanged, getSavedFrameState } = options;
|
|
45
50
|
let mainFrameId = null;
|
|
46
51
|
let currentFrameId = null;
|
|
47
52
|
let currentExecutionContextId = null;
|
|
@@ -54,6 +59,7 @@ export function createPageController(cdpClient) {
|
|
|
54
59
|
const networkIdleDelay = 500;
|
|
55
60
|
let navigationInProgress = false;
|
|
56
61
|
let currentNavigationAbort = null;
|
|
62
|
+
const dialogHandler = createDialogHandler(cdpClient);
|
|
57
63
|
let currentNavigationUrl = null;
|
|
58
64
|
let pageCrashed = false;
|
|
59
65
|
const crashWaiters = new Set();
|
|
@@ -221,6 +227,54 @@ export function createPageController(cdpClient) {
|
|
|
221
227
|
});
|
|
222
228
|
}
|
|
223
229
|
|
|
230
|
+
/**
|
|
231
|
+
* Best-effort network settle — waits briefly for network to quiet down.
|
|
232
|
+
* Unlike waitForNetworkQuiet, this NEVER throws on timeout. It resolves
|
|
233
|
+
* silently so callers (snapshot, post-navigation) don't fail on sites
|
|
234
|
+
* with persistent connections or long-polling.
|
|
235
|
+
*
|
|
236
|
+
* @param {Object} [options]
|
|
237
|
+
* @param {number} [options.timeout=2000] - Max time to wait
|
|
238
|
+
* @param {number} [options.idleTime=300] - Idle window to consider settled
|
|
239
|
+
* @returns {Promise<{settled: boolean, pendingCount: number}>}
|
|
240
|
+
*/
|
|
241
|
+
function waitForNetworkSettle(options = {}) {
|
|
242
|
+
const { timeout = 2000, idleTime = 300 } = options;
|
|
243
|
+
|
|
244
|
+
return new Promise((resolve) => {
|
|
245
|
+
// Already idle long enough?
|
|
246
|
+
if (pendingRequests.size === 0 && (Date.now() - lastNetworkActivity) >= idleTime) {
|
|
247
|
+
resolve({ settled: true, pendingCount: 0 });
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
let resolved = false;
|
|
252
|
+
let timeoutId = null;
|
|
253
|
+
let checkInterval = null;
|
|
254
|
+
|
|
255
|
+
const finish = (settled) => {
|
|
256
|
+
if (resolved) return;
|
|
257
|
+
resolved = true;
|
|
258
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
259
|
+
if (checkInterval) clearInterval(checkInterval);
|
|
260
|
+
networkIdleWaiters.delete(waiter);
|
|
261
|
+
resolve({ settled, pendingCount: pendingRequests.size });
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const waiter = () => finish(true);
|
|
265
|
+
networkIdleWaiters.add(waiter);
|
|
266
|
+
|
|
267
|
+
timeoutId = setTimeout(() => finish(false), timeout);
|
|
268
|
+
|
|
269
|
+
checkInterval = setInterval(() => {
|
|
270
|
+
if (resolved) { clearInterval(checkInterval); return; }
|
|
271
|
+
if (pendingRequests.size === 0 && (Date.now() - lastNetworkActivity) >= idleTime) {
|
|
272
|
+
finish(true);
|
|
273
|
+
}
|
|
274
|
+
}, 50);
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
224
278
|
/**
|
|
225
279
|
* Get current network status
|
|
226
280
|
* @returns {{pendingCount: number, totalRequests: number, lastActivity: number, isIdle: boolean}}
|
|
@@ -360,6 +414,9 @@ export function createPageController(cdpClient) {
|
|
|
360
414
|
cdpClient.send('Inspector.enable')
|
|
361
415
|
]);
|
|
362
416
|
|
|
417
|
+
// Enable dialog handling for JavaScript alerts, confirms, and prompts
|
|
418
|
+
await dialogHandler.enable();
|
|
419
|
+
|
|
363
420
|
const { frameTree } = await cdpClient.send('Page.getFrameTree');
|
|
364
421
|
mainFrameId = frameTree.frame.id;
|
|
365
422
|
currentFrameId = mainFrameId;
|
|
@@ -388,6 +445,56 @@ export function createPageController(cdpClient) {
|
|
|
388
445
|
addListener('Network.loadingFinished', onRequestFinished);
|
|
389
446
|
addListener('Network.loadingFailed', onRequestFinished);
|
|
390
447
|
addListener('Inspector.targetCrashed', onTargetCrashed);
|
|
448
|
+
|
|
449
|
+
// Restore persisted frame context from a previous CLI invocation
|
|
450
|
+
if (getSavedFrameState) {
|
|
451
|
+
const saved = getSavedFrameState();
|
|
452
|
+
if (saved && saved.frameId && saved.frameId !== mainFrameId) {
|
|
453
|
+
// Verify the saved frame still exists in the frame tree
|
|
454
|
+
function findAllFrames(node) {
|
|
455
|
+
const frames = [node];
|
|
456
|
+
if (node.childFrames) {
|
|
457
|
+
for (const child of node.childFrames) {
|
|
458
|
+
frames.push(...findAllFrames(child));
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return frames;
|
|
462
|
+
}
|
|
463
|
+
const allFrames = findAllFrames(frameTree);
|
|
464
|
+
const savedFrame = allFrames.find(f => f.frame.id === saved.frameId);
|
|
465
|
+
|
|
466
|
+
if (savedFrame) {
|
|
467
|
+
currentFrameId = saved.frameId;
|
|
468
|
+
// Try to use the saved contextId if it's still in our context map
|
|
469
|
+
const knownContextId = frameExecutionContexts.get(saved.frameId);
|
|
470
|
+
if (knownContextId) {
|
|
471
|
+
currentExecutionContextId = knownContextId;
|
|
472
|
+
} else {
|
|
473
|
+
// Create a fresh isolated world for this frame
|
|
474
|
+
try {
|
|
475
|
+
const { executionContextId } = await cdpClient.send('Page.createIsolatedWorld', {
|
|
476
|
+
frameId: saved.frameId,
|
|
477
|
+
worldName: 'cdp-automation'
|
|
478
|
+
});
|
|
479
|
+
currentExecutionContextId = executionContextId;
|
|
480
|
+
frameExecutionContexts.set(saved.frameId, executionContextId);
|
|
481
|
+
} catch {
|
|
482
|
+
// Frame context restoration failed — fall back to main frame
|
|
483
|
+
currentFrameId = mainFrameId;
|
|
484
|
+
currentExecutionContextId = frameExecutionContexts.get(mainFrameId) || null;
|
|
485
|
+
if (onFrameChanged) {
|
|
486
|
+
onFrameChanged({ frameId: null, contextId: null });
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
} else {
|
|
491
|
+
// Frame no longer exists — clear stale state
|
|
492
|
+
if (onFrameChanged) {
|
|
493
|
+
onFrameChanged({ frameId: null, contextId: null });
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
391
498
|
}
|
|
392
499
|
|
|
393
500
|
/**
|
|
@@ -670,7 +777,13 @@ export function createPageController(cdpClient) {
|
|
|
670
777
|
}
|
|
671
778
|
|
|
672
779
|
/**
|
|
673
|
-
* Get frame tree with all iframes
|
|
780
|
+
* Get frame tree with all iframes, including cross-origin ones.
|
|
781
|
+
*
|
|
782
|
+
* Page.getFrameTree only returns same-origin frames. Cross-origin iframes
|
|
783
|
+
* live in separate renderer processes and are invisible to that API.
|
|
784
|
+
* We supplement the CDP tree by querying the DOM for all <iframe> elements
|
|
785
|
+
* and merging any that aren't already represented.
|
|
786
|
+
*
|
|
674
787
|
* @returns {Promise<{mainFrameId: string, currentFrameId: string, frames: Array}>}
|
|
675
788
|
*/
|
|
676
789
|
async function getFrameTree() {
|
|
@@ -694,10 +807,60 @@ export function createPageController(cdpClient) {
|
|
|
694
807
|
return frames;
|
|
695
808
|
}
|
|
696
809
|
|
|
810
|
+
const frames = flattenFrames(frameTree);
|
|
811
|
+
|
|
812
|
+
// Discover cross-origin iframes via DOM query
|
|
813
|
+
try {
|
|
814
|
+
const domResult = await cdpClient.send('Runtime.evaluate', {
|
|
815
|
+
expression: `
|
|
816
|
+
(function() {
|
|
817
|
+
const iframes = document.querySelectorAll('iframe');
|
|
818
|
+
return Array.from(iframes).map(function(el, i) {
|
|
819
|
+
var src = el.src || el.getAttribute('src') || '';
|
|
820
|
+
var name = el.name || el.id || '';
|
|
821
|
+
var crossOrigin = false;
|
|
822
|
+
try { var _d = el.contentDocument; } catch(e) { crossOrigin = true; }
|
|
823
|
+
if (!el.contentDocument) crossOrigin = true;
|
|
824
|
+
return { index: i, src: src, name: name, crossOrigin: crossOrigin };
|
|
825
|
+
});
|
|
826
|
+
})()
|
|
827
|
+
`,
|
|
828
|
+
returnByValue: true
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
const domIframes = domResult.result?.value;
|
|
832
|
+
if (Array.isArray(domIframes)) {
|
|
833
|
+
for (const iframe of domIframes) {
|
|
834
|
+
if (!iframe.crossOrigin) continue;
|
|
835
|
+
|
|
836
|
+
// Check if this iframe is already in the CDP frame tree
|
|
837
|
+
const alreadyListed = frames.some(f =>
|
|
838
|
+
f.parentId && (
|
|
839
|
+
(iframe.src && f.url === iframe.src) ||
|
|
840
|
+
(iframe.name && f.name === iframe.name)
|
|
841
|
+
)
|
|
842
|
+
);
|
|
843
|
+
|
|
844
|
+
if (!alreadyListed) {
|
|
845
|
+
frames.push({
|
|
846
|
+
frameId: `cross-origin-${iframe.index}`,
|
|
847
|
+
url: iframe.src || 'about:blank',
|
|
848
|
+
name: iframe.name || null,
|
|
849
|
+
parentId: mainFrameId,
|
|
850
|
+
depth: 1,
|
|
851
|
+
crossOrigin: true
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
} catch {
|
|
857
|
+
// DOM query failed — return CDP-only tree
|
|
858
|
+
}
|
|
859
|
+
|
|
697
860
|
return {
|
|
698
861
|
mainFrameId,
|
|
699
862
|
currentFrameId,
|
|
700
|
-
frames
|
|
863
|
+
frames
|
|
701
864
|
};
|
|
702
865
|
}
|
|
703
866
|
|
|
@@ -830,6 +993,11 @@ export function createPageController(cdpClient) {
|
|
|
830
993
|
result.warning = warning;
|
|
831
994
|
}
|
|
832
995
|
|
|
996
|
+
// Persist frame state across CLI invocations
|
|
997
|
+
if (onFrameChanged) {
|
|
998
|
+
onFrameChanged({ frameId: currentFrameId, contextId: currentExecutionContextId });
|
|
999
|
+
}
|
|
1000
|
+
|
|
833
1001
|
return result;
|
|
834
1002
|
}
|
|
835
1003
|
|
|
@@ -841,6 +1009,11 @@ export function createPageController(cdpClient) {
|
|
|
841
1009
|
currentFrameId = mainFrameId;
|
|
842
1010
|
currentExecutionContextId = frameExecutionContexts.get(mainFrameId) || null;
|
|
843
1011
|
|
|
1012
|
+
// Clear persisted frame state (back to main)
|
|
1013
|
+
if (onFrameChanged) {
|
|
1014
|
+
onFrameChanged({ frameId: null, contextId: null });
|
|
1015
|
+
}
|
|
1016
|
+
|
|
844
1017
|
const { frameTree } = await cdpClient.send('Page.getFrameTree');
|
|
845
1018
|
|
|
846
1019
|
return {
|
|
@@ -850,6 +1023,63 @@ export function createPageController(cdpClient) {
|
|
|
850
1023
|
};
|
|
851
1024
|
}
|
|
852
1025
|
|
|
1026
|
+
/**
|
|
1027
|
+
* Get the current frame execution context ID (if in a non-main frame).
|
|
1028
|
+
* Used for dependency injection into modules that need frame-aware evaluation.
|
|
1029
|
+
* @returns {number|null} contextId for current frame, or null if in main frame
|
|
1030
|
+
*/
|
|
1031
|
+
function getFrameContext() {
|
|
1032
|
+
if (currentFrameId !== mainFrameId && currentExecutionContextId) {
|
|
1033
|
+
return currentExecutionContextId;
|
|
1034
|
+
}
|
|
1035
|
+
return null;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/**
|
|
1039
|
+
* Get the current frame identifier for ref generation.
|
|
1040
|
+
* Returns 'f0' for main frame, 'f1', 'f2', etc. for iframes by index.
|
|
1041
|
+
* Uses frame name if available for better stability.
|
|
1042
|
+
* @returns {Promise<string>} Frame identifier (e.g., 'f0', 'f1', 'f[frame-name]')
|
|
1043
|
+
*/
|
|
1044
|
+
async function getFrameIdentifier() {
|
|
1045
|
+
// Main frame is always f0
|
|
1046
|
+
if (currentFrameId === mainFrameId || !currentFrameId) {
|
|
1047
|
+
return 'f0';
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// Get frame tree to find index or name
|
|
1051
|
+
const { frameTree } = await cdpClient.send('Page.getFrameTree');
|
|
1052
|
+
|
|
1053
|
+
function findAllChildFrames(node) {
|
|
1054
|
+
const frames = [];
|
|
1055
|
+
if (node.childFrames) {
|
|
1056
|
+
for (const child of node.childFrames) {
|
|
1057
|
+
frames.push(child);
|
|
1058
|
+
frames.push(...findAllChildFrames(child));
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
return frames;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
const childFrames = findAllChildFrames(frameTree);
|
|
1065
|
+
|
|
1066
|
+
// Find current frame
|
|
1067
|
+
for (let i = 0; i < childFrames.length; i++) {
|
|
1068
|
+
if (childFrames[i].frame.id === currentFrameId) {
|
|
1069
|
+
// Prefer name if available (more stable than index)
|
|
1070
|
+
const frameName = childFrames[i].frame.name;
|
|
1071
|
+
if (frameName) {
|
|
1072
|
+
return `f[${frameName}]`;
|
|
1073
|
+
}
|
|
1074
|
+
// Fall back to index (1-based for iframes)
|
|
1075
|
+
return `f${i + 1}`;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// Fallback: unknown frame, use hash of frameId
|
|
1080
|
+
return `f[${currentFrameId.substring(0, 8)}]`;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
853
1083
|
/**
|
|
854
1084
|
* Execute code in the current frame context
|
|
855
1085
|
* @param {string} expression - JavaScript expression
|
|
@@ -1106,8 +1336,11 @@ export function createPageController(cdpClient) {
|
|
|
1106
1336
|
waitForNavigationEvent,
|
|
1107
1337
|
withNavigation,
|
|
1108
1338
|
waitForNetworkQuiet,
|
|
1339
|
+
waitForNetworkSettle,
|
|
1109
1340
|
getNetworkStatus,
|
|
1110
1341
|
searchAllFrames,
|
|
1342
|
+
getFrameContext,
|
|
1343
|
+
getFrameIdentifier,
|
|
1111
1344
|
dispose,
|
|
1112
1345
|
get mainFrameId() { return mainFrameId; },
|
|
1113
1346
|
get currentFrameId() { return currentFrameId; },
|
|
@@ -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', {
|