browser-debugger-cli 0.13.0 → 0.15.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 (159) hide show
  1. package/.claude/skills/bdg/SKILL.md +101 -187
  2. package/README.md +4 -4
  3. package/dist/commands/cdp.js +1 -0
  4. package/dist/commands/cleanup.js +3 -0
  5. package/dist/commands/console.js +5 -1
  6. package/dist/commands/dom/a11y.d.ts +1 -1
  7. package/dist/commands/dom/a11y.js +20 -20
  8. package/dist/commands/dom/eval.d.ts +3 -1
  9. package/dist/commands/dom/eval.js +8 -5
  10. package/dist/commands/dom/formInteraction.js +1 -1
  11. package/dist/commands/dom/get.js +25 -7
  12. package/dist/commands/dom/helpers/evalResult.d.ts +36 -0
  13. package/dist/commands/dom/helpers/evalResult.js +59 -0
  14. package/dist/commands/dom/index.js +7 -2
  15. package/dist/commands/dom/query.d.ts +2 -1
  16. package/dist/commands/dom/query.js +5 -3
  17. package/dist/commands/dom/screenshot.js +1 -0
  18. package/dist/commands/helpJson.d.ts +1 -1
  19. package/dist/commands/helpJson.js +4 -4
  20. package/dist/commands/helpTopic.js +10 -4
  21. package/dist/commands/network/har.js +18 -14
  22. package/dist/commands/network/list.js +46 -3
  23. package/dist/commands/optionBehaviors.d.ts +25 -2
  24. package/dist/commands/optionBehaviors.js +60 -42
  25. package/dist/commands/peek.js +3 -0
  26. package/dist/commands/shared/CommandRunner.js +13 -13
  27. package/dist/commands/shared/daemonErrorHandler.js +2 -2
  28. package/dist/commands/shared/dataFetcher.d.ts +4 -2
  29. package/dist/commands/shared/dataFetcher.js +11 -3
  30. package/dist/commands/shared/handleValidationError.js +3 -3
  31. package/dist/commands/shared/optionTypes.d.ts +15 -3
  32. package/dist/commands/shared/outputFile.d.ts +2 -1
  33. package/dist/commands/shared/outputFile.js +7 -4
  34. package/dist/commands/shared/startHelpers.js +3 -3
  35. package/dist/commands/status.js +3 -1
  36. package/dist/commands/stop.js +2 -1
  37. package/dist/connection/chromeIdentity.d.ts +8 -2
  38. package/dist/connection/chromeIdentity.js +85 -13
  39. package/dist/connection/launcher/flagsBuilder.d.ts +46 -0
  40. package/dist/connection/launcher/flagsBuilder.js +107 -23
  41. package/dist/connection/launcher.d.ts +1 -1
  42. package/dist/connection/launcher.js +1 -2
  43. package/dist/constants.d.ts +31 -5
  44. package/dist/constants.js +37 -5
  45. package/dist/daemon/SessionController.js +2 -0
  46. package/dist/daemon/launcher.d.ts +17 -3
  47. package/dist/daemon/launcher.js +37 -7
  48. package/dist/daemon/session/Session.d.ts +2 -1
  49. package/dist/daemon/session/Session.js +10 -2
  50. package/dist/daemon/session/TelemetryStore.d.ts +7 -0
  51. package/dist/daemon/session/TelemetryStore.js +6 -0
  52. package/dist/daemon/session/commandRegistry.js +25 -7
  53. package/dist/daemon/session/matchedStylesReset.d.ts +26 -0
  54. package/dist/daemon/session/matchedStylesReset.js +46 -0
  55. package/dist/daemon/session/plugins.js +1 -0
  56. package/dist/daemon/session/triggeredRequests.d.ts +0 -5
  57. package/dist/daemon/session/triggeredRequests.js +13 -7
  58. package/dist/daemon.js +8742 -8315
  59. package/dist/errors/messages.d.ts +31 -0
  60. package/dist/errors/messages.js +96 -6
  61. package/dist/index.js +1129 -548
  62. package/dist/ipc/client.d.ts +6 -1
  63. package/dist/ipc/client.js +11 -2
  64. package/dist/ipc/protocol/commands.d.ts +8 -0
  65. package/dist/ipc/protocol/inspectTypes.d.ts +5 -2
  66. package/dist/ipc/session/types.d.ts +5 -1
  67. package/dist/ipc/transport/index.d.ts +6 -0
  68. package/dist/ipc/transport/index.js +16 -1
  69. package/dist/program.d.ts +14 -0
  70. package/dist/program.js +53 -0
  71. package/dist/runtime/dom/elementGeometry.d.ts +23 -0
  72. package/dist/runtime/dom/elementGeometry.js +17 -15
  73. package/dist/runtime/dom/elementInfo.d.ts +13 -4
  74. package/dist/runtime/dom/elementInfo.js +15 -5
  75. package/dist/runtime/dom/evalHelpers.d.ts +24 -4
  76. package/dist/runtime/dom/evalHelpers.js +40 -12
  77. package/dist/runtime/dom/frameScopedConnection.d.ts +7 -0
  78. package/dist/runtime/dom/frameScopedConnection.js +2 -2
  79. package/dist/runtime/dom/frames.d.ts +2 -1
  80. package/dist/runtime/dom/frames.js +3 -1
  81. package/dist/runtime/dom/inspect.d.ts +17 -3
  82. package/dist/runtime/dom/inspect.js +40 -26
  83. package/dist/runtime/dom/inspectModel.d.ts +3 -3
  84. package/dist/runtime/dom/inspectRules.d.ts +29 -3
  85. package/dist/runtime/dom/inspectRules.js +205 -11
  86. package/dist/runtime/dom/layout.d.ts +0 -2
  87. package/dist/runtime/dom/layout.js +1 -2
  88. package/dist/runtime/dom/reactEventHelpers.d.ts +4 -1
  89. package/dist/runtime/dom/reactEventHelpers.js +9 -2
  90. package/dist/runtime/dom/targetNode.d.ts +10 -6
  91. package/dist/runtime/dom/targetNode.js +15 -8
  92. package/dist/runtime/page/emulation.js +6 -5
  93. package/dist/runtime/page/userAgent.d.ts +86 -2
  94. package/dist/runtime/page/userAgent.js +154 -33
  95. package/dist/session/paths.d.ts +38 -3
  96. package/dist/session/paths.js +154 -7
  97. package/dist/session/portClaims.d.ts +0 -8
  98. package/dist/session/portClaims.js +1 -22
  99. package/dist/session/sessionList.d.ts +5 -1
  100. package/dist/session/sessionList.js +5 -1
  101. package/dist/telemetry/a11y.d.ts +15 -1
  102. package/dist/telemetry/a11y.js +83 -0
  103. package/dist/telemetry/har/builder.d.ts +12 -1
  104. package/dist/telemetry/har/builder.js +11 -3
  105. package/dist/telemetry/har/sanitize.d.ts +24 -0
  106. package/dist/telemetry/har/sanitize.js +138 -0
  107. package/dist/telemetry/har/sanitizeBody.d.ts +38 -0
  108. package/dist/telemetry/har/sanitizeBody.js +168 -0
  109. package/dist/telemetry/network.d.ts +13 -16
  110. package/dist/telemetry/network.js +30 -52
  111. package/dist/telemetry/networkRetention.d.ts +83 -0
  112. package/dist/telemetry/networkRetention.js +117 -0
  113. package/dist/types.d.ts +26 -0
  114. package/dist/ui/OutputBuilder.d.ts +10 -0
  115. package/dist/ui/OutputBuilder.js +12 -0
  116. package/dist/ui/formatters/a11y.d.ts +5 -7
  117. package/dist/ui/formatters/a11y.js +7 -61
  118. package/dist/ui/formatters/console/chronological.js +4 -4
  119. package/dist/ui/formatters/console/follow.d.ts +4 -2
  120. package/dist/ui/formatters/console/follow.js +6 -3
  121. package/dist/ui/formatters/console/json.d.ts +3 -6
  122. package/dist/ui/formatters/console/json.js +9 -13
  123. package/dist/ui/formatters/console/shared.d.ts +17 -2
  124. package/dist/ui/formatters/console/shared.js +17 -0
  125. package/dist/ui/formatters/console/summarize.d.ts +2 -2
  126. package/dist/ui/formatters/console/summarize.js +22 -7
  127. package/dist/ui/formatters/console.d.ts +1 -1
  128. package/dist/ui/formatters/console.js +1 -5
  129. package/dist/ui/formatters/details.js +1 -1
  130. package/dist/ui/formatters/dom.d.ts +13 -4
  131. package/dist/ui/formatters/dom.js +25 -7
  132. package/dist/ui/formatters/layout.js +2 -1
  133. package/dist/ui/formatters/longValues.d.ts +14 -0
  134. package/dist/ui/formatters/longValues.js +23 -0
  135. package/dist/ui/formatters/networkList.d.ts +8 -2
  136. package/dist/ui/formatters/networkList.js +11 -2
  137. package/dist/ui/formatters/preview.d.ts +4 -1
  138. package/dist/ui/formatters/preview.js +55 -13
  139. package/dist/ui/formatters/sessions.d.ts +3 -2
  140. package/dist/ui/formatters/sessions.js +10 -3
  141. package/dist/ui/formatters/status.js +7 -0
  142. package/dist/ui/formatters/triggeredRequests.js +2 -1
  143. package/dist/ui/messages/chrome.d.ts +34 -7
  144. package/dist/ui/messages/chrome.js +81 -15
  145. package/dist/ui/messages/commands.d.ts +29 -8
  146. package/dist/ui/messages/commands.js +36 -8
  147. package/dist/ui/messages/networkMessages.d.ts +50 -0
  148. package/dist/ui/messages/networkMessages.js +66 -0
  149. package/dist/ui/messages/session.d.ts +8 -0
  150. package/dist/ui/messages/session.js +10 -0
  151. package/dist/utils/atomicFile.d.ts +2 -1
  152. package/dist/utils/atomicFile.js +5 -2
  153. package/dist/utils/directories.d.ts +41 -0
  154. package/dist/utils/directories.js +48 -0
  155. package/dist/utils/http.d.ts +9 -2
  156. package/dist/utils/http.js +4 -3
  157. package/dist/utils/strings.d.ts +19 -0
  158. package/dist/utils/strings.js +16 -0
  159. package/package.json +2 -2
