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.
Files changed (139) hide show
  1. package/.claude/skills/bdg/SKILL.md +268 -0
  2. package/README.md +15 -1
  3. package/dist/commands/dom/a11y.js +2 -1
  4. package/dist/commands/dom/formInteraction.js +56 -25
  5. package/dist/commands/dom/helpers/keyAttributes.d.ts +20 -0
  6. package/dist/commands/dom/helpers/keyAttributes.js +54 -0
  7. package/dist/commands/dom/helpers/query.d.ts +1 -1
  8. package/dist/commands/dom/helpers/query.js +66 -19
  9. package/dist/commands/dom/helpers/runElementCommand.js +4 -3
  10. package/dist/commands/dom/helpers/screenshot.js +85 -12
  11. package/dist/commands/dom/index.d.ts +1 -0
  12. package/dist/commands/dom/index.js +8 -3
  13. package/dist/commands/dom/inspect.d.ts +15 -0
  14. package/dist/commands/dom/inspect.js +82 -0
  15. package/dist/commands/dom/layout.js +2 -2
  16. package/dist/commands/dom/listeners.js +2 -2
  17. package/dist/commands/dom/semanticUtils.d.ts +14 -1
  18. package/dist/commands/dom/semanticUtils.js +44 -3
  19. package/dist/commands/installSkill.d.ts +20 -0
  20. package/dist/commands/installSkill.js +87 -0
  21. package/dist/commands/network/list.js +13 -2
  22. package/dist/commands/optionBehaviors.js +48 -6
  23. package/dist/commands/page.d.ts +1 -1
  24. package/dist/commands/page.js +62 -3
  25. package/dist/commands/shared/commonOptions.d.ts +4 -0
  26. package/dist/commands/shared/commonOptions.js +9 -0
  27. package/dist/commands/shared/optionTypes.d.ts +21 -0
  28. package/dist/commands/shared/startHelpers.d.ts +66 -0
  29. package/dist/commands/shared/startHelpers.js +91 -10
  30. package/dist/commands/shared/validation.d.ts +11 -0
  31. package/dist/commands/shared/validation.js +16 -0
  32. package/dist/commands.js +3 -0
  33. package/dist/daemon/launcher.d.ts +8 -1
  34. package/dist/daemon/launcher.js +3 -1
  35. package/dist/daemon/session/Session.d.ts +7 -0
  36. package/dist/daemon/session/Session.js +23 -1
  37. package/dist/daemon/session/commandRegistry.d.ts +14 -1
  38. package/dist/daemon/session/commandRegistry.js +65 -9
  39. package/dist/daemon/session/interactions.d.ts +18 -5
  40. package/dist/daemon/session/interactions.js +22 -12
  41. package/dist/daemon.js +3565 -329
  42. package/dist/errors/messages.d.ts +85 -0
  43. package/dist/errors/messages.js +128 -1
  44. package/dist/index.js +2151 -960
  45. package/dist/ipc/client.d.ts +9 -0
  46. package/dist/ipc/client.js +13 -0
  47. package/dist/ipc/protocol/commands.d.ts +56 -1
  48. package/dist/ipc/protocol/commands.js +2 -0
  49. package/dist/ipc/protocol/domTypes.d.ts +35 -2
  50. package/dist/ipc/protocol/inspectTypes.d.ts +388 -0
  51. package/dist/ipc/protocol/inspectTypes.js +10 -0
  52. package/dist/runtime/dom/actionEffects.d.ts +94 -15
  53. package/dist/runtime/dom/actionEffects.js +173 -27
  54. package/dist/runtime/dom/actionEffectsScripts.d.ts +52 -14
  55. package/dist/runtime/dom/actionEffectsScripts.js +224 -32
  56. package/dist/runtime/dom/elementInfo.d.ts +26 -0
  57. package/dist/runtime/dom/elementInfo.js +65 -0
  58. package/dist/runtime/dom/eventListeners.js +14 -4
  59. package/dist/runtime/dom/formFillHelpers/fill.d.ts +3 -4
  60. package/dist/runtime/dom/formFillHelpers/fill.js +77 -28
  61. package/dist/runtime/dom/frameSelection.d.ts +11 -0
  62. package/dist/runtime/dom/frameSelection.js +20 -1
  63. package/dist/runtime/dom/frames.d.ts +38 -5
  64. package/dist/runtime/dom/frames.js +136 -21
  65. package/dist/runtime/dom/inspect.d.ts +28 -0
  66. package/dist/runtime/dom/inspect.js +557 -0
  67. package/dist/runtime/dom/inspectAllStyles.d.ts +62 -0
  68. package/dist/runtime/dom/inspectAllStyles.js +385 -0
  69. package/dist/runtime/dom/inspectCascade.d.ts +94 -0
  70. package/dist/runtime/dom/inspectCascade.js +371 -0
  71. package/dist/runtime/dom/inspectCascadeModel.d.ts +39 -0
  72. package/dist/runtime/dom/inspectCascadeModel.js +232 -0
  73. package/dist/runtime/dom/inspectHints.d.ts +62 -0
  74. package/dist/runtime/dom/inspectHints.js +305 -0
  75. package/dist/runtime/dom/inspectLayoutModel.d.ts +87 -0
  76. package/dist/runtime/dom/inspectLayoutModel.js +346 -0
  77. package/dist/runtime/dom/inspectModel.d.ts +74 -0
  78. package/dist/runtime/dom/inspectModel.js +184 -0
  79. package/dist/runtime/dom/inspectPaintModel.d.ts +157 -0
  80. package/dist/runtime/dom/inspectPaintModel.js +461 -0
  81. package/dist/runtime/dom/inspectRules.d.ts +37 -0
  82. package/dist/runtime/dom/inspectRules.js +101 -0
  83. package/dist/runtime/dom/inspectScripts.d.ts +132 -0
  84. package/dist/runtime/dom/inspectScripts.js +263 -0
  85. package/dist/runtime/dom/inspectTree.d.ts +40 -0
  86. package/dist/runtime/dom/inspectTree.js +134 -0
  87. package/dist/runtime/dom/inspectVariables.d.ts +33 -0
  88. package/dist/runtime/dom/inspectVariables.js +94 -0
  89. package/dist/runtime/dom/inspectWhyModel.d.ts +20 -0
  90. package/dist/runtime/dom/inspectWhyModel.js +134 -0
  91. package/dist/runtime/dom/layout.d.ts +5 -1
  92. package/dist/runtime/dom/layout.js +10 -3
  93. package/dist/runtime/dom/listenerPageScripts.d.ts +11 -5
  94. package/dist/runtime/dom/listenerPageScripts.js +95 -9
  95. package/dist/runtime/dom/listenerSummary.d.ts +4 -0
  96. package/dist/runtime/dom/listenerSummary.js +26 -9
  97. package/dist/runtime/dom/reactEventHelpers.d.ts +5 -0
  98. package/dist/runtime/dom/reactEventHelpers.js +12 -4
  99. package/dist/runtime/page/emulation.d.ts +20 -0
  100. package/dist/runtime/page/emulation.js +37 -0
  101. package/dist/telemetry/a11y.d.ts +10 -0
  102. package/dist/telemetry/a11y.js +78 -1
  103. package/dist/telemetry/console.d.ts +1 -0
  104. package/dist/telemetry/console.js +100 -5
  105. package/dist/telemetry/network.js +3 -1
  106. package/dist/types.d.ts +40 -0
  107. package/dist/ui/formatters/details.d.ts +8 -0
  108. package/dist/ui/formatters/details.js +59 -3
  109. package/dist/ui/formatters/dom.d.ts +2 -1
  110. package/dist/ui/formatters/dom.js +25 -9
  111. package/dist/ui/formatters/inspect.d.ts +39 -0
  112. package/dist/ui/formatters/inspect.js +596 -0
  113. package/dist/ui/formatters/installSkill.d.ts +11 -0
  114. package/dist/ui/formatters/installSkill.js +31 -0
  115. package/dist/ui/formatters/keyAttributes.d.ts +19 -0
  116. package/dist/ui/formatters/keyAttributes.js +84 -0
  117. package/dist/ui/formatters/layout.js +2 -2
  118. package/dist/ui/formatters/networkHeaders.d.ts +13 -0
  119. package/dist/ui/formatters/networkHeaders.js +23 -3
  120. package/dist/ui/formatters/networkList.d.ts +29 -1
  121. package/dist/ui/formatters/networkList.js +86 -20
  122. package/dist/ui/formatters/status.js +1 -1
  123. package/dist/ui/formatting.d.ts +9 -0
  124. package/dist/ui/formatting.js +6 -3
  125. package/dist/ui/messages/commands.d.ts +123 -7
  126. package/dist/ui/messages/commands.js +181 -10
  127. package/dist/ui/messages/networkMessages.d.ts +14 -0
  128. package/dist/ui/messages/networkMessages.js +18 -0
  129. package/dist/ui/messages/session.d.ts +14 -0
  130. package/dist/ui/messages/session.js +20 -0
  131. package/dist/utils/async.d.ts +9 -0
  132. package/dist/utils/async.js +17 -0
  133. package/dist/utils/color.d.ts +84 -0
  134. package/dist/utils/color.js +376 -0
  135. package/dist/utils/cssValues.d.ts +109 -0
  136. package/dist/utils/cssValues.js +236 -0
  137. package/dist/utils/selectorFilters.d.ts +12 -0
  138. package/dist/utils/selectorFilters.js +29 -0
  139. 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
