browser-debugger-cli 0.12.0 → 0.14.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 (224) hide show
  1. package/.claude/skills/bdg/SKILL.md +100 -186
  2. package/README.md +5 -4
  3. package/dist/commands/cdp.d.ts +22 -1
  4. package/dist/commands/cdp.js +100 -43
  5. package/dist/commands/console.d.ts +12 -0
  6. package/dist/commands/console.js +67 -13
  7. package/dist/commands/dom/DomElementResolver.d.ts +3 -1
  8. package/dist/commands/dom/DomElementResolver.js +10 -3
  9. package/dist/commands/dom/a11y.d.ts +1 -1
  10. package/dist/commands/dom/a11y.js +23 -22
  11. package/dist/commands/dom/eval.d.ts +4 -2
  12. package/dist/commands/dom/eval.js +31 -7
  13. package/dist/commands/dom/form.js +10 -9
  14. package/dist/commands/dom/formInteraction.js +9 -8
  15. package/dist/commands/dom/get.js +32 -14
  16. package/dist/commands/dom/helpers/index.d.ts +1 -1
  17. package/dist/commands/dom/helpers/index.js +1 -1
  18. package/dist/commands/dom/helpers/query.d.ts +27 -3
  19. package/dist/commands/dom/helpers/query.js +152 -64
  20. package/dist/commands/dom/helpers/screenshot.js +13 -13
  21. package/dist/commands/dom/index.js +10 -3
  22. package/dist/commands/dom/query.d.ts +20 -2
  23. package/dist/commands/dom/query.js +39 -6
  24. package/dist/commands/dom/screenshot.js +3 -1
  25. package/dist/commands/dom/semanticUtils.d.ts +3 -2
  26. package/dist/commands/dom/semanticUtils.js +40 -9
  27. package/dist/commands/helpJson.d.ts +82 -19
  28. package/dist/commands/helpJson.js +112 -41
  29. package/dist/commands/helpTopic.d.ts +16 -1
  30. package/dist/commands/helpTopic.js +59 -1
  31. package/dist/commands/installSkill.d.ts +15 -5
  32. package/dist/commands/installSkill.js +86 -16
  33. package/dist/commands/network/list.js +65 -12
  34. package/dist/commands/optionBehaviors.d.ts +25 -2
  35. package/dist/commands/optionBehaviors.js +81 -46
  36. package/dist/commands/peek.js +3 -0
  37. package/dist/commands/shared/CommandRunner.js +13 -13
  38. package/dist/commands/shared/daemonErrorHandler.d.ts +5 -2
  39. package/dist/commands/shared/daemonErrorHandler.js +21 -10
  40. package/dist/commands/shared/dataFetcher.d.ts +14 -4
  41. package/dist/commands/shared/dataFetcher.js +20 -4
  42. package/dist/commands/shared/followMode.d.ts +9 -1
  43. package/dist/commands/shared/followMode.js +22 -4
  44. package/dist/commands/shared/handleValidationError.js +3 -3
  45. package/dist/commands/shared/optionTypes.d.ts +17 -3
  46. package/dist/commands/shared/outputFile.js +6 -1
  47. package/dist/commands/shared/startHelpers.js +3 -3
  48. package/dist/commands/start.d.ts +7 -5
  49. package/dist/commands/start.js +65 -21
  50. package/dist/commands/stop.d.ts +11 -0
  51. package/dist/commands/stop.js +24 -1
  52. package/dist/commands.js +1 -1
  53. package/dist/connection/cdp.d.ts +7 -0
  54. package/dist/connection/cdp.js +9 -0
  55. package/dist/connection/chromeIdentity.d.ts +8 -2
  56. package/dist/connection/chromeIdentity.js +85 -13
  57. package/dist/connection/launcher.js +3 -2
  58. package/dist/constants.d.ts +29 -1
  59. package/dist/constants.js +35 -1
  60. package/dist/daemon/SessionController.js +8 -1
  61. package/dist/daemon/launcher.d.ts +3 -2
  62. package/dist/daemon/launcher.js +47 -3
  63. package/dist/daemon/session/Session.d.ts +5 -1
  64. package/dist/daemon/session/Session.js +42 -3
  65. package/dist/daemon/session/TelemetryStore.d.ts +15 -1
  66. package/dist/daemon/session/TelemetryStore.js +19 -1
  67. package/dist/daemon/session/commandRegistry.js +52 -18
  68. package/dist/daemon/session/interactions.d.ts +2 -1
  69. package/dist/daemon/session/interactions.js +13 -1
  70. package/dist/daemon/session/matchedStylesReset.d.ts +26 -0
  71. package/dist/daemon/session/matchedStylesReset.js +46 -0
  72. package/dist/daemon/session/plugins.js +17 -2
  73. package/dist/daemon/session/teardown.js +1 -1
  74. package/dist/daemon/session/triggeredRequests.d.ts +0 -5
  75. package/dist/daemon/session/triggeredRequests.js +13 -7
  76. package/dist/daemon.js +2385 -1229
  77. package/dist/errors/messages.d.ts +62 -11
  78. package/dist/errors/messages.js +119 -22
  79. package/dist/index.js +14995 -9866
  80. package/dist/ipc/client.d.ts +18 -2
  81. package/dist/ipc/client.js +26 -5
  82. package/dist/ipc/protocol/auditTypes.d.ts +8 -2
  83. package/dist/ipc/protocol/commands.d.ts +16 -0
  84. package/dist/ipc/protocol/domTypes.d.ts +12 -0
  85. package/dist/ipc/protocol/inspectTypes.d.ts +7 -2
  86. package/dist/ipc/session/types.d.ts +7 -1
  87. package/dist/program.d.ts +14 -0
  88. package/dist/program.js +53 -0
  89. package/dist/runtime/dom/actionEffects.d.ts +5 -1
  90. package/dist/runtime/dom/actionEffects.js +26 -14
  91. package/dist/runtime/dom/audit.js +3 -2
  92. package/dist/runtime/dom/auditModel.js +6 -1
  93. package/dist/runtime/dom/auditScripts.d.ts +9 -3
  94. package/dist/runtime/dom/auditScripts.js +41 -5
  95. package/dist/runtime/dom/elementGeometry.d.ts +33 -3
  96. package/dist/runtime/dom/elementGeometry.js +44 -19
  97. package/dist/runtime/dom/elementInfo.d.ts +76 -18
  98. package/dist/runtime/dom/elementInfo.js +190 -40
  99. package/dist/runtime/dom/evalHelpers.d.ts +12 -2
  100. package/dist/runtime/dom/evalHelpers.js +67 -7
  101. package/dist/runtime/dom/formDiscovery.d.ts +6 -2
  102. package/dist/runtime/dom/formDiscovery.js +20 -3
  103. package/dist/runtime/dom/formFillHelpers/fill.js +7 -11
  104. package/dist/runtime/dom/formFillHelpers/pressKey.js +2 -2
  105. package/dist/runtime/dom/formFillHelpers/shared.d.ts +16 -10
  106. package/dist/runtime/dom/formFillHelpers/shared.js +19 -52
  107. package/dist/runtime/dom/formSubmitHelpers.js +4 -3
  108. package/dist/runtime/dom/frameLayout.js +1 -0
  109. package/dist/runtime/dom/frameScopedConnection.d.ts +7 -0
  110. package/dist/runtime/dom/frameScopedConnection.js +2 -2
  111. package/dist/runtime/dom/inspect.d.ts +17 -3
  112. package/dist/runtime/dom/inspect.js +45 -32
  113. package/dist/runtime/dom/inspectAllStyles.js +1 -0
  114. package/dist/runtime/dom/inspectHints.d.ts +1 -1
  115. package/dist/runtime/dom/inspectModel.d.ts +5 -4
  116. package/dist/runtime/dom/inspectModel.js +7 -3
  117. package/dist/runtime/dom/inspectPaintModel.d.ts +2 -0
  118. package/dist/runtime/dom/inspectPaintModel.js +3 -1
  119. package/dist/runtime/dom/inspectRules.d.ts +29 -3
  120. package/dist/runtime/dom/inspectRules.js +205 -11
  121. package/dist/runtime/dom/inspectScripts.d.ts +29 -2
  122. package/dist/runtime/dom/inspectScripts.js +49 -10
  123. package/dist/runtime/dom/layout.d.ts +0 -2
  124. package/dist/runtime/dom/layout.js +10 -9
  125. package/dist/runtime/dom/reactEventHelpers.d.ts +17 -4
  126. package/dist/runtime/dom/reactEventHelpers.js +71 -28
  127. package/dist/runtime/dom/targetNode.d.ts +27 -10
  128. package/dist/runtime/dom/targetNode.js +283 -16
  129. package/dist/runtime/dom/wait.js +2 -1
  130. package/dist/runtime/page/bdgWorld.d.ts +57 -0
  131. package/dist/runtime/page/bdgWorld.js +180 -0
  132. package/dist/runtime/page/replacedBuiltins.d.ts +28 -0
  133. package/dist/runtime/page/replacedBuiltins.js +136 -0
  134. package/dist/session/QueryCacheManager.d.ts +4 -1
  135. package/dist/session/QueryCacheManager.js +5 -2
  136. package/dist/session/chrome.d.ts +4 -1
  137. package/dist/session/chrome.js +7 -1
  138. package/dist/session/cleanup/staleSession.d.ts +21 -4
  139. package/dist/session/cleanup/staleSession.js +79 -9
  140. package/dist/session/cleanup/userCommands.d.ts +4 -1
  141. package/dist/session/cleanup/userCommands.js +10 -5
  142. package/dist/session/daemonSocket.d.ts +10 -0
  143. package/dist/session/daemonSocket.js +22 -0
  144. package/dist/session/lastSession.d.ts +6 -3
  145. package/dist/session/lastSession.js +11 -5
  146. package/dist/session/paths.d.ts +3 -1
  147. package/dist/session/paths.js +5 -5
  148. package/dist/session/portClaims.js +4 -3
  149. package/dist/session/sessionList.d.ts +13 -5
  150. package/dist/session/sessionList.js +31 -7
  151. package/dist/telemetry/a11y.d.ts +15 -1
  152. package/dist/telemetry/a11y.js +85 -2
  153. package/dist/telemetry/console.d.ts +2 -1
  154. package/dist/telemetry/console.js +30 -21
  155. package/dist/telemetry/har/builder.js +1 -1
  156. package/dist/telemetry/network.d.ts +13 -16
  157. package/dist/telemetry/network.js +30 -52
  158. package/dist/telemetry/networkRetention.d.ts +83 -0
  159. package/dist/telemetry/networkRetention.js +117 -0
  160. package/dist/telemetry/pageCrash.d.ts +26 -0
  161. package/dist/telemetry/pageCrash.js +53 -0
  162. package/dist/types.d.ts +42 -0
  163. package/dist/ui/OutputBuilder.d.ts +10 -0
  164. package/dist/ui/OutputBuilder.js +12 -0
  165. package/dist/ui/formatters/a11y.d.ts +5 -7
  166. package/dist/ui/formatters/a11y.js +7 -61
  167. package/dist/ui/formatters/audit.js +14 -5
  168. package/dist/ui/formatters/cdp.d.ts +138 -0
  169. package/dist/ui/formatters/cdp.js +131 -0
  170. package/dist/ui/formatters/console/chronological.js +7 -5
  171. package/dist/ui/formatters/console/follow.d.ts +5 -2
  172. package/dist/ui/formatters/console/follow.js +7 -4
  173. package/dist/ui/formatters/console/json.d.ts +4 -7
  174. package/dist/ui/formatters/console/json.js +16 -14
  175. package/dist/ui/formatters/console/shared.d.ts +47 -2
  176. package/dist/ui/formatters/console/shared.js +33 -0
  177. package/dist/ui/formatters/console/summarize.d.ts +9 -2
  178. package/dist/ui/formatters/console/summarize.js +57 -11
  179. package/dist/ui/formatters/console.d.ts +3 -2
  180. package/dist/ui/formatters/console.js +8 -10
  181. package/dist/ui/formatters/details.js +4 -2
  182. package/dist/ui/formatters/dom.d.ts +14 -5
  183. package/dist/ui/formatters/dom.js +30 -13
  184. package/dist/ui/formatters/helpFormatters.js +1 -1
  185. package/dist/ui/formatters/inspect.js +9 -3
  186. package/dist/ui/formatters/installSkill.d.ts +9 -1
  187. package/dist/ui/formatters/installSkill.js +32 -6
  188. package/dist/ui/formatters/layout.js +4 -2
  189. package/dist/ui/formatters/longValues.d.ts +14 -0
  190. package/dist/ui/formatters/longValues.js +23 -0
  191. package/dist/ui/formatters/networkList.d.ts +8 -2
  192. package/dist/ui/formatters/networkList.js +11 -3
  193. package/dist/ui/formatters/preview.d.ts +6 -1
  194. package/dist/ui/formatters/preview.js +67 -15
  195. package/dist/ui/formatters/sessions.d.ts +2 -2
  196. package/dist/ui/formatters/sessions.js +9 -2
  197. package/dist/ui/formatters/status.js +7 -0
  198. package/dist/ui/formatters/triggeredRequests.js +2 -1
  199. package/dist/ui/logging/logger.d.ts +1 -1
  200. package/dist/ui/messages/chrome.d.ts +20 -1
  201. package/dist/ui/messages/chrome.js +29 -3
  202. package/dist/ui/messages/commands.d.ts +153 -12
  203. package/dist/ui/messages/commands.js +198 -15
  204. package/dist/ui/messages/consoleMessages.d.ts +24 -0
  205. package/dist/ui/messages/consoleMessages.js +32 -0
  206. package/dist/ui/messages/networkMessages.d.ts +24 -0
  207. package/dist/ui/messages/networkMessages.js +45 -0
  208. package/dist/ui/messages/preview.d.ts +6 -0
  209. package/dist/ui/messages/preview.js +9 -1
  210. package/dist/ui/messages/session.d.ts +13 -2
  211. package/dist/ui/messages/session.js +22 -3
  212. package/dist/utils/directories.d.ts +34 -0
  213. package/dist/utils/directories.js +88 -0
  214. package/dist/utils/display.d.ts +16 -0
  215. package/dist/utils/display.js +42 -0
  216. package/dist/utils/exitCodes.d.ts +1 -0
  217. package/dist/utils/exitCodes.js +6 -0
  218. package/dist/utils/http.d.ts +9 -2
  219. package/dist/utils/http.js +4 -3
  220. package/dist/utils/process.d.ts +12 -0
  221. package/dist/utils/process.js +25 -0
  222. package/dist/utils/strings.d.ts +19 -0
  223. package/dist/utils/strings.js +16 -0
  224. package/package.json +2 -2
