browser-debugger-cli 0.15.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 (133) hide show
  1. package/.claude/skills/bdg/SKILL.md +2 -1
  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 +200 -133
  11. package/dist/commands/cleanup.js +18 -4
  12. package/dist/commands/dom/formInteraction.js +8 -4
  13. package/dist/commands/dom/helpers/index.d.ts +4 -4
  14. package/dist/commands/dom/helpers/index.js +3 -3
  15. package/dist/commands/dom/helpers/query.d.ts +2 -2
  16. package/dist/commands/dom/helpers/query.js +2 -2
  17. package/dist/commands/dom/helpers/screenshot.d.ts +21 -26
  18. package/dist/commands/dom/helpers/screenshot.js +50 -668
  19. package/dist/commands/dom/screenshot.js +56 -36
  20. package/dist/commands/optionBehaviors.js +18 -8
  21. package/dist/commands/shared/CommandRunner.d.ts +5 -0
  22. package/dist/commands/shared/CommandRunner.js +18 -3
  23. package/dist/commands/shared/interrupt.d.ts +40 -0
  24. package/dist/commands/shared/interrupt.js +73 -0
  25. package/dist/commands/shared/optionTypes.d.ts +2 -0
  26. package/dist/commands/shared/startHelpers.d.ts +26 -3
  27. package/dist/commands/shared/startHelpers.js +145 -23
  28. package/dist/commands/types.d.ts +5 -0
  29. package/dist/connection/cdp.js +1 -16
  30. package/dist/connection/chromeIdentity.d.ts +24 -5
  31. package/dist/connection/chromeIdentity.js +53 -22
  32. package/dist/connection/launcher.d.ts +34 -1
  33. package/dist/connection/launcher.js +98 -10
  34. package/dist/connection/typed-cdp.d.ts +3 -2
  35. package/dist/constants.d.ts +1 -1
  36. package/dist/constants.js +1 -1
  37. package/dist/daemon/SessionController.d.ts +10 -5
  38. package/dist/daemon/SessionController.js +15 -8
  39. package/dist/daemon/ipcServer.js +1 -1
  40. package/dist/daemon/launcher.d.ts +5 -0
  41. package/dist/daemon/launcher.js +8 -1
  42. package/dist/daemon/session/Session.d.ts +5 -1
  43. package/dist/daemon/session/Session.js +9 -8
  44. package/dist/daemon/session/TelemetryStore.d.ts +5 -0
  45. package/dist/daemon/session/TelemetryStore.js +4 -0
  46. package/dist/daemon/session/captureGate.d.ts +59 -0
  47. package/dist/daemon/session/captureGate.js +96 -0
  48. package/dist/daemon/session/chromeConnection.d.ts +16 -1
  49. package/dist/daemon/session/chromeConnection.js +34 -4
  50. package/dist/daemon/session/collectors.d.ts +15 -0
  51. package/dist/daemon/session/collectors.js +39 -2
  52. package/dist/daemon/session/commandRegistry.d.ts +14 -1
  53. package/dist/daemon/session/commandRegistry.js +46 -11
  54. package/dist/daemon/session/downloads.d.ts +32 -0
  55. package/dist/daemon/session/downloads.js +96 -0
  56. package/dist/daemon/session/interactions.d.ts +3 -2
  57. package/dist/daemon/session/interactions.js +7 -2
  58. package/dist/daemon/session/plugins.js +6 -0
  59. package/dist/daemon.js +12843 -11482
  60. package/dist/errors/CommandError.d.ts +2 -0
  61. package/dist/errors/issues.d.ts +1 -1
  62. package/dist/errors/messages.d.ts +58 -0
  63. package/dist/errors/messages.js +112 -0
  64. package/dist/index.js +999 -1020
  65. package/dist/ipc/client.d.ts +14 -1
  66. package/dist/ipc/client.js +21 -4
  67. package/dist/ipc/protocol/commands.d.ts +32 -2
  68. package/dist/ipc/protocol/commands.js +1 -0
  69. package/dist/ipc/protocol/domTypes.d.ts +24 -1
  70. package/dist/ipc/session/queries.d.ts +3 -0
  71. package/dist/ipc/session/types.d.ts +5 -0
  72. package/dist/ipc/transport/IPCError.d.ts +9 -0
  73. package/dist/ipc/transport/IPCError.js +12 -0
  74. package/dist/ipc/transport/errors.d.ts +2 -1
  75. package/dist/ipc/transport/errors.js +4 -1
  76. package/dist/ipc/transport/index.d.ts +4 -2
  77. package/dist/ipc/transport/index.js +13 -3
  78. package/dist/runtime/dom/actionEffects.d.ts +48 -9
  79. package/dist/runtime/dom/actionEffects.js +269 -34
  80. package/dist/runtime/dom/actionEffectsScripts.d.ts +45 -0
  81. package/dist/runtime/dom/actionEffectsScripts.js +101 -2
  82. package/dist/runtime/dom/captureArea.d.ts +35 -0
  83. package/dist/runtime/dom/captureArea.js +203 -0
  84. package/dist/runtime/dom/elementInfo.d.ts +10 -8
  85. package/dist/runtime/dom/elementInfo.js +8 -6
  86. package/dist/runtime/dom/formDiscovery.d.ts +1 -1
  87. package/dist/runtime/page/bdgWorld.d.ts +9 -0
  88. package/dist/runtime/page/bdgWorld.js +11 -0
  89. package/dist/runtime/page/captureEmulation.d.ts +119 -0
  90. package/dist/runtime/page/captureEmulation.js +189 -0
  91. package/dist/runtime/page/captureScroll.d.ts +24 -0
  92. package/dist/runtime/page/captureScroll.js +124 -0
  93. package/dist/runtime/page/screenshot.d.ts +41 -0
  94. package/dist/runtime/page/screenshot.js +394 -0
  95. package/dist/session/paths.d.ts +14 -0
  96. package/dist/session/paths.js +25 -0
  97. package/dist/telemetry/downloads.d.ts +127 -0
  98. package/dist/telemetry/downloads.js +265 -0
  99. package/dist/telemetry/har/builder.js +22 -7
  100. package/dist/telemetry/har/sanitize.d.ts +7 -3
  101. package/dist/telemetry/har/sanitize.js +52 -6
  102. package/dist/telemetry/har/sanitizeBody.d.ts +47 -7
  103. package/dist/telemetry/har/sanitizeBody.js +429 -56
  104. package/dist/telemetry/har/types.d.ts +2 -0
  105. package/dist/telemetry/network.d.ts +4 -4
  106. package/dist/telemetry/network.js +38 -4
  107. package/dist/telemetry/networkRetention.d.ts +35 -14
  108. package/dist/telemetry/networkRetention.js +62 -26
  109. package/dist/types.d.ts +9 -14
  110. package/dist/ui/OutputBuilder.d.ts +3 -2
  111. package/dist/ui/OutputBuilder.js +4 -3
  112. package/dist/ui/formatters/cdp.d.ts +32 -9
  113. package/dist/ui/formatters/cdp.js +77 -6
  114. package/dist/ui/formatters/details.js +7 -15
  115. package/dist/ui/formatters/preview.d.ts +2 -0
  116. package/dist/ui/formatters/preview.js +7 -1
  117. package/dist/ui/formatters/status.js +6 -1
  118. package/dist/ui/formatting.d.ts +7 -0
  119. package/dist/ui/formatting.js +13 -0
  120. package/dist/ui/logging/logger.d.ts +1 -1
  121. package/dist/ui/messages/chrome.d.ts +13 -0
  122. package/dist/ui/messages/chrome.js +26 -0
  123. package/dist/ui/messages/commands.d.ts +71 -3
  124. package/dist/ui/messages/commands.js +98 -3
  125. package/dist/ui/messages/networkMessages.d.ts +24 -5
  126. package/dist/ui/messages/networkMessages.js +31 -8
  127. package/dist/utils/async.d.ts +3 -2
  128. package/dist/utils/async.js +16 -3
  129. package/dist/utils/http.d.ts +11 -4
  130. package/dist/utils/http.js +5 -3
  131. package/package.json +18 -4
  132. /package/dist/{commands/dom → runtime/page}/screenshotResize.d.ts +0 -0
  133. /package/dist/{commands/dom → runtime/page}/screenshotResize.js +0 -0
