browser-debugger-cli 0.12.0 → 0.13.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 (180) hide show
  1. package/.claude/skills/bdg/SKILL.md +4 -4
  2. package/README.md +1 -0
  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 +62 -12
  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.js +3 -2
  10. package/dist/commands/dom/eval.d.ts +3 -2
  11. package/dist/commands/dom/eval.js +11 -5
  12. package/dist/commands/dom/form.js +10 -9
  13. package/dist/commands/dom/formInteraction.js +8 -7
  14. package/dist/commands/dom/get.js +8 -8
  15. package/dist/commands/dom/helpers/index.d.ts +1 -1
  16. package/dist/commands/dom/helpers/index.js +1 -1
  17. package/dist/commands/dom/helpers/query.d.ts +27 -3
  18. package/dist/commands/dom/helpers/query.js +152 -64
  19. package/dist/commands/dom/helpers/screenshot.js +13 -13
  20. package/dist/commands/dom/index.js +4 -2
  21. package/dist/commands/dom/query.d.ts +19 -2
  22. package/dist/commands/dom/query.js +37 -6
  23. package/dist/commands/dom/screenshot.js +2 -1
  24. package/dist/commands/dom/semanticUtils.d.ts +3 -2
  25. package/dist/commands/dom/semanticUtils.js +40 -9
  26. package/dist/commands/helpJson.d.ts +82 -19
  27. package/dist/commands/helpJson.js +111 -40
  28. package/dist/commands/helpTopic.d.ts +16 -1
  29. package/dist/commands/helpTopic.js +59 -1
  30. package/dist/commands/installSkill.d.ts +15 -5
  31. package/dist/commands/installSkill.js +86 -16
  32. package/dist/commands/network/list.js +22 -12
  33. package/dist/commands/optionBehaviors.js +33 -11
  34. package/dist/commands/shared/daemonErrorHandler.d.ts +5 -2
  35. package/dist/commands/shared/daemonErrorHandler.js +20 -9
  36. package/dist/commands/shared/dataFetcher.d.ts +12 -4
  37. package/dist/commands/shared/dataFetcher.js +12 -4
  38. package/dist/commands/shared/followMode.d.ts +9 -1
  39. package/dist/commands/shared/followMode.js +22 -4
  40. package/dist/commands/shared/optionTypes.d.ts +4 -1
  41. package/dist/commands/shared/outputFile.js +6 -1
  42. package/dist/commands/start.d.ts +7 -5
  43. package/dist/commands/start.js +65 -21
  44. package/dist/commands/stop.d.ts +11 -0
  45. package/dist/commands/stop.js +24 -1
  46. package/dist/commands.js +1 -1
  47. package/dist/connection/cdp.d.ts +7 -0
  48. package/dist/connection/cdp.js +9 -0
  49. package/dist/connection/launcher.js +3 -2
  50. package/dist/daemon/SessionController.js +6 -1
  51. package/dist/daemon/launcher.d.ts +3 -2
  52. package/dist/daemon/launcher.js +47 -3
  53. package/dist/daemon/session/Session.d.ts +4 -1
  54. package/dist/daemon/session/Session.js +33 -2
  55. package/dist/daemon/session/TelemetryStore.d.ts +8 -1
  56. package/dist/daemon/session/TelemetryStore.js +13 -1
  57. package/dist/daemon/session/commandRegistry.js +29 -13
  58. package/dist/daemon/session/interactions.d.ts +2 -1
  59. package/dist/daemon/session/interactions.js +13 -1
  60. package/dist/daemon/session/plugins.js +16 -2
  61. package/dist/daemon/session/teardown.js +1 -1
  62. package/dist/daemon.js +1622 -748
  63. package/dist/errors/messages.d.ts +54 -11
  64. package/dist/errors/messages.js +109 -22
  65. package/dist/index.js +13733 -8796
  66. package/dist/ipc/client.d.ts +18 -2
  67. package/dist/ipc/client.js +26 -5
  68. package/dist/ipc/protocol/auditTypes.d.ts +8 -2
  69. package/dist/ipc/protocol/commands.d.ts +12 -0
  70. package/dist/ipc/protocol/domTypes.d.ts +12 -0
  71. package/dist/ipc/protocol/inspectTypes.d.ts +2 -0
  72. package/dist/ipc/session/types.d.ts +2 -0
  73. package/dist/runtime/dom/actionEffects.d.ts +5 -1
  74. package/dist/runtime/dom/actionEffects.js +26 -14
  75. package/dist/runtime/dom/audit.js +3 -2
  76. package/dist/runtime/dom/auditModel.js +6 -1
  77. package/dist/runtime/dom/auditScripts.d.ts +9 -3
  78. package/dist/runtime/dom/auditScripts.js +41 -5
  79. package/dist/runtime/dom/elementGeometry.d.ts +10 -3
  80. package/dist/runtime/dom/elementGeometry.js +27 -4
  81. package/dist/runtime/dom/elementInfo.d.ts +74 -18
  82. package/dist/runtime/dom/elementInfo.js +187 -40
  83. package/dist/runtime/dom/evalHelpers.d.ts +12 -2
  84. package/dist/runtime/dom/evalHelpers.js +67 -7
  85. package/dist/runtime/dom/formDiscovery.d.ts +6 -2
  86. package/dist/runtime/dom/formDiscovery.js +20 -3
  87. package/dist/runtime/dom/formFillHelpers/fill.js +7 -11
  88. package/dist/runtime/dom/formFillHelpers/pressKey.js +2 -2
  89. package/dist/runtime/dom/formFillHelpers/shared.d.ts +16 -10
  90. package/dist/runtime/dom/formFillHelpers/shared.js +19 -52
  91. package/dist/runtime/dom/formSubmitHelpers.js +4 -3
  92. package/dist/runtime/dom/frameLayout.js +1 -0
  93. package/dist/runtime/dom/inspect.js +5 -6
  94. package/dist/runtime/dom/inspectAllStyles.js +1 -0
  95. package/dist/runtime/dom/inspectHints.d.ts +1 -1
  96. package/dist/runtime/dom/inspectModel.d.ts +2 -1
  97. package/dist/runtime/dom/inspectModel.js +7 -3
  98. package/dist/runtime/dom/inspectPaintModel.d.ts +2 -0
  99. package/dist/runtime/dom/inspectPaintModel.js +3 -1
  100. package/dist/runtime/dom/inspectScripts.d.ts +29 -2
  101. package/dist/runtime/dom/inspectScripts.js +49 -10
  102. package/dist/runtime/dom/layout.js +9 -7
  103. package/dist/runtime/dom/reactEventHelpers.d.ts +14 -4
  104. package/dist/runtime/dom/reactEventHelpers.js +63 -27
  105. package/dist/runtime/dom/targetNode.d.ts +18 -5
  106. package/dist/runtime/dom/targetNode.js +268 -8
  107. package/dist/runtime/dom/wait.js +2 -1
  108. package/dist/runtime/page/bdgWorld.d.ts +57 -0
  109. package/dist/runtime/page/bdgWorld.js +180 -0
  110. package/dist/runtime/page/replacedBuiltins.d.ts +28 -0
  111. package/dist/runtime/page/replacedBuiltins.js +136 -0
  112. package/dist/session/QueryCacheManager.d.ts +4 -1
  113. package/dist/session/QueryCacheManager.js +5 -2
  114. package/dist/session/chrome.d.ts +4 -1
  115. package/dist/session/chrome.js +7 -1
  116. package/dist/session/cleanup/staleSession.d.ts +21 -4
  117. package/dist/session/cleanup/staleSession.js +79 -9
  118. package/dist/session/cleanup/userCommands.d.ts +4 -1
  119. package/dist/session/cleanup/userCommands.js +10 -5
  120. package/dist/session/daemonSocket.d.ts +10 -0
  121. package/dist/session/daemonSocket.js +22 -0
  122. package/dist/session/lastSession.d.ts +6 -3
  123. package/dist/session/lastSession.js +11 -5
  124. package/dist/session/paths.d.ts +3 -1
  125. package/dist/session/paths.js +5 -5
  126. package/dist/session/portClaims.js +4 -3
  127. package/dist/session/sessionList.d.ts +13 -5
  128. package/dist/session/sessionList.js +31 -7
  129. package/dist/telemetry/a11y.js +2 -2
  130. package/dist/telemetry/console.d.ts +2 -1
  131. package/dist/telemetry/console.js +30 -21
  132. package/dist/telemetry/pageCrash.d.ts +26 -0
  133. package/dist/telemetry/pageCrash.js +53 -0
  134. package/dist/types.d.ts +16 -0
  135. package/dist/ui/formatters/audit.js +14 -5
  136. package/dist/ui/formatters/cdp.d.ts +138 -0
  137. package/dist/ui/formatters/cdp.js +131 -0
  138. package/dist/ui/formatters/console/chronological.js +3 -1
  139. package/dist/ui/formatters/console/follow.d.ts +2 -1
  140. package/dist/ui/formatters/console/follow.js +2 -2
  141. package/dist/ui/formatters/console/json.d.ts +2 -2
  142. package/dist/ui/formatters/console/json.js +11 -5
  143. package/dist/ui/formatters/console/shared.d.ts +30 -0
  144. package/dist/ui/formatters/console/shared.js +16 -0
  145. package/dist/ui/formatters/console/summarize.d.ts +9 -2
  146. package/dist/ui/formatters/console/summarize.js +40 -9
  147. package/dist/ui/formatters/console.d.ts +2 -1
  148. package/dist/ui/formatters/console.js +7 -5
  149. package/dist/ui/formatters/details.js +3 -1
  150. package/dist/ui/formatters/dom.d.ts +1 -1
  151. package/dist/ui/formatters/dom.js +5 -6
  152. package/dist/ui/formatters/helpFormatters.js +1 -1
  153. package/dist/ui/formatters/inspect.js +9 -3
  154. package/dist/ui/formatters/installSkill.d.ts +9 -1
  155. package/dist/ui/formatters/installSkill.js +32 -6
  156. package/dist/ui/formatters/layout.js +2 -1
  157. package/dist/ui/formatters/networkList.d.ts +1 -1
  158. package/dist/ui/formatters/networkList.js +1 -2
  159. package/dist/ui/formatters/preview.d.ts +2 -0
  160. package/dist/ui/formatters/preview.js +17 -7
  161. package/dist/ui/formatters/sessions.d.ts +2 -2
  162. package/dist/ui/formatters/sessions.js +9 -2
  163. package/dist/ui/logging/logger.d.ts +1 -1
  164. package/dist/ui/messages/commands.d.ts +124 -4
  165. package/dist/ui/messages/commands.js +162 -7
  166. package/dist/ui/messages/consoleMessages.d.ts +24 -0
  167. package/dist/ui/messages/consoleMessages.js +32 -0
  168. package/dist/ui/messages/preview.d.ts +6 -0
  169. package/dist/ui/messages/preview.js +9 -1
  170. package/dist/ui/messages/session.d.ts +13 -2
  171. package/dist/ui/messages/session.js +22 -3
  172. package/dist/utils/directories.d.ts +34 -0
  173. package/dist/utils/directories.js +88 -0
  174. package/dist/utils/display.d.ts +16 -0
  175. package/dist/utils/display.js +42 -0
  176. package/dist/utils/exitCodes.d.ts +1 -0
  177. package/dist/utils/exitCodes.js +6 -0
  178. package/dist/utils/process.d.ts +12 -0
  179. package/dist/utils/process.js +25 -0
  180. package/package.json +1 -1