- * Whether the mouse press just dispatched reached the target element.
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 False only when the target received no pointerdown/mousedown
287
+ * @returns Outcome; not reached only when the target received no pointerdown/mousedown
286
288
  */
287
- export async function pressReachedTarget(cdp) {
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
- return response.result?.value !== false;
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. If dispatching fails first, the press probe
303
- * is still removed from the page.
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 x - Page x
308
- * @param y - Page y
309
- * @returns False if the press never reached the target
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, x, y) {
312
- let reached;
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'] === 'mousePressed' && reached === undefined) {
317
- reached = await pressReachedTarget(cdp);
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 (reached === undefined)
323
- await pressReachedTarget(cdp);
342
+ if (outcome === undefined)
343
+ await readPressProbe(cdp);
324
344
  throw error;
325
345
  }
326
- return reached ?? true;
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 reached = await dispatchMouseAction(cdp, action, x, y);
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<DomFrame[]>;
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, (83)
58
- * when it navigated or was removed while the script ran, else as evaluateScript
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
- for (const node of nodes.filter((n) => n.frame.parentId === parentId)) {
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
- * Attributes of the iframe element of a frame (read in its parent's session).
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 ownerAttributes(fc, frameId, ownerSession) {
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 { ...(name && { name }), ...(id && { id }), ...(sandbox !== undefined && { sandbox }) };
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
- * Find every iframe of the page, nested and out-of-process ones included.
321
- * Frames that go away while they are being listed are skipped.
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 iframes = iframesInOrder(nodes, page.tree.frame.id);
331
- const owners = await Promise.all(iframes.map((node) => ownerAttributes(fc, node.frame.id, sessionOf.get(node.frame.parentId ?? ''))));
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[index] ?? {}, state));
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 context
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, (83)
420
- * when it navigated or was removed while the script ran, else as evaluateScript
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