browser-debugger-cli 0.11.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 (222) hide show
  1. package/.claude/skills/bdg/SKILL.md +4 -4
  2. package/README.md +143 -79
  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/css.d.ts +13 -0
  8. package/dist/commands/css.js +53 -0
  9. package/dist/commands/dom/DomElementResolver.d.ts +3 -1
  10. package/dist/commands/dom/DomElementResolver.js +10 -3
  11. package/dist/commands/dom/a11y.js +3 -2
  12. package/dist/commands/dom/audit.d.ts +14 -0
  13. package/dist/commands/dom/audit.js +87 -0
  14. package/dist/commands/dom/eval.d.ts +3 -2
  15. package/dist/commands/dom/eval.js +11 -5
  16. package/dist/commands/dom/form.js +10 -9
  17. package/dist/commands/dom/formInteraction.js +42 -11
  18. package/dist/commands/dom/get.js +8 -8
  19. package/dist/commands/dom/helpers/index.d.ts +1 -1
  20. package/dist/commands/dom/helpers/index.js +1 -1
  21. package/dist/commands/dom/helpers/keyAttributes.d.ts +3 -2
  22. package/dist/commands/dom/helpers/keyAttributes.js +6 -4
  23. package/dist/commands/dom/helpers/query.d.ts +27 -3
  24. package/dist/commands/dom/helpers/query.js +152 -64
  25. package/dist/commands/dom/helpers/screenshot.d.ts +1 -0
  26. package/dist/commands/dom/helpers/screenshot.js +169 -49
  27. package/dist/commands/dom/index.js +7 -2
  28. package/dist/commands/dom/query.d.ts +19 -2
  29. package/dist/commands/dom/query.js +37 -6
  30. package/dist/commands/dom/screenshot.js +12 -7
  31. package/dist/commands/dom/semanticUtils.d.ts +3 -2
  32. package/dist/commands/dom/semanticUtils.js +40 -9
  33. package/dist/commands/dom/wait.js +5 -3
  34. package/dist/commands/helpJson.d.ts +82 -19
  35. package/dist/commands/helpJson.js +112 -41
  36. package/dist/commands/helpTopic.d.ts +16 -1
  37. package/dist/commands/helpTopic.js +59 -1
  38. package/dist/commands/installSkill.d.ts +15 -5
  39. package/dist/commands/installSkill.js +86 -16
  40. package/dist/commands/network/list.js +22 -12
  41. package/dist/commands/optionBehaviors.js +53 -16
  42. package/dist/commands/page.js +7 -4
  43. package/dist/commands/peek.d.ts +7 -0
  44. package/dist/commands/peek.js +65 -23
  45. package/dist/commands/shared/daemonErrorHandler.d.ts +5 -2
  46. package/dist/commands/shared/daemonErrorHandler.js +20 -9
  47. package/dist/commands/shared/dataFetcher.d.ts +12 -4
  48. package/dist/commands/shared/dataFetcher.js +12 -4
  49. package/dist/commands/shared/followMode.d.ts +9 -1
  50. package/dist/commands/shared/followMode.js +22 -4
  51. package/dist/commands/shared/optionTypes.d.ts +9 -2
  52. package/dist/commands/shared/outputFile.js +6 -1
  53. package/dist/commands/start.d.ts +20 -5
  54. package/dist/commands/start.js +84 -23
  55. package/dist/commands/stop.d.ts +11 -0
  56. package/dist/commands/stop.js +24 -1
  57. package/dist/commands/tail.d.ts +7 -1
  58. package/dist/commands/tail.js +13 -62
  59. package/dist/commands.js +2 -0
  60. package/dist/connection/cdp.d.ts +7 -0
  61. package/dist/connection/cdp.js +9 -0
  62. package/dist/connection/launcher.js +3 -2
  63. package/dist/daemon/SessionController.js +6 -1
  64. package/dist/daemon/launcher.d.ts +3 -2
  65. package/dist/daemon/launcher.js +47 -3
  66. package/dist/daemon/session/Session.d.ts +4 -1
  67. package/dist/daemon/session/Session.js +33 -2
  68. package/dist/daemon/session/TelemetryStore.d.ts +8 -1
  69. package/dist/daemon/session/TelemetryStore.js +13 -1
  70. package/dist/daemon/session/commandRegistry.js +36 -14
  71. package/dist/daemon/session/interactions.d.ts +2 -1
  72. package/dist/daemon/session/interactions.js +13 -1
  73. package/dist/daemon/session/plugins.js +19 -53
  74. package/dist/daemon/session/teardown.js +1 -1
  75. package/dist/daemon.js +9234 -7222
  76. package/dist/errors/messages.d.ts +88 -15
  77. package/dist/errors/messages.js +177 -27
  78. package/dist/index.js +19322 -13961
  79. package/dist/ipc/client.d.ts +22 -2
  80. package/dist/ipc/client.js +34 -5
  81. package/dist/ipc/protocol/auditTypes.d.ts +135 -0
  82. package/dist/ipc/protocol/auditTypes.js +6 -0
  83. package/dist/ipc/protocol/commands.d.ts +35 -0
  84. package/dist/ipc/protocol/commands.js +2 -0
  85. package/dist/ipc/protocol/domTypes.d.ts +16 -0
  86. package/dist/ipc/protocol/inspectTypes.d.ts +73 -8
  87. package/dist/ipc/session/types.d.ts +2 -0
  88. package/dist/runtime/css/search.d.ts +39 -0
  89. package/dist/runtime/css/search.js +122 -0
  90. package/dist/runtime/dom/actionEffects.d.ts +9 -2
  91. package/dist/runtime/dom/actionEffects.js +30 -14
  92. package/dist/runtime/dom/audit.d.ts +19 -0
  93. package/dist/runtime/dom/audit.js +37 -0
  94. package/dist/runtime/dom/auditModel.d.ts +45 -0
  95. package/dist/runtime/dom/auditModel.js +220 -0
  96. package/dist/runtime/dom/auditScripts.d.ts +113 -0
  97. package/dist/runtime/dom/auditScripts.js +148 -0
  98. package/dist/runtime/dom/elementGeometry.d.ts +16 -3
  99. package/dist/runtime/dom/elementGeometry.js +49 -10
  100. package/dist/runtime/dom/elementInfo.d.ts +74 -17
  101. package/dist/runtime/dom/elementInfo.js +187 -34
  102. package/dist/runtime/dom/evalHelpers.d.ts +12 -2
  103. package/dist/runtime/dom/evalHelpers.js +67 -7
  104. package/dist/runtime/dom/formDiscovery.d.ts +6 -2
  105. package/dist/runtime/dom/formDiscovery.js +20 -3
  106. package/dist/runtime/dom/formFillHelpers/fill.js +8 -12
  107. package/dist/runtime/dom/formFillHelpers/pressKey.js +2 -2
  108. package/dist/runtime/dom/formFillHelpers/shared.d.ts +16 -10
  109. package/dist/runtime/dom/formFillHelpers/shared.js +19 -52
  110. package/dist/runtime/dom/formSubmitHelpers.js +4 -3
  111. package/dist/runtime/dom/frameLayout.js +1 -0
  112. package/dist/runtime/dom/inspect.d.ts +7 -0
  113. package/dist/runtime/dom/inspect.js +92 -28
  114. package/dist/runtime/dom/inspectAllStyles.d.ts +16 -4
  115. package/dist/runtime/dom/inspectAllStyles.js +90 -7
  116. package/dist/runtime/dom/inspectCascade.d.ts +19 -2
  117. package/dist/runtime/dom/inspectCascade.js +214 -44
  118. package/dist/runtime/dom/inspectCascadeModel.d.ts +8 -0
  119. package/dist/runtime/dom/inspectCascadeModel.js +108 -34
  120. package/dist/runtime/dom/inspectHints.d.ts +26 -3
  121. package/dist/runtime/dom/inspectHints.js +125 -9
  122. package/dist/runtime/dom/inspectModel.d.ts +5 -1
  123. package/dist/runtime/dom/inspectModel.js +37 -10
  124. package/dist/runtime/dom/inspectPaintModel.d.ts +50 -22
  125. package/dist/runtime/dom/inspectPaintModel.js +182 -68
  126. package/dist/runtime/dom/inspectRules.d.ts +19 -0
  127. package/dist/runtime/dom/inspectRules.js +21 -5
  128. package/dist/runtime/dom/inspectScripts.d.ts +112 -12
  129. package/dist/runtime/dom/inspectScripts.js +357 -32
  130. package/dist/runtime/dom/inspectTree.js +10 -2
  131. package/dist/runtime/dom/inspectWhyModel.d.ts +2 -1
  132. package/dist/runtime/dom/inspectWhyModel.js +52 -10
  133. package/dist/runtime/dom/layout.js +40 -16
  134. package/dist/runtime/dom/reactEventHelpers.d.ts +21 -4
  135. package/dist/runtime/dom/reactEventHelpers.js +90 -36
  136. package/dist/runtime/dom/targetNode.d.ts +18 -5
  137. package/dist/runtime/dom/targetNode.js +268 -8
  138. package/dist/runtime/dom/wait.js +2 -1
  139. package/dist/runtime/page/bdgWorld.d.ts +57 -0
  140. package/dist/runtime/page/bdgWorld.js +180 -0
  141. package/dist/runtime/page/emulation.d.ts +13 -4
  142. package/dist/runtime/page/emulation.js +69 -4
  143. package/dist/runtime/page/replacedBuiltins.d.ts +28 -0
  144. package/dist/runtime/page/replacedBuiltins.js +136 -0
  145. package/dist/runtime/page/userAgent.d.ts +17 -0
  146. package/dist/runtime/page/userAgent.js +57 -0
  147. package/dist/session/QueryCacheManager.d.ts +4 -1
  148. package/dist/session/QueryCacheManager.js +5 -2
  149. package/dist/session/chrome.d.ts +4 -1
  150. package/dist/session/chrome.js +7 -1
  151. package/dist/session/cleanup/staleSession.d.ts +21 -4
  152. package/dist/session/cleanup/staleSession.js +79 -9
  153. package/dist/session/cleanup/userCommands.d.ts +4 -1
  154. package/dist/session/cleanup/userCommands.js +10 -5
  155. package/dist/session/daemonSocket.d.ts +10 -0
  156. package/dist/session/daemonSocket.js +22 -0
  157. package/dist/session/lastSession.d.ts +6 -3
  158. package/dist/session/lastSession.js +11 -5
  159. package/dist/session/paths.d.ts +3 -1
  160. package/dist/session/paths.js +5 -5
  161. package/dist/session/portClaims.js +4 -3
  162. package/dist/session/sessionList.d.ts +13 -5
  163. package/dist/session/sessionList.js +31 -7
  164. package/dist/telemetry/a11y.js +2 -2
  165. package/dist/telemetry/console.d.ts +2 -1
  166. package/dist/telemetry/console.js +30 -21
  167. package/dist/telemetry/pageCrash.d.ts +26 -0
  168. package/dist/telemetry/pageCrash.js +53 -0
  169. package/dist/types.d.ts +20 -0
  170. package/dist/ui/formatters/audit.d.ts +19 -0
  171. package/dist/ui/formatters/audit.js +115 -0
  172. package/dist/ui/formatters/cdp.d.ts +138 -0
  173. package/dist/ui/formatters/cdp.js +131 -0
  174. package/dist/ui/formatters/console/chronological.js +3 -1
  175. package/dist/ui/formatters/console/follow.d.ts +2 -1
  176. package/dist/ui/formatters/console/follow.js +2 -2
  177. package/dist/ui/formatters/console/json.d.ts +2 -2
  178. package/dist/ui/formatters/console/json.js +11 -5
  179. package/dist/ui/formatters/console/shared.d.ts +30 -0
  180. package/dist/ui/formatters/console/shared.js +16 -0
  181. package/dist/ui/formatters/console/summarize.d.ts +9 -2
  182. package/dist/ui/formatters/console/summarize.js +40 -9
  183. package/dist/ui/formatters/console.d.ts +2 -1
  184. package/dist/ui/formatters/console.js +7 -5
  185. package/dist/ui/formatters/details.js +3 -1
  186. package/dist/ui/formatters/dom.d.ts +2 -2
  187. package/dist/ui/formatters/dom.js +10 -8
  188. package/dist/ui/formatters/helpFormatters.js +1 -1
  189. package/dist/ui/formatters/inspect.js +50 -17
  190. package/dist/ui/formatters/installSkill.d.ts +9 -1
  191. package/dist/ui/formatters/installSkill.js +32 -6
  192. package/dist/ui/formatters/layout.js +2 -1
  193. package/dist/ui/formatters/networkList.d.ts +1 -1
  194. package/dist/ui/formatters/networkList.js +1 -2
  195. package/dist/ui/formatters/preview.d.ts +2 -0
  196. package/dist/ui/formatters/preview.js +17 -7
  197. package/dist/ui/formatters/sessions.d.ts +2 -2
  198. package/dist/ui/formatters/sessions.js +9 -2
  199. package/dist/ui/formatters/status.js +1 -1
  200. package/dist/ui/logging/logger.d.ts +1 -1
  201. package/dist/ui/messages/commands.d.ts +168 -11
  202. package/dist/ui/messages/commands.js +245 -18
  203. package/dist/ui/messages/consoleMessages.d.ts +24 -0
  204. package/dist/ui/messages/consoleMessages.js +32 -0
  205. package/dist/ui/messages/preview.d.ts +12 -0
  206. package/dist/ui/messages/preview.js +18 -2
  207. package/dist/ui/messages/session.d.ts +13 -2
  208. package/dist/ui/messages/session.js +22 -3
  209. package/dist/utils/cssValues.js +36 -4
  210. package/dist/utils/decisionTrees.js +0 -5
  211. package/dist/utils/directories.d.ts +34 -0
  212. package/dist/utils/directories.js +88 -0
  213. package/dist/utils/display.d.ts +16 -0
  214. package/dist/utils/display.js +42 -0
  215. package/dist/utils/exitCodes.d.ts +1 -0
  216. package/dist/utils/exitCodes.js +6 -0
  217. package/dist/utils/process.d.ts +12 -0
  218. package/dist/utils/process.js +25 -0
  219. package/dist/utils/suggestions.d.ts +4 -2
  220. package/dist/utils/suggestions.js +7 -5
  221. package/dist/utils/taskMappings.js +1 -1
  222. package/package.json +3 -2