@@ -3,6 +3,31 @@
3
3
  * `dom layout`, DOM context of console messages): their text as a user sees
4
4
  * it and where they live (iframes, shadow roots).
5
5
  */
6
+ /**
7
+ * A class that is a fragment of a declaration-like class attribute
8
+ * (`class="brush: html"` has the class `brush:`), left out of labels.
9
+ */
10
+ const CLASS_FRAGMENT = /[:;,]$/;
11
+ /**
12
+ * Whether a class name is shown in element labels: not empty and not a
13
+ * fragment of a declaration-like class attribute ({@link CLASS_FRAGMENT}).
14
+ *
15
+ * @param name - Class name
16
+ * @returns True to show it
17
+ */
18
+ export function isLabelClass(name) {
19
+ return name !== '' && !CLASS_FRAGMENT.test(name);
20
+ }
21
+ /**
22
+ * Page-side `(node) => string[]`: the element's classes shown in labels
23
+ * ({@link isLabelClass}), read without array helpers a page may replace
24
+ */
25
+ export const LABEL_CLASSES_JS = `(node) => {
26
+ const shown = [];
27
+ const list = node.classList || [];
28
+ for (let i = 0; i < list.length; i++) if (!${CLASS_FRAGMENT}.test(list[i])) shown[shown.length] = list[i];
29
+ return shown;
30
+ }`;
6
31
  /** Length of the text preview shown for elements. */
