browser-debugger-cli 0.9.0 → 0.10.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/README.md +4 -1
  2. package/dist/commands/dom/a11y.js +2 -1
  3. package/dist/commands/dom/formInteraction.js +56 -25
  4. package/dist/commands/dom/helpers/keyAttributes.d.ts +20 -0
  5. package/dist/commands/dom/helpers/keyAttributes.js +54 -0
  6. package/dist/commands/dom/helpers/query.d.ts +1 -1
  7. package/dist/commands/dom/helpers/query.js +66 -19
  8. package/dist/commands/dom/helpers/runElementCommand.js +4 -3
  9. package/dist/commands/dom/helpers/screenshot.js +85 -12
  10. package/dist/commands/dom/index.d.ts +1 -0
  11. package/dist/commands/dom/index.js +8 -3
  12. package/dist/commands/dom/inspect.d.ts +15 -0
  13. package/dist/commands/dom/inspect.js +82 -0
  14. package/dist/commands/dom/layout.js +2 -2
  15. package/dist/commands/dom/listeners.js +2 -2
  16. package/dist/commands/dom/semanticUtils.d.ts +14 -1
  17. package/dist/commands/dom/semanticUtils.js +44 -3
  18. package/dist/commands/network/list.js +13 -2
  19. package/dist/commands/optionBehaviors.js +48 -6
  20. package/dist/commands/page.d.ts +1 -1
  21. package/dist/commands/page.js +62 -3
  22. package/dist/commands/shared/commonOptions.d.ts +4 -0
  23. package/dist/commands/shared/commonOptions.js +9 -0
  24. package/dist/commands/shared/optionTypes.d.ts +21 -0
  25. package/dist/commands/shared/startHelpers.d.ts +66 -0
  26. package/dist/commands/shared/startHelpers.js +91 -10
  27. package/dist/commands/shared/validation.d.ts +11 -0
  28. package/dist/commands/shared/validation.js +16 -0
  29. package/dist/daemon/launcher.d.ts +8 -1
  30. package/dist/daemon/launcher.js +3 -1
  31. package/dist/daemon/session/Session.d.ts +7 -0
  32. package/dist/daemon/session/Session.js +23 -1
  33. package/dist/daemon/session/commandRegistry.d.ts +14 -1
  34. package/dist/daemon/session/commandRegistry.js +65 -9
  35. package/dist/daemon/session/interactions.d.ts +18 -5
  36. package/dist/daemon/session/interactions.js +22 -12
  37. package/dist/daemon.js +3565 -329
  38. package/dist/errors/messages.d.ts +69 -0
  39. package/dist/errors/messages.js +102 -1
  40. package/dist/index.js +2416 -1320
  41. package/dist/ipc/client.d.ts +9 -0
  42. package/dist/ipc/client.js +13 -0
  43. package/dist/ipc/protocol/commands.d.ts +56 -1
  44. package/dist/ipc/protocol/commands.js +2 -0
  45. package/dist/ipc/protocol/domTypes.d.ts +35 -2
  46. package/dist/ipc/protocol/inspectTypes.d.ts +388 -0
  47. package/dist/ipc/protocol/inspectTypes.js +10 -0
  48. package/dist/runtime/dom/actionEffects.d.ts +94 -15
  49. package/dist/runtime/dom/actionEffects.js +173 -27
  50. package/dist/runtime/dom/actionEffectsScripts.d.ts +52 -14
  51. package/dist/runtime/dom/actionEffectsScripts.js +224 -32
  52. package/dist/runtime/dom/elementInfo.d.ts +26 -0
  53. package/dist/runtime/dom/elementInfo.js +65 -0
  54. package/dist/runtime/dom/eventListeners.js +14 -4
  55. package/dist/runtime/dom/formFillHelpers/fill.d.ts +3 -4
  56. package/dist/runtime/dom/formFillHelpers/fill.js +77 -28
  57. package/dist/runtime/dom/frameSelection.d.ts +11 -0
  58. package/dist/runtime/dom/frameSelection.js +20 -1
  59. package/dist/runtime/dom/frames.d.ts +38 -5
  60. package/dist/runtime/dom/frames.js +136 -21
  61. package/dist/runtime/dom/inspect.d.ts +28 -0
  62. package/dist/runtime/dom/inspect.js +557 -0
  63. package/dist/runtime/dom/inspectAllStyles.d.ts +62 -0
  64. package/dist/runtime/dom/inspectAllStyles.js +385 -0
  65. package/dist/runtime/dom/inspectCascade.d.ts +94 -0
  66. package/dist/runtime/dom/inspectCascade.js +371 -0
  67. package/dist/runtime/dom/inspectCascadeModel.d.ts +39 -0
  68. package/dist/runtime/dom/inspectCascadeModel.js +232 -0
  69. package/dist/runtime/dom/inspectHints.d.ts +62 -0
  70. package/dist/runtime/dom/inspectHints.js +305 -0
  71. package/dist/runtime/dom/inspectLayoutModel.d.ts +87 -0
  72. package/dist/runtime/dom/inspectLayoutModel.js +346 -0
  73. package/dist/runtime/dom/inspectModel.d.ts +74 -0
  74. package/dist/runtime/dom/inspectModel.js +184 -0
  75. package/dist/runtime/dom/inspectPaintModel.d.ts +157 -0
  76. package/dist/runtime/dom/inspectPaintModel.js +461 -0
  77. package/dist/runtime/dom/inspectRules.d.ts +37 -0
  78. package/dist/runtime/dom/inspectRules.js +101 -0
  79. package/dist/runtime/dom/inspectScripts.d.ts +132 -0
  80. package/dist/runtime/dom/inspectScripts.js +263 -0
  81. package/dist/runtime/dom/inspectTree.d.ts +40 -0
  82. package/dist/runtime/dom/inspectTree.js +134 -0
  83. package/dist/runtime/dom/inspectVariables.d.ts +33 -0
  84. package/dist/runtime/dom/inspectVariables.js +94 -0
  85. package/dist/runtime/dom/inspectWhyModel.d.ts +20 -0
  86. package/dist/runtime/dom/inspectWhyModel.js +134 -0
  87. package/dist/runtime/dom/layout.d.ts +5 -1
  88. package/dist/runtime/dom/layout.js +10 -3
  89. package/dist/runtime/dom/listenerPageScripts.d.ts +11 -5
  90. package/dist/runtime/dom/listenerPageScripts.js +95 -9
  91. package/dist/runtime/dom/listenerSummary.d.ts +4 -0
  92. package/dist/runtime/dom/listenerSummary.js +26 -9
  93. package/dist/runtime/dom/reactEventHelpers.d.ts +5 -0
  94. package/dist/runtime/dom/reactEventHelpers.js +12 -4
  95. package/dist/runtime/page/emulation.d.ts +20 -0
  96. package/dist/runtime/page/emulation.js +37 -0
  97. package/dist/telemetry/a11y.d.ts +10 -0
  98. package/dist/telemetry/a11y.js +78 -1
  99. package/dist/telemetry/console.d.ts +1 -0
  100. package/dist/telemetry/console.js +100 -5
  101. package/dist/telemetry/network.js +3 -1
  102. package/dist/types.d.ts +32 -0
  103. package/dist/ui/formatters/details.d.ts +8 -0
  104. package/dist/ui/formatters/details.js +59 -3
  105. package/dist/ui/formatters/dom.d.ts +2 -1
  106. package/dist/ui/formatters/dom.js +25 -9
  107. package/dist/ui/formatters/inspect.d.ts +39 -0
  108. package/dist/ui/formatters/inspect.js +596 -0
  109. package/dist/ui/formatters/keyAttributes.d.ts +19 -0
  110. package/dist/ui/formatters/keyAttributes.js +84 -0
  111. package/dist/ui/formatters/layout.js +2 -2
  112. package/dist/ui/formatters/networkHeaders.d.ts +13 -0
  113. package/dist/ui/formatters/networkHeaders.js +23 -3
  114. package/dist/ui/formatters/networkList.d.ts +29 -1
  115. package/dist/ui/formatters/networkList.js +86 -20
  116. package/dist/ui/formatters/status.js +1 -1
  117. package/dist/ui/formatting.d.ts +9 -0
  118. package/dist/ui/formatting.js +6 -3
  119. package/dist/ui/messages/commands.d.ts +123 -7
  120. package/dist/ui/messages/commands.js +181 -10
  121. package/dist/ui/messages/networkMessages.d.ts +14 -0
  122. package/dist/ui/messages/networkMessages.js +18 -0
  123. package/dist/ui/messages/session.d.ts +14 -0
  124. package/dist/ui/messages/session.js +20 -0
  125. package/dist/utils/async.d.ts +9 -0
  126. package/dist/utils/async.js +17 -0
  127. package/dist/utils/color.d.ts +84 -0
  128. package/dist/utils/color.js +376 -0
  129. package/dist/utils/cssValues.d.ts +109 -0
  130. package/dist/utils/cssValues.js +236 -0
  131. package/dist/utils/selectorFilters.d.ts +12 -0
  132. package/dist/utils/selectorFilters.js +29 -0
  133. package/package.json +1 -1