@@ -12,8 +12,8 @@ import type { CDPConnection } from '../../connection/cdp.js';
12
12
  import { CommandError } from '../../errors/index.js';
13
13
  /** Selector placeholder that makes page scripts use the bound node. */
14
14
  export declare const BOUND_TARGET_SELECTOR = "__bdg_bound_target__";
15
- /** Removes the node bound for index-based commands from the window it was stored on */
16
- export declare const UNBIND_TARGET_SCRIPT = "delete window.__bdgTarget";
15
+ /** Removes the node bound for index-based commands, and matches bound for a selector, from the window they were stored on */
16
+ export declare const UNBIND_TARGET_SCRIPT = "delete window.__bdgTarget; delete window.__bdgMatches";
17
17
  /**
18
18
  * Page-side matching of filters ({@link SelectorFilter}) and scoped steps
19
19
  * ({@link ScopedStep}): called with the open shadow roots of the page, returns
@@ -54,9 +54,11 @@ export declare const DEEP_QUERY_JS = "function (selector, parts) {\n const css
54
54
  * Page-side element lookup shared by the interaction scripts.
55
55
  *
56
56
  * Returns the bound node (if still in the page) for the placeholder selector,
57
- * otherwise all matches of the selector ({@link DEEP_QUERY_JS}).
57
+ * the matches bound for the selector when the page replaced the search
58
+ * built-ins ({@link bindMatches}), otherwise all matches of the selector
59
+ * ({@link DEEP_QUERY_JS}).
58
60
  */