@@ -3,6 +3,19 @@
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
+ * Whether a class name is shown in element labels: not empty and not a
8
+ * fragment of a declaration-like class attribute ({@link CLASS_FRAGMENT}).
9
+ *
10
+ * @param name - Class name
11
+ * @returns True to show it
12
+ */
13
+ export declare function isLabelClass(name: string): boolean;
14
+ /**
15
+ * Page-side `(node) => string[]`: the element's classes shown in labels
16
+ * ({@link isLabelClass}), read without array helpers a page may replace
17
+ */
18
+ export declare const LABEL_CLASSES_JS: string;
6
19
  /** Length of the text `dom get` shows for an element. */
7
20
  export declare const ELEMENT_TEXT_LENGTH = 500;
8
21
  /**
@@ -14,18 +27,45 @@ export declare const ELEMENT_TEXT_LENGTH = 500;
14
27
  * text is removed once, from the end.
15
28
  */
16
29
  export declare const WITHOUT_DECORATIONS_JS = "(el, text) => {\n if (!text || typeof el.querySelectorAll !== 'function') return text;\n const glyph = /^\\s*[\u00D7\u2715\u2716\u2717\u2A2F]\\s*$/;\n const textOf = (node) => (typeof node.innerText === 'string' ? node.innerText : node.textContent || '').trim();\n const closer = '.close, [aria-label=\"close\" i], [aria-label=\"dismiss\" i]';\n const rendered = (node) => !node.checkVisibility || node.checkVisibility();\n const found = Array.from(el.querySelectorAll(closer + ', [aria-hidden=\"true\"]'))\n .filter((node) => rendered(node) && (node.matches(closer) || !/[\\p{L}\\p{N}]/u.test(textOf(node))));\n if (/[\u00D7\u2715\u2716\u2717\u2A2F]/.test(text)) {\n found.push(...Array.from(el.querySelectorAll('button, a, [role=\"button\"]')).filter((node) => glyph.test(node.textContent || '')));\n }\n const unique = Array.from(new Set(found));\n const outermost = unique.filter((node) => !unique.some((other) => other !== node && other.contains(node)));\n let result = text;\n for (const node of outermost) {\n const part = textOf(node);\n const at = part ? result.lastIndexOf(part) : -1;\n if (at >= 0) result = result.slice(0, at) + result.slice(at + part.length);\n }\n return result;\n}";