7
32
  const PREVIEW_LENGTH = 80;
8
33
  /** Length of the text `dom get` shows for an element. */
@@ -36,27 +61,91 @@ export const WITHOUT_DECORATIONS_JS = `(el, text) => {
36
61
  }
37
62
  return result;
38
63
  }`;
64
+ /**
65
+ * Page-side check whether `innerText` misses part of what an element shows:
66
+ * it is a `<slot>` or hosts an open shadow root, or one of its descendants
67
+ * is or does. `innerText` follows neither shadow roots nor slots.
68
+ */
69
+ export const COMPOSED_JS = `(node) => {
70
+ const composes = (n) => Boolean(n.shadowRoot) || n.localName === 'slot';
71
+ if (composes(node)) return true;
72
+ const walker = node.ownerDocument.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);
73
+ while (walker.nextNode()) if (composes(walker.currentNode)) return true;
74
+ return false;
75
+ }`;
76
+ /**
77
+ * Page-side text of an element as the flat tree renders it, for elements
78
+ * `innerText` cannot read ({@link COMPOSED_JS}): a shadow host is read from
79
+ * its open shadow root (its own labels and fallback content; light children
80
+ * only where a slot shows them), and each slot is replaced by what it
81
+ * shows, its assigned nodes flattened through nested slots (or its fallback
82
+ * content when nothing is assigned). Parts without slots or shadow roots
83
+ * are read with `innerText`; elements that are not rendered and text under
84
+ * `visibility: hidden` are left out, elements that are not inline are set
85
+ * apart by line breaks, as is a `<br>`. Fields and editable regions inside
86
+ * it (inputs, textareas, selects, a contenteditable editor) are skipped, so
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
91
+ * part whose text alone passes the limit is read from its text nodes
92
+ * (`textContent`) instead of `innerText`, which would lay out all of it.
93
+ */
94
+ export const FLAT_TEXT_JS = `(el, limit, fields) => {
95
+ const composed = ${COMPOSED_JS};
96
+ const view = el.ownerDocument.defaultView;
97
+ let text = '';
98
+ const nodesOf = (node) =>
99
+ node.localName === 'slot' ? node.assignedNodes({ flatten: true }) : Array.from((node.shadowRoot || node).childNodes);
100
+ const read = (node, visible) => {
101
+ if (text.length >= limit) return;
102
+ if (node.nodeType === 3 && visible) text += node.data.replace(/\\s+/g, ' ');
103
+ if (node.nodeType !== 1) return;
104
+ if (/^(input|textarea)$/.test(node.localName)) return;
105
+ if (!fields && (node.localName === 'select' || node.isContentEditable)) return;
106
+ const style = view.getComputedStyle(node);
107
+ if (node.checkVisibility && !node.checkVisibility() && style.display !== 'contents') return;
108
+ if (node.localName === 'br') text += '\\n';
109
+ const apart = style.display.startsWith('inline') || style.display === 'contents' ? '' : '\\n';
110
+ text += apart;
111
+ const own = node.textContent || '';
112
+ if (composed(node)) nodesOf(node).forEach((child) => read(child, style.visibility === 'visible'));
113
+ else if (own.length > limit - text.length) text += own.slice(0, limit - text.length);
114
+ else text += typeof node.innerText === 'string' ? node.innerText : own;
115
+ text += apart;
116
+ };
117
+ const visible = view.getComputedStyle(el).visibility === 'visible';
118
+ nodesOf(el).forEach((child) => read(child, visible));
119
+ return text.slice(0, limit);
120
+ }`;
39
121
  /**
40
122
  * Page-side text of an element as a user sees it: `innerText` for a rendered
41
123
  * element (CSS-hidden parts left out, inline elements not split apart), none
42
124
  * for an element that is not rendered, `textContent` for SVG and other
43
125
  * elements without `innerText` and for `display: contents` wrappers (no box
44
126
  * of their own, but their children are shown), and the label of an
45
- * `<option>` (which its `<select>` renders). For large containers (more
46
- * than 2000 characters of text) only the start is read, from the text nodes
47
- * whose parent is rendered, so a preview never lays out a whole page's text,
48
- * unless `full` is set. Decorations are left out ({@link WITHOUT_DECORATIONS_JS}).
127
+ * `<option>` (which its `<select>` renders). A web component, a slot, and
128
+ * an element holding either are read through the flat tree
129
+ * ({@link FLAT_TEXT_JS}): the text a component renders from its shadow
130
+ * root, slotted content in place of its slots, blocks set apart. For large
131
+ * containers (more than 2000 characters of text) only the start is read,
132
+ * from the text nodes whose parent is rendered, so a preview never lays out
133
+ * a whole page's text, unless `full` is set. Decorations are left out
134
+ * ({@link WITHOUT_DECORATIONS_JS}).
49
135
  */