59
- export declare const FIND_ELEMENTS_JS = "function (selector, parts) {\n if (selector === '__bdg_bound_target__') {\n const el = window.__bdgTarget;\n return el && el.isConnected ? [el] : [];\n }\n return (function (selector, parts) {\n const css = parts ? parts.map((part) => part.css).join(', ') : selector;\n const found = [];\n const roots = [];\n const visit = (root) => {\n roots.push(root);\n for (const match of root.querySelectorAll(css)) found.push(match);\n for (const el of root.querySelectorAll('*')) {\n if (el.shadowRoot) visit(el.shadowRoot);\n if (el.tagName === 'IFRAME' || el.tagName === 'FRAME') {\n let frameDocument = null;\n try { frameDocument = el.contentDocument; } catch (e) { frameDocument = null; }\n if (frameDocument) visit(frameDocument);\n }\n }\n };\n visit(document);\n if (!parts) return found;\n const shadowRoots = roots.filter((root) => root.host);\n const { passesAll, descend } = ((shadowRoots) => {\n const skipped = /^(script|style|noscript|template)$/;\n const hiddenText = (el) => {\n const walker = el.ownerDocument.createTreeWalker(el, 5, {\n acceptNode: (node) => (node.nodeType === 1 && skipped.test(node.localName) ? 2 : 1)\n });\n let text = '';\n for (let node = walker.nextNode(); node; node = walker.nextNode()) {\n if (node.nodeType === 3) text += node.data;\n }\n return text;\n };\n const textOf = (el) => {\n if (el.localName === 'input' && /^(submit|button|reset)$/i.test(el.type)) return el.value;\n const rendered = typeof el.innerText === 'string' &&\n (typeof el.checkVisibility !== 'function' || el.checkVisibility({ visibilityProperty: true }));\n return (rendered ? el.innerText : hiddenText(el)).replace(/\\s+/g, ' ').trim();\n };\n const passes = (el, filter) => {\n if (filter.kind === 'visible') {\n if (typeof el.checkVisibility === 'function' && !el.checkVisibility()) return false;\n const rect = el.getBoundingClientRect();\n return rect.width > 0 && rect.height > 0 && el.ownerDocument.defaultView.getComputedStyle(el).visibility === 'visible';\n }\n if (filter.kind === 'has') return filter.selectors.some((chain) => descend(el, chain).length > 0);\n const text = textOf(el);\n return filter.kind === 'text-is' ? text === filter.text : text.toLowerCase().includes(filter.text);\n };\n const passesAll = (el, filters) =>\n filters.every((filter) => filter.kind !== 'visible' || passes(el, filter)) &&\n filters.every((filter) => filter.kind === 'visible' || passes(el, filter));\n const isUnder = (scope, node) => {\n for (let current = node; current; current = current.getRootNode().host) {\n if (scope === current || scope.contains(current)) return true;\n }\n return false;\n };\n const stepMatches = (scope, step) => {\n const matches = [...scope.querySelectorAll(':scope ' + step.combinator + ' ' + step.css)];\n if (step.combinator !== ' ') return matches;\n for (const root of shadowRoots) {\n if (isUnder(scope, root.host)) matches.push(...root.querySelectorAll(step.css));\n }\n return matches;\n };\n const descend = (scope, steps) => {\n let current = [scope];\n for (const step of steps) {\n const next = new Set();\n for (const el of current) {\n for (const match of stepMatches(el, step)) {\n if (passesAll(match, step.filters)) next.add(match);\n }\n }\n current = [...next];\n }\n return current;\n };\n return { passesAll: passesAll, descend: descend };\n})(shadowRoots);\n const result = new Set();\n for (const el of found) {\n for (const part of parts) {\n if (!el.matches(part.css) || !passesAll(el, part.filters)) continue;\n for (const match of descend(el, part.steps || [])) result.add(match);\n }\n }\n const rootOrder = new Map(roots.map((root, i) => [root, i]));\n const rootIndex = (el) => rootOrder.get(el.getRootNode()) ?? roots.length;\n return [...result].sort((a, b) =>\n rootIndex(a) - rootIndex(b) || (a.compareDocumentPosition(b) & 4 ? -1 : a === b ? 0 : 1)\n );\n})(selector, parts);\n}";
61
+ export declare const FIND_ELEMENTS_JS = "function (selector, parts) {\n if (selector === '__bdg_bound_target__') {\n const el = window.__bdgTarget;\n return el && el.isConnected ? [el] : [];\n }\n const bound = window.__bdgMatches;\n if (bound && bound.selector === selector) {\n const connected = [];\n for (let i = 0; i < bound.nodes.length; i++) {\n if (bound.nodes[i] && bound.nodes[i].isConnected) connected[connected.length] = bound.nodes[i];\n }\n return connected;\n }\n return (function (selector, parts) {\n const css = parts ? parts.map((part) => part.css).join(', ') : selector;\n const found = [];\n const roots = [];\n const visit = (root) => {\n roots.push(root);\n for (const match of root.querySelectorAll(css)) found.push(match);\n for (const el of root.querySelectorAll('*')) {\n if (el.shadowRoot) visit(el.shadowRoot);\n if (el.tagName === 'IFRAME' || el.tagName === 'FRAME') {\n let frameDocument = null;\n try { frameDocument = el.contentDocument; } catch (e) { frameDocument = null; }\n if (frameDocument) visit(frameDocument);\n }\n }\n };\n visit(document);\n if (!parts) return found;\n const shadowRoots = roots.filter((root) => root.host);\n const { passesAll, descend } = ((shadowRoots) => {\n const skipped = /^(script|style|noscript|template)$/;\n const hiddenText = (el) => {\n const walker = el.ownerDocument.createTreeWalker(el, 5, {\n acceptNode: (node) => (node.nodeType === 1 && skipped.test(node.localName) ? 2 : 1)\n });\n let text = '';\n for (let node = walker.nextNode(); node; node = walker.nextNode()) {\n if (node.nodeType === 3) text += node.data;\n }\n return text;\n };\n const textOf = (el) => {\n if (el.localName === 'input' && /^(submit|button|reset)$/i.test(el.type)) return el.value;\n const rendered = typeof el.innerText === 'string' &&\n (typeof el.checkVisibility !== 'function' || el.checkVisibility({ visibilityProperty: true }));\n return (rendered ? el.innerText : hiddenText(el)).replace(/\\s+/g, ' ').trim();\n };\n const passes = (el, filter) => {\n if (filter.kind === 'visible') {\n if (typeof el.checkVisibility === 'function' && !el.checkVisibility()) return false;\n const rect = el.getBoundingClientRect();\n return rect.width > 0 && rect.height > 0 && el.ownerDocument.defaultView.getComputedStyle(el).visibility === 'visible';\n }\n if (filter.kind === 'has') return filter.selectors.some((chain) => descend(el, chain).length > 0);\n const text = textOf(el);\n return filter.kind === 'text-is' ? text === filter.text : text.toLowerCase().includes(filter.text);\n };\n const passesAll = (el, filters) =>\n filters.every((filter) => filter.kind !== 'visible' || passes(el, filter)) &&\n filters.every((filter) => filter.kind === 'visible' || passes(el, filter));\n const isUnder = (scope, node) => {\n for (let current = node; current; current = current.getRootNode().host) {\n if (scope === current || scope.contains(current)) return true;\n }\n return false;\n };\n const stepMatches = (scope, step) => {\n const matches = [...scope.querySelectorAll(':scope ' + step.combinator + ' ' + step.css)];\n if (step.combinator !== ' ') return matches;\n for (const root of shadowRoots) {\n if (isUnder(scope, root.host)) matches.push(...root.querySelectorAll(step.css));\n }\n return matches;\n };\n const descend = (scope, steps) => {\n let current = [scope];\n for (const step of steps) {\n const next = new Set();\n for (const el of current) {\n for (const match of stepMatches(el, step)) {\n if (passesAll(match, step.filters)) next.add(match);\n }\n }\n current = [...next];\n }\n return current;\n };\n return { passesAll: passesAll, descend: descend };\n})(shadowRoots);\n const result = new Set();\n for (const el of found) {\n for (const part of parts) {\n if (!el.matches(part.css) || !passesAll(el, part.filters)) continue;\n for (const match of descend(el, part.steps || [])) result.add(match);\n }\n }\n const rootOrder = new Map(roots.map((root, i) => [root, i]));\n const rootIndex = (el) => rootOrder.get(el.getRootNode()) ?? roots.length;\n return [...result].sort((a, b) =>\n rootIndex(a) - rootIndex(b) || (a.compareDocumentPosition(b) & 4 ? -1 : a === b ? 0 : 1)\n );\n})(selector, parts);\n}";
60
62
  /**
61
63
  * Page-side check of what selectors cannot search in the top document:
62
64
  * iframes whose document cannot be read (cross-origin) and `<object>`/`<embed>`
@@ -94,23 +96,34 @@ export interface ScriptTarget {
94
96
  index?: number;
95
97
  /** Connection for the page scripts (scoped to the element's frame when the top page cannot reach it) */