30
+ /**
31
+ * Page-side check whether `innerText` misses part of what an element shows:
32
+ * it is a `<slot>` or hosts an open shadow root, or one of its descendants
33
+ * is or does. `innerText` follows neither shadow roots nor slots.
34
+ */
35
+ export declare const COMPOSED_JS = "(node) => {\n const composes = (n) => Boolean(n.shadowRoot) || n.localName === 'slot';\n if (composes(node)) return true;\n const walker = node.ownerDocument.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);\n while (walker.nextNode()) if (composes(walker.currentNode)) return true;\n return false;\n}";
36
+ /**
37
+ * Page-side text of an element as the flat tree renders it, for elements
38
+ * `innerText` cannot read ({@link COMPOSED_JS}): a shadow host is read from
39
+ * its open shadow root (its own labels and fallback content; light children
40
+ * only where a slot shows them), and each slot is replaced by what it
41
+ * shows, its assigned nodes flattened through nested slots (or its fallback
42
+ * content when nothing is assigned). Parts without slots or shadow roots
43
+ * are read with `innerText`; elements that are not rendered and text under
44
+ * `visibility: hidden` are left out, elements that are not inline are set
45
+ * apart by line breaks, as is a `<br>`. Fields and editable regions inside
46
+ * it (inputs, textareas, selects, a contenteditable editor) are skipped, so
47
+ * what a user typed is never read; raw text keeps `innerText`'s collapsed
48
+ * whitespace. The text is cut at `limit` characters, and a
49
+ * part whose text alone passes the limit is read from its text nodes
50
+ * (`textContent`) instead of `innerText`, which would lay out all of it.
51
+ */
52
+ export declare const FLAT_TEXT_JS = "(el, limit) => {\n const composed = (node) => {\n const composes = (n) => Boolean(n.shadowRoot) || n.localName === 'slot';\n if (composes(node)) return true;\n const walker = node.ownerDocument.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);\n while (walker.nextNode()) if (composes(walker.currentNode)) return true;\n return false;\n};\n const view = el.ownerDocument.defaultView;\n let text = '';\n const nodesOf = (node) =>\n node.localName === 'slot' ? node.assignedNodes({ flatten: true }) : Array.from((node.shadowRoot || node).childNodes);\n const read = (node, visible) => {\n if (text.length >= limit) return;\n if (node.nodeType === 3 && visible) text += node.data.replace(/\\s+/g, ' ');\n if (node.nodeType !== 1) return;\n if (/^(input|textarea|select)$/.test(node.localName) || node.isContentEditable) return;\n const style = view.getComputedStyle(node);\n if (node.checkVisibility && !node.checkVisibility() && style.display !== 'contents') return;\n if (node.localName === 'br') text += '\\n';\n const apart = style.display.startsWith('inline') || style.display === 'contents' ? '' : '\\n';\n text += apart;\n const own = node.textContent || '';\n if (composed(node)) nodesOf(node).forEach((child) => read(child, style.visibility === 'visible'));\n else if (own.length > limit - text.length) text += own.slice(0, limit - text.length);\n else text += typeof node.innerText === 'string' ? node.innerText : own;\n text += apart;\n };\n const visible = view.getComputedStyle(el).visibility === 'visible';\n nodesOf(el).forEach((child) => read(child, visible));\n return text.slice(0, limit);\n}";
17
53
  /**
18
54
  * Page-side text of an element as a user sees it: `innerText` for a rendered
19
55
  * element (CSS-hidden parts left out, inline elements not split apart), none
20
56
  * for an element that is not rendered, `textContent` for SVG and other
21
57
  * elements without `innerText` and for `display: contents` wrappers (no box
22
58
  * of their own, but their children are shown), and the label of an
23
- * `<option>` (which its `<select>` renders). For large containers (more
24
- * than 2000 characters of text) only the start is read, from the text nodes
25
- * whose parent is rendered, so a preview never lays out a whole page's text,
26
- * unless `full` is set. Decorations are left out ({@link WITHOUT_DECORATIONS_JS}).
59
+ * `<option>` (which its `<select>` renders). A web component, a slot, and
60
+ * an element holding either are read through the flat tree
61
+ * ({@link FLAT_TEXT_JS}): the text a component renders from its shadow
62
+ * root, slotted content in place of its slots, blocks set apart. For large
63
+ * containers (more than 2000 characters of text) only the start is read,
64
+ * from the text nodes whose parent is rendered, so a preview never lays out
65
+ * a whole page's text, unless `full` is set. Decorations are left out
66
+ * ({@link WITHOUT_DECORATIONS_JS}).
27
67
  */
