browser-debugger-cli 0.14.0 → 0.16.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 (167) hide show
  1. package/.claude/skills/bdg/SKILL.md +3 -2
  2. package/dist/cdp/methodTarget.d.ts +92 -0
  3. package/dist/cdp/methodTarget.js +159 -0
  4. package/dist/cdp/protocol.d.ts +16 -1
  5. package/dist/cdp/protocol.js +21 -0
  6. package/dist/cdp/schema.d.ts +55 -1
  7. package/dist/cdp/schema.js +134 -25
  8. package/dist/cdp/types.d.ts +3 -1
  9. package/dist/commands/cdp.d.ts +38 -1
  10. package/dist/commands/cdp.js +201 -133
  11. package/dist/commands/cleanup.js +21 -4
  12. package/dist/commands/dom/eval.d.ts +2 -1
  13. package/dist/commands/dom/eval.js +6 -21
  14. package/dist/commands/dom/formInteraction.js +8 -4
  15. package/dist/commands/dom/helpers/evalResult.d.ts +36 -0
  16. package/dist/commands/dom/helpers/evalResult.js +59 -0
  17. package/dist/commands/dom/helpers/index.d.ts +4 -4
  18. package/dist/commands/dom/helpers/index.js +3 -3
  19. package/dist/commands/dom/helpers/query.d.ts +2 -2
  20. package/dist/commands/dom/helpers/query.js +2 -2
  21. package/dist/commands/dom/helpers/screenshot.d.ts +21 -26
  22. package/dist/commands/dom/helpers/screenshot.js +50 -668
  23. package/dist/commands/dom/screenshot.js +56 -36
  24. package/dist/commands/helpJson.d.ts +1 -1
  25. package/dist/commands/helpJson.js +3 -3
  26. package/dist/commands/helpTopic.js +10 -4
  27. package/dist/commands/network/har.js +18 -14
  28. package/dist/commands/optionBehaviors.js +24 -9
  29. package/dist/commands/shared/CommandRunner.d.ts +5 -0
  30. package/dist/commands/shared/CommandRunner.js +18 -3
  31. package/dist/commands/shared/interrupt.d.ts +40 -0
  32. package/dist/commands/shared/interrupt.js +73 -0
  33. package/dist/commands/shared/optionTypes.d.ts +3 -0
  34. package/dist/commands/shared/outputFile.d.ts +2 -1
  35. package/dist/commands/shared/outputFile.js +7 -4
  36. package/dist/commands/shared/startHelpers.d.ts +26 -3
  37. package/dist/commands/shared/startHelpers.js +145 -23
  38. package/dist/commands/status.js +3 -1
  39. package/dist/commands/stop.js +2 -1
  40. package/dist/commands/types.d.ts +5 -0
  41. package/dist/connection/cdp.js +1 -16
  42. package/dist/connection/chromeIdentity.d.ts +24 -5
  43. package/dist/connection/chromeIdentity.js +53 -22
  44. package/dist/connection/launcher/flagsBuilder.d.ts +46 -0
  45. package/dist/connection/launcher/flagsBuilder.js +107 -23
  46. package/dist/connection/launcher.d.ts +35 -2
  47. package/dist/connection/launcher.js +99 -12
  48. package/dist/connection/typed-cdp.d.ts +3 -2
  49. package/dist/constants.d.ts +3 -5
  50. package/dist/constants.js +3 -5
  51. package/dist/daemon/SessionController.d.ts +10 -5
  52. package/dist/daemon/SessionController.js +15 -8
  53. package/dist/daemon/ipcServer.js +1 -1
  54. package/dist/daemon/launcher.d.ts +22 -3
  55. package/dist/daemon/launcher.js +45 -8
  56. package/dist/daemon/session/Session.d.ts +5 -1
  57. package/dist/daemon/session/Session.js +9 -8
  58. package/dist/daemon/session/TelemetryStore.d.ts +5 -0
  59. package/dist/daemon/session/TelemetryStore.js +4 -0
  60. package/dist/daemon/session/captureGate.d.ts +59 -0
  61. package/dist/daemon/session/captureGate.js +96 -0
  62. package/dist/daemon/session/chromeConnection.d.ts +16 -1
  63. package/dist/daemon/session/chromeConnection.js +34 -4
  64. package/dist/daemon/session/collectors.d.ts +15 -0
  65. package/dist/daemon/session/collectors.js +39 -2
  66. package/dist/daemon/session/commandRegistry.d.ts +14 -1
  67. package/dist/daemon/session/commandRegistry.js +48 -13
  68. package/dist/daemon/session/downloads.d.ts +32 -0
  69. package/dist/daemon/session/downloads.js +96 -0
  70. package/dist/daemon/session/interactions.d.ts +3 -2
  71. package/dist/daemon/session/interactions.js +7 -2
  72. package/dist/daemon/session/plugins.js +6 -0
  73. package/dist/daemon.js +18520 -17014
  74. package/dist/errors/CommandError.d.ts +2 -0
  75. package/dist/errors/issues.d.ts +1 -1
  76. package/dist/errors/messages.d.ts +81 -0
  77. package/dist/errors/messages.js +198 -6
  78. package/dist/index.js +1446 -1078
  79. package/dist/ipc/client.d.ts +20 -2
  80. package/dist/ipc/client.js +32 -6
  81. package/dist/ipc/protocol/commands.d.ts +36 -2
  82. package/dist/ipc/protocol/commands.js +1 -0
  83. package/dist/ipc/protocol/domTypes.d.ts +24 -1
  84. package/dist/ipc/session/queries.d.ts +3 -0
  85. package/dist/ipc/session/types.d.ts +5 -0
  86. package/dist/ipc/transport/IPCError.d.ts +9 -0
  87. package/dist/ipc/transport/IPCError.js +12 -0
  88. package/dist/ipc/transport/errors.d.ts +2 -1
  89. package/dist/ipc/transport/errors.js +4 -1
  90. package/dist/ipc/transport/index.d.ts +10 -2
  91. package/dist/ipc/transport/index.js +29 -4
  92. package/dist/runtime/dom/actionEffects.d.ts +48 -9
  93. package/dist/runtime/dom/actionEffects.js +269 -34
  94. package/dist/runtime/dom/actionEffectsScripts.d.ts +45 -0
  95. package/dist/runtime/dom/actionEffectsScripts.js +101 -2
  96. package/dist/runtime/dom/captureArea.d.ts +35 -0
  97. package/dist/runtime/dom/captureArea.js +203 -0
  98. package/dist/runtime/dom/elementInfo.d.ts +13 -4
  99. package/dist/runtime/dom/elementInfo.js +12 -3
  100. package/dist/runtime/dom/evalHelpers.d.ts +24 -4
  101. package/dist/runtime/dom/evalHelpers.js +40 -12
  102. package/dist/runtime/dom/formDiscovery.d.ts +1 -1
  103. package/dist/runtime/dom/frames.d.ts +2 -1
  104. package/dist/runtime/dom/frames.js +3 -1
  105. package/dist/runtime/page/bdgWorld.d.ts +9 -0
  106. package/dist/runtime/page/bdgWorld.js +11 -0
  107. package/dist/runtime/page/captureEmulation.d.ts +119 -0
  108. package/dist/runtime/page/captureEmulation.js +189 -0
  109. package/dist/runtime/page/captureScroll.d.ts +24 -0
  110. package/dist/runtime/page/captureScroll.js +124 -0
  111. package/dist/runtime/page/emulation.js +6 -5
  112. package/dist/runtime/page/screenshot.d.ts +41 -0
  113. package/dist/runtime/page/screenshot.js +394 -0
  114. package/dist/runtime/page/userAgent.d.ts +86 -2
  115. package/dist/runtime/page/userAgent.js +154 -33
  116. package/dist/session/paths.d.ts +52 -3
  117. package/dist/session/paths.js +179 -7
  118. package/dist/session/portClaims.d.ts +0 -8
  119. package/dist/session/portClaims.js +1 -22
  120. package/dist/session/sessionList.d.ts +5 -1
  121. package/dist/session/sessionList.js +5 -1
  122. package/dist/telemetry/downloads.d.ts +127 -0
  123. package/dist/telemetry/downloads.js +265 -0
  124. package/dist/telemetry/har/builder.d.ts +12 -1
  125. package/dist/telemetry/har/builder.js +32 -9
  126. package/dist/telemetry/har/sanitize.d.ts +28 -0
  127. package/dist/telemetry/har/sanitize.js +184 -0
  128. package/dist/telemetry/har/sanitizeBody.d.ts +78 -0
  129. package/dist/telemetry/har/sanitizeBody.js +541 -0
  130. package/dist/telemetry/har/types.d.ts +2 -0
  131. package/dist/telemetry/network.d.ts +4 -4
  132. package/dist/telemetry/network.js +38 -4
  133. package/dist/telemetry/networkRetention.d.ts +35 -14
  134. package/dist/telemetry/networkRetention.js +62 -26
  135. package/dist/types.d.ts +9 -14
  136. package/dist/ui/OutputBuilder.d.ts +3 -2
  137. package/dist/ui/OutputBuilder.js +4 -3
  138. package/dist/ui/formatters/cdp.d.ts +32 -9
  139. package/dist/ui/formatters/cdp.js +77 -6
  140. package/dist/ui/formatters/details.js +7 -15
  141. package/dist/ui/formatters/preview.d.ts +2 -0
  142. package/dist/ui/formatters/preview.js +7 -1
  143. package/dist/ui/formatters/sessions.d.ts +3 -2
  144. package/dist/ui/formatters/sessions.js +10 -3
  145. package/dist/ui/formatters/status.js +6 -1
  146. package/dist/ui/formatting.d.ts +7 -0
  147. package/dist/ui/formatting.js +13 -0
  148. package/dist/ui/logging/logger.d.ts +1 -1
  149. package/dist/ui/messages/chrome.d.ts +27 -6
  150. package/dist/ui/messages/chrome.js +78 -12
  151. package/dist/ui/messages/commands.d.ts +71 -3
  152. package/dist/ui/messages/commands.js +98 -3
  153. package/dist/ui/messages/networkMessages.d.ts +50 -5
  154. package/dist/ui/messages/networkMessages.js +50 -6
  155. package/dist/ui/messages/session.d.ts +8 -0
  156. package/dist/ui/messages/session.js +10 -0
  157. package/dist/utils/async.d.ts +3 -2
  158. package/dist/utils/async.js +16 -3
  159. package/dist/utils/atomicFile.d.ts +2 -1
  160. package/dist/utils/atomicFile.js +5 -2
  161. package/dist/utils/directories.d.ts +41 -0
  162. package/dist/utils/directories.js +48 -0
  163. package/dist/utils/http.d.ts +11 -4
  164. package/dist/utils/http.js +5 -3
  165. package/package.json +18 -4
  166. /package/dist/{commands/dom → runtime/page}/screenshotResize.d.ts +0 -0
  167. /package/dist/{commands/dom → runtime/page}/screenshotResize.js +0 -0