96
98
  cdp: CDPConnection;
99
+ /** Built-ins the page replaced that the page scripts use (they run in the page's world) */
100
+ replacedBuiltins?: string[];
101
+ /** The page replaced the selector search, so the matches were found in bdg's world */
102
+ boundInBdgWorld?: boolean;
97
103
  }
98
104
  /**
99
105
  * Run an interaction on the element a request targets. Results and errors
100
106
  * never show the internal placeholder: a bound node the page scripts could
101
107
  * not find is reported as stale (87), and results carry the user's selector.
108
+ * On a page that replaced the selector search, a result warns that the
109
+ * element was found in bdg's world; on a page that replaced built-ins the
110
+ * scripts use, a failure adds that they may be the cause, and a script that
111
+ * threw is reported as broken by the page (90), naming them.
102
112
  *
103
113
  * @param cdp - CDP connection
104
114
  * @param params - Request with a selector (and optional index) or a backend node id
105
115
  * @param work - The interaction, given the script target
106
116
  * @returns The interaction's result
107
- * @throws CommandError (87) when the bound node left the page during the action
117
+ * @throws CommandError (87) when the bound node left the page during the
118
+ * action, (90) when its script threw on a page that replaced built-ins
108
119
  */
109
120
  export declare function onScriptTarget<T extends {
110
121
  selector?: string;
111
122
  error?: string;
112
123
  suggestion?: string;
113
124
  exitCode?: number;
125
+ warning?: string;
126
+ replacedBuiltins?: string[];
114
127
  }>(cdp: CDPConnection, params: {
115
128
  selector?: string;
116
129
  index?: number;
@@ -9,16 +9,86 @@
9
9
  * {@link BOUND_TARGET_SELECTOR}.
10
10
  */
11
11
  import { CommandError } from '../../errors/index.js';
12
- import { emptySelectorError, indexOutOfRangeError, noNodesFoundError, staleNodeError, } from '../../errors/messages.js';
12
+ import { actionBrokenByPageError, actionScriptFailedError, emptySelectorError, indexOutOfRangeError, noNodesFoundError, staleNodeError, } from '../../errors/messages.js';
13
+ import { ActionScriptError, throwIfInvalidSelector } from './formFillHelpers/shared.js';
13
14
  import { frameScopedConnection } from './frameScopedConnection.js';
15
+ import { evaluateInBdgWorld } from '../page/bdgWorld.js';
16
+ import { findReplacedBuiltins } from '../page/replacedBuiltins.js';
14
17
  import { createLogger } from '../../ui/logging/index.js';
18
+ import { brokenByReplacedBuiltinsSuggestion, replacedBuiltinsWarning, } from '../../ui/messages/commands.js';
15
19
  import { getErrorMessage } from '../../utils/errors.js';
16
20
  import { EXIT_CODES } from '../../utils/exitCodes.js';
17
21
  import { parseSelectorFilters, } from '../../utils/selectorFilters.js';
18
22
  /** Selector placeholder that makes page scripts use the bound node. */
19
23
  export const BOUND_TARGET_SELECTOR = '__bdg_bound_target__';
20
- /** Removes the node bound for index-based commands from the window it was stored on */
21
- export const UNBIND_TARGET_SCRIPT = 'delete window.__bdgTarget';
24
+ /** Removes the node bound for index-based commands, and matches bound for a selector, from the window they were stored on */
25
+ export const UNBIND_TARGET_SCRIPT = 'delete window.__bdgTarget; delete window.__bdgMatches';
26
+ /**
27
+ * Built-ins the selector search ({@link DEEP_QUERY_JS}) cannot do without:
28
+ * when the page replaced one, the search runs in bdg's world instead.
29
+ * Helpers that libraries replace with working versions (Prototype.js's
30
+ * `Array.prototype.map`) are left to {@link SCRIPT_BUILTINS}.
31
+ */
32
+ const SELECTION_BUILTINS = [
33
+ 'Element.prototype.querySelectorAll',
34
+ 'Element.prototype.querySelector',
35
+ 'Document.prototype.querySelectorAll',
36
+ 'Document.prototype.querySelector',
37
+ 'DocumentFragment.prototype.querySelectorAll',
38
+ 'Element.prototype.matches',
39
+ 'Element.prototype.closest',
40
+ 'Node.prototype.getRootNode',
41
+ 'Node.prototype.compareDocumentPosition',
42
+ 'Node.prototype.contains',
43
+ 'NodeList.prototype[Symbol.iterator]',
44
+ 'Array.prototype[Symbol.iterator]',
45
+ 'Array.prototype.push',
46
+ 'Set',
47
+ 'Map',
48
+ ];
49
+ /** Other built-ins the interaction scripts use */
50
+ const SCRIPT_BUILTINS = [
51
+ 'Array.from',
52
+ 'Array.prototype.map',
53
+ 'Array.prototype.filter',
54
+ 'Array.prototype.forEach',
55
+ 'Array.prototype.some',
56
+ 'Array.prototype.find',
57
+ 'Array.prototype.includes',
58
+ 'Object.keys',
59
+ 'Object.assign',
60
+ 'Object.getOwnPropertyDescriptor',
61
+ 'Function.prototype.call',
62
+ 'JSON.stringify',
63
+ 'JSON.parse',
64
+ ];
65
+ /** DOM built-ins the interaction scripts act through (anti-bot scripts often make them throw) */
66
+ const DOM_ACTION_BUILTINS = [
67
+ 'Element.prototype.getBoundingClientRect',
68
+ 'Element.prototype.getClientRects',
69
+ 'Element.prototype.scrollIntoView',
70
+ 'window.getComputedStyle',
71
+ 'Document.prototype.elementFromPoint',
72
+ 'Document.prototype.createEvent',
73
+ 'EventTarget.prototype.dispatchEvent',
74
+ 'HTMLElement.prototype.focus',
75
+ 'HTMLElement.prototype.blur',
76
+ 'HTMLElement.prototype.click',
77
+ 'Event',
78
+ 'MouseEvent',
79
+ 'HTMLInputElement.prototype.value',
80
+ 'HTMLTextAreaElement.prototype.value',
81
+ 'HTMLSelectElement.prototype.value',
82
+ ];
83
+ /** Matches bound for a selector when the page replaced the selector search built-ins (enough for --index) */
84
+ const MATCH_BIND_LIMIT = 100;
85
+ /** Page-side: stores a node as match `i` of the selector being bound on the top window (runs on the node, in its frame; a cross-origin frame cannot reach the top window and fails) */
86
+ const BIND_MATCH_FUNCTION = `function (selector, i) {
87
+ const w = window.top;
88
+ if (!w.__bdgMatches || w.__bdgMatches.selector !== selector) return false;
89
+ w.__bdgMatches.nodes[i] = this;
90
+ return true;
91
+ }`;
22
92
  const log = createLogger('dom');
23
93
  /**
24
94
  * Page-side matching of filters ({@link SelectorFilter}) and scoped steps
@@ -151,13 +221,23 @@ export const DEEP_QUERY_JS = `function (selector, parts) {
151
221
  * Page-side element lookup shared by the interaction scripts.
152
222
  *
153
223
  * Returns the bound node (if still in the page) for the placeholder selector,
154
- * otherwise all matches of the selector ({@link DEEP_QUERY_JS}).
224
+ * the matches bound for the selector when the page replaced the search
225
+ * built-ins ({@link bindMatches}), otherwise all matches of the selector
226
+ * ({@link DEEP_QUERY_JS}).
155
227
  */
156
228
  export const FIND_ELEMENTS_JS = `function (selector, parts) {
157
229
  if (selector === '${BOUND_TARGET_SELECTOR}') {
158
230
  const el = window.__bdgTarget;
159
231
  return el && el.isConnected ? [el] : [];
160
232
  }
233
+ const bound = window.__bdgMatches;
234
+ if (bound && bound.selector === selector) {
235
+ const connected = [];
236
+ for (let i = 0; i < bound.nodes.length; i++) {
237
+ if (bound.nodes[i] && bound.nodes[i].isConnected) connected[connected.length] = bound.nodes[i];
238
+ }
239
+ return connected;
240
+ }
161
241
  return (${DEEP_QUERY_JS})(selector, parts);
162
242
  }`;
163
243
  /**
@@ -269,13 +349,124 @@ async function bindTargetNode(cdp, backendNodeId) {
269
349
  */
270
350
  async function resolveScriptTarget(cdp, params) {
271
351
  if (params.backendNodeId === undefined) {
352
+ const selector = params.selector ?? '';
353
+ const replaced = await replacedBuiltins(cdp);
354
+ const searchReplaced = replaced.some((name) => SELECTION_BUILTINS.includes(name));
355
+ const bound = searchReplaced && (await bindMatches(cdp, selector, params.index ?? 0));
272
356
  return {
273
- selector: params.selector ?? '',
357
+ selector,
274
358
  ...(params.index !== undefined && { index: params.index }),
275
359
  cdp,
360
+ ...(replaced.length > 0 && { replacedBuiltins: replaced }),
361
+ ...(bound && { boundInBdgWorld: true }),
276
362
  };
277
363
  }
278
- return { selector: BOUND_TARGET_SELECTOR, cdp: await bindTargetNode(cdp, params.backendNodeId) };
364
+ const scriptCdp = await bindTargetNode(cdp, params.backendNodeId);
365
+ const replaced = await replacedBuiltins(scriptCdp);
366
+ return {
367
+ selector: BOUND_TARGET_SELECTOR,
368
+ cdp: scriptCdp,
369
+ ...(replaced.length > 0 && { replacedBuiltins: replaced }),
370
+ };
371
+ }
372
+ /**
373
+ * The built-ins the interaction scripts use that the page replaced.
374
+ *
375
+ * @param cdp - CDP connection
376
+ * @returns Their dotted names (empty when all are the browser's, or the check failed)
377
+ */
378
+ function replacedBuiltins(cdp) {
379
+ return findReplacedBuiltins(cdp, [
380
+ ...SELECTION_BUILTINS,
381
+ ...SCRIPT_BUILTINS,
382
+ ...DOM_ACTION_BUILTINS,
383
+ ]);
384
+ }
385
+ /**
386
+ * Search the selector in bdg's own world, where the page's replacements do
387
+ * not apply, and hand the matches (the first {@link MATCH_BIND_LIMIT}, or up
388
+ * to the index asked for) to the page scripts, which run in the page's world.
389
+ * When a match cannot be handed over (or the search fails other than on an
390
+ * invalid selector), nothing is bound and the scripts search themselves.
391
+ *
392
+ * @param cdp - CDP connection
393
+ * @param selector - Selector as the user gave it
394
+ * @param index - Match the action is for
395
+ * @returns Whether the matches were bound
396
+ * @throws CommandError (81) for an invalid selector
397
+ */
398
+ async function bindMatches(cdp, selector, index) {
399
+ const objectGroup = `bdg-bind-${Date.now()}`;
400
+ const limit = Math.max(MATCH_BIND_LIMIT, index + 1);
401
+ try {
402
+ const found = await evaluateInBdgWorld(cdp, {
403
+ expression: `(${DEEP_QUERY_JS})(${selectorArgsJS(selector)}).slice(0, ${limit})`,
404
+ objectGroup,
405
+ });
406
+ if (found.exceptionDetails)
407
+ throwIfInvalidSelector(found.exceptionDetails, selector);
408
+ const arrayId = found.result.objectId;
409
+ if (found.exceptionDetails || !arrayId)
410
+ return false;
411
+ const { result } = (await cdp.send('Runtime.getProperties', {
412
+ objectId: arrayId,
413
+ ownProperties: true,
414
+ }));
415
+ const matches = result
416
+ .filter((property) => /^\d+$/.test(property.name))
417
+ .map((property) => ({ i: Number(property.name), objectId: property.value?.objectId }));
418
+ await cdp.send('Runtime.evaluate', {
419
+ expression: `window.__bdgMatches = { selector: ${JSON.stringify(selector)}, nodes: [] }`,
420
+ });
421
+ const bound = await Promise.all(matches.map(({ i, objectId }) => objectId ? bindMatch(cdp, selector, i, objectId) : Promise.resolve(false)));
422
+ if (bound.every(Boolean))
423
+ return true;
424
+ await cdp.send('Runtime.evaluate', { expression: 'delete window.__bdgMatches' });
425
+ return false;
426
+ }
427
+ catch (error) {
428
+ if (error instanceof CommandError)
429
+ throw error;
430
+ log.debug(`Matches not bound: ${getErrorMessage(error)}`);
431
+ return false;
432
+ }
433
+ finally {
434
+ await cdp
435
+ .send('Runtime.releaseObjectGroup', { objectGroup })
436
+ .catch((error) => log.debug(`Matches not released: ${getErrorMessage(error)}`));
437
+ }
438
+ }
439
+ /**
440
+ * Hand one match from bdg's world to the page's world as match `i`.
441
+ *
442
+ * @param cdp - CDP connection
443
+ * @param selector - Selector the matches are for
444
+ * @param i - Its position among the matches
445
+ * @param objectId - The match in bdg's world
446
+ * @returns Whether it was handed over
447
+ */
448
+ async function bindMatch(cdp, selector, i, objectId) {
449
+ try {
450
+ const { node } = (await cdp.send('DOM.describeNode', {
451
+ objectId,
452
+ }));
453
+ const resolved = (await cdp.send('DOM.resolveNode', {
454
+ backendNodeId: node.backendNodeId,
455
+ }));
456
+ if (!resolved.object.objectId)
457
+ return false;
458
+ const stored = (await cdp.send('Runtime.callFunctionOn', {
459
+ objectId: resolved.object.objectId,
460
+ functionDeclaration: BIND_MATCH_FUNCTION,
461
+ arguments: [{ value: selector }, { value: i }],
462
+ returnByValue: true,
463
+ }));
464
+ return stored.result.value === true;
465
+ }
466
+ catch (error) {
467
+ log.debug(`Match ${i} not bound: ${getErrorMessage(error)}`);
468
+ return false;
469
+ }
279
470
  }
280
471
  /**
281
472
  * Whether an error is about the bound node: the placeholder only appears in
@@ -291,19 +482,28 @@ function boundNodeMissing(text) {
291
482
  * Run an interaction on the element a request targets. Results and errors
292
483
  * never show the internal placeholder: a bound node the page scripts could
293
484
  * not find is reported as stale (87), and results carry the user's selector.
485
+ * On a page that replaced the selector search, a result warns that the
486
+ * element was found in bdg's world; on a page that replaced built-ins the
487
+ * scripts use, a failure adds that they may be the cause, and a script that
488
+ * threw is reported as broken by the page (90), naming them.
294
489
  *
295
490
  * @param cdp - CDP connection
296
491
  * @param params - Request with a selector (and optional index) or a backend node id
297
492
  * @param work - The interaction, given the script target
298
493
  * @returns The interaction's result
299
- * @throws CommandError (87) when the bound node left the page during the action
494
+ * @throws CommandError (87) when the bound node left the page during the
495
+ * action, (90) when its script threw on a page that replaced built-ins
300
496
  */
301
497
  export async function onScriptTarget(cdp, params, work) {
302
498
  const err = staleNodeError();
303
499
  let target;
304
500
  try {
305
501
  target = await resolveScriptTarget(cdp, params);
306
- const result = withUserSelector(await work(target), params.selector);
502
+ const replaced = target.replacedBuiltins ?? [];
503
+ const result = withReplacedBuiltins(withUserSelector(await work(target), params.selector), target.boundInBdgWorld ? replaced : []);
504
+ if (result.error && replaced.length > 0) {
505
+ return { ...result, suggestion: withBrokenHint(result.suggestion, replaced) };
506
+ }
307
507
  if (!boundNodeMissing(result.error))
308
508
  return result;
309
509
  return {
@@ -314,6 +514,16 @@ export async function onScriptTarget(cdp, params, work) {
314
514
  };
315
515
  }
316
516
  catch (error) {
517
+ const replaced = target?.replacedBuiltins ?? [];
518
+ if (error instanceof ActionScriptError) {
519
+ throw actionScriptFailure(error, replaced, params.selector ?? '');
520
+ }
521
+ if (error instanceof CommandError && replaced.length > 0) {
522
+ const suggestion = error.metadata['suggestion'];
523
+ throw new CommandError(error.message, {
524
+ suggestion: withBrokenHint(typeof suggestion === 'string' ? suggestion : undefined, replaced),
525
+ }, error.exitCode);
526
+ }
317
527
  if (!(error instanceof CommandError) || !boundNodeMissing(error.message))
318
528
  throw error;
319
529
  throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.STALE_CACHE);
@@ -323,6 +533,24 @@ export async function onScriptTarget(cdp, params, work) {
323
533
  unbindInFrame(target.cdp);
324
534
  }
325
535
  }
536
+ /**
537
+ * The error for an action script that threw: on a page that replaced
538
+ * built-ins the scripts use, that the page broke it (90, naming them);
539
+ * otherwise what it threw (110), with the user's selector.
540
+ *
541
+ * @param error - What the action reported
542
+ * @param replaced - Built-ins the page replaced
543
+ * @param selector - Selector the user gave (or the cached query's selector)
544
+ * @returns Error to throw
545
+ */
546
+ function actionScriptFailure(error, replaced, selector) {
547
+ if (replaced.length > 0) {
548
+ const err = actionBrokenByPageError(error.action, error.exception, replaced);
549
+ return new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.RESOURCE_CONFLICT);
550
+ }
551
+ const err = actionScriptFailedError(error.action, error.exception, selector);
552
+ return new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.SOFTWARE_ERROR);
553
+ }
326
554
  /**
327
555
  * Remove a node bound in a cross-origin frame from that frame's window
328
556
  * (through the scoped connection, so the script runs where the bind ran; the
@@ -336,6 +564,38 @@ function unbindInFrame(frameConnection) {
336
564
  .send('Runtime.evaluate', { expression: UNBIND_TARGET_SCRIPT })
337
565
  .catch((error) => log.debug(`Frame target not unbound: ${getErrorMessage(error)}`));
338
566
  }
567
+ /**
568
+ * A failed action's suggestion with the hint that the page's replaced
569
+ * built-ins may have broken it.
570
+ *
571
+ * @param suggestion - The action's own suggestion
572
+ * @param replaced - Built-ins the page replaced
573
+ * @returns Both, the action's first
574
+ */
575
+ function withBrokenHint(suggestion, replaced) {
576
+ const hint = brokenByReplacedBuiltinsSuggestion(replaced);
577
+ return suggestion ? `${suggestion}. ${hint}` : hint;
578
+ }
579
+ /**
580
+ * Warn that the page replaced built-ins the page scripts use: they run in
581
+ * the page's world, so the action may misbehave (the element itself was
582
+ * found in bdg's world). The warning names the first few; `replacedBuiltins`
583
+ * lists them all.
584
+ *
585
+ * @param result - Script result
586
+ * @param replaced - Built-ins the page replaced
587
+ * @returns Result with the warning added to any it has
588
+ */
589
+ function withReplacedBuiltins(result, replaced) {
590
+ if (!replaced || replaced.length === 0)
591
+ return result;
592
+ const warning = replacedBuiltinsWarning(replaced);
593
+ return {
594
+ ...result,
595
+ warning: result.warning ? `${result.warning}; ${warning}` : warning,
596
+ replacedBuiltins: replaced,
597
+ };
598
+ }
339
599
  /**
340
600
  * Report the user's selector instead of the internal placeholder.
341
601
  *
@@ -15,6 +15,7 @@ import { invalidSelectorError, waitTimeoutError } from '../../errors/messages.js
15
15
  import { isContextLostError } from './evalHelpers.js';
16
16
  import { DEEP_QUERY_JS, FILTER_MATCHING_JS, selectorArgsJS } from './targetNode.js';
17
17
  import { isWaitConditionMet, needsGoneConfirmation, normalizeWaitText, } from './waitCondition.js';
18
+ import { evaluateInBdgWorld } from '../page/bdgWorld.js';
18
19
  import { createLogger } from '../../ui/logging/index.js';
19
20
  import { delay, raceTimeout } from '../../utils/async.js';
20
21
  import { getErrorMessage } from '../../utils/errors.js';
@@ -157,7 +158,7 @@ async function nextSnapshot(cdp, condition, previous, deadline) {
157
158
  const selectorArgs = condition.selector === undefined ? 'null, null' : selectorArgsJS(condition.selector);
158
159
  const text = condition.text === undefined ? null : normalizeWaitText(condition.text);
159
160
  const args = `${selectorArgs}, ${JSON.stringify(text)}, ${JSON.stringify(previous)}, ${Math.min(remaining, WAIT_SLICE_MS)}`;
160
- const evaluated = cdp.send('Runtime.evaluate', {
161
+ const evaluated = evaluateInBdgWorld(cdp, {
161
162
  expression: `(${WAIT_SNAPSHOT_JS})(${args})`,
162
163
  awaitPromise: true,
163
164
  returnByValue: true,
@@ -0,0 +1,57 @@
1
+ /**
2
+ * bdg's own JavaScript world in the page (a CDP isolated world): it shares
3
+ * the DOM with the page but has its own built-ins, so a page that replaces
4
+ * `Element.prototype.querySelectorAll`, `JSON.stringify` or
5
+ * `Array.prototype.map` (polyfills, old frameworks, anti-bot scripts) cannot
6
+ * change what bdg's page scripts find or return.
7
+ *
8
+ * Only the entry points need the world: `Runtime.evaluate` runs in it with
9
+ * its `contextId`, and `DOM.resolveNode` hands out objects of it with its
10
+ * `executionContextId`; `Runtime.callFunctionOn` on such an object runs in
11
+ * the same world. The world belongs to the top frame (same-origin iframes are
12
+ * reached through it); a node of another frame, a connection without the
13
+ * Page domain (a frame-scoped one) or a failure to create it fall back to the
14
+ * main world, as before.
15
+ */
16
+ import type { CDPConnection } from '../../connection/cdp.js';
17
+ import type { Protocol } from '../../connection/typed-cdp.js';
18
+ /**
19
+ * What the world needs of a connection: commands, and events to forget the
20
+ * world when the page navigates (a sender without events, such as a test
21
+ * double, runs scripts in the main world)
22
+ */
23
+ type PageConnection = Pick<CDPConnection, 'send'> & Partial<Pick<CDPConnection, 'on'>>;
24
+ /** Name of bdg's isolated world (shown in DevTools' context selector) */
25
+ export declare const BDG_WORLD_NAME = "bdg";
26
+ /**
27
+ * `Runtime.evaluate` in bdg's world of the top frame. A call that names its
28
+ * own context is sent as it is.
29
+ *
30
+ * @param cdp - Connection to the page
31
+ * @param params - Evaluate parameters
32
+ * @returns Evaluate response
33
+ */
34
+ export declare function evaluateInBdgWorld(cdp: PageConnection, params: Protocol.Runtime.EvaluateRequest): Promise<Protocol.Runtime.EvaluateResponse>;
35
+ /**
36
+ * `DOM.resolveNode` into bdg's world of the top frame, so functions called on
37
+ * the node run there. A node the world cannot use (one of a cross-origin
38
+ * frame, or one no longer in the page) is resolved in its own frame's main
39
+ * world, as before.
40
+ *
41
+ * @param cdp - Connection to the page
42
+ * @param params - Resolve parameters
43
+ * @returns Resolve response
44
+ */
45
+ export declare function resolveNodeInBdgWorld(cdp: PageConnection, params: Protocol.DOM.ResolveNodeRequest): Promise<Protocol.DOM.ResolveNodeResponse>;
46
+ /**
47
+ * Send a CDP command for bdg's own scripts: `Runtime.evaluate` and
48
+ * `DOM.resolveNode` go to bdg's world, other methods as they are.
49
+ *
50
+ * @param cdp - Connection to the page
51
+ * @param method - CDP method
52
+ * @param params - Its parameters
53
+ * @returns The command's result
54
+ */
55
+ export declare function sendForBdgScript(cdp: PageConnection, method: string, params: Record<string, unknown>): Promise<unknown>;
56
+ export {};
57
+ //# sourceMappingURL=bdgWorld.d.ts.map