28
- export declare const ELEMENT_TEXT_JS = "(el, full) => {\n const withoutDecorations = (el, text) => {\n if (!text || typeof el.querySelectorAll !== 'function') return text;\n const glyph = /^\\s*[\u00D7\u2715\u2716\u2717\u2A2F]\\s*$/;\n const textOf = (node) => (typeof node.innerText === 'string' ? node.innerText : node.textContent || '').trim();\n const closer = '.close, [aria-label=\"close\" i], [aria-label=\"dismiss\" i]';\n const rendered = (node) => !node.checkVisibility || node.checkVisibility();\n const found = Array.from(el.querySelectorAll(closer + ', [aria-hidden=\"true\"]'))\n .filter((node) => rendered(node) && (node.matches(closer) || !/[\\p{L}\\p{N}]/u.test(textOf(node))));\n if (/[\u00D7\u2715\u2716\u2717\u2A2F]/.test(text)) {\n found.push(...Array.from(el.querySelectorAll('button, a, [role=\"button\"]')).filter((node) => glyph.test(node.textContent || '')));\n }\n const unique = Array.from(new Set(found));\n const outermost = unique.filter((node) => !unique.some((other) => other !== node && other.contains(node)));\n let result = text;\n for (const node of outermost) {\n const part = textOf(node);\n const at = part ? result.lastIndexOf(part) : -1;\n if (at >= 0) result = result.slice(0, at) + result.slice(at + part.length);\n }\n return result;\n};\n const all = el.textContent || '';\n if (el.tagName === 'OPTION') return el.label;\n if (typeof el.innerText !== 'string') return full ? all : all.slice(0, 2000);\n const rendered = (node) => !node.checkVisibility || node.checkVisibility();\n if (!rendered(el)) {\n const boxless = el.ownerDocument.defaultView.getComputedStyle(el).display === 'contents';\n return boxless ? withoutDecorations(el, full ? all : all.slice(0, 2000)) : '';\n }\n if (full || all.length <= 2000) return withoutDecorations(el, el.innerText);\n const walker = el.ownerDocument.createTreeWalker(el, NodeFilter.SHOW_TEXT);\n let start = '';\n while (start.length < 1000 && walker.nextNode()) {\n const parent = walker.currentNode.parentElement;\n if (!parent || rendered(parent)) start += walker.currentNode.data;\n }\n return withoutDecorations(el, start);\n}";
68
+ export declare const ELEMENT_TEXT_JS = "(el, full) => {\n const withoutDecorations = (el, text) => {\n if (!text || typeof el.querySelectorAll !== 'function') return text;\n const glyph = /^\\s*[\u00D7\u2715\u2716\u2717\u2A2F]\\s*$/;\n const textOf = (node) => (typeof node.innerText === 'string' ? node.innerText : node.textContent || '').trim();\n const closer = '.close, [aria-label=\"close\" i], [aria-label=\"dismiss\" i]';\n const rendered = (node) => !node.checkVisibility || node.checkVisibility();\n const found = Array.from(el.querySelectorAll(closer + ', [aria-hidden=\"true\"]'))\n .filter((node) => rendered(node) && (node.matches(closer) || !/[\\p{L}\\p{N}]/u.test(textOf(node))));\n if (/[\u00D7\u2715\u2716\u2717\u2A2F]/.test(text)) {\n found.push(...Array.from(el.querySelectorAll('button, a, [role=\"button\"]')).filter((node) => glyph.test(node.textContent || '')));\n }\n const unique = Array.from(new Set(found));\n const outermost = unique.filter((node) => !unique.some((other) => other !== node && other.contains(node)));\n let result = text;\n for (const node of outermost) {\n const part = textOf(node);\n const at = part ? result.lastIndexOf(part) : -1;\n if (at >= 0) result = result.slice(0, at) + result.slice(at + part.length);\n }\n return result;\n};\n const composed = (node) => {\n const composes = (n) => Boolean(n.shadowRoot) || n.localName === 'slot';\n if (composes(node)) return true;\n const walker = node.ownerDocument.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);\n while (walker.nextNode()) if (composes(walker.currentNode)) return true;\n return false;\n};\n const flatText = (el, limit) => {\n const composed = (node) => {\n const composes = (n) => Boolean(n.shadowRoot) || n.localName === 'slot';\n if (composes(node)) return true;\n const walker = node.ownerDocument.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);\n while (walker.nextNode()) if (composes(walker.currentNode)) return true;\n return false;\n};\n const view = el.ownerDocument.defaultView;\n let text = '';\n const nodesOf = (node) =>\n node.localName === 'slot' ? node.assignedNodes({ flatten: true }) : Array.from((node.shadowRoot || node).childNodes);\n const read = (node, visible) => {\n if (text.length >= limit) return;\n if (node.nodeType === 3 && visible) text += node.data.replace(/\\s+/g, ' ');\n if (node.nodeType !== 1) return;\n if (/^(input|textarea|select)$/.test(node.localName) || node.isContentEditable) return;\n const style = view.getComputedStyle(node);\n if (node.checkVisibility && !node.checkVisibility() && style.display !== 'contents') return;\n if (node.localName === 'br') text += '\\n';\n const apart = style.display.startsWith('inline') || style.display === 'contents' ? '' : '\\n';\n text += apart;\n const own = node.textContent || '';\n if (composed(node)) nodesOf(node).forEach((child) => read(child, style.visibility === 'visible'));\n else if (own.length > limit - text.length) text += own.slice(0, limit - text.length);\n else text += typeof node.innerText === 'string' ? node.innerText : own;\n text += apart;\n };\n const visible = view.getComputedStyle(el).visibility === 'visible';\n nodesOf(el).forEach((child) => read(child, visible));\n return text.slice(0, limit);\n};\n const all = el.textContent || '';\n if (el.tagName === 'OPTION') return el.label;\n if (typeof el.innerText !== 'string') return full ? all : all.slice(0, 2000);\n const rendered = (node) => !node.checkVisibility || node.checkVisibility();\n const shown = rendered(el);\n const boxless = !shown && el.ownerDocument.defaultView.getComputedStyle(el).display === 'contents';\n if (!shown && !boxless) return '';\n if (composed(el)) return withoutDecorations(el, flatText(el, full ? Infinity : 2000));\n if (boxless) return withoutDecorations(el, full ? all : all.slice(0, 2000));\n if (full || all.length <= 2000) return withoutDecorations(el, el.innerText);\n const walker = el.ownerDocument.createTreeWalker(el, NodeFilter.SHOW_TEXT);\n let start = '';\n while (start.length < 1000 && walker.nextNode()) {\n const parent = walker.currentNode.parentElement;\n if (!parent || rendered(parent)) start += walker.currentNode.data;\n }\n return withoutDecorations(el, start);\n}";
29
69
  /** Shown instead of a secret field value (the same for every length) */
30
70
  export declare const MASKED_VALUE = "\u2022\u2022\u2022\u2022";
31
71
  /**
@@ -53,26 +93,40 @@ export declare const SENSITIVE_FIELD_JS = "(el) => {\n const autocomplete = el.
53
93
  */