50
136
  export const ELEMENT_TEXT_JS = `(el, full) => {
51
137
  const withoutDecorations = ${WITHOUT_DECORATIONS_JS};
138
+ const composed = ${COMPOSED_JS};
139
+ const flatText = ${FLAT_TEXT_JS};
52
140
  const all = el.textContent || '';
53
141
  if (el.tagName === 'OPTION') return el.label;
54
142
  if (typeof el.innerText !== 'string') return full ? all : all.slice(0, 2000);
55
143
  const rendered = (node) => !node.checkVisibility || node.checkVisibility();
56
- if (!rendered(el)) {
57
- const boxless = el.ownerDocument.defaultView.getComputedStyle(el).display === 'contents';
58
- return boxless ? withoutDecorations(el, full ? all : all.slice(0, 2000)) : '';
59
- }
144
+ const shown = rendered(el);
145
+ const boxless = !shown && el.ownerDocument.defaultView.getComputedStyle(el).display === 'contents';
146
+ if (!shown && !boxless) return '';
147
+ if (composed(el)) return withoutDecorations(el, flatText(el, full ? Infinity : 2000));
148
+ if (boxless) return withoutDecorations(el, full ? all : all.slice(0, 2000));
60
149
  if (full || all.length <= 2000) return withoutDecorations(el, el.innerText);
61
150
  const walker = el.ownerDocument.createTreeWalker(el, NodeFilter.SHOW_TEXT);
62
151
  let start = '';
@@ -132,12 +221,38 @@ export const ELEMENT_STATE_JS = `(el) => {
132
221
  }