@@ -66,7 +66,11 @@ export interface HandlerDetails {
66
66
  targetName?: string | undefined;
67
67
  /** Handlers jQuery runs from this listener (set when it is jQuery's dispatcher) */
68
68
  jquery?: ResolvedHandler[] | undefined;
69
+ /** The handler Preact runs from this listener (set when it is Preact's event proxy) */
70
+ preact?: FrameworkHandler | undefined;
69
71
  }
72
+ /** A framework's handler found behind its dispatcher, of the dispatcher's event type */
73
+ export type FrameworkHandler = Omit<ResolvedHandler, 'type'>;
70
74
  /** A handler registered through a framework, found behind its dispatcher */
71
75
  export interface ResolvedHandler {
72
76
  /** Event type it was registered for */
@@ -140,14 +140,16 @@ function toElementListener(listener, found, name) {
140
140
  };
141
141
  }
142
142
  /**
143
- * Report entries for a jQuery dispatcher: one per jQuery handler that runs
144
- * for the element, with the dispatcher's flags.
143
+ * Report entries for a framework's dispatcher: one per handler it runs for
144
+ * the element (jQuery's handlers, Preact's handler), with the dispatcher's
145
+ * type and flags.
145
146
  *
146
- * @param dispatcher - Entry built for jQuery's own listener
147
+ * @param dispatcher - Entry built for the framework's own listener
147
148
  * @param handlers - Handlers behind it
149
+ * @param framework - The framework
148
150
  * @returns Entries naming the real handlers
149
151
  */