54
94
  export declare const ELEMENT_STATE_JS = "(el) => {\n const isSensitive = (el) => {\n const autocomplete = el.getAttribute('autocomplete') || '';\n if (/(^|\\s)(cc-[a-z-]+|one-time-code|current-password|new-password)(\\s|$)/i.test(autocomplete)) return true;\n if (el.type === 'password' || /^password$/i.test(el.getAttribute('type') || '')) return true;\n const names = [el.getAttribute('name'), el.id, autocomplete].join(' ');\n if (/passw|passwd|pwd|passcode|(^|[^a-z])otp([^a-z]|$)|cvv|cvc/i.test(names)) return true;\n try {\n const security = el.ownerDocument.defaultView.getComputedStyle(el).getPropertyValue('-webkit-text-security');\n return Boolean(security) && security !== 'none';\n } catch (e) {\n return false;\n }\n};\n const mask = (value) => (value ? '\u2022\u2022\u2022\u2022' : '');\n const guarded = (state) => {\n if (!isSensitive(el)) return state;\n const result = { ...state, sensitive: true };\n if ('value' in result) result.value = mask(result.value);\n if ('selected' in result) result.selected = mask(result.selected);\n return result;\n };\n switch (el.localName) {\n case 'input':\n if (el.type === 'hidden') return { type: 'hidden' };\n return guarded(\n /^(checkbox|radio)$/.test(el.type)\n ? { type: el.type, checked: el.checked, value: el.getAttribute('value') || '' }\n : { type: el.type, value: el.value }\n );\n case 'textarea':\n return guarded({ value: el.value });\n case 'select':\n return guarded({ selected: Array.from(el.selectedOptions || [], (option) => option.label).join(', ') });\n case 'button':\n return el.form ? { type: el.type } : {};\n default:\n return {};\n }\n}";
55
95
  /**
56
- * Page-side short description of an element: tag, id and up to two classes,
57
- * e.g. `button#save.primary.large`.
96
+ * Page-side short description of an element: tag, id and up to two classes
97
+ * ({@link LABEL_CLASSES_JS}), e.g. `button#save.primary.large`.
98
+ */
99
+ export declare const ELEMENT_DESCRIPTION_JS: string;
100
+ /**
101
+ * Page-side reason an element is disabled, or null when it is not. A native
102
+ * control (`button`, `input`, `select`, `textarea`, `fieldset`, `optgroup`,
103
+ * `option`) is disabled by its `disabled attribute` or by being `inside a
104
+ * disabled <fieldset>` (not in its first `<legend>`), an `<option>` by
105
+ * being `inside a disabled <optgroup>`; any other element
106
+ * (e.g. a custom element) when its `disabled` property is set (empty
107
+ * reason). Reads the element's own attributes rather than
108
+ * `matches(':disabled')`, which pages replace.
58
109
  */
59
- export declare const ELEMENT_DESCRIPTION_JS = "(node) => node.tagName.toLowerCase() +\n (node.id ? '#' + node.id : '') +\n (node.classList && node.classList.length ? '.' + Array.from(node.classList).slice(0, 2).join('.') : '')";
110
+ export declare const DISABLED_CAUSE_JS = "(el) => {\n if (!/^(button|input|select|textarea|fieldset|optgroup|option)$/.test(el.localName)) return el.disabled ? '' : null;\n if (el.hasAttribute('disabled')) return 'disabled attribute';\n if (el.localName === 'option') {\n const group = el.parentElement;\n return group && group.localName === 'optgroup' && group.hasAttribute('disabled') ? 'inside a disabled <optgroup>' : null;\n }\n for (let node = el.parentElement; node; node = node.parentElement) {\n if (node.localName !== 'fieldset' || !node.hasAttribute('disabled')) continue;\n let legend = node.firstElementChild;\n while (legend && legend.localName !== 'legend') legend = legend.nextElementSibling;\n if (!legend || !legend.contains(el)) return 'inside a disabled <fieldset>';\n }\n return null;\n}";
60
111
  /**
61
112
  * Page-side position of an element among its parent's children with the
62
113
  * same short description ({@link ELEMENT_DESCRIPTION_JS}), e.g. `(2nd of 3)`;
63
114
  * empty when it is the only one.
64
115
  */
65
- export declare const SIBLING_POSITION_JS = "(el) => {\n const describe = (node) => node.tagName.toLowerCase() +\n (node.id ? '#' + node.id : '') +\n (node.classList && node.classList.length ? '.' + Array.from(node.classList).slice(0, 2).join('.') : '');\n const parent = el.parentElement;\n if (!parent) return '';\n const own = describe(el);\n const same = Array.from(parent.children).filter((child) => describe(child) === own);\n if (same.length < 2) return '';\n const n = same.indexOf(el) + 1;\n const teen = n % 100 >= 11 && n % 100 <= 13;\n const suffix = teen ? 'th' : ({ 1: 'st', 2: 'nd', 3: 'rd' })[n % 10] || 'th';\n return '(' + n + suffix + ' of ' + same.length + ')';\n}";
116
+ export declare const SIBLING_POSITION_JS: string;
66
117
  /**
67
118
  * Page-side identity of an element an action hit, so the output says which
68
119
  * one it was, as one string: its short description
69
120
  * ({@link ELEMENT_DESCRIPTION_JS}) and visible text (button value for button
70
- * inputs, decorations left out), e.g. `button#add.btn "Add to cart"`. A
71
- * `<select>` is named by its label, aria-label or name, else by its selected
121
+ * inputs, decorations left out; read through the flat tree for web
122
+ * components and slots, {@link FLAT_TEXT_JS}), e.g.
123
+ * `button#add.btn "Add to cart"`. A `<select>` is named by its label
124
+ * (`aria-labelledby` or `<label>`), aria-label or name, else by its selected
72
125
  * option, e.g. `select.sort "Sort products"`. An element without visible
73
126
  * text is described by itself first: by its position among same-looking
74
127
  * siblings ({@link SIBLING_POSITION_JS}), e.g. `div.figure (2nd of 3)`, with
75
- * its aria-label, placeholder or title when it has one. Only an element
128
+ * its name when it has one: aria-label, label, placeholder, title, or the
129
+ * alt text of an image (in it). Only an element
76
130
  * without an id that is the only one of its kind is named by the nearest of
77
131
  * three ancestors that has text, e.g.
78
132
  * `input.toggle in div.view "Write report"` (rows of a list share their
@@ -80,18 +134,20 @@ export declare const SIBLING_POSITION_JS = "(el) => {\n const describe = (node)
80
134
  * of `<select>`s in it (read like `innerText`: CSS-hidden text is left out of
81
135
  * a rendered ancestor; at most 500 text nodes; selects in shadow roots are
82
136
  * not looked into), and never past an editable ancestor, whose text may be
83
- * typed input; otherwise its aria-label, placeholder or title is used. Texts
137
+ * typed input; otherwise its name is used. Texts
84
138
  * are cut at 40 characters. Contenteditable elements count as controls: what
85
139
  * was typed into them is never echoed (nor their ancestors' text).
86
140
  */