133
222
  }`;
134
223
  /**
135
- * Page-side short description of an element: tag, id and up to two classes,
136
- * e.g. `button#save.primary.large`.
224
+ * Page-side short description of an element: tag, id and up to two classes
225
+ * ({@link LABEL_CLASSES_JS}), e.g. `button#save.primary.large`.
137
226
  */
138
- export const ELEMENT_DESCRIPTION_JS = `(node) => node.tagName.toLowerCase() +
139
- (node.id ? '#' + node.id : '') +
140
- (node.classList && node.classList.length ? '.' + Array.from(node.classList).slice(0, 2).join('.') : '')`;
227
+ export const ELEMENT_DESCRIPTION_JS = `(node) => {
228
+ const classes = (${LABEL_CLASSES_JS})(node);
229
+ return node.tagName.toLowerCase() + (node.id ? '#' + node.id : '') + (classes[0] ? '.' + classes[0] : '') + (classes[1] ? '.' + classes[1] : '');
230
+ }`;
231
+ /**
232
+ * Page-side reason an element is disabled, or null when it is not. A native
233
+ * control (`button`, `input`, `select`, `textarea`, `fieldset`, `optgroup`,
234
+ * `option`) is disabled by its `disabled attribute` or by being `inside a
235
+ * disabled <fieldset>` (not in its first `<legend>`), an `<option>` by
236
+ * being `inside a disabled <optgroup>`; any other element
237
+ * (e.g. a custom element) when its `disabled` property is set (empty
238
+ * reason). Reads the element's own attributes rather than
239
+ * `matches(':disabled')`, which pages replace.
240
+ */
241
+ export const DISABLED_CAUSE_JS = `(el) => {
242
+ if (!/^(button|input|select|textarea|fieldset|optgroup|option)$/.test(el.localName)) return el.disabled ? '' : null;
243
+ if (el.hasAttribute('disabled')) return 'disabled attribute';
244
+ if (el.localName === 'option') {
245
+ const group = el.parentElement;
246
+ return group && group.localName === 'optgroup' && group.hasAttribute('disabled') ? 'inside a disabled <optgroup>' : null;
247
+ }
248
+ for (let node = el.parentElement; node; node = node.parentElement) {
249
+ if (node.localName !== 'fieldset' || !node.hasAttribute('disabled')) continue;
250
+ let legend = node.firstElementChild;
251
+ while (legend && legend.localName !== 'legend') legend = legend.nextElementSibling;
252
+ if (!legend || !legend.contains(el)) return 'inside a disabled <fieldset>';
253
+ }
254
+ return null;
255
+ }`;
141
256
  /**
142
257
  * Page-side position of an element among its parent's children with the
143
258
  * same short description ({@link ELEMENT_DESCRIPTION_JS}), e.g. `(2nd of 3)`;
@@ -159,12 +274,15 @@ export const SIBLING_POSITION_JS = `(el) => {
159
274
  * Page-side identity of an element an action hit, so the output says which
160
275
  * one it was, as one string: its short description
161
276
  * ({@link ELEMENT_DESCRIPTION_JS}) and visible text (button value for button
162
- * inputs, decorations left out), e.g. `button#add.btn "Add to cart"`. A
163
- * `<select>` is named by its label, aria-label or name, else by its selected
277
+ * inputs, decorations left out; read through the flat tree for web
278
+ * components and slots, {@link FLAT_TEXT_JS}), e.g.
279
+ * `button#add.btn "Add to cart"`. A `<select>` is named by its label
280
+ * (`aria-labelledby` or `<label>`), aria-label or name, else by its selected
164
281
  * option, e.g. `select.sort "Sort products"`. An element without visible
165
282
  * text is described by itself first: by its position among same-looking
166
283
  * siblings ({@link SIBLING_POSITION_JS}), e.g. `div.figure (2nd of 3)`, with
167
- * its aria-label, placeholder or title when it has one. Only an element
284
+ * its name when it has one: aria-label, label, placeholder, title, or the
285
+ * alt text of an image (in it). Only an element
168
286
  * without an id that is the only one of its kind is named by the nearest of
169
287
  * three ancestors that has text, e.g.
170
288
  * `input.toggle in div.view "Write report"` (rows of a list share their
@@ -172,7 +290,7 @@ export const SIBLING_POSITION_JS = `(el) => {
172
290
  * of `<select>`s in it (read like `innerText`: CSS-hidden text is left out of
173
291
  * a rendered ancestor; at most 500 text nodes; selects in shadow roots are
174
292
  * not looked into), and never past an editable ancestor, whose text may be
175
- * typed input; otherwise its aria-label, placeholder or title is used. Texts
293
+ * typed input; otherwise its name is used. Texts
176
294
  * are cut at 40 characters. Contenteditable elements count as controls: what
177
295
  * was typed into them is never echoed (nor their ancestors' text).
178
296
  */
