browser-debugger-cli 0.9.0 → 0.11.0
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/.claude/skills/bdg/SKILL.md +268 -0
- package/README.md +15 -1
- package/dist/commands/dom/a11y.js +2 -1
- package/dist/commands/dom/formInteraction.js +56 -25
- package/dist/commands/dom/helpers/keyAttributes.d.ts +20 -0
- package/dist/commands/dom/helpers/keyAttributes.js +54 -0
- package/dist/commands/dom/helpers/query.d.ts +1 -1
- package/dist/commands/dom/helpers/query.js +66 -19
- package/dist/commands/dom/helpers/runElementCommand.js +4 -3
- package/dist/commands/dom/helpers/screenshot.js +85 -12
- package/dist/commands/dom/index.d.ts +1 -0
- package/dist/commands/dom/index.js +8 -3
- package/dist/commands/dom/inspect.d.ts +15 -0
- package/dist/commands/dom/inspect.js +82 -0
- package/dist/commands/dom/layout.js +2 -2
- package/dist/commands/dom/listeners.js +2 -2
- package/dist/commands/dom/semanticUtils.d.ts +14 -1
- package/dist/commands/dom/semanticUtils.js +44 -3
- package/dist/commands/installSkill.d.ts +20 -0
- package/dist/commands/installSkill.js +87 -0
- package/dist/commands/network/list.js +13 -2
- package/dist/commands/optionBehaviors.js +48 -6
- package/dist/commands/page.d.ts +1 -1
- package/dist/commands/page.js +62 -3
- package/dist/commands/shared/commonOptions.d.ts +4 -0
- package/dist/commands/shared/commonOptions.js +9 -0
- package/dist/commands/shared/optionTypes.d.ts +21 -0
- package/dist/commands/shared/startHelpers.d.ts +66 -0
- package/dist/commands/shared/startHelpers.js +91 -10
- package/dist/commands/shared/validation.d.ts +11 -0
- package/dist/commands/shared/validation.js +16 -0
- package/dist/commands.js +3 -0
- package/dist/daemon/launcher.d.ts +8 -1
- package/dist/daemon/launcher.js +3 -1
- package/dist/daemon/session/Session.d.ts +7 -0
- package/dist/daemon/session/Session.js +23 -1
- package/dist/daemon/session/commandRegistry.d.ts +14 -1
- package/dist/daemon/session/commandRegistry.js +65 -9
- package/dist/daemon/session/interactions.d.ts +18 -5
- package/dist/daemon/session/interactions.js +22 -12
- package/dist/daemon.js +3565 -329
- package/dist/errors/messages.d.ts +85 -0
- package/dist/errors/messages.js +128 -1
- package/dist/index.js +2151 -960
- package/dist/ipc/client.d.ts +9 -0
- package/dist/ipc/client.js +13 -0
- package/dist/ipc/protocol/commands.d.ts +56 -1
- package/dist/ipc/protocol/commands.js +2 -0
- package/dist/ipc/protocol/domTypes.d.ts +35 -2
- package/dist/ipc/protocol/inspectTypes.d.ts +388 -0
- package/dist/ipc/protocol/inspectTypes.js +10 -0
- package/dist/runtime/dom/actionEffects.d.ts +94 -15
- package/dist/runtime/dom/actionEffects.js +173 -27
- package/dist/runtime/dom/actionEffectsScripts.d.ts +52 -14
- package/dist/runtime/dom/actionEffectsScripts.js +224 -32
- package/dist/runtime/dom/elementInfo.d.ts +26 -0
- package/dist/runtime/dom/elementInfo.js +65 -0
- package/dist/runtime/dom/eventListeners.js +14 -4
- package/dist/runtime/dom/formFillHelpers/fill.d.ts +3 -4
- package/dist/runtime/dom/formFillHelpers/fill.js +77 -28
- package/dist/runtime/dom/frameSelection.d.ts +11 -0
- package/dist/runtime/dom/frameSelection.js +20 -1
- package/dist/runtime/dom/frames.d.ts +38 -5
- package/dist/runtime/dom/frames.js +136 -21
- package/dist/runtime/dom/inspect.d.ts +28 -0
- package/dist/runtime/dom/inspect.js +557 -0
- package/dist/runtime/dom/inspectAllStyles.d.ts +62 -0
- package/dist/runtime/dom/inspectAllStyles.js +385 -0
- package/dist/runtime/dom/inspectCascade.d.ts +94 -0
- package/dist/runtime/dom/inspectCascade.js +371 -0
- package/dist/runtime/dom/inspectCascadeModel.d.ts +39 -0
- package/dist/runtime/dom/inspectCascadeModel.js +232 -0
- package/dist/runtime/dom/inspectHints.d.ts +62 -0
- package/dist/runtime/dom/inspectHints.js +305 -0
- package/dist/runtime/dom/inspectLayoutModel.d.ts +87 -0
- package/dist/runtime/dom/inspectLayoutModel.js +346 -0
- package/dist/runtime/dom/inspectModel.d.ts +74 -0
- package/dist/runtime/dom/inspectModel.js +184 -0
- package/dist/runtime/dom/inspectPaintModel.d.ts +157 -0
- package/dist/runtime/dom/inspectPaintModel.js +461 -0
- package/dist/runtime/dom/inspectRules.d.ts +37 -0
- package/dist/runtime/dom/inspectRules.js +101 -0
- package/dist/runtime/dom/inspectScripts.d.ts +132 -0
- package/dist/runtime/dom/inspectScripts.js +263 -0
- package/dist/runtime/dom/inspectTree.d.ts +40 -0
- package/dist/runtime/dom/inspectTree.js +134 -0
- package/dist/runtime/dom/inspectVariables.d.ts +33 -0
- package/dist/runtime/dom/inspectVariables.js +94 -0
- package/dist/runtime/dom/inspectWhyModel.d.ts +20 -0
- package/dist/runtime/dom/inspectWhyModel.js +134 -0
- package/dist/runtime/dom/layout.d.ts +5 -1
- package/dist/runtime/dom/layout.js +10 -3
- package/dist/runtime/dom/listenerPageScripts.d.ts +11 -5
- package/dist/runtime/dom/listenerPageScripts.js +95 -9
- package/dist/runtime/dom/listenerSummary.d.ts +4 -0
- package/dist/runtime/dom/listenerSummary.js +26 -9
- package/dist/runtime/dom/reactEventHelpers.d.ts +5 -0
- package/dist/runtime/dom/reactEventHelpers.js +12 -4
- package/dist/runtime/page/emulation.d.ts +20 -0
- package/dist/runtime/page/emulation.js +37 -0
- package/dist/telemetry/a11y.d.ts +10 -0
- package/dist/telemetry/a11y.js +78 -1
- package/dist/telemetry/console.d.ts +1 -0
- package/dist/telemetry/console.js +100 -5
- package/dist/telemetry/network.js +3 -1
- package/dist/types.d.ts +40 -0
- package/dist/ui/formatters/details.d.ts +8 -0
- package/dist/ui/formatters/details.js +59 -3
- package/dist/ui/formatters/dom.d.ts +2 -1
- package/dist/ui/formatters/dom.js +25 -9
- package/dist/ui/formatters/inspect.d.ts +39 -0
- package/dist/ui/formatters/inspect.js +596 -0
- package/dist/ui/formatters/installSkill.d.ts +11 -0
- package/dist/ui/formatters/installSkill.js +31 -0
- package/dist/ui/formatters/keyAttributes.d.ts +19 -0
- package/dist/ui/formatters/keyAttributes.js +84 -0
- package/dist/ui/formatters/layout.js +2 -2
- package/dist/ui/formatters/networkHeaders.d.ts +13 -0
- package/dist/ui/formatters/networkHeaders.js +23 -3
- package/dist/ui/formatters/networkList.d.ts +29 -1
- package/dist/ui/formatters/networkList.js +86 -20
- package/dist/ui/formatters/status.js +1 -1
- package/dist/ui/formatting.d.ts +9 -0
- package/dist/ui/formatting.js +6 -3
- package/dist/ui/messages/commands.d.ts +123 -7
- package/dist/ui/messages/commands.js +181 -10
- package/dist/ui/messages/networkMessages.d.ts +14 -0
- package/dist/ui/messages/networkMessages.js +18 -0
- package/dist/ui/messages/session.d.ts +14 -0
- package/dist/ui/messages/session.js +20 -0
- package/dist/utils/async.d.ts +9 -0
- package/dist/utils/async.js +17 -0
- package/dist/utils/color.d.ts +84 -0
- package/dist/utils/color.js +376 -0
- package/dist/utils/cssValues.d.ts +109 -0
- package/dist/utils/cssValues.js +236 -0
- package/dist/utils/selectorFilters.d.ts +12 -0
- package/dist/utils/selectorFilters.js +29 -0
- package/package.json +2 -1
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
import * as fs from 'node:fs';
|
|
6
6
|
import * as path from 'node:path';
|
|
7
7
|
import { CommandError } from '../../../errors/index.js';
|
|
8
|
-
import { fileNotFoundError, uploadDirectoryError, singleFileInputError, fillableElementNotFoundError, clickableElementNotFoundError, clickTargetDetachedError, unexpectedResponseFormatError, operationFailedError, } from '../../../errors/messages.js';
|
|
8
|
+
import { fileNotFoundError, uploadDirectoryError, singleFileInputError, fillableElementNotFoundError, clickableElementNotFoundError, clickTargetDetachedError, unexpectedResponseFormatError, operationFailedError, pressNotReceivedError, unreachableElementError, } from '../../../errors/messages.js';
|
|
9
9
|
import { escapeValueForJS, formatScriptExecutionError, throwIfInvalidSelector, withMultipleMatchesWarning, withValueMismatchWarning, } from './shared.js';
|
|
10
10
|
import { FILL_READ_BACK_SCRIPT, REACT_FILL_SCRIPT, CLICK_ELEMENT_SCRIPT, isFillResult, isClickResult, } from '../reactEventHelpers.js';
|
|
11
11
|
import { FIND_ELEMENTS_JS, LABEL_CONTROL_JS, selectorArgsJS } from '../targetNode.js';
|
|
12
12
|
import { createLogger } from '../../../ui/logging/index.js';
|
|
13
|
-
import { CLICK_NOT_RECEIVED_WARNING, POINTER_ACTION_DONE, domClickFallbackWarning, } from '../../../ui/messages/commands.js';
|
|
13
|
+
import { CLICK_NOT_RECEIVED_WARNING, POINTER_ACTION_DONE, POINTER_ACTION_NOUN, domClickFallbackWarning, } from '../../../ui/messages/commands.js';
|
|
14
14
|
import { getErrorMessage } from '../../../utils/errors.js';
|
|
15
15
|
import { EXIT_CODES } from '../../../utils/exitCodes.js';
|
|
16
16
|
const log = createLogger('dom');
|
|
@@ -266,64 +266,84 @@ export function mouseEvents(action, x, y) {
|
|
|
266
266
|
}
|
|
267
267
|
/**
|
|
268
268
|
* Page script that stops the press probe installed by CLICK_ELEMENT_SCRIPT
|
|
269
|
-
* and evaluates to whether the mouse press reached the target
|
|
269
|
+
* and evaluates to whether the mouse press reached the target and, when it
|
|
270
|
+
* did not, the element it landed on (if the page saw it at all).
|
|
270
271
|
*/
|
|
271
272
|
const PRESS_PROBE_READ_SCRIPT = `(() => {
|
|
272
273
|
const probe = window.__bdgPressProbe;
|
|
273
274
|
delete window.__bdgPressProbe;
|
|
274
|
-
if (!probe) return true;
|
|
275
|
+
if (!probe) return { reached: true };
|
|
275
276
|
probe.stop();
|
|
276
|
-
return probe.reached;
|
|
277
|
+
return { reached: probe.reached, landedOn: probe.landedOn };
|
|
277
278
|
})()`;
|
|
278
279
|
/**
|
|
279
|
-
*
|
|
280
|
+
* Read the press probe: whether the mouse press just dispatched reached the
|
|
281
|
+
* target element, and otherwise where it landed.
|
|
280
282
|
*
|
|
281
283
|
* Any doubt (no probe, evaluation failure) counts as reached, so only a
|
|
282
284
|
* press that the page provably never saw is reported.
|
|
283
285
|
*
|
|
284
286
|
* @param cdp - CDP connection
|
|
285
|
-
* @returns
|
|
287
|
+
* @returns Outcome; not reached only when the target received no pointerdown/mousedown
|
|
286
288
|
*/
|
|
287
|
-
|
|
289
|
+
async function readPressProbe(cdp) {
|
|
288
290
|
try {
|
|
289
291
|
const response = (await cdp.send('Runtime.evaluate', {
|
|
290
292
|
expression: PRESS_PROBE_READ_SCRIPT,
|
|
291
293
|
returnByValue: true,
|
|
292
294
|
}));
|
|
293
|
-
|
|
295
|
+
const value = response.result?.value;
|
|
296
|
+
if (value?.reached !== false)
|
|
297
|
+
return { reached: true };
|
|
298
|
+
return typeof value.landedOn === 'string'
|
|
299
|
+
? { reached: false, landedOn: value.landedOn }
|
|
300
|
+
: { reached: false };
|
|
294
301
|
}
|
|
295
302
|
catch (error) {
|
|
296
303
|
log.debug(`Press probe not read: ${getErrorMessage(error)}`);
|
|
297
|
-
return true;
|
|
304
|
+
return { reached: true };
|
|
298
305
|
}
|
|
299
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* Whether the mouse press just dispatched reached the target element
|
|
309
|
+
* (see {@link readPressProbe}).
|
|
310
|
+
*
|
|
311
|
+
* @param cdp - CDP connection
|
|
312
|
+
* @returns False only when the target received no pointerdown/mousedown
|
|
313
|
+
*/
|
|
314
|
+
export async function pressReachedTarget(cdp) {
|
|
315
|
+
return (await readPressProbe(cdp)).reached;
|
|
316
|
+
}
|
|
300
317
|
/**
|
|
301
318
|
* Dispatch real mouse events for an action, checking after the first press
|
|
302
|
-
* that the target received it.
|
|
303
|
-
*
|
|
319
|
+
* that the target received it. With `stopIfMissed`, a press that missed is
|
|
320
|
+
* only released (no further presses). If dispatching fails first, the press
|
|
321
|
+
* probe is still removed from the page.
|
|
304
322
|
*
|
|
305
323
|
* @param cdp - CDP connection
|
|
306
324
|
* @param action - What to do
|
|
307
|
-
* @param
|
|
308
|
-
* @param
|
|
309
|
-
* @returns
|
|
325
|
+
* @param point - Page coordinates
|
|
326
|
+
* @param stopIfMissed - Stop after releasing a press that missed (--strict)
|
|
327
|
+
* @returns Whether the first press reached the target, and where it landed otherwise
|
|
310
328
|
*/
|
|
311
|
-
async function dispatchMouseAction(cdp, action,
|
|
312
|
-
let
|
|
329
|
+
async function dispatchMouseAction(cdp, action, point, stopIfMissed) {
|
|
330
|
+
let outcome;
|
|
313
331
|
try {
|
|
314
|
-
for (const event of mouseEvents(action, x, y)) {
|
|
332
|
+
for (const event of mouseEvents(action, point.x, point.y)) {
|
|
315
333
|
await cdp.send('Input.dispatchMouseEvent', event);
|
|
316
|
-
if (event['type'] === '
|
|
317
|
-
|
|
334
|
+
if (event['type'] === 'mouseReleased' && outcome?.reached === false && stopIfMissed)
|
|
335
|
+
break;
|
|
336
|
+
if (event['type'] === 'mousePressed' && outcome === undefined) {
|
|
337
|
+
outcome = await readPressProbe(cdp);
|
|
318
338
|
}
|
|
319
339
|
}
|
|
320
340
|
}
|
|
321
341
|
catch (error) {
|
|
322
|
-
if (
|
|
323
|
-
await
|
|
342
|
+
if (outcome === undefined)
|
|
343
|
+
await readPressProbe(cdp);
|
|
324
344
|
throw error;
|
|
325
345
|
}
|
|
326
|
-
return
|
|
346
|
+
return outcome ?? { reached: true };
|
|
327
347
|
}
|
|
328
348
|
/**
|
|
329
349
|
* Drop the located element from the page without waiting: right after a
|
|
@@ -337,34 +357,63 @@ function releaseClickTarget(cdp) {
|
|
|
337
357
|
.send('Runtime.evaluate', { expression: 'delete window.__bdgClickTarget' })
|
|
338
358
|
.catch((error) => log.debug(`Click target not released: ${getErrorMessage(error)}`));
|
|
339
359
|
}
|
|
360
|
+
/**
|
|
361
|
+
* Refuse a pointer action under `--strict` (exit 90, the page's state
|
|
362
|
+
* conflicts with the request): the element is unreachable by the mouse, or
|
|
363
|
+
* the press never reached it.
|
|
364
|
+
*
|
|
365
|
+
* @param result - Located click (selector and element description)
|
|
366
|
+
* @param action - What was refused
|
|
367
|
+
* @param why - Why the mouse could not reach it (nothing was sent), or the
|
|
368
|
+
* press that was sent and missed
|
|
369
|
+
* @returns Never
|
|
370
|
+
* @throws CommandError always
|
|
371
|
+
*/
|
|
372
|
+
function refuseUnreachable(result, action, why) {
|
|
373
|
+
const target = { selector: result.selector ?? '', element: result.element };
|
|
374
|
+
const verb = POINTER_ACTION_NOUN[action];
|
|
375
|
+
const err = 'missed' in why
|
|
376
|
+
? pressNotReceivedError(target, verb, why.missed.landedOn)
|
|
377
|
+
: unreachableElementError(target, why.obstruction, verb);
|
|
378
|
+
throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.RESOURCE_CONFLICT);
|
|
379
|
+
}
|
|
340
380
|
/**
|
|
341
381
|
* Click a located element.
|
|
342
382
|
*
|
|
343
383
|
* Uses real mouse events (pointerdown/mousedown/pointerup/mouseup/click, all
|
|
344
384
|
* trusted) at the element's center when it is the topmost element there, so
|
|
345
385
|
* components that react to pointer or mouse events (menus, selects) respond.
|
|
346
|
-
* Otherwise (covered or zero-size) falls back to `el.click()` and says so
|
|
386
|
+
* Otherwise (covered or zero-size) falls back to `el.click()` and says so,
|
|
387
|
+
* or with `strict` refuses (as it does when the press never reached it).
|
|
347
388
|
* Double and right clicks, and hovering, work the same way.
|
|
348
389
|
*
|
|
349
390
|
* @param cdp - CDP connection
|
|
350
391
|
* @param located - Locate result with center point
|
|
351
392
|
* @param action - Click, double click, right click or hover
|
|
393
|
+
* @param strict - Refuse instead of falling back to DOM events
|
|
352
394
|
* @returns Click result
|
|
395
|
+
* @throws CommandError under `strict` when a user could not reach the element
|
|
353
396
|
*/
|
|
354
|
-
async function performClick(cdp, located, action) {
|
|
397
|
+
async function performClick(cdp, located, action, strict) {
|
|
355
398
|
const { x, y, hittable, obstruction, ...result } = located;
|
|
356
399
|
if (!result.success)
|
|
357
400
|
return result;
|
|
358
401
|
if (hittable && x !== undefined && y !== undefined) {
|
|
359
|
-
const
|
|
402
|
+
const press = await dispatchMouseAction(cdp, action, { x, y }, strict);
|
|
360
403
|
releaseClickTarget(cdp);
|
|
404
|
+
if (!press.reached && strict)
|
|
405
|
+
refuseUnreachable(result, action, { missed: press });
|
|
361
406
|
return withMultipleMatchesWarning({
|
|
362
407
|
...result,
|
|
363
408
|
action,
|
|
364
409
|
method: 'mouse',
|
|
365
|
-
...(!reached && { warning: CLICK_NOT_RECEIVED_WARNING }),
|
|
410
|
+
...(!press.reached && { warning: CLICK_NOT_RECEIVED_WARNING }),
|
|
366
411
|
}, result.selectedIndex, `${POINTER_ACTION_DONE[action].toLowerCase()} the first visible one`);
|
|
367
412
|
}
|
|
413
|
+
if (strict) {
|
|
414
|
+
releaseClickTarget(cdp);
|
|
415
|
+
refuseUnreachable(result, action, { obstruction: obstruction ?? null });
|
|
416
|
+
}
|
|
368
417
|
const response = (await cdp.send('Runtime.evaluate', {
|
|
369
418
|
expression: domFallbackScript(action),
|
|
370
419
|
returnByValue: true,
|
|
@@ -397,7 +446,7 @@ export async function clickElement(cdp, selector, options = {}) {
|
|
|
397
446
|
throw new CommandError(errorMessage, { suggestion: err.suggestion }, EXIT_CODES.SOFTWARE_ERROR);
|
|
398
447
|
}
|
|
399
448
|
if (cdpResponse.result?.value && isClickResult(cdpResponse.result.value)) {
|
|
400
|
-
return await performClick(cdp, cdpResponse.result.value, action);
|
|
449
|
+
return await performClick(cdp, cdpResponse.result.value, action, options.strict === true);
|
|
401
450
|
}
|
|
402
451
|
const err = unexpectedResponseFormatError('ClickResult');
|
|
403
452
|
throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.SOFTWARE_ERROR);
|
|
@@ -13,6 +13,17 @@ import type { DomFrame } from '../../ipc/protocol/commands.js';
|
|
|
13
13
|
* @throws CommandError (81) when empty or ambiguous, (83) when nothing matches
|
|
14
14
|
*/
|
|
15
15
|
export declare function selectFrame(frames: DomFrame[], query: string): DomFrame;
|
|
16
|
+
/**
|
|
17
|
+
* Check that a `--frame` index still names the frame the last
|
|
18
|
+
* `bdg dom frames` listed at that index (frames added, removed or
|
|
19
|
+
* reordered since shift the indices; a navigation replaces every frame).
|
|
20
|
+
*
|
|
21
|
+
* @param query - Requested frame
|
|
22
|
+
* @param currentIds - Frame id of each frame now, by index
|
|
23
|
+
* @param listedIds - Frame id of each frame when last listed, undefined when never listed
|
|
24
|
+
* @throws CommandError (87) when the index names another frame (or none) now
|
|
25
|
+
*/
|
|
26
|
+
export declare function assertFrameIndexCurrent(query: string, currentIds: string[], listedIds: string[] | undefined): void;
|
|
16
27
|
/**
|
|
17
28
|
* Build a CommandError from a message and suggestion.
|
|
18
29
|
*
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Which iframe a `bdg dom eval --frame` value names.
|
|
3
3
|
*/
|
|
4
4
|
import { CommandError } from '../../errors/index.js';
|
|
5
|
-
import { ambiguousFrameError, emptyFrameError, frameNotFoundError } from '../../errors/messages.js';
|
|
5
|
+
import { ambiguousFrameError, emptyFrameError, frameNotFoundError, staleFrameIndexError, } from '../../errors/messages.js';
|
|
6
6
|
import { EXIT_CODES } from '../../utils/exitCodes.js';
|
|
7
7
|
/**
|
|
8
8
|
* Pick the frame a `--frame` value names: a 0-based index, an exact `name`
|
|
@@ -19,6 +19,25 @@ export function selectFrame(frames, query) {
|
|
|
19
19
|
throw frameError(emptyFrameError(), EXIT_CODES.INVALID_ARGUMENTS);
|
|
20
20
|
return single(frames, wanted, candidatesFor(frames, wanted));
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Check that a `--frame` index still names the frame the last
|
|
24
|
+
* `bdg dom frames` listed at that index (frames added, removed or
|
|
25
|
+
* reordered since shift the indices; a navigation replaces every frame).
|
|
26
|
+
*
|
|
27
|
+
* @param query - Requested frame
|
|
28
|
+
* @param currentIds - Frame id of each frame now, by index
|
|
29
|
+
* @param listedIds - Frame id of each frame when last listed, undefined when never listed
|
|
30
|
+
* @throws CommandError (87) when the index names another frame (or none) now
|
|
31
|
+
*/
|
|
32
|
+
export function assertFrameIndexCurrent(query, currentIds, listedIds) {
|
|
33
|
+
const wanted = query.trim();
|
|
34
|
+
if (!listedIds || !/^\d+$/.test(wanted))
|
|
35
|
+
return;
|
|
36
|
+
const index = Number(wanted);
|
|
37
|
+
if (currentIds[index] === listedIds[index])
|
|
38
|
+
return;
|
|
39
|
+
throw frameError(staleFrameIndexError(index), EXIT_CODES.STALE_CACHE);
|
|
40
|
+
}
|
|
22
41
|
/**
|
|
23
42
|
* Frames a non-empty `--frame` value matches, by the first rule that applies.
|
|
24
43
|
*
|
|
@@ -14,17 +14,47 @@
|
|
|
14
14
|
* all, not even to `Runtime.terminateExecution`.
|
|
15
15
|
*/
|
|
16
16
|
import { CDPConnection } from '../../connection/cdp.js';
|
|
17
|
+
import type { Protocol } from '../../connection/typed-cdp.js';
|
|
17
18
|
import type { CommandError } from '../../errors/index.js';
|
|
18
19
|
import type { DomEvalData, DomFrame } from '../../ipc/protocol/commands.js';
|
|
19
20
|
import { type CDPSender } from '../../telemetry/objectExpander.js';
|
|
21
|
+
/** The default (main-world) execution context of a frame */
|
|
22
|
+
interface FrameContext {
|
|
23
|
+
/** `ExecutionContextDescription.uniqueId` */
|
|
24
|
+
uniqueId: string;
|
|
25
|
+
/** Origin its scripts run with (`"://"` when opaque) */
|
|
26
|
+
origin: string;
|
|
27
|
+
}
|
|
28
|
+
/** A frame with the session that owns it */
|
|
29
|
+
interface FrameNode {
|
|
30
|
+
frame: Protocol.Page.Frame;
|
|
31
|
+
sessionId?: string;
|
|
32
|
+
context?: FrameContext;
|
|
33
|
+
}
|
|
34
|
+
/** The page's iframes as listed, and the frame id behind each index */
|
|
35
|
+
export interface FrameListing {
|
|
36
|
+
frames: DomFrame[];
|
|
37
|
+
/** Frame id of each listed frame, by index */
|
|
38
|
+
frameIds: string[];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Iframes below the main frame, depth-first, siblings in the document order
|
|
42
|
+
* of their iframe elements (frames without a known place last, in tree order).
|
|
43
|
+
*
|
|
44
|
+
* @param nodes - All frames
|
|
45
|
+
* @param mainFrameId - The page's main frame
|
|
46
|
+
* @param rank - Place of each frame among its siblings, by id
|
|
47
|
+
* @returns Iframes in listing order
|
|
48
|
+
*/
|
|
49
|
+
export declare function iframesInOrder<T extends Pick<FrameNode, 'frame'>>(nodes: T[], mainFrameId: string, rank: Map<string, number>): T[];
|
|
20
50
|
/**
|
|
21
51
|
* List the page's iframes.
|
|
22
52
|
*
|
|
23
53
|
* @param page - The session's connection
|
|
24
54
|
* @param wsUrl - WebSocket URL of the page target
|
|
25
|
-
* @returns Iframes in listing order
|
|
55
|
+
* @returns Iframes in listing order, and the frame id behind each index
|
|
26
56
|
*/
|
|
27
|
-
export declare function listFrames(page: CDPConnection, wsUrl: string): Promise<
|
|
57
|
+
export declare function listFrames(page: CDPConnection, wsUrl: string): Promise<FrameListing>;
|
|
28
58
|
/** A frame whose script lost its context */
|
|
29
59
|
export interface LostFrame {
|
|
30
60
|
frameId: string;
|
|
@@ -53,9 +83,12 @@ export declare function frameContextLostError(conn: Pick<CDPConnection, 'send'>,
|
|
|
53
83
|
* @param wsUrl - WebSocket URL of the page target
|
|
54
84
|
* @param script - JavaScript expression
|
|
55
85
|
* @param query - Requested frame (index, name/id attribute, or part of the name, id or URL)
|
|
86
|
+
* @param listedIds - Frame id behind each index of the last `dom frames` listing, if any
|
|
56
87
|
* @returns Value, type and the frame's URL
|
|
57
|
-
* @throws CommandError (81/83) when the frame is ambiguous or missing, (
|
|
58
|
-
* when
|
|
88
|
+
* @throws CommandError (81/83) when the frame is ambiguous or missing, (87)
|
|
89
|
+
* when an index names another frame than when it was listed, (83) when it
|
|
90
|
+
* navigated or was removed while the script ran, else as evaluateScript
|
|
59
91
|
*/
|
|
60
|
-
export declare function evaluateInFrame(page: CDPConnection, wsUrl: string, script: string, query: string): Promise<DomEvalData>;
|
|
92
|
+
export declare function evaluateInFrame(page: CDPConnection, wsUrl: string, script: string, query: string, listedIds?: string[]): Promise<DomEvalData>;
|
|
93
|
+
export {};
|
|
61
94
|
//# sourceMappingURL=frames.d.ts.map
|
|
@@ -18,7 +18,7 @@ import { CDPProtocolError } from '../../connection/errors.js';
|
|
|
18
18
|
import { frameLostDuringEvalError, frameNavigatedDuringEvalError, frameNotReadyError, frameRemovedDuringEvalError, pageClosedDuringEvalError, } from '../../errors/messages.js';
|
|
19
19
|
import { evaluateScript, isContextLostError, pageStillOpen, settledWithin, withBusyPageRecovery, } from './evalHelpers.js';
|
|
20
20
|
import { effectiveFrameOrigin, isCrossOrigin } from './frameOrigin.js';
|
|
21
|
-
import { frameError, selectFrame } from './frameSelection.js';
|
|
21
|
+
import { assertFrameIndexCurrent, frameError, selectFrame } from './frameSelection.js';
|
|
22
22
|
import { attachedSessionOf } from '../../telemetry/attachedTargets.js';
|
|
23
23
|
import { senderFor } from '../../telemetry/objectExpander.js';
|
|
24
24
|
import { createLogger } from '../../ui/logging/index.js';
|
|
@@ -27,6 +27,32 @@ import { EXIT_CODES } from '../../utils/exitCodes.js';
|
|
|
27
27
|
const log = createLogger('dom');
|
|
28
28
|
/** Auto-attach without pausing new targets: only existing frames are needed */
|
|
29
29
|
const AUTO_ATTACH = { autoAttach: true, waitForDebuggerOnStart: false, flatten: true };
|
|
30
|
+
/**
|
|
31
|
+
* Page function, called with iframe elements of one document: their
|
|
32
|
+
* indices in document order. Shadow roots are walked through their host
|
|
33
|
+
* (a shadow root's content comes before the host's children, as in
|
|
34
|
+
* shadow-including tree order).
|
|
35
|
+
*/
|
|
36
|
+
const DOCUMENT_ORDER_JS = `function (...nodes) {
|
|
37
|
+
const path = (node) => {
|
|
38
|
+
const nodesUp = [];
|
|
39
|
+
for (let n = node; n; n = n.parentNode || (n.nodeType === 11 ? n.host : null)) nodesUp.unshift(n);
|
|
40
|
+
return nodesUp;
|
|
41
|
+
};
|
|
42
|
+
const paths = nodes.map(path);
|
|
43
|
+
const compare = (i, j) => {
|
|
44
|
+
const a = paths[i];
|
|
45
|
+
const b = paths[j];
|
|
46
|
+
let depth = 0;
|
|
47
|
+
while (depth < a.length && depth < b.length && a[depth] === b[depth]) depth++;
|
|
48
|
+
const x = a[depth];
|
|
49
|
+
const y = b[depth];
|
|
50
|
+
if (!x || !y) return a.length - b.length;
|
|
51
|
+
if (x.nodeType === 11 || y.nodeType === 11) return x.nodeType === 11 ? -1 : 1;
|
|
52
|
+
return x.compareDocumentPosition(y) & 4 ? -1 : 1;
|
|
53
|
+
};
|
|
54
|
+
return nodes.map((_, i) => i).sort(compare);
|
|
55
|
+
}`;
|
|
30
56
|
/**
|
|
31
57
|
* Run `work` on a second connection to the page, closed afterwards.
|
|
32
58
|
*
|
|
@@ -188,16 +214,20 @@ function flattenTrees(trees) {
|
|
|
188
214
|
return nodes;
|
|
189
215
|
}
|
|
190
216
|
/**
|
|
191
|
-
* Iframes below the main frame, depth-first
|
|
217
|
+
* Iframes below the main frame, depth-first, siblings in the document order
|
|
218
|
+
* of their iframe elements (frames without a known place last, in tree order).
|
|
192
219
|
*
|
|
193
220
|
* @param nodes - All frames
|
|
194
221
|
* @param mainFrameId - The page's main frame
|
|
222
|
+
* @param rank - Place of each frame among its siblings, by id
|
|
195
223
|
* @returns Iframes in listing order
|
|
196
224
|
*/
|
|
197
|
-
function iframesInOrder(nodes, mainFrameId) {
|
|
225
|
+
export function iframesInOrder(nodes, mainFrameId, rank) {
|
|
198
226
|
const ordered = [];
|
|
227
|
+
const place = (node) => rank.get(node.frame.id) ?? Number.MAX_SAFE_INTEGER;
|
|
199
228
|
const visit = (parentId) => {
|
|
200
|
-
|
|
229
|
+
const children = nodes.filter((n) => n.frame.parentId === parentId);
|
|
230
|
+
for (const node of children.sort((a, b) => place(a) - place(b))) {
|
|
201
231
|
ordered.push(node);
|
|
202
232
|
visit(node.frame.id);
|
|
203
233
|
}
|
|
@@ -220,21 +250,26 @@ function attributeValue(attributes = [], key) {
|
|
|
220
250
|
return undefined;
|
|
221
251
|
}
|
|
222
252
|
/**
|
|
223
|
-
*
|
|
253
|
+
* The iframe element of a frame (read in its parent's session).
|
|
224
254
|
*
|
|
225
255
|
* @param fc - Frame connection
|
|
226
256
|
* @param frameId - Frame
|
|
227
257
|
* @param ownerSession - Session of the parent frame
|
|
228
|
-
* @returns `name`, `id` and `sandbox` attributes when set
|
|
258
|
+
* @returns Its node, and `name`, `id` and `sandbox` attributes when set
|
|
229
259
|
*/
|
|
230
|
-
async function
|
|
260
|
+
async function frameOwner(fc, frameId, ownerSession) {
|
|
231
261
|
try {
|
|
232
262
|
const { backendNodeId } = await sendToSession(fc, 'DOM.getFrameOwner', { frameId }, ownerSession);
|
|
233
263
|
const { node } = await sendToSession(fc, 'DOM.describeNode', { backendNodeId }, ownerSession);
|
|
234
264
|
const name = attributeValue(node.attributes, 'name');
|
|
235
265
|
const id = attributeValue(node.attributes, 'id');
|
|
236
266
|
const sandbox = attributeValue(node.attributes, 'sandbox');
|
|
237
|
-
return {
|
|
267
|
+
return {
|
|
268
|
+
backendNodeId,
|
|
269
|
+
...(name && { name }),
|
|
270
|
+
...(id && { id }),
|
|
271
|
+
...(sandbox !== undefined && { sandbox }),
|
|
272
|
+
};
|
|
238
273
|
}
|
|
239
274
|
catch (error) {
|
|
240
275
|
log.debug(`No iframe element for frame ${frameId}: ${getErrorMessage(error)}`);
|
|
@@ -317,8 +352,78 @@ function initialListingState(page, sessionOf) {
|
|
|
317
352
|
return { topOrigin, originOf: new Map([[top.id, topOrigin]]), indexOf: new Map(), sessionOf };
|
|
318
353
|
}
|
|
319
354
|
/**
|
|
320
|
-
*
|
|
321
|
-
*
|
|
355
|
+
* Indices of iframe elements of one document, in document order.
|
|
356
|
+
*
|
|
357
|
+
* @param fc - Frame connection
|
|
358
|
+
* @param backendNodeIds - The iframe elements
|
|
359
|
+
* @param ownerSession - Session of the document
|
|
360
|
+
* @returns Their indices in document order
|
|
361
|
+
* @throws Errors of CDP (e.g. an element removed meanwhile)
|
|
362
|
+
*/
|
|
363
|
+
async function documentOrder(fc, backendNodeIds, ownerSession) {
|
|
364
|
+
const objectIds = await Promise.all(backendNodeIds.map(async (backendNodeId) => {
|
|
365
|
+
const { object } = await sendToSession(fc, 'DOM.resolveNode', { backendNodeId }, ownerSession);
|
|
366
|
+
return object.objectId ?? '';
|
|
367
|
+
}));
|
|
368
|
+
const { result } = await sendToSession(fc, 'Runtime.callFunctionOn', {
|
|
369
|
+
objectId: objectIds[0],
|
|
370
|
+
functionDeclaration: DOCUMENT_ORDER_JS,
|
|
371
|
+
arguments: objectIds.map((objectId) => ({ objectId })),
|
|
372
|
+
returnByValue: true,
|
|
373
|
+
}, ownerSession);
|
|
374
|
+
return result.value;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Frames grouped by their parent.
|
|
378
|
+
*
|
|
379
|
+
* @param iframes - Frames
|
|
380
|
+
* @returns Frames of each parent, by parent id
|
|
381
|
+
*/
|
|
382
|
+
function groupByParent(iframes) {
|
|
383
|
+
const byParent = new Map();
|
|
384
|
+
for (const node of iframes) {
|
|
385
|
+
const parentId = node.frame.parentId ?? '';
|
|
386
|
+
byParent.set(parentId, [...(byParent.get(parentId) ?? []), node]);
|
|
387
|
+
}
|
|
388
|
+
return byParent;
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Place of each frame among its siblings, from the document order of their
|
|
392
|
+
* iframe elements. Chrome's frame tree lists frames in the order they were
|
|
393
|
+
* attached and leaves out-of-process ones to their own sessions, so neither
|
|
394
|
+
* gives a stable order. Siblings that cannot be ordered get no place.
|
|
395
|
+
*
|
|
396
|
+
* @param fc - Frame connection
|
|
397
|
+
* @param iframes - All iframes
|
|
398
|
+
* @param owners - Iframe element of each frame, by id
|
|
399
|
+
* @param sessionOf - Session of each frame, by id
|
|
400
|
+
* @returns Place of each frame among its siblings, by id
|
|
401
|
+
*/
|
|
402
|
+
async function siblingRanks(fc, iframes, owners, sessionOf) {
|
|
403
|
+
const rank = new Map();
|
|
404
|
+
const groups = [...groupByParent(iframes)].map(async ([parentId, siblings]) => {
|
|
405
|
+
const placed = siblings.flatMap((node) => {
|
|
406
|
+
const backendNodeId = owners.get(node.frame.id)?.backendNodeId;
|
|
407
|
+
return backendNodeId === undefined ? [] : [{ id: node.frame.id, backendNodeId }];
|
|
408
|
+
});
|
|
409
|
+
if (placed.length < 2)
|
|
410
|
+
return;
|
|
411
|
+
try {
|
|
412
|
+
const nodeIds = placed.map((item) => item.backendNodeId);
|
|
413
|
+
const order = await documentOrder(fc, nodeIds, sessionOf.get(parentId));
|
|
414
|
+
order.forEach((index, place) => rank.set(placed[index]?.id ?? '', place));
|
|
415
|
+
}
|
|
416
|
+
catch (error) {
|
|
417
|
+
log.debug(`Frames of ${parentId} not ordered: ${getErrorMessage(error)}`);
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
await Promise.all(groups);
|
|
421
|
+
return rank;
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Find every iframe of the page, nested and out-of-process ones included,
|
|
425
|
+
* siblings in the document order of their iframe elements. Frames that go
|
|
426
|
+
* away while they are being listed are skipped.
|
|
322
427
|
*
|
|
323
428
|
* @param fc - Frame connection
|
|
324
429
|
* @returns Iframes in listing order
|
|
@@ -327,32 +432,40 @@ async function discoverFrames(fc) {
|
|
|
327
432
|
const [page, outOfProcess] = await Promise.all([readSession(fc), outOfProcessTrees(fc)]);
|
|
328
433
|
const nodes = flattenTrees([page, ...outOfProcess]);
|
|
329
434
|
const sessionOf = new Map(nodes.map((node) => [node.frame.id, node.sessionId]));
|
|
330
|
-
const
|
|
331
|
-
const
|
|
435
|
+
const mainFrameId = page.tree.frame.id;
|
|
436
|
+
const iframes = nodes.filter((node) => node.frame.id !== mainFrameId);
|
|
437
|
+
const owners = new Map(await Promise.all(iframes.map(async (node) => [
|
|
438
|
+
node.frame.id,
|
|
439
|
+
await frameOwner(fc, node.frame.id, sessionOf.get(node.frame.parentId ?? '')),
|
|
440
|
+
])));
|
|
441
|
+
const rank = await siblingRanks(fc, iframes, owners, sessionOf);
|
|
332
442
|
const state = initialListingState(page, sessionOf);
|
|
333
|
-
return iframes.map((node, index) => describeFrame(node, index, owners
|
|
443
|
+
return iframesInOrder(iframes, mainFrameId, rank).map((node, index) => describeFrame(node, index, owners.get(node.frame.id) ?? {}, state));
|
|
334
444
|
}
|
|
335
445
|
/**
|
|
336
446
|
* List the page's iframes.
|
|
337
447
|
*
|
|
338
448
|
* @param page - The session's connection
|
|
339
449
|
* @param wsUrl - WebSocket URL of the page target
|
|
340
|
-
* @returns Iframes in listing order
|
|
450
|
+
* @returns Iframes in listing order, and the frame id behind each index
|
|
341
451
|
*/
|
|
342
452
|
export async function listFrames(page, wsUrl) {
|
|
343
453
|
const frames = await withFrameConnection(page, wsUrl, discoverFrames);
|
|
344
|
-
return frames.map((frame) => frame.info);
|
|
454
|
+
return { frames: frames.map((frame) => frame.info), frameIds: frames.map((f) => f.frameId) };
|
|
345
455
|
}
|
|
346
456
|
/**
|
|
347
457
|
* Find the requested frame and its default execution context.
|
|
348
458
|
*
|
|
349
459
|
* @param fc - Frame connection
|
|
350
460
|
* @param query - Requested frame
|
|
461
|
+
* @param listedIds - Frame id behind each index of the last `dom frames` listing, if any
|
|
351
462
|
* @returns The frame and its context's unique id
|
|
352
|
-
* @throws CommandError (81/83) when the frame is ambiguous, missing or has no
|
|
463
|
+
* @throws CommandError (81/83) when the frame is ambiguous, missing or has no
|
|
464
|
+
* context, (87) when an index names another frame than when it was listed
|
|
353
465
|
*/
|
|
354
|
-
async function resolveFrame(fc, query) {
|
|
466
|
+
async function resolveFrame(fc, query, listedIds) {
|
|
355
467
|
const frames = await discoverFrames(fc);
|
|
468
|
+
assertFrameIndexCurrent(query, frames.map((frame) => frame.frameId), listedIds);
|
|
356
469
|
const selected = selectFrame(frames.map((frame) => frame.info), query);
|
|
357
470
|
const frame = frames[selected.index];
|
|
358
471
|
if (!frame.context) {
|
|
@@ -415,13 +528,15 @@ export async function frameContextLostError(conn, page, frame) {
|
|
|
415
528
|
* @param wsUrl - WebSocket URL of the page target
|
|
416
529
|
* @param script - JavaScript expression
|
|
417
530
|
* @param query - Requested frame (index, name/id attribute, or part of the name, id or URL)
|
|
531
|
+
* @param listedIds - Frame id behind each index of the last `dom frames` listing, if any
|
|
418
532
|
* @returns Value, type and the frame's URL
|
|
419
|
-
* @throws CommandError (81/83) when the frame is ambiguous or missing, (
|
|
420
|
-
* when
|
|
533
|
+
* @throws CommandError (81/83) when the frame is ambiguous or missing, (87)
|
|
534
|
+
* when an index names another frame than when it was listed, (83) when it
|
|
535
|
+
* navigated or was removed while the script ran, else as evaluateScript
|
|
421
536
|
*/
|
|
422
|
-
export async function evaluateInFrame(page, wsUrl, script, query) {
|
|
537
|
+
export async function evaluateInFrame(page, wsUrl, script, query, listedIds) {
|
|
423
538
|
return withFrameConnection(page, wsUrl, async (fc) => {
|
|
424
|
-
const { frame, uniqueContextId } = await resolveFrame(fc, query);
|
|
539
|
+
const { frame, uniqueContextId } = await resolveFrame(fc, query, listedIds);
|
|
425
540
|
try {
|
|
426
541
|
const result = await evaluateScript(fc.conn, script, {
|
|
427
542
|
...(frame.sessionId && { sessionId: frame.sessionId }),
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `bdg dom inspect`: what one element looks like, read in the daemon.
|
|
3
|
+
*
|
|
4
|
+
* The element is found like the other element commands find it (selector
|
|
5
|
+
* with filters through open shadow roots and same-origin iframes, or the
|
|
6
|
+
* exact cached node). Then, in parallel: one page-side walk on the element
|
|
7
|
+
* ({@link INSPECT_PAGE_JS}: text, placement in the parent, backgrounds, child
|
|
8
|
+
* tree), `dom layout`'s measurement (page position, hidden, covered,
|
|
9
|
+
* offscreen) and, once the nodes are pushed to CDP, `CSS.getComputedStyleForNode`
|
|
10
|
+
* for the element, its layout parent and its `::before`/`::after`,
|
|
11
|
+
* `CSS.getPlatformFontsForNode` for its text and `DOM.getBoxModel`. DOM and
|
|
12
|
+
* CSS are enabled on the first inspect and kept on. Matched rules are not
|
|
13
|
+
* read (no cascade).
|
|
14
|
+
*/
|
|
15
|
+
import type { CDPConnection } from '../../connection/cdp.js';
|
|
16
|
+
import type { DomInspectCommand } from '../../ipc/protocol/commands.js';
|
|
17
|
+
import type { InspectResult } from '../../ipc/protocol/inspectTypes.js';
|
|
18
|
+
/**
|
|
19
|
+
* Inspect one element.
|
|
20
|
+
*
|
|
21
|
+
* @param cdp - CDP connection
|
|
22
|
+
* @param params - Selector (and index) or backend node id, and options
|
|
23
|
+
* @returns Inspect result
|
|
24
|
+
* @throws CommandError (83) no match, (81) index out of range, invalid
|
|
25
|
+
* selector or unknown property, (87) the cached element left the page
|
|
26
|
+
*/
|
|
27
|
+
export declare function inspectElement(cdp: CDPConnection, params: DomInspectCommand): Promise<InspectResult>;
|
|
28
|
+
//# sourceMappingURL=inspect.d.ts.map
|