87
- export declare const ELEMENT_IDENTITY_JS = "(el) => {\n const describe = (node) => node.tagName.toLowerCase() +\n (node.id ? '#' + node.id : '') +\n (node.classList && node.classList.length ? '.' + Array.from(node.classList).slice(0, 2).join('.') : '');\n const siblingPosition = (el) => {\n const describe = (node) => node.tagName.toLowerCase() +\n (node.id ? '#' + node.id : '') +\n (node.classList && node.classList.length ? '.' + Array.from(node.classList).slice(0, 2).join('.') : '');\n const parent = el.parentElement;\n if (!parent) return '';\n const own = describe(el);\n const same = Array.from(parent.children).filter((child) => describe(child) === own);\n if (same.length < 2) return '';\n const n = same.indexOf(el) + 1;\n const teen = n % 100 >= 11 && n % 100 <= 13;\n const suffix = teen ? 'th' : ({ 1: 'st', 2: 'nd', 3: 'rd' })[n % 10] || 'th';\n return '(' + n + suffix + ' of ' + same.length + ')';\n};\n const withoutDecorations = (el, text) => {\n if (!text || typeof el.querySelectorAll !== 'function') return text;\n const glyph = /^\\s*[\u00D7\u2715\u2716\u2717\u2A2F]\\s*$/;\n const textOf = (node) => (typeof node.innerText === 'string' ? node.innerText : node.textContent || '').trim();\n const closer = '.close, [aria-label=\"close\" i], [aria-label=\"dismiss\" i]';\n const rendered = (node) => !node.checkVisibility || node.checkVisibility();\n const found = Array.from(el.querySelectorAll(closer + ', [aria-hidden=\"true\"]'))\n .filter((node) => rendered(node) && (node.matches(closer) || !/[\\p{L}\\p{N}]/u.test(textOf(node))));\n if (/[\u00D7\u2715\u2716\u2717\u2A2F]/.test(text)) {\n found.push(...Array.from(el.querySelectorAll('button, a, [role=\"button\"]')).filter((node) => glyph.test(node.textContent || '')));\n }\n const unique = Array.from(new Set(found));\n const outermost = unique.filter((node) => !unique.some((other) => other !== node && other.contains(node)));\n let result = text;\n for (const node of outermost) {\n const part = textOf(node);\n const at = part ? result.lastIndexOf(part) : -1;\n if (at >= 0) result = result.slice(0, at) + result.slice(at + part.length);\n }\n return result;\n};\n const clean = (text) => (text || '').replace(/\\s+/g, ' ').trim();\n const cut = (text) => {\n const characters = Array.from(text);\n return characters.length > 40 ? characters.slice(0, 40).join('') + '\u2026' : text;\n };\n const isControl = (node) => /^(input|select|textarea)$/.test(node.localName) || node.isContentEditable;\n const shownText = (node) =>\n isControl(node) ? '' : clean(withoutDecorations(node, typeof node.innerText === 'string' ? node.innerText : node.textContent));\n const buttonValue = (node) => (node.localName === 'input' && /^(submit|button|reset)$/i.test(node.type) ? clean(node.value) : '');\n const attributeText = (node) =>\n clean(node.getAttribute('aria-label')) || clean(node.getAttribute('placeholder')) || clean(node.getAttribute('title'));\n const selectName = (node) =>\n clean(node.labels && node.labels[0] && node.labels[0].innerText) ||\n clean(node.getAttribute('aria-label')) ||\n clean(node.getAttribute('name')) ||\n clean(node.selectedOptions && node.selectedOptions[0] && node.selectedOptions[0].label);\n const shown = (node, options) => !node.checkVisibility || node.checkVisibility(options);\n const textOutsideSelects = (node) => {\n if (!node.querySelector('select')) return shownText(node);\n const hiddenLeftOut = shown(node);\n const walker = node.ownerDocument.createTreeWalker(node, NodeFilter.SHOW_TEXT);\n let text = '';\n for (let visited = 0; visited < 500 && text.length < 200 && walker.nextNode(); visited++) {\n const parent = walker.currentNode.parentElement;\n if (!parent || parent.closest('select, textarea')) continue;\n if (!hiddenLeftOut || shown(parent, { visibilityProperty: true })) text += ' ' + walker.currentNode.data;\n }\n return clean(text);\n };\n const quoted = (text) => (text ? ' \"' + cut(text) + '\"' : '');\n const visible = shownText(el) || buttonValue(el) || (el.localName === 'select' ? selectName(el) : '');\n if (visible) return describe(el) + quoted(visible);\n const attribute = attributeText(el);\n const position = siblingPosition(el);\n if (position) return describe(el) + ' ' + position + quoted(attribute);\n let ancestor = el.id || el.isContentEditable ? null : el.parentElement;\n for (let depth = 0; ancestor && depth < 3; depth++, ancestor = ancestor.parentElement) {\n if (isControl(ancestor)) break;\n const text = textOutsideSelects(ancestor);\n if (text) return describe(el) + ' in ' + describe(ancestor) + quoted(text);\n }\n return describe(el) + quoted(attribute);\n}";
141
+ export declare const ELEMENT_IDENTITY_JS: string;
88
142
  /**
89
143
  * Page-side location of an element: the iframes it is in (outermost first)
90
- * and the shadow root holding it, with the host's first class and light-DOM
91
- * text, e.g. `iframe#pay > shadow root of <sl-button.primary "Save">`;
92
- * empty for the main document.
144
+ * and the shadow root holding it, with the host's first class and the text
145
+ * a user sees of it ({@link ELEMENT_TEXT_JS}, cut at 30 characters with
146
+ * `…`), e.g. `iframe#pay > shadow root of <sl-button.primary "Save">`;
147
+ * empty for the main document. Each host's text is read once per script
148
+ * run, as many matches share a host.
93
149
  */