@@ -180,19 +298,36 @@ export const ELEMENT_IDENTITY_JS = `(el) => {
180
298
  const describe = ${ELEMENT_DESCRIPTION_JS};
181
299
  const siblingPosition = ${SIBLING_POSITION_JS};
182
300
  const withoutDecorations = ${WITHOUT_DECORATIONS_JS};
301
+ const composed = ${COMPOSED_JS};
302
+ const flatText = ${FLAT_TEXT_JS};
183
303
  const clean = (text) => (text || '').replace(/\\s+/g, ' ').trim();
184
304
  const cut = (text) => {
185
305
  const characters = Array.from(text);
186
306
  return characters.length > 40 ? characters.slice(0, 40).join('') + '…' : text;
187
307
  };
188
308
  const isControl = (node) => /^(input|select|textarea)$/.test(node.localName) || node.isContentEditable;
189
- const shownText = (node) =>
190
- isControl(node) ? '' : clean(withoutDecorations(node, typeof node.innerText === 'string' ? node.innerText : node.textContent));
309
+ const textOf = (node) =>
310
+ composed(node) ? flatText(node, 200) : typeof node.innerText === 'string' ? node.innerText : node.textContent;
311
+ const shownText = (node) => (isControl(node) ? '' : clean(withoutDecorations(node, textOf(node))));
191
312
  const buttonValue = (node) => (node.localName === 'input' && /^(submit|button|reset)$/i.test(node.type) ? clean(node.value) : '');
313
+ const labelText = (node) => {
314
+ const ids = (node.getAttribute('aria-labelledby') || '').split(/\\s+/).filter(Boolean);
315
+ const root = node.getRootNode();
316
+ const labels = ids.length ? ids.map((id) => root.getElementById(id)) : Array.from(node.labels || []);
317
+ return clean(labels.filter(Boolean).map(textOf).join(' '));
318
+ };
319
+ const imageAlt = (node) => {
320
+ const image = node.localName === 'img' ? node : node.querySelector('img[alt]');
321
+ return image ? clean(image.getAttribute('alt')) : '';
322
+ };
192
323
  const attributeText = (node) =>
193
- clean(node.getAttribute('aria-label')) || clean(node.getAttribute('placeholder')) || clean(node.getAttribute('title'));
324
+ clean(node.getAttribute('aria-label')) ||
325
+ labelText(node) ||
326
+ clean(node.getAttribute('placeholder')) ||
327
+ clean(node.getAttribute('title')) ||
328
+ imageAlt(node);
194
329
  const selectName = (node) =>
195
- clean(node.labels && node.labels[0] && node.labels[0].innerText) ||
330
+ labelText(node) ||
196
331
  clean(node.getAttribute('aria-label')) ||
197
332
  clean(node.getAttribute('name')) ||
198
333
  clean(node.selectedOptions && node.selectedOptions[0] && node.selectedOptions[0].label);
@@ -225,27 +360,42 @@ export const ELEMENT_IDENTITY_JS = `(el) => {
225
360
  }`;
226
361
  /**
227
362
  * Page-side location of an element: the iframes it is in (outermost first)
228
- * and the shadow root holding it, with the host's first class and light-DOM
229
- * text, e.g. `iframe#pay > shadow root of <sl-button.primary "Save">`;
230
- * empty for the main document.
363
+ * and the shadow root holding it, with the host's first class and the text
364
+ * a user sees of it ({@link ELEMENT_TEXT_JS}, cut at 30 characters with
365
+ * `…`), e.g. `iframe#pay > shadow root of <sl-button.primary "Save">`;
366
+ * empty for the main document. Each host's text is read once per script
367
+ * run, as many matches share a host.
231
368
  */
