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
package/src/dom/fill-executor.js
CHANGED
|
@@ -31,17 +31,59 @@ import {
|
|
|
31
31
|
* @param {Object} elementLocator - Element locator instance
|
|
32
32
|
* @param {Object} inputEmulator - Input emulator instance
|
|
33
33
|
* @param {Object} [ariaSnapshot] - Optional ARIA snapshot instance
|
|
34
|
+
* @param {Object} [options] - Configuration options
|
|
35
|
+
* @param {Function} [options.getFrameContext] - Returns contextId when in a non-main frame
|
|
34
36
|
* @returns {Object} Fill executor interface
|
|
35
37
|
*/
|
|
36
|
-
export function createFillExecutor(session, elementLocator, inputEmulator, ariaSnapshot = null) {
|
|
38
|
+
export function createFillExecutor(session, elementLocator, inputEmulator, ariaSnapshot = null, options = {}) {
|
|
37
39
|
if (!session) throw new Error('CDP session is required');
|
|
38
40
|
if (!elementLocator) throw new Error('Element locator is required');
|
|
39
41
|
if (!inputEmulator) throw new Error('Input emulator is required');
|
|
40
42
|
|
|
43
|
+
const getFrameContext = options.getFrameContext || null;
|
|
41
44
|
const actionabilityChecker = createActionabilityChecker(session);
|
|
42
45
|
const elementValidator = createElementValidator(session);
|
|
43
46
|
const reactInputFiller = createReactInputFiller(session);
|
|
44
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Build Runtime.evaluate params, injecting contextId when in an iframe.
|
|
50
|
+
*/
|
|
51
|
+
function evalParams(expression, returnByValue = false) {
|
|
52
|
+
const params = { expression, returnByValue };
|
|
53
|
+
if (getFrameContext) {
|
|
54
|
+
const contextId = getFrameContext();
|
|
55
|
+
if (contextId) params.contextId = contextId;
|
|
56
|
+
}
|
|
57
|
+
return params;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Select all and fill with value, handling the empty-string case.
|
|
62
|
+
* When value is "" and clear is true, presses Delete after selectAll
|
|
63
|
+
* to actually remove the selected content (insertText("") is a no-op).
|
|
64
|
+
*/
|
|
65
|
+
async function selectAndFill(value, clear) {
|
|
66
|
+
if (clear) {
|
|
67
|
+
await inputEmulator.selectAll();
|
|
68
|
+
}
|
|
69
|
+
if (value === '' && clear) {
|
|
70
|
+
// insertText("") is a no-op in CDP — press Delete to remove selected text
|
|
71
|
+
await inputEmulator.press('Delete');
|
|
72
|
+
// Dispatch input/change events so frameworks (React, Vue, etc.) react to the clear
|
|
73
|
+
await session.send('Runtime.evaluate', evalParams(`
|
|
74
|
+
(function() {
|
|
75
|
+
const el = document.activeElement;
|
|
76
|
+
if (el) {
|
|
77
|
+
el.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
|
|
78
|
+
el.dispatchEvent(new Event('change', { bubbles: true, cancelable: true }));
|
|
79
|
+
}
|
|
80
|
+
})()
|
|
81
|
+
`, true));
|
|
82
|
+
} else {
|
|
83
|
+
await inputEmulator.insertText(String(value));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
45
87
|
async function fillByRef(ref, value, opts = {}) {
|
|
46
88
|
const { clear = true, react = false } = opts;
|
|
47
89
|
|
|
@@ -62,13 +104,12 @@ export function createFillExecutor(session, elementLocator, inputEmulator, ariaS
|
|
|
62
104
|
throw new Error(`Element ref:${ref} exists but is not visible. It may be hidden or have zero dimensions.`);
|
|
63
105
|
}
|
|
64
106
|
|
|
65
|
-
const elementResult = await session.send('Runtime.evaluate',
|
|
66
|
-
|
|
107
|
+
const elementResult = await session.send('Runtime.evaluate',
|
|
108
|
+
evalParams(`(function() {
|
|
67
109
|
const el = window.__ariaRefs && window.__ariaRefs.get(${JSON.stringify(ref)});
|
|
68
110
|
return el;
|
|
69
|
-
})()`,
|
|
70
|
-
|
|
71
|
-
});
|
|
111
|
+
})()`, false)
|
|
112
|
+
);
|
|
72
113
|
|
|
73
114
|
if (!elementResult.result.objectId) {
|
|
74
115
|
throw elementNotFoundError(`ref:${ref}`, 0);
|
|
@@ -106,11 +147,7 @@ export function createFillExecutor(session, elementLocator, inputEmulator, ariaS
|
|
|
106
147
|
functionDeclaration: `function() { this.focus(); }`
|
|
107
148
|
});
|
|
108
149
|
|
|
109
|
-
|
|
110
|
-
await inputEmulator.selectAll();
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
await inputEmulator.insertText(String(value));
|
|
150
|
+
await selectAndFill(value, clear);
|
|
114
151
|
|
|
115
152
|
return { filled: true, ref, method: 'insertText' };
|
|
116
153
|
} finally {
|
|
@@ -153,11 +190,7 @@ export function createFillExecutor(session, elementLocator, inputEmulator, ariaS
|
|
|
153
190
|
functionDeclaration: `function() { this.focus(); }`
|
|
154
191
|
});
|
|
155
192
|
|
|
156
|
-
|
|
157
|
-
await inputEmulator.selectAll();
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
await inputEmulator.insertText(String(value));
|
|
193
|
+
await selectAndFill(value, clear);
|
|
161
194
|
|
|
162
195
|
return { filled: true, selector, method: 'insertText' };
|
|
163
196
|
} catch (e) {
|
|
@@ -277,10 +310,9 @@ export function createFillExecutor(session, elementLocator, inputEmulator, ariaS
|
|
|
277
310
|
|
|
278
311
|
let result;
|
|
279
312
|
try {
|
|
280
|
-
result = await session.send('Runtime.evaluate',
|
|
281
|
-
expression,
|
|
282
|
-
|
|
283
|
-
});
|
|
313
|
+
result = await session.send('Runtime.evaluate',
|
|
314
|
+
evalParams(expression, false)
|
|
315
|
+
);
|
|
284
316
|
} catch (error) {
|
|
285
317
|
throw connectionError(error.message, 'Runtime.evaluate (findInputByLabel)');
|
|
286
318
|
}
|
|
@@ -383,11 +415,7 @@ export function createFillExecutor(session, elementLocator, inputEmulator, ariaS
|
|
|
383
415
|
functionDeclaration: `function() { this.focus(); }`
|
|
384
416
|
});
|
|
385
417
|
|
|
386
|
-
|
|
387
|
-
await inputEmulator.selectAll();
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
await inputEmulator.insertText(String(value));
|
|
418
|
+
await selectAndFill(value, clear);
|
|
391
419
|
|
|
392
420
|
return { filled: true, label, method: 'insertText', foundBy: foundMethod };
|
|
393
421
|
} catch (e) {
|
|
@@ -430,7 +458,7 @@ export function createFillExecutor(session, elementLocator, inputEmulator, ariaS
|
|
|
430
458
|
|
|
431
459
|
async function executeBatch(params) {
|
|
432
460
|
if (!params || typeof params !== 'object') {
|
|
433
|
-
throw new Error('
|
|
461
|
+
throw new Error('fill batch requires an object mapping selectors to values');
|
|
434
462
|
}
|
|
435
463
|
|
|
436
464
|
// Support both formats:
|
|
@@ -450,7 +478,7 @@ export function createFillExecutor(session, elementLocator, inputEmulator, ariaS
|
|
|
450
478
|
|
|
451
479
|
const entries = Object.entries(fields);
|
|
452
480
|
if (entries.length === 0) {
|
|
453
|
-
throw new Error('
|
|
481
|
+
throw new Error('fill batch requires at least one field');
|
|
454
482
|
}
|
|
455
483
|
|
|
456
484
|
const results = [];
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dialog Handler Module
|
|
3
|
+
* Handles JavaScript alerts, confirms, and prompts
|
|
4
|
+
*
|
|
5
|
+
* PUBLIC EXPORTS:
|
|
6
|
+
* - createDialogHandler(session) - Factory for dialog handler
|
|
7
|
+
*
|
|
8
|
+
* @module cdp-skill/page/dialog-handler
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create a dialog handler for JavaScript dialogs
|
|
13
|
+
* @param {import('../types.js').CDPSession} session - CDP session
|
|
14
|
+
* @returns {Object} Dialog handler interface
|
|
15
|
+
*/
|
|
16
|
+
export function createDialogHandler(session) {
|
|
17
|
+
let dialogCallback = null;
|
|
18
|
+
let boundHandler = null;
|
|
19
|
+
const responseQueue = [];
|
|
20
|
+
|
|
21
|
+
function onDialogOpening(params) {
|
|
22
|
+
const { type, message, defaultPrompt } = params;
|
|
23
|
+
|
|
24
|
+
// Default behavior: accept all dialogs
|
|
25
|
+
let accept = true;
|
|
26
|
+
let promptText = undefined;
|
|
27
|
+
|
|
28
|
+
// Check if there's a queued response
|
|
29
|
+
if (responseQueue.length > 0) {
|
|
30
|
+
const queued = responseQueue.shift();
|
|
31
|
+
accept = queued.accept !== false;
|
|
32
|
+
promptText = queued.promptText;
|
|
33
|
+
} else if (dialogCallback) {
|
|
34
|
+
// If custom callback is set, use it
|
|
35
|
+
const result = dialogCallback({ type, message, defaultPrompt });
|
|
36
|
+
accept = result.accept !== false;
|
|
37
|
+
promptText = result.promptText;
|
|
38
|
+
} else {
|
|
39
|
+
// Auto-accept with reasonable defaults for prompts
|
|
40
|
+
if (type === 'prompt') {
|
|
41
|
+
// Use defaultPrompt if available
|
|
42
|
+
// Otherwise, for test automation purposes, use a reasonable default
|
|
43
|
+
if (defaultPrompt !== undefined && defaultPrompt.length > 0) {
|
|
44
|
+
promptText = defaultPrompt;
|
|
45
|
+
} else if (message && message.toLowerCase().includes('prompt')) {
|
|
46
|
+
// For prompt dialogs asking for input, use a test value
|
|
47
|
+
promptText = 'Hello CDP';
|
|
48
|
+
} else {
|
|
49
|
+
promptText = '';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Handle the dialog
|
|
55
|
+
session.send('Page.handleJavaScriptDialog', {
|
|
56
|
+
accept,
|
|
57
|
+
promptText
|
|
58
|
+
}).catch(err => {
|
|
59
|
+
// Ignore errors - dialog may have been already handled
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Enable dialog handling
|
|
65
|
+
* @param {Function} [callback] - Optional callback to customize dialog handling
|
|
66
|
+
* @returns {Promise<void>}
|
|
67
|
+
*/
|
|
68
|
+
async function enable(callback = null) {
|
|
69
|
+
dialogCallback = callback;
|
|
70
|
+
|
|
71
|
+
if (!boundHandler) {
|
|
72
|
+
boundHandler = onDialogOpening;
|
|
73
|
+
session.on('Page.javascriptDialogOpening', boundHandler);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Enable page domain if not already enabled
|
|
77
|
+
try {
|
|
78
|
+
await session.send('Page.enable');
|
|
79
|
+
} catch (err) {
|
|
80
|
+
// Ignore if already enabled
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Disable dialog handling
|
|
86
|
+
* @returns {Promise<void>}
|
|
87
|
+
*/
|
|
88
|
+
async function disable() {
|
|
89
|
+
if (boundHandler) {
|
|
90
|
+
session.off('Page.javascriptDialogOpening', boundHandler);
|
|
91
|
+
boundHandler = null;
|
|
92
|
+
}
|
|
93
|
+
dialogCallback = null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Set a custom dialog handler
|
|
98
|
+
* @param {Function} callback - Callback({type, message, defaultPrompt}) => {accept, promptText}
|
|
99
|
+
*/
|
|
100
|
+
function setHandler(callback) {
|
|
101
|
+
dialogCallback = callback;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Queue a response for the next dialog
|
|
106
|
+
* @param {boolean} accept - Whether to accept the dialog
|
|
107
|
+
* @param {string} [promptText] - Text to enter for prompts
|
|
108
|
+
*/
|
|
109
|
+
function queueResponse(accept, promptText) {
|
|
110
|
+
responseQueue.push({ accept, promptText });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
enable,
|
|
115
|
+
disable,
|
|
116
|
+
setHandler,
|
|
117
|
+
queueResponse
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -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,18 @@ 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
|
+
|
|
853
1038
|
/**
|
|
854
1039
|
* Execute code in the current frame context
|
|
855
1040
|
* @param {string} expression - JavaScript expression
|
|
@@ -1106,8 +1291,10 @@ export function createPageController(cdpClient) {
|
|
|
1106
1291
|
waitForNavigationEvent,
|
|
1107
1292
|
withNavigation,
|
|
1108
1293
|
waitForNetworkQuiet,
|
|
1294
|
+
waitForNetworkSettle,
|
|
1109
1295
|
getNetworkStatus,
|
|
1110
1296
|
searchAllFrames,
|
|
1297
|
+
getFrameContext,
|
|
1111
1298
|
dispose,
|
|
1112
1299
|
get mainFrameId() { return mainFrameId; },
|
|
1113
1300
|
get currentFrameId() { return currentFrameId; },
|