94
- export declare const ELEMENT_CONTEXT_JS = "(el) => {\n const describe = (node) => node.tagName.toLowerCase() + (node.id ? '#' + node.id : '');\n const parts = [];\n for (let doc = el.ownerDocument; doc && doc.defaultView && doc.defaultView.frameElement; ) {\n const frame = doc.defaultView.frameElement;\n parts.unshift(describe(frame));\n doc = frame.ownerDocument;\n }\n const root = el.getRootNode();\n if (root.host) {\n const host = root.host;\n const label = describe(host) + (!host.id && host.classList.length ? '.' + host.classList[0] : '');\n const text = (host.textContent || '').replace(/\\s+/g, ' ').trim().slice(0, 30);\n parts.push('shadow root of <' + label + (text ? ' \"' + text + '\"' : '') + '>');\n }\n return parts.join(' > ');\n}";
150
+ export declare const ELEMENT_CONTEXT_JS: string;
95
151
  /**
96
152
  * Short text preview of an element's text: whitespace collapsed, cut on a
97
153
  * whole character (an emoji is never split, which would make JSON invalid).
@@ -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,88 @@ 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; raw text keeps `innerText`'s collapsed
88
+ * whitespace. The text is cut at `limit` characters, and a
89
+ * part whose text alone passes the limit is read from its text nodes
90
+ * (`textContent`) instead of `innerText`, which would lay out all of it.
91
+ */
92
+ export const FLAT_TEXT_JS = `(el, limit) => {
93
+ const composed = ${COMPOSED_JS};
94
+ const view = el.ownerDocument.defaultView;
95
+ let text = '';
96
+ const nodesOf = (node) =>
97
+ node.localName === 'slot' ? node.assignedNodes({ flatten: true }) : Array.from((node.shadowRoot || node).childNodes);
98
+ const read = (node, visible) => {
99
+ if (text.length >= limit) return;
100
+ if (node.nodeType === 3 && visible) text += node.data.replace(/\\s+/g, ' ');
101
+ if (node.nodeType !== 1) return;
102
+ if (/^(input|textarea|select)$/.test(node.localName) || node.isContentEditable) return;
103
+ const style = view.getComputedStyle(node);
104
+ if (node.checkVisibility && !node.checkVisibility() && style.display !== 'contents') return;
105
+ if (node.localName === 'br') text += '\\n';
106
+ const apart = style.display.startsWith('inline') || style.display === 'contents' ? '' : '\\n';
107
+ text += apart;
108
+ const own = node.textContent || '';
109
+ if (composed(node)) nodesOf(node).forEach((child) => read(child, style.visibility === 'visible'));
110
+ else if (own.length > limit - text.length) text += own.slice(0, limit - text.length);
111
+ else text += typeof node.innerText === 'string' ? node.innerText : own;
112
+ text += apart;
113
+ };
114
+ const visible = view.getComputedStyle(el).visibility === 'visible';
115
+ nodesOf(el).forEach((child) => read(child, visible));
116
+ return text.slice(0, limit);
117
+ }`;
39
118
  /**
40
119
  * Page-side text of an element as a user sees it: `innerText` for a rendered
41
120
  * element (CSS-hidden parts left out, inline elements not split apart), none
42
121
  * for an element that is not rendered, `textContent` for SVG and other
43
122
  * elements without `innerText` and for `display: contents` wrappers (no box
44
123
  * 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}).
124
+ * `<option>` (which its `<select>` renders). A web component, a slot, and
125
+ * an element holding either are read through the flat tree
126
+ * ({@link FLAT_TEXT_JS}): the text a component renders from its shadow
127
+ * root, slotted content in place of its slots, blocks set apart. For large
128
+ * containers (more than 2000 characters of text) only the start is read,
129
+ * from the text nodes whose parent is rendered, so a preview never lays out
130
+ * a whole page's text, unless `full` is set. Decorations are left out
131
+ * ({@link WITHOUT_DECORATIONS_JS}).
49
132
  */
50
133
  export const ELEMENT_TEXT_JS = `(el, full) => {
51
134
  const withoutDecorations = ${WITHOUT_DECORATIONS_JS};
135
+ const composed = ${COMPOSED_JS};
136
+ const flatText = ${FLAT_TEXT_JS};
52
137
  const all = el.textContent || '';
53
138
  if (el.tagName === 'OPTION') return el.label;
54
139
  if (typeof el.innerText !== 'string') return full ? all : all.slice(0, 2000);
55
140
  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
- }
141
+ const shown = rendered(el);
142
+ const boxless = !shown && el.ownerDocument.defaultView.getComputedStyle(el).display === 'contents';
143
+ if (!shown && !boxless) return '';
144
+ if (composed(el)) return withoutDecorations(el, flatText(el, full ? Infinity : 2000));
145
+ if (boxless) return withoutDecorations(el, full ? all : all.slice(0, 2000));
60
146
  if (full || all.length <= 2000) return withoutDecorations(el, el.innerText);
61
147
  const walker = el.ownerDocument.createTreeWalker(el, NodeFilter.SHOW_TEXT);
62
148
  let start = '';
@@ -132,12 +218,38 @@ export const ELEMENT_STATE_JS = `(el) => {
132
218
  }