@@ -14,23 +14,47 @@ import { writeChromePid } from '../../session/chrome.js';
14
14
  import { findConflictingOwner } from '../../session/chromeOwners.js';
15
15
  import { getSessionDir } from '../../session/paths.js';
16
16
  import { EXIT_CODES } from '../../utils/exitCodes.js';
17
- import { createPageTarget, fetchCDPTargets, probeDevToolsEndpoint } from '../../utils/http.js';
17
+ import { createPageTarget, fetchBrowserWsUrl, fetchCDPTargets, probeDevToolsEndpoint, } from '../../utils/http.js';
18
18
  import { filterDefined } from '../../utils/objects.js';
19
19
  /**
20
20
  * Setup Chrome connection - either launch new instance or connect to existing.
21
21
  *
22
+ * @param config - Session configuration
23
+ * @param telemetryStore - Store receiving the target info
24
+ * @param log - Logger
25
+ * @param notify - Receives notices about an external Chrome
26
+ * @param signal - Ends a launch at once when aborted (the session was stopped)
22
27
  * @returns Launched Chrome instance (null if connecting to external Chrome)
23
28
  */
24
- export async function setupChromeConnection(config, telemetryStore, log, notify) {
29
+ export async function setupChromeConnection(config, telemetryStore, log, notify, signal) {
25
30
  if (config.chromeWsUrl) {
26
31
  return setupExternalChrome(config, telemetryStore, log, notify);
27
32
  }
28
33
  else {
29
- return setupLaunchedChrome(config, log);
34
+ return setupLaunchedChrome(config, log, signal);
30
35
  }
31
36
  }
32
37
  /** Path of browser-level DevTools WebSocket URLs (`/devtools/browser/<uuid>`) */
33
38
  const BROWSER_WS_PATH = '/devtools/browser/';
39
+ /**
40
+ * Browser-level DevTools WebSocket URL of the session's Chrome: a launched
41
+ * Chrome's, or for `--chrome-ws-url` the URL itself when it is browser-level,
42
+ * else the one Chrome reports, reached the way the user's URL is.
43
+ *
44
+ * @param config - Session configuration (port resolved)
45
+ * @param log - Logger
46
+ * @returns The URL, or null when Chrome does not answer `/json/version`
47
+ */
48
+ export async function browserWebSocketUrl(config, log) {
49
+ if (!config.chromeWsUrl)
50
+ return fetchBrowserWsUrl(config.port, log);
51
+ const { hostname, pathname, protocol, port } = new URL(config.chromeWsUrl);
52
+ if (pathname.startsWith(BROWSER_WS_PATH))
53
+ return config.chromeWsUrl;
54
+ const http = { host: hostname, secure: protocol === 'wss:' };
55
+ const reported = await fetchBrowserWsUrl(config.port, log, http);
56
+ return reported && withEndpoint(reported, protocol, hostname, port);
57
+ }
34
58
  /**
35
59
  * Debugging port of an external Chrome, taken from its WebSocket URL.
36
60
  *
@@ -162,12 +186,18 @@ export function windowSizeFlags(config) {
162
186
  }
163
187
  /**
164
188
  * Launch a new Chrome instance and record its PID for crash cleanup.
189
+ *
190
+ * @param config - Session configuration
191
+ * @param log - Logger
192
+ * @param signal - Ends the launch at once when aborted
193
+ * @returns Launched Chrome
165
194
  */
166
- async function setupLaunchedChrome(config, log) {
195
+ async function setupLaunchedChrome(config, log, signal) {
167
196
  const chrome = await launchChrome({
168
197
  port: config.port,
169
198
  logger: log,
170
199
  sessionDir: getSessionDir(),
200
+ signal,
171
201
  ...filterDefined({
172
202
  userDataDir: config.userDataDir,
173
203
  headless: config.headless,
@@ -4,5 +4,20 @@ import type { SessionConfig } from './types.js';
4
4
  import type { CDPConnection } from '../../connection/cdp.js';
5
5
  import type { CleanupFunction } from '../../types.js';
6
6
  import type { Logger } from '../../ui/logging/index.js';
7
+ /**
8
+ * Start the session's collectors (telemetry plugins) in order.
9
+ *
10
+ * When one fails to start, the ones already started are cleaned up (their
11
+ * failures logged) before the error is rethrown, so nothing they opened (a
12
+ * browser-level connection, event handlers) outlives the failed start.
13
+ *
14
+ * @param cdp - Page connection
15
+ * @param config - Session configuration
16
+ * @param store - Telemetry store
17
+ * @param logger - Logger
18
+ * @param plugins - Plugins to start (default: the registered ones)
19
+ * @returns Cleanup functions of the started collectors
20
+ * @throws The error of the collector that failed to start
21
+ */
7
22
  export declare function startTelemetryCollectors(cdp: CDPConnection, config: SessionConfig, store: TelemetryStore, logger: Logger, plugins?: TelemetryPlugin[]): Promise<CleanupFunction[]>;
8
23
  //# sourceMappingURL=collectors.d.ts.map
@@ -1,6 +1,22 @@
1
1
  import { sessionActivatingCollector, sessionCollectorsActivated } from '../messages.js';
2
+ import { getErrorMessage } from '../../utils/errors.js';
2
3
  import { getRegisteredTelemetryPlugins, shouldActivatePlugin } from './plugins.js';
3
4
  const DEFAULT_TELEMETRY = ['network', 'console', 'dom'];
5
+ /**
6
+ * Start the session's collectors (telemetry plugins) in order.
7
+ *
8
+ * When one fails to start, the ones already started are cleaned up (their
9
+ * failures logged) before the error is rethrown, so nothing they opened (a
10
+ * browser-level connection, event handlers) outlives the failed start.
11
+ *
12
+ * @param cdp - Page connection
13
+ * @param config - Session configuration
14
+ * @param store - Telemetry store
15
+ * @param logger - Logger
16
+ * @param plugins - Plugins to start (default: the registered ones)
17
+ * @returns Cleanup functions of the started collectors
18
+ * @throws The error of the collector that failed to start
19
+ */
4
20
  export async function startTelemetryCollectors(cdp, config, store, logger, plugins) {
5
21
  const cleanupFunctions = [];
6
22
  store.activeTelemetry = config.telemetry ?? DEFAULT_TELEMETRY;
@@ -10,10 +26,31 @@ export async function startTelemetryCollectors(cdp, config, store, logger, plugi
10
26
  continue;
11
27
  }
12
28
  logger.debug(sessionActivatingCollector(plugin.name));
13
- const cleanup = await plugin.start({ cdp, config, store, logger });
14
- cleanupFunctions.push(cleanup);
29
+ try {
30
+ cleanupFunctions.push(await plugin.start({ cdp, config, store, logger }));
31
+ }
32
+ catch (error) {
33
+ await runCleanups(cleanupFunctions, logger);
34
+ throw error;
35
+ }
15
36
  }
16
37
  logger.debug(sessionCollectorsActivated(store.activeTelemetry));
17
38
  return cleanupFunctions;
18
39
  }
40
+ /**
41
+ * Run cleanup functions, logging (not throwing) their failures.
42
+ *
43
+ * @param cleanups - Cleanup functions
44
+ * @param logger - Logger
45
+ */
46
+ async function runCleanups(cleanups, logger) {
47
+ for (const cleanup of cleanups) {
48
+ try {
49
+ await cleanup();
50
+ }
51
+ catch (error) {
52
+ logger.debug(`Collector cleanup error: ${getErrorMessage(error)}`);
53
+ }
54
+ }
55
+ }
19
56
  //# sourceMappingURL=collectors.js.map
@@ -1,11 +1,24 @@
1
1
  import type { TelemetryStore } from './TelemetryStore.js';
2
2
  import type { CDPConnection } from '../../connection/cdp.js';
3
+ import { CommandError } from '../../errors/index.js';
3
4
  import type { CommandName, CommandSchemas } from '../../ipc/index.js';
4
5
  import { type SessionEmulation } from '../../runtime/page/emulation.js';
5
- type Handler<K extends CommandName> = (cdp: CDPConnection, params: CommandSchemas[K]['requestSchema']) => Promise<CommandSchemas[K]['responseSchema']>;
6
+ /** A command's handler; `abandoned` aborts when its client disconnects */
7
+ type Handler<K extends CommandName> = (cdp: CDPConnection, params: CommandSchemas[K]['requestSchema'], abandoned?: AbortSignal) => Promise<CommandSchemas[K]['responseSchema']>;
6
8
  export type CommandRegistry = {
7
9
  [K in CommandName]: Handler<K>;
8
10
  };
11
+ /**
12
+ * A `bdg cdp` failure that is the caller's: a method this Chrome doesn't
13
+ * implement (83), wrong parameters (81), or an id of a node, target or frame
14
+ * that does not exist (83). Other failures (internal errors, a detached
15
+ * page) stay software errors.
16
+ *
17
+ * @param method - CDP method
18
+ * @param error - What the call threw
19
+ * @returns The error to report, or undefined to keep the original
20
+ */
21
+ export declare function callerError(method: string, error: unknown): CommandError | undefined;
9
22
  /** The session's page emulation, which `page emulate` reads and changes */
10
23
  export interface EmulationState {
11
24
  get: () => SessionEmulation;
@@ -1,9 +1,10 @@
1
+ import { missingMethodCause } from '../../cdp/methodTarget.js';
1
2
  import { CDPConnectionError, CDPProtocolError } from '../../connection/errors.js';
2
3
  import { PatternDetector } from '../patternDetector.js';
3
4
  import { createInteractionRunner } from './interactions.js';
4
5
  import { withTriggeredRequestCount } from './triggeredRequests.js';
5
6
  import { CommandError } from '../../errors/index.js';
6
- import { cdpCallError, formDiscoveryFailedError } from '../../errors/messages.js';
7
+ import { cdpCallError, cdpMethodNotImplementedError, formDiscoveryFailedError, } from '../../errors/messages.js';
7
8
  import { searchStyleSheets } from '../../runtime/css/search.js';
8
9
  import { auditPage } from '../../runtime/dom/audit.js';
9
10
  import { evaluateScript, withBusyPageRecovery } from '../../runtime/dom/evalHelpers.js';
@@ -21,6 +22,8 @@ import { evaluateInBdgWorld, sendForBdgScript } from '../../runtime/page/bdgWorl
21
22
  import { emulatePage, pageAppearance } from '../../runtime/page/emulation.js';
22
23
  import { readDocumentReadyState } from '../../runtime/page/loadingState.js';
23
24
  import { navigatePage } from '../../runtime/page/navigation.js';
25
+ import { takeScreenshot } from '../../runtime/page/screenshot.js';
26
+ import { toDownloadInfo } from '../../telemetry/downloads.js';
24
27
  import { skippedBodyReason } from '../../telemetry/networkRetention.js';
25
28
  import { consoleMessageDroppedError } from '../../ui/messages/consoleMessages.js';
26
29
  import { sessionCommand } from '../../ui/messages/sessionCommand.js';
@@ -140,6 +143,15 @@ function webSocketAsRequest(connection) {
140
143
  webSocket: { frames: connection.frames, ...(closedTime !== undefined && { closedTime }) },
141
144
  };
142
145
  }
146
+ /**
147
+ * The session's downloads for `status` and `peek`, left out when none began.
148
+ *
149
+ * @param store - Telemetry store
150
+ * @returns `downloads`, oldest first, as they stand now
151
+ */
152
+ function sessionDownloads(store) {
153
+ return store.downloads.length > 0 ? { downloads: store.downloads.map(toDownloadInfo) } : {};
154
+ }
143
155
  /**
144
156
  * Status activity counts of what the network capture let go at its limits,
145
157
  * left out when nothing was.
@@ -169,18 +181,28 @@ function allNetworkRequests(store) {
169
181
  ].sort((a, b) => a.timestamp - b.timestamp);
170
182
  }
171
183
  /**
172
- * Present a body bdg chose not to fetch as `bodyNotCaptured` (the reason)
173
- * instead of a placeholder string in `responseBody`.
184
+ * Present bodies bdg did not fetch or keep as their reasons
185
+ * (`requestBodyNotCaptured`, `bodyNotCaptured`) instead of placeholder
186
+ * strings in `requestBody` and `responseBody`.
174
187
  *
175
188
  * @param request - Captured request
176
189
  * @returns The request as `details` reports it
177
190
  */
178
191
  function withBodyNotCaptured(request) {
179
- const reason = skippedBodyReason(request.responseBody);
180
- if (reason === undefined)
192
+ const requestReason = skippedBodyReason(request.requestBody);
193
+ const responseReason = skippedBodyReason(request.responseBody);
194
+ if (requestReason === undefined && responseReason === undefined)
181
195
  return request;
182
- const { responseBody: _placeholder, ...rest } = request;
183
- return { ...rest, bodyNotCaptured: reason };
196
+ const result = { ...request };
197
+ if (requestReason !== undefined) {
198
+ delete result.requestBody;
199
+ result.requestBodyNotCaptured = requestReason;
200
+ }
201
+ if (responseReason !== undefined) {
202
+ delete result.responseBody;
203
+ result.bodyNotCaptured = responseReason;
204
+ }
205
+ return result;
184
206
  }
185
207
  /**
186
208
  * Find a network request by ID: finished, in flight, or a WebSocket.
@@ -287,18 +309,25 @@ function pageWebSocketUrl(store) {
287
309
  }
288
310
  /** Chrome's "server error" code, used for failures on the page's state (a missing node, a bad id…) */
289
311
  const CDP_SERVER_ERROR = -32000;
312
+ /** JSON-RPC "method not found": Chrome has no such method (for the target) */
313
+ const CDP_METHOD_NOT_FOUND = -32601;
290
314
  /**
291
- * A `bdg cdp` failure that is the caller's: wrong parameters (81), or an
292
- * id of a node, target or frame that does not exist (83). Other failures
293
- * (internal errors, a detached page) stay software errors.
315
+ * A `bdg cdp` failure that is the caller's: a method this Chrome doesn't
316
+ * implement (83), wrong parameters (81), or an id of a node, target or frame
317
+ * that does not exist (83). Other failures (internal errors, a detached
318
+ * page) stay software errors.
294
319
  *
295
320
  * @param method - CDP method
296
321
  * @param error - What the call threw
297
322
  * @returns The error to report, or undefined to keep the original
298
323
  */
299
- function callerError(method, error) {
324
+ export function callerError(method, error) {
300
325
  if (!(error instanceof CDPProtocolError))
301
326
  return undefined;
327
+ if (error.code === CDP_METHOD_NOT_FOUND) {
328
+ const missing = cdpMethodNotImplementedError(method, error.message, missingMethodCause(method));
329
+ return new CommandError(missing.message, { suggestion: missing.suggestion }, EXIT_CODES.RESOURCE_NOT_FOUND);
330
+ }
302
331
  const err = cdpCallError(method, error.message);
303
332
  if (error.isRequestError()) {
304
333
  return new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
@@ -364,6 +393,7 @@ export function createCommandRegistry(store, emulation) {
364
393
  ...(dropped > 0 && { droppedConsole: dropped }),
365
394
  ...(requestsDropped > 0 && { droppedNetwork: requestsDropped }),
366
395
  ...(bodiesEvicted > 0 && { evictedNetworkBodies: bodiesEvicted }),
396
+ ...sessionDownloads(store),
367
397
  hasMoreNetwork: networkBounds.start > 0,
368
398
  hasMoreConsole: consoleBounds.start > 0,
369
399
  });
@@ -400,6 +430,10 @@ export function createCommandRegistry(store, emulation) {
400
430
  ...(lastNetworkRequest && { lastNetworkRequestAt: lastNetworkRequest.timestamp }),
401
431
  ...(lastConsoleMessage && { lastConsoleMessageAt: lastConsoleMessage.timestamp }),
402
432
  ...networkEvictionActivity(store),
433
+ ...sessionDownloads(store),
434
+ ...(store.downloadsWarning !== undefined && {
435
+ downloadsWarning: store.downloadsWarning,
436
+ }),
403
437
  },
404
438
  navigationId: store.getCurrentNavigationId?.() ?? 0,
405
439
  };
@@ -499,6 +533,7 @@ export function createCommandRegistry(store, emulation) {
499
533
  dom_inspect: async (cdp, params) => withBusyPageRecovery(cdp, inspectElement(cdp, params).then((result) => result.theme === 'dark' && emulation.get().colorScheme === 'dark'
500
534
  ? { ...result, themeFrom: 'emulation' }
501
535
  : result)),
536
+ dom_screenshot: async (cdp, params, abandoned) => takeScreenshot(cdp, params, () => emulation.get().viewport, { abandoned }),
502
537
  dom_wait: async (cdp, params) => waitForCondition(cdp, params),
503
538
  page_navigate: async (cdp, params) => interact(cdp, () => navigatePage(cdp, params.action, {
504
539
  ...filterDefined({ url: params.url, wait: params.wait }),
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Setting up a session's download tracking: where downloads go, and the
3
+ * connection their events arrive on.
4
+ */
5
+ import type { TelemetryPluginContext } from './plugins.js';
6
+ import type { SessionConfig } from './types.js';
7
+ import { CDPConnection } from '../../connection/cdp.js';
8
+ import type { CleanupFunction } from '../../types.js';
9
+ import type { Logger } from '../../ui/logging/index.js';
10
+ /**
11
+ * Opens a browser-level connection to the session's Chrome, or null when
12
+ * there is none; `onLost` is called if it closes while the session runs.
13
+ */
14
+ export type BrowserConnector = (config: SessionConfig, logger: Logger, onLost: () => void) => Promise<CDPConnection | null>;
15
+ /**
16
+ * Track the session's downloads (see {@link DownloadTracker}).
17
+ *
18
+ * For a Chrome bdg launched, on a browser-level connection, so downloads of
19
+ * tabs the page opens are named and recorded too; on the page's connection
20
+ * when there is none, or once it is lost (Chrome drops a connection's
21
+ * download behavior with it, so it is applied again there). An attached
22
+ * Chrome (`--chrome-ws-url`) is followed on the page's connection only: a
23
+ * browser-level one would count the user's other tabs' downloads as the
24
+ * session's, and a Chrome that allows one connection (toggle mode) would
25
+ * ask again for each.
26
+ *
27
+ * @param context - Plugin context
28
+ * @param connectBrowser - Opens the browser-level connection
29
+ * @returns Cleanup: stops tracking and closes the browser-level connection
30
+ */
31
+ export declare function startSessionDownloads(context: TelemetryPluginContext, connectBrowser?: BrowserConnector): Promise<CleanupFunction>;
32
+ //# sourceMappingURL=downloads.d.ts.map
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Setting up a session's download tracking: where downloads go, and the
3
+ * connection their events arrive on.
4
+ */
5
+ import { CDPConnection } from '../../connection/cdp.js';
6
+ import { browserWebSocketUrl } from './chromeConnection.js';
7
+ import { ensureSessionDownloadsDir } from '../../session/paths.js';
8
+ import { DownloadTracker } from '../../telemetry/downloads.js';
9
+ import { downloadsDirUnavailableReason } from '../../ui/messages/commands.js';
10
+ import { getErrorMessage } from '../../utils/errors.js';
11
+ /** Connection attempts for the browser-level connection (Chrome is already running) */
12
+ const BROWSER_CONNECT_ATTEMPTS = 2;
13
+ /**
14
+ * Track the session's downloads (see {@link DownloadTracker}).
15
+ *
16
+ * For a Chrome bdg launched, on a browser-level connection, so downloads of
17
+ * tabs the page opens are named and recorded too; on the page's connection
18
+ * when there is none, or once it is lost (Chrome drops a connection's
19
+ * download behavior with it, so it is applied again there). An attached
20
+ * Chrome (`--chrome-ws-url`) is followed on the page's connection only: a
21
+ * browser-level one would count the user's other tabs' downloads as the
22
+ * session's, and a Chrome that allows one connection (toggle mode) would
23
+ * ask again for each.
24
+ *
25
+ * @param context - Plugin context
26
+ * @param connectBrowser - Opens the browser-level connection
27
+ * @returns Cleanup: stops tracking and closes the browser-level connection
28
+ */
29
+ export async function startSessionDownloads(context, connectBrowser = openBrowserConnection) {
30
+ const { cdp, config, store, logger } = context;
31
+ const tracker = new DownloadTracker(store, downloadDestination(config, logger));
32
+ const browser = config.chromeWsUrl
33
+ ? null
34
+ : await connectBrowser(config, logger, () => void followOnPage(tracker, cdp, logger));
35
+ await tracker.attach(browser ?? cdp);
36
+ return () => {
37
+ tracker.stop();
38
+ browser?.close();
39
+ };
40
+ }
41
+ /**
42
+ * Follow downloads on the page's connection after the browser-level one was lost.
43
+ *
44
+ * @param tracker - Session download tracker
45
+ * @param cdp - Page connection
46
+ * @param logger - Logger
47
+ */
48
+ async function followOnPage(tracker, cdp, logger) {
49
+ logger.info('Browser connection for downloads lost; download behavior set again on the page');
50
+ await tracker.attach(cdp);
51
+ }
52
+ /**
53
+ * Open a browser-level connection to the session's Chrome.
54
+ *
55
+ * @param config - Session configuration
56
+ * @param logger - Logger
57
+ * @param onLost - Called if the connection closes while the session runs
58
+ * @returns Open connection, or null when there is none to open
59
+ */
60
+ async function openBrowserConnection(config, logger, onLost) {
61
+ const url = await browserWebSocketUrl(config, logger);
62
+ if (!url)
63
+ return null;
64
+ const browser = new CDPConnection(logger);
65
+ try {
66
+ await browser.connect(url, { maxRetries: BROWSER_CONNECT_ATTEMPTS, onDisconnect: onLost });
67
+ return browser;
68
+ }
69
+ catch (error) {
70
+ logger.debug(`No browser-level connection for downloads: ${getErrorMessage(error)}`);
71
+ return null;
72
+ }
73
+ }
74
+ /**
75
+ * Where the session's downloads go: the session's `downloads/` for a Chrome
76
+ * bdg launched (refused when it cannot be created, rather than saved to the
77
+ * browser's default `~/Downloads`), the browser's own place for an attached
78
+ * Chrome.
79
+ *
80
+ * @param config - Session configuration
81
+ * @param logger - Logger
82
+ * @returns Destination
83
+ */
84
+ function downloadDestination(config, logger) {
85
+ if (config.chromeWsUrl)
86
+ return { kind: 'browser' };
87
+ try {
88
+ return { kind: 'directory', dir: ensureSessionDownloadsDir() };
89
+ }
90
+ catch (error) {
91
+ const reason = downloadsDirUnavailableReason(getErrorMessage(error));
92
+ logger.info(reason);
93
+ return { kind: 'refused', reason };
94
+ }
95
+ }
96
+ //# sourceMappingURL=downloads.js.map
@@ -52,8 +52,9 @@ export type InteractionRunner = <T extends object>(cdp: CDPConnection, action: (
52
52
  * from the page (without waiting: during a pending navigation that takes until
53
53
  * the new page commits), and the dialogs it opened, what it changed on the
54
54
  * page (see {@link watchActionEffects}; a console message logged meanwhile
55
- * counts as an effect) and the network requests it
56
- * triggered (see {@link watchTriggeredRequests}) are added to its result,
55
+ * counts as an effect), the network requests it
56
+ * triggered (see {@link watchTriggeredRequests}) and the downloads that began
57
+ * meanwhile (as they stand when it returns) are added to its result,
57
58
  * and, when asked, what the page was still working on (see
58
59
  * {@link pendingChanges}). They are attributed by time: a dialog or request
59
60
  * started by a page timer or a navigation started earlier is reported by
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { pendingChanges, watchActionEffects } from '../../runtime/dom/actionEffects.js';
7
7
  import { UNBIND_TARGET_SCRIPT } from '../../runtime/dom/targetNode.js';
8
+ import { toDownloadInfo } from '../../telemetry/downloads.js';
8
9
  import { createLogger } from '../../ui/logging/index.js';
9
10
  import { getErrorMessage } from '../../utils/errors.js';
10
11
  import { watchTriggeredRequests } from './triggeredRequests.js';
@@ -36,8 +37,9 @@ function consoleMessagesLogged(store) {
36
37
  * from the page (without waiting: during a pending navigation that takes until
37
38
  * the new page commits), and the dialogs it opened, what it changed on the
38
39
  * page (see {@link watchActionEffects}; a console message logged meanwhile
39
- * counts as an effect) and the network requests it
40
- * triggered (see {@link watchTriggeredRequests}) are added to its result,
40
+ * counts as an effect), the network requests it
41
+ * triggered (see {@link watchTriggeredRequests}) and the downloads that began
42
+ * meanwhile (as they stand when it returns) are added to its result,
41
43
  * and, when asked, what the page was still working on (see
42
44
  * {@link pendingChanges}). They are attributed by time: a dialog or request
43
45
  * started by a page timer or a navigation started earlier is reported by
@@ -51,6 +53,7 @@ export function createInteractionRunner(store) {
51
53
  return (cdp, action, options = {}) => {
52
54
  const run = queue.then(async () => {
53
55
  const firstDialog = store.dialogs.length;
56
+ const firstDownload = store.downloads.length;
54
57
  const firstConsoleMessage = consoleMessagesLogged(store);
55
58
  const collectRequests = options.reportRequests === false ? undefined : watchTriggeredRequests(store);
56
59
  const effects = options.reportEffects === false ? undefined : watchActionEffects(cdp);
@@ -68,6 +71,7 @@ export function createInteractionRunner(store) {
68
71
  });
69
72
  const { work, ...changes } = collected ?? {};
70
73
  const requests = collectRequests?.();
74
+ const downloads = store.downloads.slice(firstDownload).map(toDownloadInfo);
71
75
  const pending = options.detectUnsettled && work
72
76
  ? pendingChanges(work, requests?.triggeredRequests)
73
77
  : undefined;
@@ -76,6 +80,7 @@ export function createInteractionRunner(store) {
76
80
  ...(dialogs.length > 0 && { dialogs }),
77
81
  ...changes,
78
82
  ...(pending && { settled: false, pending }),
83
+ ...(downloads.length > 0 && { downloads }),
79
84
  ...requests,
80
85
  };
81
86
  }
@@ -1,3 +1,4 @@
1
+ import { startSessionDownloads } from './downloads.js';
1
2
  import { hideHeadlessUserAgent } from '../../runtime/page/userAgent.js';
2
3
  import { startConsoleCollection } from '../../telemetry/console.js';
3
4
  import { startDialogHandling } from '../../telemetry/dialogs.js';
@@ -94,6 +95,11 @@ export function createDefaultTelemetryPlugins() {
94
95
  return startDialogHandling(cdp, (dialog) => store.recordDialog(dialog));
95
96
  },
96
97
  },
98
+ {
99
+ name: 'downloads',
100
+ runAlways: true,
101
+ start: startSessionDownloads,
102
+ },
97
103
  {
98
104
  name: 'page-identity',
99
105
  runAlways: true,