150
- function jqueryListeners(dispatcher, handlers) {
152
+ function frameworkListeners(dispatcher, handlers, framework) {
151
153
  return handlers.map((resolved) => ({
152
154
  ...dispatcher,
153
155
  handler: {
@@ -157,10 +159,25 @@ function jqueryListeners(dispatcher, handlers) {
157
159
  lineNumber: resolved.lineNumber,
158
160
  columnNumber: resolved.columnNumber,
159
161
  },
160
- framework: 'jQuery',
162
+ framework,
161
163
  ...(resolved.selector && { delegateSelector: resolved.selector }),
162
164
  }));
163
165
  }
166
+ /**
167
+ * Report entries for one listener: itself, or the handlers behind a
168
+ * framework's dispatcher.
169
+ *
170
+ * @param built - Entry built for the listener
171
+ * @param detail - What the page reported about its handler
172
+ * @returns Entries
173
+ */
174
+ function resolveDispatcher(built, detail) {
175
+ if (detail.jquery)
176
+ return frameworkListeners(built, detail.jquery, 'jQuery');
177
+ if (detail.preact)
178
+ return frameworkListeners(built, [detail.preact], 'Preact');
179
+ return [built];
180
+ }
164
181
  /** Chain position given to React parents outside the chain: after the last one inside */
165
182
  const OUTSIDE_CHAIN_OFFSET = 0.5;
166
183
  /**
@@ -319,11 +336,11 @@ export function collapseFrameworkRoots(placed) {
319
336
  };
320
337
  }
321
338
  /**
322
- * Report entries of every listener, jQuery dispatchers replaced by the
323
- * handlers they run for the element.
339
+ * Report entries of every listener, jQuery dispatchers and Preact proxies
340
+ * replaced by the handlers they run for the element.
324
341
  *
325
342
  * @param found - Listeners per chain entry, in chain order
326
- * @param details - Per flattened listener: handler name, jQuery handlers
343
+ * @param details - Per flattened listener: handler name, jQuery or Preact handlers
327
344
  * @returns Entries in chain order
328
345
  */
329
346
  function placeListeners(found, details) {
@@ -331,7 +348,7 @@ function placeListeners(found, details) {
331
348
  return flat.flatMap(({ entry, listener }, i) => {
332
349
  const detail = details[i] ?? {};
333
350
  const built = toElementListener(listener, entry, detail.name);
334
- const listeners = detail.jquery ? jqueryListeners(built, detail.jquery) : [built];
351
+ const listeners = resolveDispatcher(built, detail);
335
352
  return listeners.map((item) => ({
336
353
  position: entry.position,
337
354
  entry: entry.entry,
@@ -86,6 +86,11 @@ export declare const FILL_READ_BACK_SCRIPT = "(() => {\n const check = window._
86
86
  * When selector matches multiple elements without index, prioritizes visible ones.
87
87
  * A `<label>` is clicked (and double-clicked) through its control when that
88
88
  * is visible ({@link LABEL_CONTROL_JS}), reported as e.g. `input (via label)`.
89
+ * A hover first notes what is hidden around the element
90
+ * ({@link REVEAL_SNAPSHOT_JS}), so its result can say what it revealed. For
91
+ * a press, a probe (`window.__bdgPressProbe`) records whether the press
92
+ * reaches the element, and otherwise which element it landed on: a browser
93
+ * dialog or bubble can swallow input while the page looks normal.
89
94
  */
90
95
  export declare const CLICK_ELEMENT_SCRIPT: string;
91
96
  /**
@@ -6,6 +6,7 @@
6
6
  * to properly trigger React's event system.
7
7
  */
8
8
  import { FILL_REFUSALS, LABEL_WITHOUT_CONTROL, NAME_QUERY_PLACEHOLDER, VIA_LABEL_SUFFIX, } from '../../errors/messages.js';
9
+ import { REVEAL_SNAPSHOT_JS } from './actionEffectsScripts.js';
9
10
  import { ELEMENT_DESCRIPTION_JS, ELEMENT_IDENTITY_JS } from './elementInfo.js';
10
11
  import { FIND_ELEMENTS_JS, LABEL_CONTROL_JS } from './targetNode.js';
11
12
  /**
@@ -437,6 +438,11 @@ export const FILL_READ_BACK_SCRIPT = `(() => {
437
438
  * When selector matches multiple elements without index, prioritizes visible ones.
438
439
  * A `<label>` is clicked (and double-clicked) through its control when that
439
440
  * is visible ({@link LABEL_CONTROL_JS}), reported as e.g. `input (via label)`.
441
+ * A hover first notes what is hidden around the element
442
+ * ({@link REVEAL_SNAPSHOT_JS}), so its result can say what it revealed. For
443
+ * a press, a probe (`window.__bdgPressProbe`) records whether the press
444
+ * reaches the element, and otherwise which element it landed on: a browser
445
+ * dialog or bubble can swallow input while the page looks normal.
440
446
  */
441
447
  export const CLICK_ELEMENT_SCRIPT = `
442
448
  (function(selector, parts, index, action) {
@@ -588,12 +594,14 @@ export const CLICK_ELEMENT_SCRIPT = `
588
594
  else if (!hasSize) obstruction = 'zero-size';
589
595
  else if (!hittable) obstruction = 'covered by another element' + coveredBy();
590
596
 
591
- // Records whether the coming mouse press reaches the element at all; a
592
- // browser dialog or bubble can swallow input while the page looks normal.
597
+ if (action === 'hover') (${REVEAL_SNAPSHOT_JS})(el);
598
+
593
599
  if (hittable && action !== 'hover') {
594
- const probe = { reached: false };
600
+ const probe = { reached: false, landedOn: null };
595
601
  const markReached = (event) => {
596
- if (event.composedPath().includes(el)) probe.reached = true;
602
+ const path = event.composedPath();
603
+ if (path.includes(el)) probe.reached = true;
604
+ else if (!probe.landedOn && path[0] && path[0].nodeType === 1) probe.landedOn = describe(path[0]);
597
605
  };
598
606
  ['pointerdown', 'mousedown'].forEach((type) => view.addEventListener(type, markReached, true));
599
607
  probe.stop = () => ['pointerdown', 'mousedown'].forEach((type) => view.removeEventListener(type, markReached, true));
@@ -32,6 +32,26 @@ export declare function applySessionEmulation(cdp: CDPConnection, emulation: {
32
32
  viewport?: ViewportSize;
33
33
  colorScheme?: ColorScheme;
34
34
  }): Promise<void>;
35
+ /** Page emulation of a session: what `--viewport` and `--color-scheme` set */
36
+ export interface SessionEmulation {
37
+ viewport?: ViewportSize;
38
+ colorScheme?: ColorScheme;
39
+ }
40
+ /**
41
+ * Change the page emulation mid-session (`bdg page emulate`): set the
42
+ * viewport or the color scheme, or clear both (back to the browser window
43
+ * and the system setting).
44
+ *
45
+ * @param cdp - Session connection
46
+ * @param current - Emulation now in effect
47
+ * @param change - What to set, or `reset`
48
+ * @param record - Called after each change Chrome accepted (so a later
49
+ * failure leaves the recorded emulation true)
50
+ * @returns Emulation in effect afterwards
51
+ */
52
+ export declare function emulatePage(cdp: CDPConnection, current: SessionEmulation, change: SessionEmulation & {
53
+ reset?: boolean;
54
+ }, record: (emulation: SessionEmulation) => void): Promise<SessionEmulation>;
35
55
  /** What the page currently renders with, for `bdg status` */
36
56
  export interface PageAppearance {
37
57
  /** Layout viewport without scrollbars (as `dom layout` reports it) */
@@ -37,6 +37,43 @@ export async function applySessionEmulation(cdp, emulation) {
37
37
  });
38
38
  }
39
39
  }
40
+ /**
41
+ * Change the page emulation mid-session (`bdg page emulate`): set the
42
+ * viewport or the color scheme, or clear both (back to the browser window
43
+ * and the system setting).
44
+ *
45
+ * @param cdp - Session connection
46
+ * @param current - Emulation now in effect
47
+ * @param change - What to set, or `reset`
48
+ * @param record - Called after each change Chrome accepted (so a later
49
+ * failure leaves the recorded emulation true)
50
+ * @returns Emulation in effect afterwards
51
+ */
52
+ export async function emulatePage(cdp, current, change, record) {
53
+ let state = current;
54
+ const step = async (send, next) => {
55
+ await send();
56
+ state = next;
57
+ record(state);
58
+ };
59
+ const { viewport: _viewport, colorScheme, ...rest } = state;
60
+ if (change.reset || change.viewport) {
61
+ await step(() => change.viewport
62
+ ? cdp.send('Emulation.setDeviceMetricsOverride', viewportOverride(change.viewport))
63
+ : cdp.send('Emulation.clearDeviceMetricsOverride', {}), {
64
+ ...rest,
65
+ ...(colorScheme && { colorScheme }),
66
+ ...(change.viewport && { viewport: change.viewport }),
67
+ });
68
+ }
69
+ if (change.reset || change.colorScheme) {
70
+ const { colorScheme: _scheme, ...withoutScheme } = state;
71
+ await step(() => cdp.send('Emulation.setEmulatedMedia', {
72
+ features: [{ name: 'prefers-color-scheme', value: change.colorScheme ?? '' }],
73
+ }), { ...withoutScheme, ...(change.colorScheme && { colorScheme: change.colorScheme }) });
74
+ }
75
+ return state;
76
+ }
40
77
  /** How long `bdg status` waits for the page to report its appearance */
41
78
  const APPEARANCE_TIMEOUT_MS = 1000;
42
79
  /**
@@ -24,6 +24,16 @@ export declare function buildTreeFromRawNodes(rawNodes: Protocol.Accessibility.A
24
24
  * @throws Error if CDP Accessibility domain fails
25
25
  */
26
26
  export declare function collectA11yTree(): Promise<A11yTree>;
27
+ /**
28
+ * The tree with the values of secret fields masked ({@link MASKED_VALUE},
29
+ * whatever their length), and the names and values of the nodes inside them
30
+ * left out (the text Chrome lists under a text field is its value).
31
+ *
32
+ * @param tree - Accessibility tree
33
+ * @param secretIds - Node ids of secret fields
34
+ * @returns The same tree (nodes replaced in place)
35
+ */
36
+ export declare function maskSecretValues(tree: A11yTree, secretIds: ReadonlySet<string>): A11yTree;
27
37
  /**
28
38
  * Queries accessibility tree by pattern (role, name, description).
29
39
  *
@@ -1,7 +1,9 @@
1
1
  import { CommandError } from '../errors/index.js';
2
2
  import { unknownQueryFieldError } from '../errors/messages.js';
3
3
  import { callCDP } from '../ipc/client.js';
4
+ import { MASKED_VALUE, SENSITIVE_FIELD_JS } from '../runtime/dom/elementInfo.js';
4
5
  import { childFrameIds } from '../runtime/dom/frameLayout.js';
6
+ import { ConcurrencyLimiter } from '../utils/concurrency.js';
5
7
  import { EXIT_CODES } from '../utils/exitCodes.js';
6
8
  import { levenshteinDistance } from '../utils/levenshtein.js';
7
9
  /**
@@ -80,12 +82,87 @@ export async function collectA11yTree() {
80
82
  if (!result?.nodes) {
81
83
  throw new CommandError('Failed to get accessibility tree', { suggestion: 'CDP returned no nodes. The page may not be fully loaded.' }, EXIT_CODES.SOFTWARE_ERROR);
82
84
  }
83
- return buildTreeFromRawNodes([...result.nodes, ...(await collectFrameNodes(result.nodes))]);
85
+ const tree = buildTreeFromRawNodes([
86
+ ...result.nodes,
87
+ ...(await collectFrameNodes(result.nodes)),
88
+ ]);
89
+ return maskSecretValues(tree, await sensitiveNodeIds(tree));
84
90
  }
85
91
  finally {
86
92
  await callCDP('Accessibility.disable', {});
87
93
  }
88
94
  }
95
+ /** Concurrent page checks for {@link sensitiveNodeIds} */
96
+ const SENSITIVE_CHECK_CONCURRENCY = 10;
97
+ /**
98
+ * The accessibility nodes whose value is a secret: nodes with a value whose
99
+ * element is a sensitive field ({@link SENSITIVE_FIELD_JS}: passwords, card
100
+ * fields, one-time codes). Only nodes with a value are checked, one page
101
+ * call each (a page has few).
102
+ *
103
+ * @param tree - Accessibility tree
104
+ * @returns Node ids of secret fields
105
+ */
106
+ async function sensitiveNodeIds(tree) {
107
+ const candidates = [...tree.nodes.values()].filter((node) => node.value !== undefined && node.backendDOMNodeId !== undefined);
108
+ const limiter = new ConcurrencyLimiter(SENSITIVE_CHECK_CONCURRENCY);
109
+ const checked = await Promise.all(candidates.map((node) => limiter.run(async () => (await isSensitiveField(node.backendDOMNodeId ?? 0)) ? node.nodeId : null)));
110
+ return new Set(checked.filter((id) => id !== null));
111
+ }
112
+ /**
113
+ * Whether an element is a sensitive field. An element that cannot be read
114
+ * counts as one, so a value is never shown by mistake.
115
+ *
116
+ * @param backendNodeId - The element
117
+ * @returns True for secret fields (and unreadable elements)
118
+ */
119
+ async function isSensitiveField(backendNodeId) {
120
+ const objectGroup = 'bdg-a11y-secret';
121
+ const resolved = await callCDP('DOM.resolveNode', { backendNodeId, objectGroup });
122
+ const objectId = resolved.data?.result?.object
123
+ ?.objectId;
124
+ if (!objectId)
125
+ return true;
126
+ try {
127
+ const response = await callCDP('Runtime.callFunctionOn', {
128
+ objectId,
129
+ functionDeclaration: `function () { return this.nodeType !== 1 || (${SENSITIVE_FIELD_JS})(this); }`,
130
+ returnByValue: true,
131
+ });
132
+ const value = response.data?.result?.result
133
+ ?.value;
134
+ return value !== false;
135
+ }
136
+ finally {
137
+ await callCDP('Runtime.releaseObjectGroup', { objectGroup });
138
+ }
139
+ }
140
+ /**
141
+ * The tree with the values of secret fields masked ({@link MASKED_VALUE},
142
+ * whatever their length), and the names and values of the nodes inside them
143
+ * left out (the text Chrome lists under a text field is its value).
144
+ *
145
+ * @param tree - Accessibility tree
146
+ * @param secretIds - Node ids of secret fields
147
+ * @returns The same tree (nodes replaced in place)
148
+ */
149
+ export function maskSecretValues(tree, secretIds) {
150
+ const hide = (nodeId, inside) => {
151
+ const node = tree.nodes.get(nodeId);
152
+ if (!node)
153
+ return;
154
+ const { name: _name, value: _value, ...rest } = node;
155
+ const masked = inside
156
+ ? rest
157
+ : { ...node, ...(node.value !== undefined && { value: MASKED_VALUE }) };
158
+ tree.nodes.set(nodeId, masked);
159
+ if (tree.root.nodeId === nodeId)
160
+ tree.root = masked;
161
+ node.childIds?.forEach((childId) => hide(childId, true));
162
+ };
163
+ secretIds.forEach((nodeId) => hide(nodeId, false));
164
+ return tree;
165
+ }
89
166
  /**
90
167
  * Accessibility nodes of the page's iframes, attached under their `Iframe`
91
168
  * node (the page's own tree stops there). Frames in another process
@@ -2,6 +2,7 @@
2
2
  * Console message collection via CDP Runtime and Log domains.
3
3
  *
4
4
  * Captures console.log, console.error, etc. and JavaScript exceptions
5
+ * (unhandled rejections removed again when a handler is attached later)
5
6
  * with automatic nested object expansion, browser messages (failed loads,
6
7
  * CORS, security, deprecations), and the same from cross-origin iframes and
7
8
  * workers.
@@ -2,6 +2,7 @@
2
2
  * Console message collection via CDP Runtime and Log domains.
3
3
  *
4
4
  * Captures console.log, console.error, etc. and JavaScript exceptions
5
+ * (unhandled rejections removed again when a handler is attached later)
5
6
  * with automatic nested object expansion, browser messages (failed loads,
6
7
  * CORS, security, deprecations), and the same from cross-origin iframes and
7
8
  * workers.
@@ -76,14 +77,17 @@ function createMessage(type, text, timestamp, args, context) {
76
77
  /**
77
78
  * Insert a message in timestamp order.
78
79
  * Messages are kept sorted by timestamp to handle async expansion delays.
80
+ *
81
+ * @returns False when the message limit is reached (the message is dropped)
79
82
  */
80
83
  function insertMessageByTimestamp(messages, message) {
81
84
  if (messages.length >= MAX_CONSOLE_MESSAGES) {
82
85
  log.debug(`Warning: Console message limit reached (${MAX_CONSOLE_MESSAGES})`);
83
- return;
86
+ return false;
84
87
  }
85
88
  const insertIndex = findInsertIndex(messages, message.timestamp);
86
89
  messages.splice(insertIndex, 0, message);
90
+ return true;
87
91
  }
88
92
  /**
89
93
  * Find the correct insertion index to maintain timestamp order.
@@ -171,15 +175,93 @@ export function formatExceptionText(details) {
171
175
  }
172
176
  /**
173
177
  * Handle an exception thrown event.
178
+ *
179
+ * @returns The message added, undefined when filtered out or over the limit
174
180
  */
175
181
  function handleExceptionThrown(messages, params, context, includeAll) {
176
182
  const exception = params.exceptionDetails;
177
183
  const text = formatExceptionText(exception);
178
184
  if (shouldExcludeConsoleMessage(text, 'error', includeAll)) {
179
- return;
185
+ return undefined;
180
186
  }
181
187
  const message = createMessage('error', text, params.timestamp, undefined, context);
182
- insertMessageByTimestamp(messages, message);
188
+ return insertMessageByTimestamp(messages, message) ? message : undefined;
189
+ }
190
+ /** CDP's text of an unhandled promise rejection (the only kind Chrome revokes) */
191
+ const UNHANDLED_REJECTION_TEXT = 'Uncaught (in promise)';
192
+ /**
193
+ * Unhandled promise rejections that may still be revoked: Chrome reports a
194
+ * rejection without a handler at the end of the task, and revokes the
195
+ * report (`Runtime.exceptionRevoked`) when a handler is attached later, as
196
+ * DevTools does by removing the message. `bdg dom eval` attaches its
197
+ * handler only after the evaluation returns, so this is what keeps
198
+ * `bdg dom eval 'Promise.reject(…)'` out of the console; a rejection
199
+ * nothing ever handles stays.
200
+ */
201
+ class RevocableRejections {
202
+ /** Messages of rejections, by session and exception id, oldest first */
203
+ pending = new Map();
204
+ /**
205
+ * Remember a reported exception if it is an unhandled rejection. Past
206
+ * {@link MAX_CONSOLE_MESSAGES} rejections, the oldest is forgotten.
207
+ *
208
+ * @param details - Exception details
209
+ * @param message - Its console message
210
+ * @param sessionId - Session it was reported on (undefined: the page)
211
+ */
212
+ track(details, message, sessionId) {
213
+ if (!details.text.startsWith(UNHANDLED_REJECTION_TEXT))
214
+ return;
215
+ this.pending.set(this.key(details.exceptionId, sessionId), message);
216
+ if (this.pending.size <= MAX_CONSOLE_MESSAGES)
217
+ return;
218
+ const [oldest] = this.pending.keys();
219
+ if (oldest !== undefined)
220
+ this.pending.delete(oldest);
221
+ }
222
+ /**
223
+ * Remove the message of a revoked rejection.
224
+ *
225
+ * @param messages - Console messages (updated)
226
+ * @param exceptionId - Revoked exception
227
+ * @param sessionId - Session it was revoked on (undefined: the page)
228
+ */
229
+ revoke(messages, exceptionId, sessionId) {
230
+ const key = this.key(exceptionId, sessionId);
231
+ const message = this.pending.get(key);
232
+ this.pending.delete(key);
233
+ const index = message ? messages.indexOf(message) : -1;
234
+ if (index !== -1)
235
+ messages.splice(index, 1);
236
+ }
237
+ /**
238
+ * Forget the rejections of a session whose documents are gone (it
239
+ * detached, or its contexts were cleared by a navigation): they can no
240
+ * longer be revoked.
241
+ *
242
+ * @param sessionId - Session (undefined: the page)
243
+ */
244
+ forgetSession(sessionId) {
245
+ const prefix = this.key('', sessionId);
246
+ for (const key of this.pending.keys()) {
247
+ if (key.startsWith(prefix))
248
+ this.pending.delete(key);
249
+ }
250
+ }
251
+ /** Forget every rejection (the collection stopped). */
252
+ clear() {
253
+ this.pending.clear();
254
+ }
255
+ /**
256
+ * Map key of an exception: ids are counted per session.
257
+ *
258
+ * @param exceptionId - Exception id ('' for the session's prefix)
259
+ * @param sessionId - Session
260
+ * @returns Key
261
+ */
262
+ key(exceptionId, sessionId) {
263
+ return `${sessionId ?? ''}:${exceptionId}`;
264
+ }
183
265
  }
184
266
  /**
185
267
  * Handle a browser log entry (failed resource loads, CORS, security,
@@ -255,12 +337,24 @@ export async function startConsoleCollection(cdp, messages, includeAll = false,
255
337
  };
256
338
  handleConsoleAPICall(senderFor(cdp, sessionId), messages, params, context, includeAll);
257
339
  });
258
- registry.registerTyped(typed, 'Runtime.exceptionThrown', (params) => {
340
+ const rejections = new RevocableRejections();
341
+ registry.registerTyped(typed, 'Runtime.exceptionThrown', (params, sessionId) => {
259
342
  const context = {
260
343
  navigationId: getCurrentNavigationId?.(),
261
344
  stackTrace: convertStackTrace(params.exceptionDetails.stackTrace),
262
345
  };
263
- handleExceptionThrown(messages, params, context, includeAll);
346
+ const message = handleExceptionThrown(messages, params, context, includeAll);
347
+ if (message)
348
+ rejections.track(params.exceptionDetails, message, sessionId);
349
+ });
350
+ registry.registerTyped(typed, 'Runtime.exceptionRevoked', ({ exceptionId }, sessionId) => {
351
+ rejections.revoke(messages, exceptionId, sessionId);
352
+ });
353
+ registry.registerTyped(typed, 'Runtime.executionContextsCleared', (_params, sessionId) => {
354
+ rejections.forgetSession(sessionId);
355
+ });
356
+ registry.registerTyped(typed, 'Target.detachedFromTarget', ({ sessionId }) => {
357
+ rejections.forgetSession(sessionId);
264
358
  });
265
359
  registry.registerTyped(typed, 'Log.entryAdded', ({ entry }) => {
266
360
  handleLogEntry(messages, entry, getCurrentNavigationId?.(), includeAll);
@@ -275,6 +369,7 @@ export async function startConsoleCollection(cdp, messages, includeAll = false,
275
369
  const detachChildren = await startOptionalSources(cdp, typed);
276
370
  return async () => {
277
371
  registry.cleanup();
372
+ rejections.clear();
278
373
  await detachChildren();
279
374
  };
280
375
  }
@@ -110,7 +110,7 @@ export function skippedBodyReason(body) {
110
110
  * @param resourceType - Resource type reported with the response
111
111
  */
112
112
  function applyResponse(request, response, resourceType) {
113
- const { status, mimeType, headers, timing, remoteIPAddress, connectionId } = response;
113
+ const { status, mimeType, headers, timing, remoteIPAddress, remotePort, connectionId } = response;
114
114
  request.status = status;
115
115
  if (response.statusText)
116
116
  request.statusText = response.statusText;
@@ -138,6 +138,8 @@ function applyResponse(request, response, resourceType) {
138
138
  }
139
139
  if (remoteIPAddress)
140
140
  request.serverIPAddress = remoteIPAddress;
141
+ if (remotePort)
142
+ request.serverPort = remotePort;
141
143
  if (connectionId !== undefined)
142
144
  request.connection = String(connectionId);
143
145
  }
package/dist/types.d.ts CHANGED
@@ -137,7 +137,10 @@ export interface NetworkRequest {
137
137
  redirectURL?: string;
138
138
  encodedDataLength?: number;
139
139
  decodedBodyLength?: number;
140
+ /** Address Chrome connected to: the server's, or a proxy's (CDP `remoteIPAddress`) */
140
141
  serverIPAddress?: string;
142
+ /** Port Chrome connected to (CDP `remotePort`) */
143
+ serverPort?: number;
141
144
  connection?: string;
142
145
  /** Status text sent by the server (empty for HTTP/2) */
143
146
  statusText?: string;
@@ -324,7 +327,34 @@ export interface DomContext {
324
327
  children?: string[];
325
328
  /** Number of child elements, for an element without text */
326
329
  childCount?: number;
330
+ /** Attributes that identify it by its type (see {@link KeyAttributes}) */
331
+ attributes?: KeyAttributes;
332
+ /** A field holding a secret (password, card, one-time code): its value is shown masked */
333
+ sensitive?: boolean;
327
334
  }
335
+ /**
336
+ * Live state of a form control read in the page: an input's type and value
337
+ * (`checked` for checkboxes and radios), a textarea's value, the labels of a
338
+ * select's selected options, the type of a button in a form.
339
+ */
340
+ export interface ElementState {
341
+ type?: string;
342
+ /** Masked in the page for sensitive fields; never read for hidden inputs */
343
+ value?: string;
344
+ checked?: boolean;
345
+ selected?: string;
346
+ /** A field holding a secret (its value and selected option are masked) */
347
+ sensitive?: boolean;
348
+ }
349
+ /**
350
+ * The attributes that identify an element by its type, with full values:
351
+ * img `src`, `alt`; a `href`; input `type`, `name`, `placeholder`, `value`
352
+ * (current value, masked for passwords; for checkboxes and radios their
353
+ * `value` attribute) and `checked`; textarea `name`,
354
+ * `placeholder`, `value`; button `type` (`submit` by default in a form), `name`; select `name`, `selected`
355
+ * (labels of the selected options); iframe `src`; form `action`, `method`.
356
+ */
357
+ export type KeyAttributes = Record<string, string | boolean>;
328
358
  /**
329
359
  * Reference to a DOM node for CDP calls: a per-connection `nodeId` (valid only
330
360
  * within one command) or a `backendNodeId` (valid while the node exists).
@@ -392,6 +422,8 @@ export interface DomQueryResult {
392
422
  type?: string;
393
423
  /** `value` attribute of an `<option>` */
394
424
  value?: string;
425
+ /** Attributes that identify it by its type (see {@link KeyAttributes}) */
426
+ attributes?: KeyAttributes;
395
427
  classes?: string[];
396
428
  /** Text content preview (display only, never used for targeting) */
397
429
  preview?: string;
@@ -1,4 +1,12 @@
1
1
  import type { NetworkRequest, ConsoleMessage } from '../../types.js';
2
+ /**
3
+ * The address Chrome connected to, with its port, noting when it looks like
4
+ * a proxy on this machine ({@link looksLikeLocalProxy}).
5
+ *
6
+ * @param request - Request with `serverIPAddress`
7
+ * @returns e.g. `93.184.215.14:443`, `[2606:4700::1]:443`, `127.0.0.1:9000 (loopback; likely a local proxy)`
8
+ */
9
+ export declare function remoteAddress(request: NetworkRequest): string;
2
10
  /**
3
11
  * Format network request details for human-readable output.
4
12
  *