232
- export const ELEMENT_CONTEXT_JS = `(el) => {
369
+ export const ELEMENT_CONTEXT_JS = `(() => {
370
+ const textOf = ${ELEMENT_TEXT_JS};
371
+ const hostTexts = new WeakMap();
372
+ const hostText = (host) => {
373
+ if (!hostTexts.has(host)) {
374
+ const characters = Array.from(String(textOf(host, false)).replace(/\\s+/g, ' ').trim());
375
+ hostTexts.set(host, characters.length > 30 ? characters.slice(0, 30).join('') + '…' : characters.join(''));
376
+ }
377
+ return hostTexts.get(host);
378
+ };
233
379
  const describe = (node) => node.tagName.toLowerCase() + (node.id ? '#' + node.id : '');
234
- const parts = [];
235
- for (let doc = el.ownerDocument; doc && doc.defaultView && doc.defaultView.frameElement; ) {
236
- const frame = doc.defaultView.frameElement;
237
- parts.unshift(describe(frame));
238
- doc = frame.ownerDocument;
239
- }
240
- const root = el.getRootNode();
241
- if (root.host) {
242
- const host = root.host;
243
- const label = describe(host) + (!host.id && host.classList.length ? '.' + host.classList[0] : '');
244
- const text = (host.textContent || '').replace(/\\s+/g, ' ').trim().slice(0, 30);
245
- parts.push('shadow root of <' + label + (text ? ' "' + text + '"' : '') + '>');
246
- }
247
- return parts.join(' > ');
248
- }`;
380
+ const labelClasses = ${LABEL_CLASSES_JS};
381
+ return (el) => {
382
+ const parts = [];
383
+ for (let doc = el.ownerDocument; doc && doc.defaultView && doc.defaultView.frameElement; ) {
384
+ const frame = doc.defaultView.frameElement;
385
+ parts.unshift(describe(frame));
386
+ doc = frame.ownerDocument;
387
+ }
388
+ const root = el.getRootNode();
389
+ if (root.host) {
390
+ const host = root.host;
391
+ const firstClass = labelClasses(host)[0];
392
+ const label = describe(host) + (!host.id && firstClass ? '.' + firstClass : '');
393
+ const text = hostText(host);
394
+ parts.push('shadow root of <' + label + (text ? ' "' + text + '"' : '') + '>');
395
+ }
396
+ return parts.join(' > ');
397
+ };
398
+ })()`;
249
399
  /**
250
400
  * Short text preview of an element's text: whitespace collapsed, cut on a
251
401
  * whole character (an emoji is never split, which would make JSON invalid).
@@ -11,6 +11,13 @@ import { type CDPSender } from '../../telemetry/objectExpander.js';
11
11
  * @throws Error When script execution throws exception or returns invalid response
12
12
  */
13
13
  export declare function executeScript(cdp: CDPSender, script: string, options?: Omit<Protocol.Runtime.EvaluateRequest, 'expression'>): Promise<Protocol.Runtime.EvaluateResponse>;
14
+ /**
15
+ * Message of an exception, including thrown non-Error values (`throw "x"`).
16
+ *
17
+ * @param details - Exception details
18
+ * @returns Readable message
19
+ */
20
+ export declare function describeException(details: Protocol.Runtime.ExceptionDetails): string;
14
21
  /** Result of `bdg dom eval`: a JSON-safe value plus its JavaScript type. */
15
22
  export interface EvalResult {
16
23
  /** JSON value, or a readable description for values JSON cannot represent */
@@ -19,6 +26,8 @@ export interface EvalResult {
19
26
  type: string;
20
27
  /** Object subtype (`node`, `date`, `map`, `array`, ...) */
21
28
  subtype?: string;
29
+ /** Set when the page replaced built-ins bdg's copy of the result uses, so the browser copied it */
30
+ warning?: string;
22
31
  }
23
32
  /**
24
33
  * Page-side copy of an object as JSON, keeping what JSON would lose inside
@@ -28,9 +37,10 @@ export interface EvalResult {
28
37
  * `12n`, functions `function name()`, and cycles `[Circular]` (an object
29
38
  * shared by two properties is copied twice). Works for objects of iframes
30
39
  * (other realms); lists and objects are cut after 1000 entries, and a
31
- * throwing getter becomes `[Error: …]`.
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.
32
42
  */
33
- 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 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}";
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}";
34
44
  /** What a busy target is called in messages: the page, or an iframe (`--frame`) */
35
45
  export type BusyScope = 'page' | 'frame';
36
46
  /** When {@link withBusyPageRecovery} checks the target, and what it calls it */
@@ -2,8 +2,10 @@ import { CDPProtocolError } from '../../connection/errors.js';
2
2
  import { CommandError } from '../../errors/index.js';
3
3
  import { navigationPendingError, pageBusyError, pageClosedDuringEvalError, pageNavigatedDuringEvalError, promiseTimeoutError, scriptExecutionError, scriptTimeoutError, } from '../../errors/messages.js';
4
4
  import { pendingNavigationUrl } from '../page/navigation.js';
5
+ import { findReplacedBuiltins } from '../page/replacedBuiltins.js';
5
6
  import { senderFor } from '../../telemetry/objectExpander.js';
6
7
  import { formatRemoteObject } from '../../telemetry/remoteObject.js';
8
+ import { evalCopiedByBrowserWarning, evalPreviewWarning } from '../../ui/messages/commands.js';
7
9
  import { createLogger } from '../../ui/logging/index.js';
8
10
  import { getErrorMessage } from '../../utils/errors.js';
9
11
  import { EXIT_CODES } from '../../utils/exitCodes.js';