133
219
  }`;
134
220
  /**
135
- * Page-side short description of an element: tag, id and up to two classes,
136
- * e.g. `button#save.primary.large`.
221
+ * Page-side short description of an element: tag, id and up to two classes
222
+ * ({@link LABEL_CLASSES_JS}), e.g. `button#save.primary.large`.
137
223
  */
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('.') : '')`;
224
+ export const ELEMENT_DESCRIPTION_JS = `(node) => {
225
+ const classes = (${LABEL_CLASSES_JS})(node);
226
+ return node.tagName.toLowerCase() + (node.id ? '#' + node.id : '') + (classes[0] ? '.' + classes[0] : '') + (classes[1] ? '.' + classes[1] : '');
227
+ }`;
228
+ /**
229
+ * Page-side reason an element is disabled, or null when it is not. A native
230
+ * control (`button`, `input`, `select`, `textarea`, `fieldset`, `optgroup`,
231
+ * `option`) is disabled by its `disabled attribute` or by being `inside a
232
+ * disabled <fieldset>` (not in its first `<legend>`), an `<option>` by
233
+ * being `inside a disabled <optgroup>`; any other element
234
+ * (e.g. a custom element) when its `disabled` property is set (empty
235
+ * reason). Reads the element's own attributes rather than
236
+ * `matches(':disabled')`, which pages replace.
237
+ */
238
+ export const DISABLED_CAUSE_JS = `(el) => {
239
+ if (!/^(button|input|select|textarea|fieldset|optgroup|option)$/.test(el.localName)) return el.disabled ? '' : null;
240
+ if (el.hasAttribute('disabled')) return 'disabled attribute';
241
+ if (el.localName === 'option') {
242
+ const group = el.parentElement;
243
+ return group && group.localName === 'optgroup' && group.hasAttribute('disabled') ? 'inside a disabled <optgroup>' : null;
244
+ }
245
+ for (let node = el.parentElement; node; node = node.parentElement) {
246
+ if (node.localName !== 'fieldset' || !node.hasAttribute('disabled')) continue;
247
+ let legend = node.firstElementChild;
248
+ while (legend && legend.localName !== 'legend') legend = legend.nextElementSibling;
249
+ if (!legend || !legend.contains(el)) return 'inside a disabled <fieldset>';
250
+ }
251
+ return null;
252
+ }`;
141
253
  /**
142
254
  * Page-side position of an element among its parent's children with the
143
255
  * same short description ({@link ELEMENT_DESCRIPTION_JS}), e.g. `(2nd of 3)`;
@@ -159,12 +271,15 @@ export const SIBLING_POSITION_JS = `(el) => {
159
271
  * Page-side identity of an element an action hit, so the output says which
160
272
  * one it was, as one string: its short description
161
273
  * ({@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
274
+ * inputs, decorations left out; read through the flat tree for web
275
+ * components and slots, {@link FLAT_TEXT_JS}), e.g.
276
+ * `button#add.btn "Add to cart"`. A `<select>` is named by its label
277
+ * (`aria-labelledby` or `<label>`), aria-label or name, else by its selected
164
278
  * option, e.g. `select.sort "Sort products"`. An element without visible
165
279
  * text is described by itself first: by its position among same-looking
166
280
  * 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
281
+ * its name when it has one: aria-label, label, placeholder, title, or the
282
+ * alt text of an image (in it). Only an element
168
283
  * without an id that is the only one of its kind is named by the nearest of
169
284
  * three ancestors that has text, e.g.
170
285
  * `input.toggle in div.view "Write report"` (rows of a list share their
@@ -172,7 +287,7 @@ export const SIBLING_POSITION_JS = `(el) => {
172
287
  * of `<select>`s in it (read like `innerText`: CSS-hidden text is left out of
173
288
  * a rendered ancestor; at most 500 text nodes; selects in shadow roots are
174
289
  * 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
290
+ * typed input; otherwise its name is used. Texts
176
291
  * are cut at 40 characters. Contenteditable elements count as controls: what
177
292
  * was typed into them is never echoed (nor their ancestors' text).
178
293
  */
@@ -180,19 +295,36 @@ export const ELEMENT_IDENTITY_JS = `(el) => {
180
295
  const describe = ${ELEMENT_DESCRIPTION_JS};
181
296
  const siblingPosition = ${SIBLING_POSITION_JS};
182
297
  const withoutDecorations = ${WITHOUT_DECORATIONS_JS};
298
+ const composed = ${COMPOSED_JS};
299
+ const flatText = ${FLAT_TEXT_JS};
183
300
  const clean = (text) => (text || '').replace(/\\s+/g, ' ').trim();
184
301
  const cut = (text) => {
185
302
  const characters = Array.from(text);
186
303
  return characters.length > 40 ? characters.slice(0, 40).join('') + '…' : text;
187
304
  };
188
305
  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));
306
+ const textOf = (node) =>
307
+ composed(node) ? flatText(node, 200) : typeof node.innerText === 'string' ? node.innerText : node.textContent;
308
+ const shownText = (node) => (isControl(node) ? '' : clean(withoutDecorations(node, textOf(node))));
191
309
  const buttonValue = (node) => (node.localName === 'input' && /^(submit|button|reset)$/i.test(node.type) ? clean(node.value) : '');
310
+ const labelText = (node) => {
311
+ const ids = (node.getAttribute('aria-labelledby') || '').split(/\\s+/).filter(Boolean);
312
+ const root = node.getRootNode();
313
+ const labels = ids.length ? ids.map((id) => root.getElementById(id)) : Array.from(node.labels || []);
314
+ return clean(labels.filter(Boolean).map(textOf).join(' '));
315
+ };
316
+ const imageAlt = (node) => {
317
+ const image = node.localName === 'img' ? node : node.querySelector('img[alt]');
318
+ return image ? clean(image.getAttribute('alt')) : '';
319
+ };
192
320
  const attributeText = (node) =>
193
- clean(node.getAttribute('aria-label')) || clean(node.getAttribute('placeholder')) || clean(node.getAttribute('title'));
321
+ clean(node.getAttribute('aria-label')) ||
322
+ labelText(node) ||
323
+ clean(node.getAttribute('placeholder')) ||
324
+ clean(node.getAttribute('title')) ||
325
+ imageAlt(node);
194
326
  const selectName = (node) =>
195
- clean(node.labels && node.labels[0] && node.labels[0].innerText) ||
327
+ labelText(node) ||
196
328
  clean(node.getAttribute('aria-label')) ||
197
329
  clean(node.getAttribute('name')) ||
198
330
  clean(node.selectedOptions && node.selectedOptions[0] && node.selectedOptions[0].label);
@@ -225,27 +357,42 @@ export const ELEMENT_IDENTITY_JS = `(el) => {
225
357
  }`;
226
358
  /**
227
359
  * 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.
360
+ * and the shadow root holding it, with the host's first class and the text
361
+ * a user sees of it ({@link ELEMENT_TEXT_JS}, cut at 30 characters with
362
+ * `…`), e.g. `iframe#pay > shadow root of <sl-button.primary "Save">`;
363
+ * empty for the main document. Each host's text is read once per script
364
+ * run, as many matches share a host.
231
365
  */
232
- export const ELEMENT_CONTEXT_JS = `(el) => {
366
+ export const ELEMENT_CONTEXT_JS = `(() => {
367
+ const textOf = ${ELEMENT_TEXT_JS};
368
+ const hostTexts = new WeakMap();
369
+ const hostText = (host) => {
370
+ if (!hostTexts.has(host)) {
371
+ const characters = Array.from(String(textOf(host, false)).replace(/\\s+/g, ' ').trim());
372
+ hostTexts.set(host, characters.length > 30 ? characters.slice(0, 30).join('') + '…' : characters.join(''));
373
+ }
374
+ return hostTexts.get(host);
375
+ };
233
376
  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
- }`;
377
+ const labelClasses = ${LABEL_CLASSES_JS};
378
+ return (el) => {
379
+ const parts = [];
380
+ for (let doc = el.ownerDocument; doc && doc.defaultView && doc.defaultView.frameElement; ) {
381
+ const frame = doc.defaultView.frameElement;
382
+ parts.unshift(describe(frame));
383
+ doc = frame.ownerDocument;
384
+ }
385
+ const root = el.getRootNode();
386
+ if (root.host) {
387
+ const host = root.host;
388
+ const firstClass = labelClasses(host)[0];
389
+ const label = describe(host) + (!host.id && firstClass ? '.' + firstClass : '');
390
+ const text = hostText(host);
391
+ parts.push('shadow root of <' + label + (text ? ' "' + text + '"' : '') + '>');
392
+ }
393
+ return parts.join(' > ');
394
+ };
395
+ })()`;
249
396
  /**
250
397
  * Short text preview of an element's text: whitespace collapsed, cut on a
251
398
  * 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 */