@@ -529,12 +529,13 @@ export async function frameContextLostError(conn, page, frame) {
529
529
  * @param script - JavaScript expression
530
530
  * @param query - Requested frame (index, name/id attribute, or part of the name, id or URL)
531
531
  * @param listedIds - Frame id behind each index of the last `dom frames` listing, if any
532
+ * @param full - `--full`: copy the result with every entry
532
533
  * @returns Value, type and the frame's URL
533
534
  * @throws CommandError (81/83) when the frame is ambiguous or missing, (87)
534
535
  * when an index names another frame than when it was listed, (83) when it
535
536
  * navigated or was removed while the script ran, else as evaluateScript
536
537
  */
537
- export async function evaluateInFrame(page, wsUrl, script, query, listedIds) {
538
+ export async function evaluateInFrame(page, wsUrl, script, query, listedIds, full = false) {
538
539
  return withFrameConnection(page, wsUrl, async (fc) => {
539
540
  const { frame, uniqueContextId } = await resolveFrame(fc, query, listedIds);
540
541
  try {
@@ -542,6 +543,7 @@ export async function evaluateInFrame(page, wsUrl, script, query, listedIds) {
542
543
  ...(frame.sessionId && { sessionId: frame.sessionId }),
543
544
  uniqueContextId,
544
545
  recovery: recoverySender(fc, frame.sessionId),
546
+ full,
545
547
  });
546
548
  return { ...result, frame: frame.info.url };
547
549
  }
@@ -23,6 +23,15 @@ import type { Protocol } from '../../connection/typed-cdp.js';
23
23
  type PageConnection = Pick<CDPConnection, 'send'> & Partial<Pick<CDPConnection, 'on'>>;
24
24
  /** Name of bdg's isolated world (shown in DevTools' context selector) */
25
25
  export declare const BDG_WORLD_NAME = "bdg";
26
+ /**
27
+ * Whether bdg's world is known for a connection: created or being created,
28
+ * and not forgotten since (the top frame navigated, or its contexts were
29
+ * cleared). Scripts left in a forgotten world went with its document.
30
+ *
31
+ * @param cdp - Connection to the page
32
+ * @returns True while the world is known
33
+ */
34
+ export declare function hasBdgWorld(cdp: PageConnection): boolean;
26
35
  /**
27
36
  * `Runtime.evaluate` in bdg's world of the top frame. A call that names its
28
37
  * own context is sent as it is.
@@ -37,6 +37,17 @@ function worldContext(cdp) {
37
37
  worlds.set(cdp, created);
38
38
  return created;
39
39
  }
40
+ /**
41
+ * Whether bdg's world is known for a connection: created or being created,
42
+ * and not forgotten since (the top frame navigated, or its contexts were
43
+ * cleared). Scripts left in a forgotten world went with its document.
44
+ *
45
+ * @param cdp - Connection to the page
46
+ * @returns True while the world is known
47
+ */
48
+ export function hasBdgWorld(cdp) {
49
+ return worlds.has(cdp);
50
+ }
40
51
  /**
41
52
  * Create the world in the top frame and forget it when the page changes.
42
53
  *
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Page emulation a screenshot changes for its capture, and puts back.
3
+ *
4
+ * A capture at a pixel ratio of 1 on a high-DPI page, and one beyond the
5
+ * viewport, override the device metrics (and hide the scrollbars). Each
6
+ * change is recorded before it is sent, so {@link CaptureEmulation.restore}
7
+ * (called once the capture ended, however it ended) undoes exactly what may
8
+ * have been changed: the session's emulation is put back from the daemon's
9
+ * own record of it as it is then (a `page emulate` during the capture
10
+ * counts), not from a file.
11
+ */
12
+ import type { CDPConnection } from '../../connection/cdp.js';
13
+ import type { ViewportSize } from '../../types.js';
14
+ /** Width and height in CSS px */
15
+ export interface Size {
16
+ width: number;
17
+ height: number;
18
+ }
19
+ /** A page position in CSS px */
20
+ export interface ScrollPosition {
21
+ x: number;
22
+ y: number;
23
+ }
24
+ /**
25
+ * Evaluate one of bdg's page scripts and return its value.
26
+ *
27
+ * @param cdp - Session connection
28
+ * @param expression - Script
29
+ * @returns Its value, or undefined when it threw
30
+ */
31
+ export declare function evaluateValue(cdp: CDPConnection, expression: string): Promise<unknown>;
32
+ /**
33
+ * Two numbers a page script returned as an array.
34
+ *
35
+ * @param cdp - Session connection
36
+ * @param expression - Script returning `[a, b]`
37
+ * @returns The numbers, undefined where the page did not answer
38
+ */
39
+ export declare function evaluatePair(cdp: CDPConnection, expression: string): Promise<[number | undefined, number | undefined]>;
40
+ /**
41
+ * The page's scroll position.
42
+ *
43
+ * @param cdp - Session connection
44
+ * @returns Scroll offsets in CSS px
45
+ */
46
+ export declare function scrollPosition(cdp: CDPConnection): Promise<ScrollPosition>;
47
+ /**
48
+ * The emulation changes of one capture, undone by {@link restore}.
49
+ */
50
+ export declare class CaptureEmulation {
51
+ private readonly connection;
52
+ private readonly sessionViewport;
53
+ private readonly cdp;
54
+ private metricsChanged;
55
+ private scrollbarsHidden;
56
+ private scrolledFrom;
57
+ /**
58
+ * @param connection - Session connection
59
+ * @param sessionViewport - Reads the session's emulated viewport
60
+ * (`--viewport`, `--mobile`, `page emulate`) as it is now; put back
61
+ * afterwards, none clears the override
62
+ */
63
+ constructor(connection: CDPConnection, sessionViewport: () => ViewportSize | undefined);
64
+ /**
65
+ * Capture at a pixel ratio of 1 (CSS px = image px) on a high-DPI page:
66
+ * the viewport is overridden at the session's viewport, else the window's
67
+ * size with its scrollbars (so the layout does not change).
68
+ *
69
+ * @param devicePixelRatio - Page's pixel ratio
70
+ * @param viewport - Visible viewport size, used when the page does not answer
71
+ */
72
+ useUnitPixelRatio(devicePixelRatio: number, viewport: Size): Promise<void>;
73
+ /**
74
+ * Lay the page out at its current width without scrollbars: a capture
75
+ * beyond the viewport hides them, and without this the page would widen by
76
+ * the scrollbar and centered content move after it was measured. The
77
+ * viewport is overridden at the visible size (CSS px, pixel ratio 1; still
78
+ * a phone in a phone session).
79
+ *
80
+ * @param view - Visible viewport size
81
+ */
82
+ keepLayoutWithoutScrollbars(view: Size): Promise<void>;
83
+ /**
84
+ * Record where the page was scrolled before the capture moved it, to
85
+ * scroll it back afterwards (the first position recorded wins).
86
+ *
87
+ * @param position - Scroll position before the capture
88
+ */
89
+ scrolledAwayFrom(position: ScrollPosition): void;
90
+ /**
91
+ * Put back what the capture changed: scrollbars shown, the session's
92
+ * device metrics (its viewport and a phone's touch input, which a capture
93
+ * beyond the viewport turns off), else none, and the scroll position. Each
94
+ * step runs even when an earlier one failed.
95
+ *
96
+ * @throws The first step's error, once every step ran
97
+ */
98
+ restore(): Promise<void>;
99
+ /**
100
+ * The steps {@link restore} runs, for what the capture changed.
101
+ *
102
+ * @returns Named steps, in order
103
+ */
104
+ private restoreSteps;
105
+ /**
106
+ * Put back the session's device metrics, or clear the override.
107
+ */
108
+ private restoreSessionMetrics;
109
+ /**
110
+ * The window's size with its scrollbars (`innerWidth`/`innerHeight`): an
111
+ * override at this size keeps the page's layout, where the visible size
112
+ * (without scrollbars) would narrow it and move centered content.
113
+ *
114
+ * @param viewport - Visible viewport size, used when the page does not answer
115
+ * @returns Width and height in CSS px
116
+ */
117
+ private windowSize;
118
+ }
119
+ //# sourceMappingURL=captureEmulation.d.ts.map
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Page emulation a screenshot changes for its capture, and puts back.
3
+ *
4
+ * A capture at a pixel ratio of 1 on a high-DPI page, and one beyond the
5
+ * viewport, override the device metrics (and hide the scrollbars). Each
6
+ * change is recorded before it is sent, so {@link CaptureEmulation.restore}
7
+ * (called once the capture ended, however it ended) undoes exactly what may
8
+ * have been changed: the session's emulation is put back from the daemon's
9
+ * own record of it as it is then (a `page emulate` during the capture
10
+ * counts), not from a file.
11
+ */
12
+ import { TypedCDPConnection } from '../../connection/typed-cdp.js';
13
+ import { evaluateInBdgWorld } from './bdgWorld.js';
14
+ import { viewportOverride } from './emulation.js';
15
+ import { createLogger } from '../../ui/logging/index.js';
16
+ import { getErrorMessage } from '../../utils/errors.js';
17
+ const log = createLogger('dom');
18
+ /**
19
+ * Evaluate one of bdg's page scripts and return its value.
20
+ *
21
+ * @param cdp - Session connection
22
+ * @param expression - Script
23
+ * @returns Its value, or undefined when it threw
24
+ */
25
+ export async function evaluateValue(cdp, expression) {
26
+ const response = await evaluateInBdgWorld(cdp, { expression, returnByValue: true });
27
+ return response.result.value;
28
+ }
29
+ /**
30
+ * Two numbers a page script returned as an array.
31
+ *
32
+ * @param cdp - Session connection
33
+ * @param expression - Script returning `[a, b]`
34
+ * @returns The numbers, undefined where the page did not answer
35
+ */
36
+ export async function evaluatePair(cdp, expression) {
37
+ const value = await evaluateValue(cdp, expression);
38
+ const [a, b] = Array.isArray(value) ? value : [];
39
+ return [a, b];
40
+ }
41
+ /**
42
+ * The page's scroll position.
43
+ *
44
+ * @param cdp - Session connection
45
+ * @returns Scroll offsets in CSS px
46
+ */
47
+ export async function scrollPosition(cdp) {
48
+ const [x, y] = await evaluatePair(cdp, '[window.scrollX, window.scrollY]');
49
+ return { x: x ?? 0, y: y ?? 0 };
50
+ }
51
+ /**
52
+ * The emulation changes of one capture, undone by {@link restore}.
53
+ */
54
+ export class CaptureEmulation {
55
+ connection;
56
+ sessionViewport;
57
+ cdp;
58
+ metricsChanged = false;
59
+ scrollbarsHidden = false;
60
+ scrolledFrom;
61
+ /**
62
+ * @param connection - Session connection
63
+ * @param sessionViewport - Reads the session's emulated viewport
64
+ * (`--viewport`, `--mobile`, `page emulate`) as it is now; put back
65
+ * afterwards, none clears the override
66
+ */
67
+ constructor(connection, sessionViewport) {
68
+ this.connection = connection;
69
+ this.sessionViewport = sessionViewport;
70
+ this.cdp = new TypedCDPConnection(connection);
71
+ }
72
+ /**
73
+ * Capture at a pixel ratio of 1 (CSS px = image px) on a high-DPI page:
74
+ * the viewport is overridden at the session's viewport, else the window's
75
+ * size with its scrollbars (so the layout does not change).
76
+ *
77
+ * @param devicePixelRatio - Page's pixel ratio
78
+ * @param viewport - Visible viewport size, used when the page does not answer
79
+ */
80
+ async useUnitPixelRatio(devicePixelRatio, viewport) {
81
+ if (devicePixelRatio === 1)
82
+ return;
83
+ const size = this.sessionViewport() ?? (await this.windowSize(viewport));
84
+ this.metricsChanged = true;
85
+ await this.cdp.send('Emulation.setDeviceMetricsOverride', viewportOverride(size, 1));
86
+ }
87
+ /**
88
+ * Lay the page out at its current width without scrollbars: a capture
89
+ * beyond the viewport hides them, and without this the page would widen by
90
+ * the scrollbar and centered content move after it was measured. The
91
+ * viewport is overridden at the visible size (CSS px, pixel ratio 1; still
92
+ * a phone in a phone session).
93
+ *
94
+ * @param view - Visible viewport size
95
+ */
96
+ async keepLayoutWithoutScrollbars(view) {
97
+ const phone = this.sessionViewport()?.mobile;
98
+ this.scrollbarsHidden = true;
99
+ await this.cdp.send('Emulation.setScrollbarsHidden', { hidden: true });
100
+ this.metricsChanged = true;
101
+ await this.cdp.send('Emulation.setDeviceMetricsOverride', viewportOverride({ ...view, ...(phone && { mobile: true }) }, 1));
102
+ }
103
+ /**
104
+ * Record where the page was scrolled before the capture moved it, to
105
+ * scroll it back afterwards (the first position recorded wins).
106
+ *
107
+ * @param position - Scroll position before the capture
108
+ */
109
+ scrolledAwayFrom(position) {
110
+ this.scrolledFrom ??= position;
111
+ }
112
+ /**
113
+ * Put back what the capture changed: scrollbars shown, the session's
114
+ * device metrics (its viewport and a phone's touch input, which a capture
115
+ * beyond the viewport turns off), else none, and the scroll position. Each
116
+ * step runs even when an earlier one failed.
117
+ *
118
+ * @throws The first step's error, once every step ran
119
+ */
120
+ async restore() {
121
+ const failures = [];
122
+ for (const [name, step] of this.restoreSteps()) {
123
+ try {
124
+ await step();
125
+ }
126
+ catch (error) {
127
+ log.debug(`Screenshot restore (${name}) failed: ${getErrorMessage(error)}`);
128
+ failures.push(error);
129
+ }
130
+ }
131
+ if (failures.length > 0)
132
+ throw failures[0];
133
+ }
134
+ /**
135
+ * The steps {@link restore} runs, for what the capture changed.
136
+ *
137
+ * @returns Named steps, in order
138
+ */
139
+ restoreSteps() {
140
+ const steps = [];
141
+ if (this.scrollbarsHidden) {
142
+ steps.push([
143
+ 'scrollbars',
144
+ () => this.cdp.send('Emulation.setScrollbarsHidden', { hidden: false }),
145
+ ]);
146
+ }
147
+ if (this.metricsChanged)
148
+ steps.push(['device metrics', () => this.restoreSessionMetrics()]);
149
+ const scrolledFrom = this.scrolledFrom;
150
+ if (scrolledFrom) {
151
+ const { x, y } = scrolledFrom;
152
+ steps.push(['scroll', () => evaluateValue(this.connection, `window.scrollTo(${x}, ${y})`)]);
153
+ }
154
+ return steps;
155
+ }
156
+ /**
157
+ * Put back the session's device metrics, or clear the override.
158
+ */
159
+ async restoreSessionMetrics() {
160
+ const viewport = this.sessionViewport();
161
+ if (!viewport) {
162
+ await this.cdp.send('Emulation.clearDeviceMetricsOverride', {});
163
+ return;
164
+ }
165
+ await this.cdp.send('Emulation.setDeviceMetricsOverride', viewportOverride(viewport));
166
+ if (viewport.mobile) {
167
+ await this.cdp.send('Emulation.setTouchEmulationEnabled', {
168
+ enabled: true,
169
+ maxTouchPoints: 5,
170
+ });
171
+ }
172
+ }
173
+ /**
174
+ * The window's size with its scrollbars (`innerWidth`/`innerHeight`): an
175
+ * override at this size keeps the page's layout, where the visible size
176
+ * (without scrollbars) would narrow it and move centered content.
177
+ *
178
+ * @param viewport - Visible viewport size, used when the page does not answer
179
+ * @returns Width and height in CSS px
180
+ */
181
+ async windowSize(viewport) {
182
+ const [width, height] = await evaluatePair(this.connection, '[window.innerWidth, window.innerHeight]');
183
+ return {
184
+ width: Math.round(width ?? viewport.width),
185
+ height: Math.round(height ?? viewport.height),
186
+ };
187
+ }
188
+ }
189
+ //# sourceMappingURL=captureEmulation.js.map
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Scrolling an element into view for a page screenshot (`--scroll`), and
3
+ * waiting for the page to settle afterwards (lazy loading, mutations).
4
+ */
5
+ import type { CDPConnection } from '../../connection/cdp.js';
6
+ import { type ScrollPosition } from './captureEmulation.js';
7
+ /**
8
+ * Scroll an element into view (centered) and wait for the page to settle.
9
+ *
10
+ * @param cdp - Session connection
11
+ * @param selector - The element
12
+ * @returns Scroll position before, to put the page back afterwards
13
+ * @throws CommandError (83) when nothing matches
14
+ */
15
+ export declare function scrollToElement(cdp: CDPConnection, selector: string): Promise<ScrollPosition>;
16
+ /**
17
+ * Scroll an element into view again (centered), e.g. after an override moved
18
+ * the page.
19
+ *
20
+ * @param cdp - Session connection
21
+ * @param selector - The element
22
+ */
23
+ export declare function scrollIntoViewAgain(cdp: CDPConnection, selector: string): Promise<void>;
24
+ //# sourceMappingURL=captureScroll.d.ts.map
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Scrolling an element into view for a page screenshot (`--scroll`), and
3
+ * waiting for the page to settle afterwards (lazy loading, mutations).
4
+ */
5
+ import { CommandError } from '../../errors/index.js';
6
+ import { noNodesFoundError } from '../../errors/messages.js';
7
+ import { DEEP_QUERY_JS, selectorArgsJS } from '../dom/targetNode.js';
8
+ import { evaluateValue } from './captureEmulation.js';
9
+ import { createLogger } from '../../ui/logging/index.js';
10
+ import { delay } from '../../utils/async.js';
11
+ import { EXIT_CODES } from '../../utils/exitCodes.js';
12
+ const log = createLogger('dom');
13
+ const POST_SCROLL_NETWORK_IDLE_MS = 150;
14
+ const POST_SCROLL_DOM_STABLE_MS = 200;
15
+ const POST_SCROLL_MAX_WAIT_MS = 2000;
16
+ const STABILITY_CHECK_INTERVAL_MS = 50;
17
+ /** Page-side: start recording resource loads and DOM mutations */
18
+ const WATCH_STABILITY_JS = `(() => {
19
+ window.__bdg_scrollStability = {
20
+ lastNetworkActivity: Date.now(),
21
+ lastDomMutation: Date.now(),
22
+ activeRequests: 0
23
+ };
24
+ const state = window.__bdg_scrollStability;
25
+ if (window.PerformanceObserver) {
26
+ const perfObserver = new PerformanceObserver((list) => {
27
+ for (const entry of list.getEntries()) {
28
+ if (entry.entryType === 'resource') state.lastNetworkActivity = Date.now();
29
+ }
30
+ });
31
+ try {
32
+ perfObserver.observe({ entryTypes: ['resource'] });
33
+ state.perfObserver = perfObserver;
34
+ } catch (e) {}
35
+ }
36
+ const mutationObserver = new MutationObserver(() => {
37
+ state.lastDomMutation = Date.now();
38
+ });
39
+ mutationObserver.observe(document.body || document.documentElement, {
40
+ childList: true,
41
+ subtree: true,
42
+ attributes: true
43
+ });
44
+ state.mutationObserver = mutationObserver;
45
+ })()`;
46
+ /** Page-side: milliseconds since the last resource load and DOM mutation */
47
+ const STABILITY_JS = `(() => {
48
+ const state = window.__bdg_scrollStability;
49
+ if (!state) return { networkIdle: 999, domIdle: 999 };
50
+ return {
51
+ networkIdle: Date.now() - state.lastNetworkActivity,
52
+ domIdle: Date.now() - state.lastDomMutation
53
+ };
54
+ })()`;
55
+ /** Page-side: stop recording */
56
+ const UNWATCH_STABILITY_JS = `(() => {
57
+ const state = window.__bdg_scrollStability;
58
+ if (state) {
59
+ state.perfObserver?.disconnect();
60
+ state.mutationObserver?.disconnect();
61
+ delete window.__bdg_scrollStability;
62
+ }
63
+ })()`;
64
+ /**
65
+ * Wait for the page to settle after a programmatic scroll (lazy-load idle +
66
+ * DOM mutation idle). Uses shorter thresholds than full page load.
67
+ *
68
+ * @param cdp - Session connection
69
+ */
70
+ async function waitForPostScrollStability(cdp) {
71
+ const deadline = Date.now() + POST_SCROLL_MAX_WAIT_MS;
72
+ await evaluateValue(cdp, WATCH_STABILITY_JS);
73
+ try {
74
+ while (Date.now() < deadline) {
75
+ const value = (await evaluateValue(cdp, STABILITY_JS));
76
+ const networkIdle = value?.networkIdle ?? 0;
77
+ const domIdle = value?.domIdle ?? 0;
78
+ if (networkIdle >= POST_SCROLL_NETWORK_IDLE_MS && domIdle >= POST_SCROLL_DOM_STABLE_MS) {
79
+ log.debug(`Post-scroll stable: network ${networkIdle}ms, DOM ${domIdle}ms`);
80
+ return;
81
+ }
82
+ await delay(STABILITY_CHECK_INTERVAL_MS);
83
+ }
84
+ log.debug('Post-scroll stability timeout, proceeding anyway');
85
+ }
86
+ finally {
87
+ await evaluateValue(cdp, UNWATCH_STABILITY_JS);
88
+ }
89
+ }
90
+ /**
91
+ * Scroll an element into view (centered) and wait for the page to settle.
92
+ *
93
+ * @param cdp - Session connection
94
+ * @param selector - The element
95
+ * @returns Scroll position before, to put the page back afterwards
96
+ * @throws CommandError (83) when nothing matches
97
+ */
98
+ export async function scrollToElement(cdp, selector) {
99
+ const value = (await evaluateValue(cdp, `(() => {
100
+ const el = (${DEEP_QUERY_JS})(${selectorArgsJS(selector)})[0];
101
+ if (!el) return { found: false };
102
+ const originalX = window.scrollX;
103
+ const originalY = window.scrollY;
104
+ el.scrollIntoView({ block: 'center', behavior: 'instant' });
105
+ return { found: true, originalX, originalY };
106
+ })()`));
107
+ if (!value?.found) {
108
+ const err = noNodesFoundError(selector);
109
+ throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.RESOURCE_NOT_FOUND);
110
+ }
111
+ await waitForPostScrollStability(cdp);
112
+ return { x: value.originalX ?? 0, y: value.originalY ?? 0 };
113
+ }
114
+ /**
115
+ * Scroll an element into view again (centered), e.g. after an override moved
116
+ * the page.
117
+ *
118
+ * @param cdp - Session connection
119
+ * @param selector - The element
120
+ */
121
+ export async function scrollIntoViewAgain(cdp, selector) {
122
+ await evaluateValue(cdp, `(${DEEP_QUERY_JS})(${selectorArgsJS(selector)})[0]?.scrollIntoView({ block: 'center', behavior: 'instant' })`);
123
+ }
124
+ //# sourceMappingURL=captureScroll.js.map
@@ -60,12 +60,13 @@ export async function applySessionEmulation(cdp, emulation) {
60
60
  */
61
61
  async function emulatePhone(cdp, on) {
62
62
  await cdp.send('Emulation.setTouchEmulationEnabled', { enabled: on, maxTouchPoints: on ? 5 : 1 });
63
- const { userAgent } = (await cdp.send('Browser.getVersion', {}));
63
+ const version = (await cdp.send('Browser.getVersion', {}));
64
+ const { userAgent } = version;
64
65
  if (!on) {
65
- const headless = userAgent.includes('HeadlessChrome');
66
- await cdp.send('Emulation.setUserAgentOverride', { userAgent: headless ? userAgent : '' });
67
- if (headless)
68
- await hideHeadlessUserAgent(cdp, log);
66
+ if (userAgent.includes('HeadlessChrome'))
67
+ await hideHeadlessUserAgent(cdp, log, version);
68
+ else
69
+ await cdp.send('Emulation.setUserAgentOverride', { userAgent: '' });
69
70
  return;
70
71
  }
71
72
  const major = /Chrome\/(\d+)/.exec(userAgent)?.[1] ?? '';
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Screenshots of the page or one element (`bdg dom screenshot`), taken in the
3
+ * daemon: measuring, the emulation changes the capture needs, the capture and
4
+ * putting the emulation back all happen here, inside one `try`/`finally`, so
5
+ * a CLI interrupted mid-capture (Ctrl-C) cannot leave the page changed. The
6
+ * CLI only writes the returned image.
7
+ *
8
+ * The image travels base64-encoded in one IPC line: Chrome sends it the same
9
+ * way over its WebSocket (100 MiB at most), well below the IPC line limit
10
+ * (`MAX_JSONL_BUFFER_SIZE`).
11
+ */
12
+ import type { CDPConnection } from '../../connection/cdp.js';
13
+ import type { DomScreenshotCommand, DomScreenshotData } from '../../ipc/protocol/commands.js';
14
+ import { type BusyRecoveryOptions } from '../dom/evalHelpers.js';
15
+ import type { ViewportSize } from '../../types.js';
16
+ /** How a screenshot is run */
17
+ export interface ScreenshotOptions {
18
+ /** Aborted when the requesting client disconnects: the capture is skipped, the restore runs */
19
+ abandoned?: AbortSignal | undefined;
20
+ /** When a busy page is checked */
21
+ recovery?: BusyRecoveryOptions;
22
+ }
23
+ /**
24
+ * Take a screenshot: of the element `backendNodeId` names, else of the page.
25
+ * Whatever emulation the capture changed is put back before this returns or
26
+ * throws, from the session's record of its emulation at that time.
27
+ *
28
+ * A page whose scripts keep it busy gets them terminated
29
+ * ({@link withBusyPageRecovery}, exit 102); the capture then ends, and the
30
+ * error is reported once its emulation is back, so the next command does not
31
+ * race the restore. A capture whose client left (Ctrl-C) is not taken once
32
+ * that is known: only the restore runs.
33
+ *
34
+ * @param cdp - Session connection
35
+ * @param params - What to capture and how
36
+ * @param sessionViewport - Reads the session's emulated viewport, if any
37
+ * @param options - When the client left, and when a busy page is checked (tests shorten it)
38
+ * @returns The image (base64) and what was captured
39
+ */
40
+ export declare function takeScreenshot(cdp: CDPConnection, params: DomScreenshotCommand, sessionViewport: () => ViewportSize | undefined, options?: ScreenshotOptions): Promise<DomScreenshotData>;
41
+ //# sourceMappingURL=screenshot.d.ts.map