@@ -121,7 +123,7 @@ async function settlePromise(cdp, result, script) {
121
123
  * @param details - Exception details
122
124
  * @returns Readable message
123
125
  */
124
- function describeException(details) {
126
+ export function describeException(details) {
125
127
  const exception = details.exception;
126
128
  if (exception?.type === 'object' && exception.subtype !== 'error' && exception.preview) {
127
129
  return `Uncaught ${formatRemoteObject(exception)}`;
@@ -164,7 +166,8 @@ const BRIEF_SUBTYPES = new Set(['node', 'arraybuffer', 'dataview']);
164
166
  * `12n`, functions `function name()`, and cycles `[Circular]` (an object
165
167
  * shared by two properties is copied twice). Works for objects of iframes
166
168
  * (other realms); lists and objects are cut after 1000 entries, and a
167
- * throwing getter becomes `[Error: …]`.
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.
168
171
  */
169
172
  export const JSON_SAFE_COPY_FUNCTION = `function () {
170
173
  const MAX_ITEMS = 1000;
@@ -178,6 +181,13 @@ export const JSON_SAFE_COPY_FUNCTION = `function () {
178
181
  };
179
182
  const items = (list, next) => {
180
183
  const result = [];
184
+ if (typeof list.length === 'number') {
185
+ for (let i = 0; i < list.length; i++) {
186
+ if (i === MAX_ITEMS) { result.push('…'); break; }
187
+ result.push(next(list[i]));
188
+ }
189
+ return result;
190
+ }
181
191
  let count = 0;
182
192
  for (const item of list) {
183
193
  if (count++ === MAX_ITEMS) { result.push('…'); break; }
@@ -226,6 +236,33 @@ export const JSON_SAFE_COPY_FUNCTION = `function () {
226
236
  };
227
237
  return copy(this, 0);
228
238
  }`;
239
+ /** Built-ins {@link JSON_SAFE_COPY_FUNCTION} uses (it runs in the page's world, next to the result) */
240
+ const COPY_BUILTINS = [
241
+ 'Object.keys',
242
+ 'Object.is',
243
+ 'Object.prototype.toString',
244
+ 'Function.prototype.call',
245
+ 'Array.from',
246
+ 'Array.isArray',
247
+ 'Array.prototype.push',
248
+ 'Array.prototype.slice',
249
+ 'Array.prototype.join',
250
+ 'ArrayBuffer.isView',
251
+ 'Number.isNaN',
252
+ 'Number.isFinite',
253
+ 'String',
254
+ 'String.prototype.slice',
255
+ 'String.prototype.toLowerCase',
256
+ 'Set',
257
+ 'Set.prototype.add',
258
+ 'Set.prototype.has',
259
+ 'Set.prototype.delete',
260
+ 'Set.prototype.values',
261
+ 'Map.prototype.entries',
262
+ 'Date.prototype.toISOString',
263
+ ];
264
+ /** Returns the object it is called on (for the browser to copy it by value) */
265
+ const SELF_FUNCTION = 'function () { return this; }';
229
266
  /**
230
267
  * Whether an exception is V8 terminating a script that ran too long.
231
268
  *
@@ -240,7 +277,10 @@ function isTerminated(details) {
240
277
  *
241
278
  * Plain objects and arrays are copied by value; values JSON cannot represent
242
279
  * (NaN, -0, BigInt, functions, symbols, DOM nodes, dates, maps, errors, the
243
- * window, cyclic objects) are given as their readable description.
280
+ * window, cyclic objects) are given as their readable description. On a
281
+ * page that replaced built-ins that copy uses, the browser copies the
282
+ * object (JSON-like values exactly, the others as null or `{}`), or else
283
+ * its preview is given, with a warning naming them.
244
284
  *
245
285
  * @param cdp - CDP connection
246
286
  * @param remote - Remote object returned by Runtime.evaluate
@@ -260,19 +300,39 @@ async function toEvalResult(cdp, remote) {
260
300
  if (remote.type !== 'object' || DESCRIBED_SUBTYPES.has(remote.subtype ?? '')) {
261
301
  return { value: formatRemoteObject(remote), ...kind };
262
302
  }
303
+ 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);
305
+ const value = copy ? copy.value : formatRemoteObject(remote);
306
+ if (replaced.length === 0)
307
+ return { value, ...kind };
308
+ const warning = copy ? evalCopiedByBrowserWarning(replaced) : evalPreviewWarning(replaced);
309
+ return { value, ...kind, warning };
310
+ }
311
+ /**
312
+ * Copy a result by value: what a function called on it returns, which the
313
+ * browser serialises itself (without the page's built-ins).
314
+ *
315
+ * @param cdp - CDP connection
316
+ * @param objectId - The result
317
+ * @param functionDeclaration - {@link JSON_SAFE_COPY_FUNCTION}, or
318
+ * {@link SELF_FUNCTION} for the browser's plain copy (it fails on cycles
319
+ * and BigInts)
320
+ * @returns The copy, or undefined when it failed
321
+ */
322
+ async function copyByValue(cdp, objectId, functionDeclaration) {
263
323
  try {
264
324
  const copy = (await cdp.send('Runtime.callFunctionOn', {
265
- objectId: remote.objectId,
266
- functionDeclaration: JSON_SAFE_COPY_FUNCTION,
325
+ objectId,
326
+ functionDeclaration,
267
327
  returnByValue: true,
268
328
  }));
269
329
  if (!copy.exceptionDetails)
270
- return { value: copy.result?.value, ...kind };
330
+ return { value: copy.result?.value };
271
331
  }
272
332
  catch (error) {
273
333
  log.debug(`Could not copy eval result by value: ${getErrorMessage(error)}`);
274
334
  }
275
- return { value: formatRemoteObject(remote), ...kind };
335
+ return undefined;
276
336
  }
277
337
  /**
278
338
  * Wait for `work`, or fail when it takes longer than `ms`.