@@ -208,8 +208,13 @@ export declare function callCDP(method: string, params?: Record<string, unknown>
208
208
  export declare function callBdgScript(method: string, params?: Record<string, unknown>): Promise<ClientResponse<'cdp_call'>>;
209
209
  /**
210
210
  * Evaluate a JavaScript expression in the active page (or one of its iframes) via the daemon.
211
+ *
212
+ * @param script - JavaScript expression
213
+ * @param frame - Iframe to evaluate in
214
+ * @param full - `--full`: copy an object or array result with every entry
215
+ * @returns The daemon's response
211
216
  */
212
- export declare function domEval(script: string, frame?: string): Promise<ClientResponse<'dom_eval'>>;
217
+ export declare function domEval(script: string, frame?: string, full?: boolean): Promise<ClientResponse<'dom_eval'>>;
213
218
  /**
214
219
  * List the page's iframes via the daemon.
215
220
  */
@@ -292,9 +292,18 @@ export function callBdgScript(method, params) {
292
292
  }
293
293
  /**
294
294
  * Evaluate a JavaScript expression in the active page (or one of its iframes) via the daemon.
295
+ *
296
+ * @param script - JavaScript expression
297
+ * @param frame - Iframe to evaluate in
298
+ * @param full - `--full`: copy an object or array result with every entry
299
+ * @returns The daemon's response
295
300
  */
296
- export async function domEval(script, frame) {
297
- return sendCommand('dom_eval', { script, ...(frame !== undefined && { frame }) });
301
+ export async function domEval(script, frame, full) {
302
+ return sendCommand('dom_eval', {
303
+ script,
304
+ ...(frame !== undefined && { frame }),
305
+ ...(full && { full }),
306
+ });
298
307
  }
299
308
  /**
300
309
  * List the page's iframes via the daemon.
@@ -74,6 +74,10 @@ export interface SessionPeekData {
74
74
  totalConsole: number;
75
75
  /** Console messages dropped at the limit (the oldest; indices start after them) */
76
76
  droppedConsole?: number;
77
+ /** Finished network requests dropped at the request cap (the oldest) */
78
+ droppedNetwork?: number;
79
+ /** Response bodies evicted at the total body budget (the oldest) */
80
+ evictedNetworkBodies?: number;
77
81
  /** Whether there are more network items available. */
78
82
  hasMoreNetwork?: boolean;
79
83
  /** Whether there are more console items available. */
@@ -183,6 +187,8 @@ export interface DomEvalCommand {
183
187
  script: string;
184
188
  /** Iframe to evaluate in: index, name/id attribute, or URL substring */
185
189
  frame?: string;
190
+ /** `--full`: copy an object or array result with every entry */
191
+ full?: boolean;
186
192
  }
187
193
  export interface DomEvalData {
188
194
  /** JSON value, or a readable description for values JSON cannot represent */
@@ -191,6 +197,8 @@ export interface DomEvalData {
191
197
  type: string;
192
198
  /** Object subtype (node, date, array, ...) */
193
199
  subtype?: string;
200
+ /** Elements of an array result in the page (`value` holds at most 1000 unless `full`) */
201
+ length?: number;
194
202
  /** URL of the iframe the script ran in (with `frame`; empty when it has none) */
195
203
  frame?: string;
196
204
  /** Set when the page replaced built-ins bdg's copy of the result uses */
@@ -447,7 +447,10 @@ export interface InspectResult {
447
447
  rules?: InspectRule[];
448
448
  /** `--why <property>`: one entry, or one per longhand of a shorthand whose sides differ */
449
449
  why?: InspectWhy[];
450
- /** The cascade was not read: Chrome took longer than the time allowed, or failed */
451
- cascade?: 'timeout' | 'failed';
450
+ /**
451
+ * The cascade was not read: Chrome took longer than the time allowed, failed,
452
+ * or (hints only) was not asked because it was too slow earlier on this page
453
+ */
454
+ cascade?: 'timeout' | 'failed' | 'skipped';
452
455
  }
453
456
  //# sourceMappingURL=inspectTypes.d.ts.map
@@ -8,8 +8,12 @@ import type { ColorScheme, ViewportSize } from '../../types.js';
8
8
  * Session activity metrics.
9
9
  */
10
10
  export interface SessionActivity {
11
- /** Total network requests captured. */
11
+ /** Network requests kept (finished ones; the newest at the cap). */
12
12
  networkRequestsCaptured: number;
13
+ /** Oldest finished requests dropped at the request cap (left out when none). */
14
+ networkRequestsDropped?: number;
15
+ /** Oldest response bodies evicted at the total body budget (left out when none). */
16
+ networkBodiesEvicted?: number;
13
17
  /** Total console messages captured. */
14
18
  consoleMessagesCaptured: number;
15
19
  /** Timestamp of last network request. */
@@ -18,6 +18,12 @@ type WithTypeAndSession = {
18
18
  * @param timeoutMs - How long to wait for the response (default: IPC timeout)
19
19
  * @param socketPath - Daemon socket (default: the selected session's)
20
20
  * @returns The daemon's response
21
+ * @throws CommandError (103) before connecting when the socket's session
22
+ * directory cannot be trusted (see {@link secureSessionDir}): a socket
23
+ * planted there by another user would receive the request. The check runs
24
+ * just before connecting, by path; replacing the socket in that window
25
+ * needs write access to a directory of the chain, which the check has
26
+ * just found only the user has
21
27
  */
22
28
  export declare function sendRequest<TRequest extends WithTypeAndSession, TResponse extends WithTypeAndSession>(request: TRequest, requestName: string, expectedType?: string, timeoutMs?: number, socketPath?: string): Promise<TResponse>;
23
29
  //# sourceMappingURL=index.d.ts.map
@@ -3,9 +3,13 @@
3
3
  *
4
4
  * Handles Unix domain socket communication with JSONL protocol.
5
5
  */
6
+ import * as path from 'path';
6
7
  import { getIPCRequestTimeout } from '../../constants.js';
7
- import { getDaemonSocketPath } from '../../session/paths.js';
8
+ import { CommandError } from '../../errors/index.js';
9
+ import { untrustedSessionDirError } from '../../errors/messages.js';
10
+ import { getDaemonSocketPath, secureSessionDir } from '../../session/paths.js';
8
11
  import { createLogger } from '../../ui/logging/index.js';
12
+ import { EXIT_CODES } from '../../utils/exitCodes.js';
9
13
  import { formatConnectionError, formatEarlyCloseError, formatParseError, formatTimeoutError, } from './errors.js';
10
14
  import { JSONLBuffer, parseJSONLFrame, toJSONLFrame } from './jsonl.js';
11
15
  import { createSocket } from './socket.js';
@@ -22,8 +26,19 @@ const log = createLogger('client');
22
26
  * @param timeoutMs - How long to wait for the response (default: IPC timeout)
23
27
  * @param socketPath - Daemon socket (default: the selected session's)
24
28
  * @returns The daemon's response
29
+ * @throws CommandError (103) before connecting when the socket's session
30
+ * directory cannot be trusted (see {@link secureSessionDir}): a socket
31
+ * planted there by another user would receive the request. The check runs
32
+ * just before connecting, by path; replacing the socket in that window
33
+ * needs write access to a directory of the chain, which the check has
34
+ * just found only the user has
25
35
  */
26
36
  export async function sendRequest(request, requestName, expectedType, timeoutMs = getIPCRequestTimeout(), socketPath = getDaemonSocketPath()) {
37
+ const untrusted = secureSessionDir(path.dirname(socketPath));
38
+ if (untrusted) {
39
+ const err = untrustedSessionDirError(untrusted);
40
+ throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.SESSION_FILE_ERROR);
41
+ }
27
42
  return new Promise((resolve, reject) => {
28
43
  const buffer = new JSONLBuffer();
29
44
  let resolved = false;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The bdg command tree: root command, its options and every registered command.
3
+ */
4
+ import { Command, type OutputConfiguration } from 'commander';
5
+ /**
6
+ * Build the bdg program with every command registered, without parsing
7
+ * anything. Commander errors throw instead of exiting; the output
8
+ * configuration is set before commands are registered so they inherit it.
9
+ *
10
+ * @param output - Where Commander writes help and errors
11
+ * @returns Root command
12
+ */
13
+ export declare function buildProgram(output?: OutputConfiguration): Command;
14
+ //# sourceMappingURL=program.d.ts.map
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The bdg command tree: root command, its options and every registered command.
3
+ */
4
+ import { Command, Option } from 'commander';
5
+ import { commandRegistry } from './commands.js';
6
+ import { VERSION } from './utils/version.js';
7
+ const CLI_NAME = 'bdg';
8
+ const CLI_DESCRIPTION = 'Browser telemetry via Chrome DevTools Protocol';
9
+ const SESSION_OPTION_FLAGS = '--session <name>';
10
+ const SESSION_OPTION_DESCRIPTION = 'Use a named session (own daemon, Chrome and port) instead of the default one; env: BDG_SESSION';
11
+ /**
12
+ * Make `--debug`, `-q` and `--session` accepted after any subcommand (program
13
+ * options are positional).
14
+ *
15
+ * @param command - Command whose subcommands get the hidden global options
16
+ */
17
+ function addGlobalOptions(command) {
18
+ for (const sub of command.commands) {
19
+ if (!sub.options.some((option) => option.long === '--debug')) {
20
+ sub.addOption(new Option('--debug', 'Enable debug logging').hideHelp());
21
+ }
22
+ if (!sub.options.some((option) => option.long === '--quiet')) {
23
+ sub.addOption(new Option('-q, --quiet', 'Hide tips and hints').hideHelp());
24
+ }
25
+ if (!sub.options.some((option) => option.long === '--session')) {
26
+ sub.addOption(new Option(SESSION_OPTION_FLAGS, SESSION_OPTION_DESCRIPTION).hideHelp());
27
+ }
28
+ addGlobalOptions(sub);
29
+ }
30
+ }
31
+ /**
32
+ * Build the bdg program with every command registered, without parsing
33
+ * anything. Commander errors throw instead of exiting; the output
34
+ * configuration is set before commands are registered so they inherit it.
35
+ *
36
+ * @param output - Where Commander writes help and errors
37
+ * @returns Root command
38
+ */
39
+ export function buildProgram(output = {}) {
40
+ const program = new Command()
41
+ .name(CLI_NAME)
42
+ .description(CLI_DESCRIPTION)
43
+ .version(VERSION)
44
+ .option('--debug', 'Enable debug logging (verbose output)')
45
+ .option(SESSION_OPTION_FLAGS, SESSION_OPTION_DESCRIPTION)
46
+ .enablePositionalOptions()
47
+ .exitOverride()
48
+ .configureOutput(output);
49
+ commandRegistry.forEach((register) => register(program));
50
+ addGlobalOptions(program);
51
+ return program;
52
+ }
53
+ //# sourceMappingURL=program.js.map
@@ -119,6 +119,29 @@ export declare const FRAME_OFFSET_JS = "(frame) => {\n const rect = frame.getBo
119
119
  * its bounding box is the box itself, scaled.
120
120
  */
121
121
  export declare const SCALES_ONLY_JS = "(style) => {\n if (style.rotate && style.rotate !== 'none') return false;\n if (!style.transform || style.transform === 'none') return true;\n const matrix = /^matrix\\(([^)]*)\\)$/.exec(style.transform);\n if (!matrix) return false;\n const values = matrix[1].split(',').map(parseFloat);\n return Math.abs(values[1]) < 1e-6 && Math.abs(values[2]) < 1e-6;\n}";
122
+ /**
123
+ * Page-side clip of a node by its ancestors in the flat tree (through the
124
+ * slots it is shown in and open shadow roots): the padding boxes
125
+ * ({@link CLIP_BOX_JS}) of those that cut off overflowing content and hold
126
+ * the node in their containing-block chain. An absolutely positioned node
127
+ * skips static ancestors (that are not transformed) up to its containing
128
+ * block, a fixed one is not clipped at all unless a transformed (or
129
+ * filtered, contained, …) ancestor holds it like an absolute one, and
130
+ * inline ancestors and `display: contents` ones have no box to clip with.
131
+ * The root element is left out (its overflow belongs to the viewport), and
132
+ * so is the body's overflow unless the root element's overflow is not
133
+ * `visible` (then the body keeps its own overflow and, e.g. as the page's
134
+ * scroller, clips like any container).
135
+ * Returns the clip, whether overlay scrollbars of the containers show along
136
+ * its right and bottom edges ({@link OVERLAY_SCROLLBARS_JS}), the innermost
137
+ * ancestor cutting off part of `rect` (null when none does), why the
138
+ * innermost clipping ancestor with no area (a collapsed
139
+ * `height: 0; overflow: hidden` accordion) hides it, e.g.
140
+ * `clipped by div#acc: zero height` (null when none has), and whether the
141
+ * node is fixed to the viewport (it or a container in its containing-block
142
+ * chain is `position: fixed`; `fixedBy` is that node).
143
+ */
144
+ export declare const ANCESTOR_CLIP_JS: string;
122
145
  /** Page-side test of a `clip-path` that cuts everything away: `inset()` with percentages leaving no area. */
123
146
  export declare const CLIP_PATH_CUTS_ALL_JS = "(clipPath) => {\n const inset = /^inset\\(([^)]*)\\)/.exec(clipPath || '');\n const values = inset ? inset[1].split(' round ')[0].trim().split(/\\s+/) : [];\n if (values.length === 0 || values.some((v) => !/%$/.test(v))) return false;\n const [top, right = top, bottom = top, left = right] = values.map(parseFloat);\n return top + bottom >= 100 || left + right >= 100;\n}";
124
147
  /**
@@ -152,28 +152,30 @@ const HOLDS_FIXED_JS = `(style) =>
152
152
  /transform|filter|perspective/.test(style.willChange) || /paint|layout|strict|content/.test(style.contain)`;
153
153
  /**
154
154
  * Page-side test of whether a `position: fixed` node is fixed to its
155
- * document's viewport (no ancestor holds it, {@link HOLDS_FIXED_JS}).
155
+ * document's viewport (no ancestor in the flat tree holds it,
156
+ * {@link HOLDS_FIXED_JS}).
156
157
  */
157
158
  const FIXED_TO_VIEWPORT_JS = `(n) => {
158
159
  const holdsFixed = ${HOLDS_FIXED_JS};
159
- const parentOf = (node) => node.parentElement || (node.parentNode && node.parentNode.host) || null;
160
+ const parentOf = ${FLAT_PARENT_JS};
160
161
  for (let p = parentOf(n); p && p !== n.ownerDocument.documentElement; p = parentOf(p)) {
161
162
  if (holdsFixed(p.ownerDocument.defaultView.getComputedStyle(p))) return false;
162
163
  }
163
164
  return true;
164
165
  }`;
165
166
  /**
166
- * Page-side clip of a node by its ancestors (looked up through open shadow
167
- * roots): the padding boxes ({@link CLIP_BOX_JS}) of those that cut off
168
- * overflowing content and hold the node in their containing-block chain. An
169
- * absolutely positioned node skips static ancestors (that are not
170
- * transformed) up to its containing block, a fixed one is not clipped at all
171
- * unless a transformed (or filtered, contained, …) ancestor holds it like an
172
- * absolute one, and inline ancestors and `display: contents` ones have no box
173
- * to clip with. The root element is left out (its overflow belongs to the
174
- * viewport), and so is the body's overflow unless the root element's
175
- * overflow is not `visible` (then the body keeps its own overflow and, e.g.
176
- * as the page's scroller, clips like any container).
167
+ * Page-side clip of a node by its ancestors in the flat tree (through the
168
+ * slots it is shown in and open shadow roots): the padding boxes
169
+ * ({@link CLIP_BOX_JS}) of those that cut off overflowing content and hold
170
+ * the node in their containing-block chain. An absolutely positioned node
171
+ * skips static ancestors (that are not transformed) up to its containing
172
+ * block, a fixed one is not clipped at all unless a transformed (or
173
+ * filtered, contained, …) ancestor holds it like an absolute one, and
174
+ * inline ancestors and `display: contents` ones have no box to clip with.
175
+ * The root element is left out (its overflow belongs to the viewport), and
176
+ * so is the body's overflow unless the root element's overflow is not
177
+ * `visible` (then the body keeps its own overflow and, e.g. as the page's
178
+ * scroller, clips like any container).
177
179
  * Returns the clip, whether overlay scrollbars of the containers show along
178
180
  * its right and bottom edges ({@link OVERLAY_SCROLLBARS_JS}), the innermost
179
181
  * ancestor cutting off part of `rect` (null when none does), why the
@@ -183,14 +185,14 @@ const FIXED_TO_VIEWPORT_JS = `(n) => {
183
185
  * node is fixed to the viewport (it or a container in its containing-block
184
186
  * chain is `position: fixed`; `fixedBy` is that node).
185
187
  */
186
- const ANCESTOR_CLIP_JS = `(node, rect, describe) => {
188
+ export const ANCESTOR_CLIP_JS = `(node, rect, describe) => {
187
189
  const clipBox = ${CLIP_BOX_JS};
188
190
  const overlayScrollbars = ${OVERLAY_SCROLLBARS_JS};
189
191
  const holdsFixed = ${HOLDS_FIXED_JS};
190
192
  const fixedToViewport = ${FIXED_TO_VIEWPORT_JS};
191
193
  const reasons = ${JSON.stringify(LAYOUT_REASONS)};
192
194
  const styleOf = (n) => n.ownerDocument.defaultView.getComputedStyle(n);
193
- const parentOf = (n) => n.parentElement || (n.parentNode && n.parentNode.host) || null;
195
+ const parentOf = ${FLAT_PARENT_JS};
194
196
  const doc = node.ownerDocument;
195
197
  const rootStyle = styleOf(doc.documentElement);
196
198
  const bodyClips = rootStyle.overflowX !== 'visible' || rootStyle.overflowY !== 'visible';
@@ -44,12 +44,14 @@ export declare const COMPOSED_JS = "(node) => {\n const composes = (n) => Boole
44
44
  * `visibility: hidden` are left out, elements that are not inline are set
45
45
  * apart by line breaks, as is a `<br>`. Fields and editable regions inside
46
46
  * it (inputs, textareas, selects, a contenteditable editor) are skipped, so
47
- * what a user typed is never read; raw text keeps `innerText`'s collapsed
48
- * whitespace. The text is cut at `limit` characters, and a
47
+ * what a user typed is never read, unless `fields` is set: then selects
48
+ * (all their options) and editable regions are read as `innerText` reads
49
+ * them (inputs and textareas it leaves out too). Raw text keeps
50
+ * `innerText`'s collapsed whitespace. The text is cut at `limit` characters, and a
49
51
  * part whose text alone passes the limit is read from its text nodes
50
52
  * (`textContent`) instead of `innerText`, which would lay out all of it.
51
53
  */
52
- export declare const FLAT_TEXT_JS = "(el, limit) => {\n const composed = (node) => {\n const composes = (n) => Boolean(n.shadowRoot) || n.localName === 'slot';\n if (composes(node)) return true;\n const walker = node.ownerDocument.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);\n while (walker.nextNode()) if (composes(walker.currentNode)) return true;\n return false;\n};\n const view = el.ownerDocument.defaultView;\n let text = '';\n const nodesOf = (node) =>\n node.localName === 'slot' ? node.assignedNodes({ flatten: true }) : Array.from((node.shadowRoot || node).childNodes);\n const read = (node, visible) => {\n if (text.length >= limit) return;\n if (node.nodeType === 3 && visible) text += node.data.replace(/\\s+/g, ' ');\n if (node.nodeType !== 1) return;\n if (/^(input|textarea|select)$/.test(node.localName) || node.isContentEditable) return;\n const style = view.getComputedStyle(node);\n if (node.checkVisibility && !node.checkVisibility() && style.display !== 'contents') return;\n if (node.localName === 'br') text += '\\n';\n const apart = style.display.startsWith('inline') || style.display === 'contents' ? '' : '\\n';\n text += apart;\n const own = node.textContent || '';\n if (composed(node)) nodesOf(node).forEach((child) => read(child, style.visibility === 'visible'));\n else if (own.length > limit - text.length) text += own.slice(0, limit - text.length);\n else text += typeof node.innerText === 'string' ? node.innerText : own;\n text += apart;\n };\n const visible = view.getComputedStyle(el).visibility === 'visible';\n nodesOf(el).forEach((child) => read(child, visible));\n return text.slice(0, limit);\n}";
54
+ export declare const FLAT_TEXT_JS = "(el, limit, fields) => {\n const composed = (node) => {\n const composes = (n) => Boolean(n.shadowRoot) || n.localName === 'slot';\n if (composes(node)) return true;\n const walker = node.ownerDocument.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);\n while (walker.nextNode()) if (composes(walker.currentNode)) return true;\n return false;\n};\n const view = el.ownerDocument.defaultView;\n let text = '';\n const nodesOf = (node) =>\n node.localName === 'slot' ? node.assignedNodes({ flatten: true }) : Array.from((node.shadowRoot || node).childNodes);\n const read = (node, visible) => {\n if (text.length >= limit) return;\n if (node.nodeType === 3 && visible) text += node.data.replace(/\\s+/g, ' ');\n if (node.nodeType !== 1) return;\n if (/^(input|textarea)$/.test(node.localName)) return;\n if (!fields && (node.localName === 'select' || node.isContentEditable)) return;\n const style = view.getComputedStyle(node);\n if (node.checkVisibility && !node.checkVisibility() && style.display !== 'contents') return;\n if (node.localName === 'br') text += '\\n';\n const apart = style.display.startsWith('inline') || style.display === 'contents' ? '' : '\\n';\n text += apart;\n const own = node.textContent || '';\n if (composed(node)) nodesOf(node).forEach((child) => read(child, style.visibility === 'visible'));\n else if (own.length > limit - text.length) text += own.slice(0, limit - text.length);\n else text += typeof node.innerText === 'string' ? node.innerText : own;\n text += apart;\n };\n const visible = view.getComputedStyle(el).visibility === 'visible';\n nodesOf(el).forEach((child) => read(child, visible));\n return text.slice(0, limit);\n}";
53
55
  /**
54
56
  * Page-side text of an element as a user sees it: `innerText` for a rendered
55
57
  * element (CSS-hidden parts left out, inline elements not split apart), none
@@ -65,9 +67,16 @@ export declare const FLAT_TEXT_JS = "(el, limit) => {\n const composed = (node)
65
67
  * a whole page's text, unless `full` is set. Decorations are left out
66
68
  * ({@link WITHOUT_DECORATIONS_JS}).
67
69
  */
68
- export declare const ELEMENT_TEXT_JS = "(el, full) => {\n const withoutDecorations = (el, text) => {\n if (!text || typeof el.querySelectorAll !== 'function') return text;\n const glyph = /^\\s*[\u00D7\u2715\u2716\u2717\u2A2F]\\s*$/;\n const textOf = (node) => (typeof node.innerText === 'string' ? node.innerText : node.textContent || '').trim();\n const closer = '.close, [aria-label=\"close\" i], [aria-label=\"dismiss\" i]';\n const rendered = (node) => !node.checkVisibility || node.checkVisibility();\n const found = Array.from(el.querySelectorAll(closer + ', [aria-hidden=\"true\"]'))\n .filter((node) => rendered(node) && (node.matches(closer) || !/[\\p{L}\\p{N}]/u.test(textOf(node))));\n if (/[\u00D7\u2715\u2716\u2717\u2A2F]/.test(text)) {\n found.push(...Array.from(el.querySelectorAll('button, a, [role=\"button\"]')).filter((node) => glyph.test(node.textContent || '')));\n }\n const unique = Array.from(new Set(found));\n const outermost = unique.filter((node) => !unique.some((other) => other !== node && other.contains(node)));\n let result = text;\n for (const node of outermost) {\n const part = textOf(node);\n const at = part ? result.lastIndexOf(part) : -1;\n if (at >= 0) result = result.slice(0, at) + result.slice(at + part.length);\n }\n return result;\n};\n const composed = (node) => {\n const composes = (n) => Boolean(n.shadowRoot) || n.localName === 'slot';\n if (composes(node)) return true;\n const walker = node.ownerDocument.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);\n while (walker.nextNode()) if (composes(walker.currentNode)) return true;\n return false;\n};\n const flatText = (el, limit) => {\n const composed = (node) => {\n const composes = (n) => Boolean(n.shadowRoot) || n.localName === 'slot';\n if (composes(node)) return true;\n const walker = node.ownerDocument.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);\n while (walker.nextNode()) if (composes(walker.currentNode)) return true;\n return false;\n};\n const view = el.ownerDocument.defaultView;\n let text = '';\n const nodesOf = (node) =>\n node.localName === 'slot' ? node.assignedNodes({ flatten: true }) : Array.from((node.shadowRoot || node).childNodes);\n const read = (node, visible) => {\n if (text.length >= limit) return;\n if (node.nodeType === 3 && visible) text += node.data.replace(/\\s+/g, ' ');\n if (node.nodeType !== 1) return;\n if (/^(input|textarea|select)$/.test(node.localName) || node.isContentEditable) return;\n const style = view.getComputedStyle(node);\n if (node.checkVisibility && !node.checkVisibility() && style.display !== 'contents') return;\n if (node.localName === 'br') text += '\\n';\n const apart = style.display.startsWith('inline') || style.display === 'contents' ? '' : '\\n';\n text += apart;\n const own = node.textContent || '';\n if (composed(node)) nodesOf(node).forEach((child) => read(child, style.visibility === 'visible'));\n else if (own.length > limit - text.length) text += own.slice(0, limit - text.length);\n else text += typeof node.innerText === 'string' ? node.innerText : own;\n text += apart;\n };\n const visible = view.getComputedStyle(el).visibility === 'visible';\n nodesOf(el).forEach((child) => read(child, visible));\n return text.slice(0, limit);\n};\n const all = el.textContent || '';\n if (el.tagName === 'OPTION') return el.label;\n if (typeof el.innerText !== 'string') return full ? all : all.slice(0, 2000);\n const rendered = (node) => !node.checkVisibility || node.checkVisibility();\n const shown = rendered(el);\n const boxless = !shown && el.ownerDocument.defaultView.getComputedStyle(el).display === 'contents';\n if (!shown && !boxless) return '';\n if (composed(el)) return withoutDecorations(el, flatText(el, full ? Infinity : 2000));\n if (boxless) return withoutDecorations(el, full ? all : all.slice(0, 2000));\n if (full || all.length <= 2000) return withoutDecorations(el, el.innerText);\n const walker = el.ownerDocument.createTreeWalker(el, NodeFilter.SHOW_TEXT);\n let start = '';\n while (start.length < 1000 && walker.nextNode()) {\n const parent = walker.currentNode.parentElement;\n if (!parent || rendered(parent)) start += walker.currentNode.data;\n }\n return withoutDecorations(el, start);\n}";
70
+ export declare const ELEMENT_TEXT_JS = "(el, full) => {\n const withoutDecorations = (el, text) => {\n if (!text || typeof el.querySelectorAll !== 'function') return text;\n const glyph = /^\\s*[\u00D7\u2715\u2716\u2717\u2A2F]\\s*$/;\n const textOf = (node) => (typeof node.innerText === 'string' ? node.innerText : node.textContent || '').trim();\n const closer = '.close, [aria-label=\"close\" i], [aria-label=\"dismiss\" i]';\n const rendered = (node) => !node.checkVisibility || node.checkVisibility();\n const found = Array.from(el.querySelectorAll(closer + ', [aria-hidden=\"true\"]'))\n .filter((node) => rendered(node) && (node.matches(closer) || !/[\\p{L}\\p{N}]/u.test(textOf(node))));\n if (/[\u00D7\u2715\u2716\u2717\u2A2F]/.test(text)) {\n found.push(...Array.from(el.querySelectorAll('button, a, [role=\"button\"]')).filter((node) => glyph.test(node.textContent || '')));\n }\n const unique = Array.from(new Set(found));\n const outermost = unique.filter((node) => !unique.some((other) => other !== node && other.contains(node)));\n let result = text;\n for (const node of outermost) {\n const part = textOf(node);\n const at = part ? result.lastIndexOf(part) : -1;\n if (at >= 0) result = result.slice(0, at) + result.slice(at + part.length);\n }\n return result;\n};\n const composed = (node) => {\n const composes = (n) => Boolean(n.shadowRoot) || n.localName === 'slot';\n if (composes(node)) return true;\n const walker = node.ownerDocument.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);\n while (walker.nextNode()) if (composes(walker.currentNode)) return true;\n return false;\n};\n const flatText = (el, limit, fields) => {\n const composed = (node) => {\n const composes = (n) => Boolean(n.shadowRoot) || n.localName === 'slot';\n if (composes(node)) return true;\n const walker = node.ownerDocument.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);\n while (walker.nextNode()) if (composes(walker.currentNode)) return true;\n return false;\n};\n const view = el.ownerDocument.defaultView;\n let text = '';\n const nodesOf = (node) =>\n node.localName === 'slot' ? node.assignedNodes({ flatten: true }) : Array.from((node.shadowRoot || node).childNodes);\n const read = (node, visible) => {\n if (text.length >= limit) return;\n if (node.nodeType === 3 && visible) text += node.data.replace(/\\s+/g, ' ');\n if (node.nodeType !== 1) return;\n if (/^(input|textarea)$/.test(node.localName)) return;\n if (!fields && (node.localName === 'select' || node.isContentEditable)) return;\n const style = view.getComputedStyle(node);\n if (node.checkVisibility && !node.checkVisibility() && style.display !== 'contents') return;\n if (node.localName === 'br') text += '\\n';\n const apart = style.display.startsWith('inline') || style.display === 'contents' ? '' : '\\n';\n text += apart;\n const own = node.textContent || '';\n if (composed(node)) nodesOf(node).forEach((child) => read(child, style.visibility === 'visible'));\n else if (own.length > limit - text.length) text += own.slice(0, limit - text.length);\n else text += typeof node.innerText === 'string' ? node.innerText : own;\n text += apart;\n };\n const visible = view.getComputedStyle(el).visibility === 'visible';\n nodesOf(el).forEach((child) => read(child, visible));\n return text.slice(0, limit);\n};\n const all = el.textContent || '';\n if (el.tagName === 'OPTION') return el.label;\n if (typeof el.innerText !== 'string') return full ? all : all.slice(0, 2000);\n const rendered = (node) => !node.checkVisibility || node.checkVisibility();\n const shown = rendered(el);\n const boxless = !shown && el.ownerDocument.defaultView.getComputedStyle(el).display === 'contents';\n if (!shown && !boxless) return '';\n if (composed(el)) return withoutDecorations(el, flatText(el, full ? Infinity : 2000));\n if (boxless) return withoutDecorations(el, full ? all : all.slice(0, 2000));\n if (full || all.length <= 2000) return withoutDecorations(el, el.innerText);\n const walker = el.ownerDocument.createTreeWalker(el, NodeFilter.SHOW_TEXT);\n let start = '';\n while (start.length < 1000 && walker.nextNode()) {\n const parent = walker.currentNode.parentElement;\n if (!parent || rendered(parent)) start += walker.currentNode.data;\n }\n return withoutDecorations(el, start);\n}";
69
71
  /** Shown instead of a secret field value (the same for every length) */
70
72
  export declare const MASKED_VALUE = "\u2022\u2022\u2022\u2022";
73
+ /**
74
+ * Regular expression source (case-insensitive) matching names of fields that
75
+ * hold a password, one-time code or card code: `password`, `passwd`, `pwd`,
76
+ * `passcode`, `otp`, `cvv`, `cvc`. Shared by the page-side
77
+ * {@link SENSITIVE_FIELD_JS} and HAR sanitization.
78
+ */
79
+ export declare const SENSITIVE_NAME_SOURCE = "passw|passwd|pwd|passcode|(^|[^a-z])otp([^a-z]|$)|cvv|cvc";
71
80
  /**
72
81
  * Page-side check whether a form control holds a secret whose value must
73
82
  * never leave the page: a password field (live type or type attribute), a
@@ -84,12 +84,14 @@ export const COMPOSED_JS = `(node) => {
84
84
  * `visibility: hidden` are left out, elements that are not inline are set
85
85
  * apart by line breaks, as is a `<br>`. Fields and editable regions inside
86
86
  * it (inputs, textareas, selects, a contenteditable editor) are skipped, so
87
- * what a user typed is never read; raw text keeps `innerText`'s collapsed
88
- * whitespace. The text is cut at `limit` characters, and a
87
+ * what a user typed is never read, unless `fields` is set: then selects
88
+ * (all their options) and editable regions are read as `innerText` reads
89
+ * them (inputs and textareas it leaves out too). Raw text keeps
90
+ * `innerText`'s collapsed whitespace. The text is cut at `limit` characters, and a
89
91
  * part whose text alone passes the limit is read from its text nodes
90
92
  * (`textContent`) instead of `innerText`, which would lay out all of it.
91
93
  */
92
- export const FLAT_TEXT_JS = `(el, limit) => {
94
+ export const FLAT_TEXT_JS = `(el, limit, fields) => {
93
95
  const composed = ${COMPOSED_JS};
94
96
  const view = el.ownerDocument.defaultView;
95
97
  let text = '';
@@ -99,7 +101,8 @@ export const FLAT_TEXT_JS = `(el, limit) => {
99
101
  if (text.length >= limit) return;
100
102
  if (node.nodeType === 3 && visible) text += node.data.replace(/\\s+/g, ' ');
101
103
  if (node.nodeType !== 1) return;
102
- if (/^(input|textarea|select)$/.test(node.localName) || node.isContentEditable) return;
104
+ if (/^(input|textarea)$/.test(node.localName)) return;
105
+ if (!fields && (node.localName === 'select' || node.isContentEditable)) return;
103
106
  const style = view.getComputedStyle(node);
104
107
  if (node.checkVisibility && !node.checkVisibility() && style.display !== 'contents') return;
105
108
  if (node.localName === 'br') text += '\\n';
@@ -154,6 +157,13 @@ export const ELEMENT_TEXT_JS = `(el, full) => {
154
157
  }`;
155
158
  /** Shown instead of a secret field value (the same for every length) */
156
159
  export const MASKED_VALUE = '••••';
160
+ /**
161
+ * Regular expression source (case-insensitive) matching names of fields that
162
+ * hold a password, one-time code or card code: `password`, `passwd`, `pwd`,
163
+ * `passcode`, `otp`, `cvv`, `cvc`. Shared by the page-side
164
+ * {@link SENSITIVE_FIELD_JS} and HAR sanitization.
165
+ */
166
+ export const SENSITIVE_NAME_SOURCE = 'passw|passwd|pwd|passcode|(^|[^a-z])otp([^a-z]|$)|cvv|cvc';
157
167
  /**
158
168
  * Page-side check whether a form control holds a secret whose value must
159
169
  * never leave the page: a password field (live type or type attribute), a
@@ -168,7 +178,7 @@ export const SENSITIVE_FIELD_JS = `(el) => {
168
178
  if (/(^|\\s)(cc-[a-z-]+|one-time-code|current-password|new-password)(\\s|$)/i.test(autocomplete)) return true;
169
179
  if (el.type === 'password' || /^password$/i.test(el.getAttribute('type') || '')) return true;
170
180
  const names = [el.getAttribute('name'), el.id, autocomplete].join(' ');
171
- if (/passw|passwd|pwd|passcode|(^|[^a-z])otp([^a-z]|$)|cvv|cvc/i.test(names)) return true;
181
+ if (/${SENSITIVE_NAME_SOURCE}/i.test(names)) return true;
172
182
  try {
173
183
  const security = el.ownerDocument.defaultView.getComputedStyle(el).getPropertyValue('-webkit-text-security');
174
184
  return Boolean(security) && security !== 'none';
@@ -26,6 +26,8 @@ export interface EvalResult {
26
26
  type: string;
27
27
  /** Object subtype (`node`, `date`, `map`, `array`, ...) */
28
28
  subtype?: string;
29
+ /** Elements of an array result in the page (its copy holds at most 1000 unless `full`) */
30
+ length?: number;
29
31
  /** Set when the page replaced built-ins bdg's copy of the result uses, so the browser copied it */
30
32
  warning?: string;
31
33
  }
@@ -36,11 +38,27 @@ export interface EvalResult {
36
38
  * dates ISO strings, maps and sets entries, errors their message, BigInts
37
39
  * `12n`, functions `function name()`, and cycles `[Circular]` (an object
38
40
  * shared by two properties is copied twice). Works for objects of iframes
39
- * (other realms); lists and objects are cut after 1000 entries, and a
40
- * throwing getter becomes `[Error: …]`. It uses the page's built-ins
41
- * ({@link COPY_BUILTINS}), so it only runs when the page left them alone.
41
+ * (other realms); lists and objects are cut after 1000 entries and levels
42
+ * below the 20th become `[…]` (with `full`: no entries are cut, levels
43
+ * below the 100th), and a throwing getter becomes `[Error: …]`. It uses the
44
+ * page's built-ins ({@link COPY_BUILTINS}), so it only runs when the page
45
+ * left them alone.
46
+ *
47
+ * @param full - `--full`: copy every entry
48
+ * @returns Function declaration for Runtime.callFunctionOn
49
+ */
50
+ export declare function jsonSafeCopyFunction(full?: boolean): string;
51
+ /**
52
+ * Elements of an array result (also a node list or typed array), read from
53
+ * its description (`Array(20000)`, `NodeList(5)`), since its copy holds
54
+ * at most 1000 unless `--full`.
55
+ *
56
+ * @param remote - Remote object returned by Runtime.evaluate
57
+ * @returns `length`, when the result is an array
42
58
  */
43
- export declare const JSON_SAFE_COPY_FUNCTION = "function () {\n const MAX_ITEMS = 1000;\n const ancestors = new Set();\n const kind = (value) => Object.prototype.toString.call(value).slice(8, -1);\n const isNode = (value) => typeof value.nodeType === 'number' && typeof value.nodeName === 'string';\n const describeNode = (node) => {\n if (node.nodeType !== 1) return node.nodeName.toLowerCase();\n return node.tagName.toLowerCase() + (node.id ? '#' + node.id : '') +\n (node.classList && node.classList.length ? '.' + Array.from(node.classList).join('.') : '');\n };\n const items = (list, next) => {\n const result = [];\n if (typeof list.length === 'number') {\n for (let i = 0; i < list.length; i++) {\n if (i === MAX_ITEMS) { result.push('\u2026'); break; }\n result.push(next(list[i]));\n }\n return result;\n }\n let count = 0;\n for (const item of list) {\n if (count++ === MAX_ITEMS) { result.push('\u2026'); break; }\n result.push(next(item));\n }\n return result;\n };\n const leaf = (value) => {\n if (value === undefined) return null;\n if (typeof value === 'number') {\n if (Number.isNaN(value) || !Number.isFinite(value)) return String(value);\n return Object.is(value, -0) ? '-0' : value;\n }\n if (typeof value === 'bigint') return value + 'n';\n if (typeof value === 'symbol') return value.toString();\n if (typeof value === 'function') return 'function ' + (value.name || '(anonymous)') + '()';\n return value;\n };\n const copy = (value, depth) => {\n if (value === null || typeof value !== 'object') return leaf(value);\n if (ancestors.has(value)) return '[Circular]';\n if (depth > 20) return '[\u2026]';\n const next = (item) => copy(item, depth + 1);\n const type = kind(value);\n if (isNode(value)) return describeNode(value);\n if (value.window === value) return 'Window';\n if (type === 'Date') return isNaN(value) ? 'Invalid Date' : value.toISOString();\n if (type === 'RegExp') return String(value);\n if (type === 'Error' || value instanceof Error) return value.name + ': ' + value.message;\n ancestors.add(value);\n try {\n if (type === 'Map') return items(value, ([k, v]) => [next(k), next(v)]);\n if (type === 'Set' || type === 'NodeList' || type === 'HTMLCollection') return items(value, next);\n if (ArrayBuffer.isView(value) && type !== 'DataView') return items(value, next);\n if (Array.isArray(value)) return items(value, next);\n const result = {};\n const keys = Object.keys(value);\n for (const key of keys.slice(0, MAX_ITEMS)) {\n try { result[key] = next(value[key]); } catch (e) { result[key] = '[Error: ' + (e && e.message) + ']'; }\n }\n if (keys.length > MAX_ITEMS) result['\u2026'] = (keys.length - MAX_ITEMS) + ' more keys';\n return result;\n } finally {\n ancestors.delete(value);\n }\n };\n return copy(this, 0);\n}";
59
+ export declare function arrayLength(remote: Protocol.Runtime.RemoteObject): {
60
+ length?: number;
61
+ };
44
62
  /** What a busy target is called in messages: the page, or an iframe (`--frame`) */
45
63
  export type BusyScope = 'page' | 'frame';
46
64
  /** When {@link withBusyPageRecovery} checks the target, and what it calls it */
@@ -100,6 +118,8 @@ export interface EvalTarget {
100
118
  recovery?: CDPSender;
101
119
  /** Execution context of a frame (`ExecutionContextDescription.uniqueId`) */
102
120
  uniqueContextId?: string;
121
+ /** `--full`: copy the result with every entry */
122
+ full?: boolean;
103
123
  }
104
124
  /**
105
125
  * Whether the page still answers. A page that does not answer in time is
@@ -156,6 +156,10 @@ const DESCRIBED_SUBTYPES = new Set([
156
156
  'iterator',
157
157
  'generator',
158
158
  ]);
159
+ /** Entries per list or object, and levels, of a copied eval result */
160
+ const COPY_LIMITS = { maxItems: 1000, maxDepth: 20 };
161
+ /** Limits of a copied eval result with `--full`: every entry, levels deep enough for real data */
162
+ const FULL_COPY_LIMITS = { maxItems: Infinity, maxDepth: 100 };
159
163
  /** Object subtypes shown by their short description (`button#submit`, `ArrayBuffer(8)`). */
160
164
  const BRIEF_SUBTYPES = new Set(['node', 'arraybuffer', 'dataview']);
161
165
  /**
@@ -165,12 +169,19 @@ const BRIEF_SUBTYPES = new Set(['node', 'arraybuffer', 'dataview']);
165
169
  * dates ISO strings, maps and sets entries, errors their message, BigInts
166
170
  * `12n`, functions `function name()`, and cycles `[Circular]` (an object
167
171
  * shared by two properties is copied twice). Works for objects of iframes
168
- * (other realms); lists and objects are cut after 1000 entries, and a
169
- * throwing getter becomes `[Error: …]`. It uses the page's built-ins
170
- * ({@link COPY_BUILTINS}), so it only runs when the page left them alone.
172
+ * (other realms); lists and objects are cut after 1000 entries and levels
173
+ * below the 20th become `[…]` (with `full`: no entries are cut, levels
174
+ * below the 100th), and a throwing getter becomes `[Error: …]`. It uses the
175
+ * page's built-ins ({@link COPY_BUILTINS}), so it only runs when the page
176
+ * left them alone.
177
+ *
178
+ * @param full - `--full`: copy every entry
179
+ * @returns Function declaration for Runtime.callFunctionOn
171
180
  */
172
- export const JSON_SAFE_COPY_FUNCTION = `function () {
173
- const MAX_ITEMS = 1000;
181
+ export function jsonSafeCopyFunction(full = false) {
182
+ const limits = full ? FULL_COPY_LIMITS : COPY_LIMITS;
183
+ return `function () {
184
+ const MAX_ITEMS = ${limits.maxItems};
174
185
  const ancestors = new Set();
175
186
  const kind = (value) => Object.prototype.toString.call(value).slice(8, -1);
176
187
  const isNode = (value) => typeof value.nodeType === 'number' && typeof value.nodeName === 'string';
@@ -209,7 +220,7 @@ export const JSON_SAFE_COPY_FUNCTION = `function () {
209
220
  const copy = (value, depth) => {
210
221
  if (value === null || typeof value !== 'object') return leaf(value);
211
222
  if (ancestors.has(value)) return '[Circular]';
212
- if (depth > 20) return '[…]';
223
+ if (depth > ${limits.maxDepth}) return '[…]';
213
224
  const next = (item) => copy(item, depth + 1);
214
225
  const type = kind(value);
215
226
  if (isNode(value)) return describeNode(value);
@@ -236,6 +247,7 @@ export const JSON_SAFE_COPY_FUNCTION = `function () {
236
247
  };
237
248
  return copy(this, 0);
238
249
  }`;
250
+ }
239
251
  /** Built-ins {@link JSON_SAFE_COPY_FUNCTION} uses (it runs in the page's world, next to the result) */
240
252
  const COPY_BUILTINS = [
241
253
  'Object.keys',
@@ -284,9 +296,10 @@ function isTerminated(details) {
284
296
  *
285
297
  * @param cdp - CDP connection
286
298
  * @param remote - Remote object returned by Runtime.evaluate
299
+ * @param full - `--full`: copy every entry ({@link jsonSafeCopyFunction})
287
300
  * @returns Value and type
288
301
  */
289
- async function toEvalResult(cdp, remote) {
302
+ async function toEvalResult(cdp, remote, full) {
290
303
  const kind = { type: remote.type, ...(remote.subtype && { subtype: remote.subtype }) };
291
304
  if (remote.unserializableValue !== undefined)
292
305
  return { value: remote.unserializableValue, ...kind };
@@ -301,12 +314,27 @@ async function toEvalResult(cdp, remote) {
301
314
  return { value: formatRemoteObject(remote), ...kind };
302
315
  }
303
316
  const replaced = await findReplacedBuiltins(cdp, COPY_BUILTINS, remote.objectId);
304
- const copy = await copyByValue(cdp, remote.objectId, replaced.length === 0 ? JSON_SAFE_COPY_FUNCTION : SELF_FUNCTION);
317
+ const copy = await copyByValue(cdp, remote.objectId, replaced.length === 0 ? jsonSafeCopyFunction(full) : SELF_FUNCTION);
305
318
  const value = copy ? copy.value : formatRemoteObject(remote);
319
+ const copied = { value, ...kind, ...arrayLength(remote) };
306
320
  if (replaced.length === 0)
307
- return { value, ...kind };
321
+ return copied;
308
322
  const warning = copy ? evalCopiedByBrowserWarning(replaced) : evalPreviewWarning(replaced);
309
- return { value, ...kind, warning };
323
+ return { ...copied, warning };
324
+ }
325
+ /**
326
+ * Elements of an array result (also a node list or typed array), read from
327
+ * its description (`Array(20000)`, `NodeList(5)`), since its copy holds
328
+ * at most 1000 unless `--full`.
329
+ *
330
+ * @param remote - Remote object returned by Runtime.evaluate
331
+ * @returns `length`, when the result is an array
332
+ */
333
+ export function arrayLength(remote) {
334
+ if (remote.subtype !== 'array' && remote.subtype !== 'typedarray')
335
+ return {};
336
+ const match = /\((\d+)\)$/.exec(remote.description ?? '');
337
+ return match ? { length: Number(match[1]) } : {};
310
338
  }
311
339
  /**
312
340
  * Copy a result by value: what a function called on it returns, which the
@@ -314,7 +342,7 @@ async function toEvalResult(cdp, remote) {
314
342
  *
315
343
  * @param cdp - CDP connection
316
344
  * @param objectId - The result
317
- * @param functionDeclaration - {@link JSON_SAFE_COPY_FUNCTION}, or
345
+ * @param functionDeclaration - {@link jsonSafeCopyFunction}'s, or
318
346
  * {@link SELF_FUNCTION} for the browser's plain copy (it fails on cycles
319
347
  * and BigInts)
320
348
  * @returns The copy, or undefined when it failed
@@ -560,7 +588,7 @@ export async function evaluateScript(cdp, script, target = {}) {
560
588
  try {
561
589
  const response = await withDeadline(executeScript(session, script, evaluateOptions(target)), EVAL_TIMEOUT_MS + TERMINATION_GRACE_MS, () => evaluationTimeoutError(target.recovery ?? session, scope));
562
590
  const settled = await withDeadline(settlePromise(session, response.result, script), EVAL_TIMEOUT_MS, promiseTimeout);
563
- return await toEvalResult(session, settled);
591
+ return await toEvalResult(session, settled, target.full ?? false);
564
592
  }
565
593
  catch (error) {
566
594
  throw scope === 'page' && isContextLostError(error)
@@ -66,6 +66,13 @@ export declare function mapPoint(mapping: FrameMapping, point: LayoutPoint): Lay
66
66
  * @returns Box in the top-level viewport
67
67
  */
68
68
  export declare function mapBox(mapping: FrameMapping, box: LayoutBox): LayoutBox;
69
+ /**
70
+ * Page-side choice of the box to measure: the element's, or its document's
71
+ * root element when the element has no size (hidden, collapsed).
72
+ */
73
+ export declare const REFERENCE_NODE_FUNCTION = "function () {\n const r = this.getBoundingClientRect();\n return r.width > 0 && r.height > 0 ? this : this.ownerDocument.documentElement;\n}";
74
+ /** Page-side `getBoundingClientRect()` as a plain box */
75
+ export declare const CLIENT_RECT_FUNCTION = "function () { const r = this.getBoundingClientRect(); return { x: r.left, y: r.top, width: r.width, height: r.height }; }";
69
76
  /**
70
77
  * How the element's frame maps into the top-level viewport.
71
78
  *
@@ -150,12 +150,12 @@ export function mapBox(mapping, box) {
150
150
  * Page-side choice of the box to measure: the element's, or its document's
151
151
  * root element when the element has no size (hidden, collapsed).
152
152
  */
153
- const REFERENCE_NODE_FUNCTION = `function () {
153
+ export const REFERENCE_NODE_FUNCTION = `function () {
154
154
  const r = this.getBoundingClientRect();
155
155
  return r.width > 0 && r.height > 0 ? this : this.ownerDocument.documentElement;
156
156
  }`;
157
157
  /** Page-side `getBoundingClientRect()` as a plain box */
158
- const CLIENT_RECT_FUNCTION = 'function () { const r = this.getBoundingClientRect(); return { x: r.left, y: r.top, width: r.width, height: r.height }; }';
158
+ export const CLIENT_RECT_FUNCTION = 'function () { const r = this.getBoundingClientRect(); return { x: r.left, y: r.top, width: r.width, height: r.height }; }';
159
159
  /**
160
160
  * Measure a box of the element's frame both in the frame and through CDP.
161
161
  *