browser-debugger-cli 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (304) hide show
  1. package/README.md +7 -1
  2. package/dist/cdp/schema.d.ts +4 -1
  3. package/dist/cdp/schema.js +48 -7
  4. package/dist/commands/cdp.js +3 -2
  5. package/dist/commands/cleanup.d.ts +11 -0
  6. package/dist/commands/cleanup.js +161 -57
  7. package/dist/commands/console.d.ts +20 -1
  8. package/dist/commands/console.js +57 -17
  9. package/dist/commands/details.js +3 -2
  10. package/dist/commands/dom/DomElementResolver.d.ts +10 -3
  11. package/dist/commands/dom/DomElementResolver.js +35 -17
  12. package/dist/commands/dom/a11y.d.ts +10 -0
  13. package/dist/commands/dom/a11y.js +29 -6
  14. package/dist/commands/dom/eval.d.ts +3 -1
  15. package/dist/commands/dom/eval.js +29 -4
  16. package/dist/commands/dom/form.js +16 -62
  17. package/dist/commands/dom/formInteraction.js +189 -119
  18. package/dist/commands/dom/formSummary.d.ts +49 -0
  19. package/dist/commands/dom/formSummary.js +180 -0
  20. package/dist/commands/dom/frames.d.ts +2 -1
  21. package/dist/commands/dom/frames.js +17 -2
  22. package/dist/commands/dom/get.d.ts +6 -5
  23. package/dist/commands/dom/get.js +92 -82
  24. package/dist/commands/dom/helpers/index.d.ts +1 -1
  25. package/dist/commands/dom/helpers/index.js +1 -1
  26. package/dist/commands/dom/helpers/keyAttributes.d.ts +20 -0
  27. package/dist/commands/dom/helpers/keyAttributes.js +54 -0
  28. package/dist/commands/dom/helpers/query.d.ts +44 -17
  29. package/dist/commands/dom/helpers/query.js +300 -106
  30. package/dist/commands/dom/helpers/runElementCommand.d.ts +10 -2
  31. package/dist/commands/dom/helpers/runElementCommand.js +98 -30
  32. package/dist/commands/dom/helpers/screenshot.d.ts +4 -1
  33. package/dist/commands/dom/helpers/screenshot.js +239 -51
  34. package/dist/commands/dom/index.d.ts +4 -1
  35. package/dist/commands/dom/index.js +22 -7
  36. package/dist/commands/dom/inspect.d.ts +15 -0
  37. package/dist/commands/dom/inspect.js +82 -0
  38. package/dist/commands/dom/layout.d.ts +14 -0
  39. package/dist/commands/dom/layout.js +54 -0
  40. package/dist/commands/dom/listeners.d.ts +5 -1
  41. package/dist/commands/dom/listeners.js +15 -5
  42. package/dist/commands/dom/query.js +2 -3
  43. package/dist/commands/dom/screenshot.d.ts +12 -2
  44. package/dist/commands/dom/screenshot.js +27 -3
  45. package/dist/commands/dom/semanticUtils.d.ts +16 -10
  46. package/dist/commands/dom/semanticUtils.js +53 -16
  47. package/dist/commands/dom/wait.d.ts +13 -0
  48. package/dist/commands/dom/wait.js +83 -0
  49. package/dist/commands/helpJson.js +2 -2
  50. package/dist/commands/network/list.js +17 -13
  51. package/dist/commands/optionBehaviors.js +154 -21
  52. package/dist/commands/page.d.ts +3 -2
  53. package/dist/commands/page.js +100 -5
  54. package/dist/commands/peek.js +4 -11
  55. package/dist/commands/sessions.d.ts +8 -0
  56. package/dist/commands/sessions.js +19 -0
  57. package/dist/commands/shared/CommandRunner.js +4 -4
  58. package/dist/commands/shared/commonOptions.d.ts +4 -0
  59. package/dist/commands/shared/commonOptions.js +9 -0
  60. package/dist/commands/shared/dataFetcher.js +2 -2
  61. package/dist/commands/shared/followMode.d.ts +21 -1
  62. package/dist/commands/shared/followMode.js +29 -2
  63. package/dist/commands/shared/handleValidationError.d.ts +2 -2
  64. package/dist/commands/shared/handleValidationError.js +12 -3
  65. package/dist/commands/shared/optionTypes.d.ts +61 -5
  66. package/dist/commands/shared/startHelpers.d.ts +66 -0
  67. package/dist/commands/shared/startHelpers.js +103 -13
  68. package/dist/commands/shared/validation.d.ts +14 -2
  69. package/dist/commands/shared/validation.js +20 -3
  70. package/dist/commands/start.d.ts +63 -0
  71. package/dist/commands/start.js +115 -15
  72. package/dist/commands/status.js +29 -7
  73. package/dist/commands/stop.js +7 -6
  74. package/dist/commands/tail.js +4 -11
  75. package/dist/commands/types.d.ts +2 -0
  76. package/dist/commands.js +2 -0
  77. package/dist/connection/chromeIdentity.d.ts +65 -0
  78. package/dist/connection/chromeIdentity.js +143 -0
  79. package/dist/connection/launcher/profilePreferences.d.ts +47 -0
  80. package/dist/connection/launcher/profilePreferences.js +151 -0
  81. package/dist/connection/launcher.d.ts +21 -2
  82. package/dist/connection/launcher.js +42 -16
  83. package/dist/connection/portReservation.d.ts +14 -4
  84. package/dist/connection/portReservation.js +21 -6
  85. package/dist/connection/startupExit.d.ts +8 -0
  86. package/dist/connection/startupExit.js +15 -6
  87. package/dist/constants.d.ts +6 -2
  88. package/dist/constants.js +9 -2
  89. package/dist/daemon/SessionController.js +23 -7
  90. package/dist/daemon/errors.d.ts +1 -1
  91. package/dist/daemon/errors.js +1 -1
  92. package/dist/daemon/launcher.d.ts +10 -2
  93. package/dist/daemon/launcher.js +8 -7
  94. package/dist/daemon/server/SocketServer.js +1 -2
  95. package/dist/daemon/session/Session.d.ts +20 -0
  96. package/dist/daemon/session/Session.js +80 -9
  97. package/dist/daemon/session/chromeConnection.d.ts +9 -0
  98. package/dist/daemon/session/chromeConnection.js +45 -8
  99. package/dist/daemon/session/commandRegistry.d.ts +14 -1
  100. package/dist/daemon/session/commandRegistry.js +113 -67
  101. package/dist/daemon/session/interactions.d.ts +48 -9
  102. package/dist/daemon/session/interactions.js +46 -9
  103. package/dist/daemon/session/triggeredRequests.d.ts +67 -0
  104. package/dist/daemon/session/triggeredRequests.js +157 -0
  105. package/dist/daemon/session/types.d.ts +5 -1
  106. package/dist/daemon.js +10630 -3601
  107. package/dist/errors/messages.d.ts +456 -24
  108. package/dist/errors/messages.js +862 -67
  109. package/dist/index.js +6915 -3401
  110. package/dist/ipc/client.d.ts +21 -1
  111. package/dist/ipc/client.js +35 -3
  112. package/dist/ipc/protocol/commands.d.ts +145 -5
  113. package/dist/ipc/protocol/commands.js +4 -0
  114. package/dist/ipc/protocol/domTypes.d.ts +291 -7
  115. package/dist/ipc/protocol/inspectTypes.d.ts +388 -0
  116. package/dist/ipc/protocol/inspectTypes.js +10 -0
  117. package/dist/ipc/session/lifecycle.d.ts +8 -1
  118. package/dist/ipc/session/queries.d.ts +5 -1
  119. package/dist/ipc/session/types.d.ts +5 -0
  120. package/dist/ipc/transport/index.d.ts +2 -1
  121. package/dist/ipc/transport/index.js +2 -2
  122. package/dist/runtime/dom/actionEffects.d.ts +185 -0
  123. package/dist/runtime/dom/actionEffects.js +402 -0
  124. package/dist/runtime/dom/actionEffectsScripts.d.ts +90 -0
  125. package/dist/runtime/dom/actionEffectsScripts.js +426 -0
  126. package/dist/runtime/dom/elementGeometry.d.ts +170 -0
  127. package/dist/runtime/dom/elementGeometry.js +553 -0
  128. package/dist/runtime/dom/elementInfo.d.ts +103 -0
  129. package/dist/runtime/dom/elementInfo.js +256 -0
  130. package/dist/runtime/dom/evalHelpers.d.ts +51 -6
  131. package/dist/runtime/dom/evalHelpers.js +136 -26
  132. package/dist/runtime/dom/eventListeners.d.ts +2 -1
  133. package/dist/runtime/dom/eventListeners.js +184 -47
  134. package/dist/runtime/dom/formDiscovery.d.ts +1 -1
  135. package/dist/runtime/dom/formDiscovery.js +116 -16
  136. package/dist/runtime/dom/formFillHelpers/fill.d.ts +9 -0
  137. package/dist/runtime/dom/formFillHelpers/fill.js +178 -14
  138. package/dist/runtime/dom/formFillHelpers/index.d.ts +2 -2
  139. package/dist/runtime/dom/formFillHelpers/index.js +2 -2
  140. package/dist/runtime/dom/formFillHelpers/pressKey.js +14 -3
  141. package/dist/runtime/dom/formFillHelpers/scroll.d.ts +3 -0
  142. package/dist/runtime/dom/formFillHelpers/scroll.js +60 -18
  143. package/dist/runtime/dom/formFillHelpers/shared.d.ts +17 -0
  144. package/dist/runtime/dom/formFillHelpers/shared.js +25 -1
  145. package/dist/runtime/dom/formFillHelpers/stability.d.ts +20 -6
  146. package/dist/runtime/dom/formFillHelpers/stability.js +50 -19
  147. package/dist/runtime/dom/formSubmitHelpers.d.ts +3 -0
  148. package/dist/runtime/dom/formSubmitHelpers.js +89 -15
  149. package/dist/runtime/dom/frameLayout.d.ts +60 -0
  150. package/dist/runtime/dom/frameLayout.js +140 -0
  151. package/dist/runtime/dom/frameOrigin.d.ts +50 -0
  152. package/dist/runtime/dom/frameOrigin.js +62 -0
  153. package/dist/runtime/dom/frameScopedConnection.d.ts +92 -0
  154. package/dist/runtime/dom/frameScopedConnection.js +252 -0
  155. package/dist/runtime/dom/frameSelection.d.ts +12 -1
  156. package/dist/runtime/dom/frameSelection.js +22 -3
  157. package/dist/runtime/dom/frames.d.ts +61 -5
  158. package/dist/runtime/dom/frames.js +329 -75
  159. package/dist/runtime/dom/inspect.d.ts +28 -0
  160. package/dist/runtime/dom/inspect.js +557 -0
  161. package/dist/runtime/dom/inspectAllStyles.d.ts +62 -0
  162. package/dist/runtime/dom/inspectAllStyles.js +385 -0
  163. package/dist/runtime/dom/inspectCascade.d.ts +94 -0
  164. package/dist/runtime/dom/inspectCascade.js +371 -0
  165. package/dist/runtime/dom/inspectCascadeModel.d.ts +39 -0
  166. package/dist/runtime/dom/inspectCascadeModel.js +232 -0
  167. package/dist/runtime/dom/inspectHints.d.ts +62 -0
  168. package/dist/runtime/dom/inspectHints.js +305 -0
  169. package/dist/runtime/dom/inspectLayoutModel.d.ts +87 -0
  170. package/dist/runtime/dom/inspectLayoutModel.js +346 -0
  171. package/dist/runtime/dom/inspectModel.d.ts +74 -0
  172. package/dist/runtime/dom/inspectModel.js +184 -0
  173. package/dist/runtime/dom/inspectPaintModel.d.ts +157 -0
  174. package/dist/runtime/dom/inspectPaintModel.js +461 -0
  175. package/dist/runtime/dom/inspectRules.d.ts +37 -0
  176. package/dist/runtime/dom/inspectRules.js +101 -0
  177. package/dist/runtime/dom/inspectScripts.d.ts +132 -0
  178. package/dist/runtime/dom/inspectScripts.js +263 -0
  179. package/dist/runtime/dom/inspectTree.d.ts +40 -0
  180. package/dist/runtime/dom/inspectTree.js +134 -0
  181. package/dist/runtime/dom/inspectVariables.d.ts +33 -0
  182. package/dist/runtime/dom/inspectVariables.js +94 -0
  183. package/dist/runtime/dom/inspectWhyModel.d.ts +20 -0
  184. package/dist/runtime/dom/inspectWhyModel.js +134 -0
  185. package/dist/runtime/dom/layout.d.ts +71 -0
  186. package/dist/runtime/dom/layout.js +340 -0
  187. package/dist/runtime/dom/listenerPageScripts.d.ts +72 -0
  188. package/dist/runtime/dom/listenerPageScripts.js +365 -0
  189. package/dist/runtime/dom/listenerSummary.d.ts +136 -11
  190. package/dist/runtime/dom/listenerSummary.js +361 -22
  191. package/dist/runtime/dom/pageActivity.d.ts +41 -0
  192. package/dist/runtime/dom/pageActivity.js +123 -0
  193. package/dist/runtime/dom/reactEventHelpers.d.ts +63 -2
  194. package/dist/runtime/dom/reactEventHelpers.js +220 -41
  195. package/dist/runtime/dom/targetNode.d.ts +80 -27
  196. package/dist/runtime/dom/targetNode.js +249 -33
  197. package/dist/runtime/dom/wait.d.ts +25 -0
  198. package/dist/runtime/dom/wait.js +199 -0
  199. package/dist/runtime/dom/waitCondition.d.ts +71 -0
  200. package/dist/runtime/dom/waitCondition.js +75 -0
  201. package/dist/runtime/page/emulation.d.ts +71 -0
  202. package/dist/runtime/page/emulation.js +117 -0
  203. package/dist/runtime/page/loadingState.d.ts +36 -0
  204. package/dist/runtime/page/loadingState.js +86 -0
  205. package/dist/runtime/page/navigation.d.ts +46 -2
  206. package/dist/runtime/page/navigation.js +69 -33
  207. package/dist/session/QueryCacheManager.d.ts +11 -1
  208. package/dist/session/QueryCacheManager.js +25 -3
  209. package/dist/session/chromeOwners.d.ts +34 -0
  210. package/dist/session/chromeOwners.js +51 -0
  211. package/dist/session/cleanup/staleSession.d.ts +11 -1
  212. package/dist/session/cleanup/staleSession.js +17 -6
  213. package/dist/session/cleanup/userCommands.js +2 -4
  214. package/dist/session/metadata.d.ts +5 -1
  215. package/dist/session/metadata.js +2 -1
  216. package/dist/session/paths.d.ts +77 -3
  217. package/dist/session/paths.js +111 -5
  218. package/dist/session/port.d.ts +31 -7
  219. package/dist/session/port.js +50 -43
  220. package/dist/session/portClaims.d.ts +66 -0
  221. package/dist/session/portClaims.js +284 -0
  222. package/dist/session/sessionList.d.ts +58 -0
  223. package/dist/session/sessionList.js +199 -0
  224. package/dist/session/sessionName.d.ts +46 -0
  225. package/dist/session/sessionName.js +97 -0
  226. package/dist/telemetry/a11y.d.ts +18 -3
  227. package/dist/telemetry/a11y.js +170 -29
  228. package/dist/telemetry/console.d.ts +1 -0
  229. package/dist/telemetry/console.js +100 -5
  230. package/dist/telemetry/network.js +3 -1
  231. package/dist/telemetry/requestKinds.d.ts +32 -0
  232. package/dist/telemetry/requestKinds.js +61 -0
  233. package/dist/telemetry/requestState.d.ts +31 -0
  234. package/dist/telemetry/requestState.js +38 -0
  235. package/dist/types.d.ts +112 -3
  236. package/dist/ui/formatters/a11y.js +3 -0
  237. package/dist/ui/formatters/console/chronological.d.ts +8 -0
  238. package/dist/ui/formatters/console/chronological.js +17 -4
  239. package/dist/ui/formatters/console/json.js +3 -4
  240. package/dist/ui/formatters/console/shared.d.ts +12 -0
  241. package/dist/ui/formatters/console.d.ts +2 -2
  242. package/dist/ui/formatters/console.js +1 -1
  243. package/dist/ui/formatters/details.d.ts +8 -0
  244. package/dist/ui/formatters/details.js +61 -4
  245. package/dist/ui/formatters/dom.d.ts +27 -14
  246. package/dist/ui/formatters/dom.js +88 -59
  247. package/dist/ui/formatters/form.js +29 -18
  248. package/dist/ui/formatters/inspect.d.ts +39 -0
  249. package/dist/ui/formatters/inspect.js +596 -0
  250. package/dist/ui/formatters/keyAttributes.d.ts +19 -0
  251. package/dist/ui/formatters/keyAttributes.js +84 -0
  252. package/dist/ui/formatters/layout.d.ts +31 -0
  253. package/dist/ui/formatters/layout.js +53 -0
  254. package/dist/ui/formatters/listeners.d.ts +3 -2
  255. package/dist/ui/formatters/listeners.js +73 -9
  256. package/dist/ui/formatters/networkHeaders.d.ts +13 -0
  257. package/dist/ui/formatters/networkHeaders.js +36 -3
  258. package/dist/ui/formatters/networkList.d.ts +29 -1
  259. package/dist/ui/formatters/networkList.js +86 -20
  260. package/dist/ui/formatters/preview.js +2 -1
  261. package/dist/ui/formatters/requestStatus.d.ts +1 -17
  262. package/dist/ui/formatters/requestStatus.js +2 -30
  263. package/dist/ui/formatters/sessions.d.ts +12 -0
  264. package/dist/ui/formatters/sessions.js +40 -0
  265. package/dist/ui/formatters/status.d.ts +21 -2
  266. package/dist/ui/formatters/status.js +47 -10
  267. package/dist/ui/formatters/triggeredRequests.d.ts +36 -0
  268. package/dist/ui/formatters/triggeredRequests.js +65 -0
  269. package/dist/ui/formatting.d.ts +19 -0
  270. package/dist/ui/formatting.js +31 -36
  271. package/dist/ui/messages/chrome.d.ts +9 -0
  272. package/dist/ui/messages/chrome.js +17 -5
  273. package/dist/ui/messages/commands.d.ts +504 -14
  274. package/dist/ui/messages/commands.js +835 -21
  275. package/dist/ui/messages/consoleMessages.d.ts +10 -0
  276. package/dist/ui/messages/consoleMessages.js +17 -0
  277. package/dist/ui/messages/hints.js +2 -1
  278. package/dist/ui/messages/networkMessages.d.ts +14 -0
  279. package/dist/ui/messages/networkMessages.js +18 -0
  280. package/dist/ui/messages/preview.js +5 -4
  281. package/dist/ui/messages/session.d.ts +30 -21
  282. package/dist/ui/messages/session.js +48 -26
  283. package/dist/ui/messages/sessionCommand.d.ts +43 -0
  284. package/dist/ui/messages/sessionCommand.js +52 -0
  285. package/dist/utils/async.d.ts +17 -0
  286. package/dist/utils/async.js +36 -0
  287. package/dist/utils/color.d.ts +84 -0
  288. package/dist/utils/color.js +376 -0
  289. package/dist/utils/cssValues.d.ts +109 -0
  290. package/dist/utils/cssValues.js +236 -0
  291. package/dist/utils/http.d.ts +22 -1
  292. package/dist/utils/http.js +28 -9
  293. package/dist/utils/selectorFilters.d.ts +48 -8
  294. package/dist/utils/selectorFilters.js +296 -53
  295. package/dist/utils/shellDetection.d.ts +8 -2
  296. package/dist/utils/shellDetection.js +120 -33
  297. package/dist/utils/suggestions.d.ts +26 -0
  298. package/dist/utils/suggestions.js +73 -0
  299. package/dist/utils/taskMappings.js +10 -0
  300. package/dist/utils/url.d.ts +12 -2
  301. package/dist/utils/url.js +69 -7
  302. package/package.json +1 -1
  303. package/dist/ui/formatters/sessionFormatters.d.ts +0 -58
  304. package/dist/ui/formatters/sessionFormatters.js +0 -121
@@ -9,27 +9,75 @@
9
9
  * {@link BOUND_TARGET_SELECTOR}.
10
10
  */
11
11
  import type { CDPConnection } from '../../connection/cdp.js';
12
+ import { CommandError } from '../../errors/index.js';
12
13
  /** Selector placeholder that makes page scripts use the bound node. */
13
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";
17
+ /**
18
+ * Page-side matching of filters ({@link SelectorFilter}) and scoped steps
19
+ * ({@link ScopedStep}): called with the open shadow roots of the page, returns
20
+ * `{ passesAll, descend }`.
21
+ *
22
+ * Visible means rendered (`checkVisibility()`: not inside a closed
23
+ * `<details>` or under `content-visibility: hidden`), a non-empty box and
24
+ * `visibility: visible`, as in Playwright (`opacity: 0` still counts as
25
+ * visible). Text is the rendered text (`innerText`) of visible elements and
26
+ * the text nodes of hidden ones (display or visibility; not those of
27
+ * `<script>`, `<style>` or `<noscript>`), so text filters match hidden
28
+ * elements like Playwright's and `:visible` decides visibility; button inputs
29
+ * use their value. Whitespace is collapsed; filter texts arrive normalized
30
+ * (`has-text` lowercased). `:visible` is checked before the text filters,
31
+ * which read the text.
32
+ *
33
+ * Steps and `:has()` match CSS relative to an element (`:scope > css`). A
34
+ * descendant step also searches the open shadow roots under the element (the
35
+ * whole step CSS inside one shadow tree); a child step stays in the
36
+ * element's own tree.
37
+ */
38
+ export declare const FILTER_MATCHING_JS = "(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}";
14
39
  /**
15
40
  * Page-side selector search that also reaches into open shadow roots and
16
41
  * same-origin iframes (recursively), like a user sees the page.
17
42
  *
18
43
  * Takes the selector and, when it has text or visibility filters, its parts
19
44
  * from {@link parseSelectorFilters}: their CSS runs as one selector list and
20
- * a match is kept when it passes the filters of a part it matches.
21
- * Matches of the document come first, then those of each shadow root and
22
- * frame in document order. Cross-origin iframes are separate processes and
23
- * cannot be searched. Throws a `SyntaxError` for an invalid selector.
45
+ * a match is kept when it passes the filters of a part it matches; a part
46
+ * with scoped steps contributes the steps' matches under it instead (each
47
+ * element once). Matches come in document order: those of the document
48
+ * first, then those of each shadow root and frame in the order they are
49
+ * reached. Cross-origin iframes are separate processes and cannot be
50
+ * searched. Throws a `SyntaxError` for an invalid selector.
24
51
  */
25
- export declare const DEEP_QUERY_JS = "function (selector, parts) {\n const css = parts ? parts.map((part) => part.css).join(', ') : selector;\n const found = [];\n const visit = (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 passes = (el, filter) => {\n if (filter.kind === 'visible') {\n const rect = el.getBoundingClientRect();\n return rect.width > 0 && rect.height > 0 && el.ownerDocument.defaultView.getComputedStyle(el).visibility === 'visible';\n }\n const text = (typeof el.innerText === 'string' ? el.innerText : el.textContent || '').replace(/\\s+/g, ' ').trim();\n return filter.kind === 'text-is' ? text === filter.text : text.toLowerCase().includes(filter.text);\n};\n return found.filter((el) => parts.some((part) => el.matches(part.css) && part.filters.every((filter) => passes(el, filter))));\n}";
52
+ export declare const DEEP_QUERY_JS = "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}";
26
53
  /**
27
54
  * Page-side element lookup shared by the interaction scripts.
28
55
  *
29
56
  * Returns the bound node (if still in the page) for the placeholder selector,
30
57
  * otherwise all matches of the selector ({@link DEEP_QUERY_JS}).
31
58
  */
32
- 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 visit = (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 passes = (el, filter) => {\n if (filter.kind === 'visible') {\n const rect = el.getBoundingClientRect();\n return rect.width > 0 && rect.height > 0 && el.ownerDocument.defaultView.getComputedStyle(el).visibility === 'visible';\n }\n const text = (typeof el.innerText === 'string' ? el.innerText : el.textContent || '').replace(/\\s+/g, ' ').trim();\n return filter.kind === 'text-is' ? text === filter.text : text.toLowerCase().includes(filter.text);\n};\n return found.filter((el) => parts.some((part) => el.matches(part.css) && part.filters.every((filter) => passes(el, filter))));\n})(selector, parts);\n}";
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}";
60
+ /**
61
+ * Page-side check of what selectors cannot search in the top document:
62
+ * iframes whose document cannot be read (cross-origin) and `<object>`/`<embed>`
63
+ * elements. Cheap: no frame is entered.
64
+ */
65
+ export declare const UNSEARCHED_CONTENT_JS = "(() => {\n const frames = Array.from(document.querySelectorAll('iframe, frame'));\n const unreadable = (frame) => {\n try { return !frame.contentDocument; } catch (e) { return true; }\n };\n return { crossOriginFrames: frames.some(unreadable), embeds: !!document.querySelector('object, embed') };\n})()";
66
+ /**
67
+ * Page-side ids (`kind` "id") or classes (`kind` "class") in the top
68
+ * document, from the first {@link NAME_SCAN_LIMIT} elements that have one.
69
+ *
70
+ * @param kind - Which names to read
71
+ * @returns Expression evaluating to the distinct names
72
+ */
73
+ export declare function pageNamesJS(kind: 'id' | 'class'): string;
74
+ /**
75
+ * Page-side: the form control a `<label>` stands for (`label.control`: its
76
+ * `for` target or the control inside it), or null for other elements and
77
+ * labels without one. Fill, click and key presses act on that control, like
78
+ * Playwright.
79
+ */
80
+ export declare const LABEL_CONTROL_JS = "(el) => (el && el.localName === 'label' ? el.control : null)";
33
81
  /**
34
82
  * Arguments for {@link DEEP_QUERY_JS} / {@link FIND_ELEMENTS_JS} and the page
35
83
  * scripts built on them: the selector as a JS string literal and its parts
@@ -37,42 +85,47 @@ export declare const FIND_ELEMENTS_JS = "function (selector, parts) {\n if (sel
37
85
  *
38
86
  * @param selector - Selector as the user gave it (or the bound-node placeholder)
39
87
  * @returns JS source of the two arguments, e.g. `"li:visible", [{...}]`
40
- * @throws CommandError (81) for a misplaced or malformed filter
88
+ * @throws CommandError (81) for an empty selector or a misplaced or malformed filter
41
89
  */
42
90
  export declare function selectorArgsJS(selector: string): string;
43
- /** Selector and index the page scripts should use. */
91
+ /** Selector and index the page scripts should use, and the connection to run them on. */
44
92
  export interface ScriptTarget {
45
93
  selector: string;
46
94
  index?: number;
95
+ /** Connection for the page scripts (scoped to the element's frame when the top page cannot reach it) */
96
+ cdp: CDPConnection;
47
97
  }
48
98
  /**
49
- * Make the node with `backendNodeId` the target of the next page script.
50
- *
51
- * @param cdp - CDP connection
52
- * @param backendNodeId - Backend node id from the query cache
53
- * @throws CommandError (exit 87) when the node no longer exists in the page
54
- */
55
- export declare function bindTargetNode(cdp: CDPConnection, backendNodeId: number): Promise<void>;
56
- /**
57
- * Resolve what a page script should target for an interaction request.
99
+ * Run an interaction on the element a request targets. Results and errors
100
+ * never show the internal placeholder: a bound node the page scripts could
101
+ * not find is reported as stale (87), and results carry the user's selector.
58
102
  *
59
103
  * @param cdp - CDP connection
60
104
  * @param params - Request with a selector (and optional index) or a backend node id
61
- * @returns Selector/index for the page script
105
+ * @param work - The interaction, given the script target
106
+ * @returns The interaction's result
107
+ * @throws CommandError (87) when the bound node left the page during the action
62
108
  */
63
- export declare function resolveScriptTarget(cdp: CDPConnection, params: {
109
+ export declare function onScriptTarget<T extends {
110
+ selector?: string;
111
+ error?: string;
112
+ suggestion?: string;
113
+ exitCode?: number;
114
+ }>(cdp: CDPConnection, params: {
64
115
  selector?: string;
65
116
  index?: number;
66
117
  backendNodeId?: number;
67
- }): Promise<ScriptTarget>;
118
+ }, work: (target: ScriptTarget) => Promise<T>): Promise<T>;
68
119
  /**
69
- * Report the user's selector instead of the internal placeholder.
120
+ * Error for an element a command could not find.
70
121
  *
71
- * @param result - Script result
72
- * @param selector - Selector the user gave (or the cached query's selector)
73
- * @returns Result with the placeholder replaced
122
+ * @param target - Selector (and index) or backend node id the command was given
123
+ * @param matchCount - Elements the selector matched
124
+ * @returns Stale (87), out of range (81) or not found (83) error
74
125
  */
75
- export declare function withUserSelector<T extends {
76
- selector?: string;
77
- }>(result: T, selector: string | undefined): T;
126
+ export declare function missingElementError(target: {
127
+ selector: string;
128
+ index?: number;
129
+ backendNodeId?: number;
130
+ }, matchCount: number): CommandError;
78
131
  //# sourceMappingURL=targetNode.d.ts.map
@@ -9,25 +9,97 @@
9
9
  * {@link BOUND_TARGET_SELECTOR}.
10
10
  */
11
11
  import { CommandError } from '../../errors/index.js';
12
- import { staleNodeError } from '../../errors/messages.js';
12
+ import { emptySelectorError, indexOutOfRangeError, noNodesFoundError, staleNodeError, } from '../../errors/messages.js';
13
+ import { frameScopedConnection } from './frameScopedConnection.js';
14
+ import { createLogger } from '../../ui/logging/index.js';
15
+ import { getErrorMessage } from '../../utils/errors.js';
13
16
  import { EXIT_CODES } from '../../utils/exitCodes.js';
14
- import { parseSelectorFilters } from '../../utils/selectorFilters.js';
17
+ import { parseSelectorFilters, } from '../../utils/selectorFilters.js';
15
18
  /** Selector placeholder that makes page scripts use the bound node. */
16
19
  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';
22
+ const log = createLogger('dom');
17
23
  /**
18
- * Page-side test of one text or visibility filter ({@link SelectorFilter}).
19
- * Visible means a non-empty box and `visibility: visible`, as in Playwright
20
- * (`opacity: 0` still counts as visible). Text is the rendered text (`innerText`, `textContent` for elements without
21
- * it) with whitespace collapsed; filter texts arrive normalized (`has-text`
22
- * lowercased).
24
+ * Page-side matching of filters ({@link SelectorFilter}) and scoped steps
25
+ * ({@link ScopedStep}): called with the open shadow roots of the page, returns
26
+ * `{ passesAll, descend }`.
27
+ *
28
+ * Visible means rendered (`checkVisibility()`: not inside a closed
29
+ * `<details>` or under `content-visibility: hidden`), a non-empty box and
30
+ * `visibility: visible`, as in Playwright (`opacity: 0` still counts as
31
+ * visible). Text is the rendered text (`innerText`) of visible elements and
32
+ * the text nodes of hidden ones (display or visibility; not those of
33
+ * `<script>`, `<style>` or `<noscript>`), so text filters match hidden
34
+ * elements like Playwright's and `:visible` decides visibility; button inputs
35
+ * use their value. Whitespace is collapsed; filter texts arrive normalized
36
+ * (`has-text` lowercased). `:visible` is checked before the text filters,
37
+ * which read the text.
38
+ *
39
+ * Steps and `:has()` match CSS relative to an element (`:scope > css`). A
40
+ * descendant step also searches the open shadow roots under the element (the
41
+ * whole step CSS inside one shadow tree); a child step stays in the
42
+ * element's own tree.
23
43
  */
24
- const FILTER_MATCHES_JS = `(el, filter) => {
25
- if (filter.kind === 'visible') {
26
- const rect = el.getBoundingClientRect();
27
- return rect.width > 0 && rect.height > 0 && el.ownerDocument.defaultView.getComputedStyle(el).visibility === 'visible';
28
- }
29
- const text = (typeof el.innerText === 'string' ? el.innerText : el.textContent || '').replace(/\\s+/g, ' ').trim();
30
- return filter.kind === 'text-is' ? text === filter.text : text.toLowerCase().includes(filter.text);
44
+ export const FILTER_MATCHING_JS = `(shadowRoots) => {
45
+ const skipped = /^(script|style|noscript|template)$/;
46
+ const hiddenText = (el) => {
47
+ const walker = el.ownerDocument.createTreeWalker(el, 5, {
48
+ acceptNode: (node) => (node.nodeType === 1 && skipped.test(node.localName) ? 2 : 1)
49
+ });
50
+ let text = '';
51
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) {
52
+ if (node.nodeType === 3) text += node.data;
53
+ }
54
+ return text;
55
+ };
56
+ const textOf = (el) => {
57
+ if (el.localName === 'input' && /^(submit|button|reset)$/i.test(el.type)) return el.value;
58
+ const rendered = typeof el.innerText === 'string' &&
59
+ (typeof el.checkVisibility !== 'function' || el.checkVisibility({ visibilityProperty: true }));
60
+ return (rendered ? el.innerText : hiddenText(el)).replace(/\\s+/g, ' ').trim();
61
+ };
62
+ const passes = (el, filter) => {
63
+ if (filter.kind === 'visible') {
64
+ if (typeof el.checkVisibility === 'function' && !el.checkVisibility()) return false;
65
+ const rect = el.getBoundingClientRect();
66
+ return rect.width > 0 && rect.height > 0 && el.ownerDocument.defaultView.getComputedStyle(el).visibility === 'visible';
67
+ }
68
+ if (filter.kind === 'has') return filter.selectors.some((chain) => descend(el, chain).length > 0);
69
+ const text = textOf(el);
70
+ return filter.kind === 'text-is' ? text === filter.text : text.toLowerCase().includes(filter.text);
71
+ };
72
+ const passesAll = (el, filters) =>
73
+ filters.every((filter) => filter.kind !== 'visible' || passes(el, filter)) &&
74
+ filters.every((filter) => filter.kind === 'visible' || passes(el, filter));
75
+ const isUnder = (scope, node) => {
76
+ for (let current = node; current; current = current.getRootNode().host) {
77
+ if (scope === current || scope.contains(current)) return true;
78
+ }
79
+ return false;
80
+ };
81
+ const stepMatches = (scope, step) => {
82
+ const matches = [...scope.querySelectorAll(':scope ' + step.combinator + ' ' + step.css)];
83
+ if (step.combinator !== ' ') return matches;
84
+ for (const root of shadowRoots) {
85
+ if (isUnder(scope, root.host)) matches.push(...root.querySelectorAll(step.css));
86
+ }
87
+ return matches;
88
+ };
89
+ const descend = (scope, steps) => {
90
+ let current = [scope];
91
+ for (const step of steps) {
92
+ const next = new Set();
93
+ for (const el of current) {
94
+ for (const match of stepMatches(el, step)) {
95
+ if (passesAll(match, step.filters)) next.add(match);
96
+ }
97
+ }
98
+ current = [...next];
99
+ }
100
+ return current;
101
+ };
102
+ return { passesAll: passesAll, descend: descend };
31
103
  }`;
32
104
  /**
33
105
  * Page-side selector search that also reaches into open shadow roots and
@@ -35,15 +107,19 @@ const FILTER_MATCHES_JS = `(el, filter) => {
35
107
  *
36
108
  * Takes the selector and, when it has text or visibility filters, its parts
37
109
  * from {@link parseSelectorFilters}: their CSS runs as one selector list and
38
- * a match is kept when it passes the filters of a part it matches.
39
- * Matches of the document come first, then those of each shadow root and
40
- * frame in document order. Cross-origin iframes are separate processes and
41
- * cannot be searched. Throws a `SyntaxError` for an invalid selector.
110
+ * a match is kept when it passes the filters of a part it matches; a part
111
+ * with scoped steps contributes the steps' matches under it instead (each
112
+ * element once). Matches come in document order: those of the document
113
+ * first, then those of each shadow root and frame in the order they are
114
+ * reached. Cross-origin iframes are separate processes and cannot be
115
+ * searched. Throws a `SyntaxError` for an invalid selector.
42
116
  */
43
117
  export const DEEP_QUERY_JS = `function (selector, parts) {
44
118
  const css = parts ? parts.map((part) => part.css).join(', ') : selector;
45
119
  const found = [];
120
+ const roots = [];
46
121
  const visit = (root) => {
122
+ roots.push(root);
47
123
  for (const match of root.querySelectorAll(css)) found.push(match);
48
124
  for (const el of root.querySelectorAll('*')) {
49
125
  if (el.shadowRoot) visit(el.shadowRoot);
@@ -56,8 +132,20 @@ export const DEEP_QUERY_JS = `function (selector, parts) {
56
132
  };
57
133
  visit(document);
58
134
  if (!parts) return found;
59
- const passes = ${FILTER_MATCHES_JS};
60
- return found.filter((el) => parts.some((part) => el.matches(part.css) && part.filters.every((filter) => passes(el, filter))));
135
+ const shadowRoots = roots.filter((root) => root.host);
136
+ const { passesAll, descend } = (${FILTER_MATCHING_JS})(shadowRoots);
137
+ const result = new Set();
138
+ for (const el of found) {
139
+ for (const part of parts) {
140
+ if (!el.matches(part.css) || !passesAll(el, part.filters)) continue;
141
+ for (const match of descend(el, part.steps || [])) result.add(match);
142
+ }
143
+ }
144
+ const rootOrder = new Map(roots.map((root, i) => [root, i]));
145
+ const rootIndex = (el) => rootOrder.get(el.getRootNode()) ?? roots.length;
146
+ return [...result].sort((a, b) =>
147
+ rootIndex(a) - rootIndex(b) || (a.compareDocumentPosition(b) & 4 ? -1 : a === b ? 0 : 1)
148
+ );
61
149
  }`;
62
150
  /**
63
151
  * Page-side element lookup shared by the interaction scripts.
@@ -72,6 +160,45 @@ export const FIND_ELEMENTS_JS = `function (selector, parts) {
72
160
  }
73
161
  return (${DEEP_QUERY_JS})(selector, parts);
74
162
  }`;
163
+ /**
164
+ * Page-side check of what selectors cannot search in the top document:
165
+ * iframes whose document cannot be read (cross-origin) and `<object>`/`<embed>`
166
+ * elements. Cheap: no frame is entered.
167
+ */
168
+ export const UNSEARCHED_CONTENT_JS = `(() => {
169
+ const frames = Array.from(document.querySelectorAll('iframe, frame'));
170
+ const unreadable = (frame) => {
171
+ try { return !frame.contentDocument; } catch (e) { return true; }
172
+ };
173
+ return { crossOriginFrames: frames.some(unreadable), embeds: !!document.querySelector('object, embed') };
174
+ })()`;
175
+ /** Elements whose ids or classes are read for "did you mean" (bounds the work on huge pages) */
176
+ const NAME_SCAN_LIMIT = 5000;
177
+ /**
178
+ * Page-side ids (`kind` "id") or classes (`kind` "class") in the top
179
+ * document, from the first {@link NAME_SCAN_LIMIT} elements that have one.
180
+ *
181
+ * @param kind - Which names to read
182
+ * @returns Expression evaluating to the distinct names
183
+ */
184
+ export function pageNamesJS(kind) {
185
+ return `(() => {
186
+ const names = new Set();
187
+ const elements = document.querySelectorAll('[${kind}]');
188
+ for (let i = 0; i < elements.length && i < ${NAME_SCAN_LIMIT}; i++) {
189
+ const el = elements[i];
190
+ ${kind === 'id' ? 'if (el.id) names.add(el.id);' : 'for (const name of el.classList) names.add(name);'}
191
+ }
192
+ return Array.from(names);
193
+ })()`;
194
+ }
195
+ /**
196
+ * Page-side: the form control a `<label>` stands for (`label.control`: its
197
+ * `for` target or the control inside it), or null for other elements and
198
+ * labels without one. Fill, click and key presses act on that control, like
199
+ * Playwright.
200
+ */
201
+ export const LABEL_CONTROL_JS = `(el) => (el && el.localName === 'label' ? el.control : null)`;
75
202
  /**
76
203
  * Arguments for {@link DEEP_QUERY_JS} / {@link FIND_ELEMENTS_JS} and the page
77
204
  * scripts built on them: the selector as a JS string literal and its parts
@@ -79,29 +206,36 @@ export const FIND_ELEMENTS_JS = `function (selector, parts) {
79
206
  *
80
207
  * @param selector - Selector as the user gave it (or the bound-node placeholder)
81
208
  * @returns JS source of the two arguments, e.g. `"li:visible", [{...}]`
82
- * @throws CommandError (81) for a misplaced or malformed filter
209
+ * @throws CommandError (81) for an empty selector or a misplaced or malformed filter
83
210
  */
84
211
  export function selectorArgsJS(selector) {
212
+ if (selector.trim() === '') {
213
+ const err = emptySelectorError();
214
+ throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
215
+ }
85
216
  return `${JSON.stringify(selector)}, ${JSON.stringify(parseSelectorFilters(selector))}`;
86
217
  }
87
218
  /**
88
219
  * Store the node for the page scripts. It runs in the node's own frame, while
89
- * the scripts run in the top page, so the node goes on the top window (always
90
- * reachable for the same-origin frames bdg can target).
220
+ * the scripts run in the top page, so the node goes on the top window, which
221
+ * same-origin frames can reach. A cross-origin frame cannot write to the top
222
+ * window: the node then goes on its own window (`"frame"`) and the scripts
223
+ * run in that frame ({@link frameScopedConnection}).
91
224
  */
92
225
  const BIND_FUNCTION = `function () {
93
- let host = window;
94
- try { host = window.top; host.__bdgTarget = this; } catch (e) { host = window; host.__bdgTarget = this; }
95
- return this.isConnected;
226
+ if (!this.isConnected) return false;
227
+ try { window.top.__bdgTarget = this; return 'top'; } catch (e) { window.__bdgTarget = this; return 'frame'; }
96
228
  }`;
97
229
  /**
98
230
  * Make the node with `backendNodeId` the target of the next page script.
99
231
  *
100
232
  * @param cdp - CDP connection
101
233
  * @param backendNodeId - Backend node id from the query cache
234
+ * @returns Connection for the page scripts: the session's, or one running them
235
+ * in the node's frame when that frame is cross-origin
102
236
  * @throws CommandError (exit 87) when the node no longer exists in the page
103
237
  */
104
- export async function bindTargetNode(cdp, backendNodeId) {
238
+ async function bindTargetNode(cdp, backendNodeId) {
105
239
  const err = staleNodeError();
106
240
  const stale = new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.STALE_CACHE);
107
241
  let objectId;
@@ -119,25 +253,88 @@ export async function bindTargetNode(cdp, backendNodeId) {
119
253
  functionDeclaration: BIND_FUNCTION,
120
254
  returnByValue: true,
121
255
  }));
122
- if (bound.result?.value !== true)
256
+ const scope = bound.result?.value;
257
+ if (scope === 'frame')
258
+ return frameScopedConnection(cdp, objectId);
259
+ if (scope !== 'top')
123
260
  throw stale;
261
+ return cdp;
124
262
  }
125
263
  /**
126
264
  * Resolve what a page script should target for an interaction request.
127
265
  *
128
266
  * @param cdp - CDP connection
129
267
  * @param params - Request with a selector (and optional index) or a backend node id
130
- * @returns Selector/index for the page script
268
+ * @returns Selector/index for the page script and the connection to run it on
131
269
  */
132
- export async function resolveScriptTarget(cdp, params) {
270
+ async function resolveScriptTarget(cdp, params) {
133
271
  if (params.backendNodeId === undefined) {
134
272
  return {
135
273
  selector: params.selector ?? '',
136
274
  ...(params.index !== undefined && { index: params.index }),
275
+ cdp,
137
276
  };
138
277
  }
139
- await bindTargetNode(cdp, params.backendNodeId);
140
- return { selector: BOUND_TARGET_SELECTOR };
278
+ return { selector: BOUND_TARGET_SELECTOR, cdp: await bindTargetNode(cdp, params.backendNodeId) };
279
+ }
280
+ /**
281
+ * Whether an error is about the bound node: the placeholder only appears in
282
+ * "not found" messages, which for a bound node mean it left the page.
283
+ *
284
+ * @param text - Error message
285
+ * @returns True when the bound node was not found
286
+ */
287
+ function boundNodeMissing(text) {
288
+ return text?.includes(BOUND_TARGET_SELECTOR) === true;
289
+ }
290
+ /**
291
+ * Run an interaction on the element a request targets. Results and errors
292
+ * never show the internal placeholder: a bound node the page scripts could
293
+ * not find is reported as stale (87), and results carry the user's selector.
294
+ *
295
+ * @param cdp - CDP connection
296
+ * @param params - Request with a selector (and optional index) or a backend node id
297
+ * @param work - The interaction, given the script target
298
+ * @returns The interaction's result
299
+ * @throws CommandError (87) when the bound node left the page during the action
300
+ */
301
+ export async function onScriptTarget(cdp, params, work) {
302
+ const err = staleNodeError();
303
+ let target;
304
+ try {
305
+ target = await resolveScriptTarget(cdp, params);
306
+ const result = withUserSelector(await work(target), params.selector);
307
+ if (!boundNodeMissing(result.error))
308
+ return result;
309
+ return {
310
+ ...result,
311
+ error: err.message,
312
+ suggestion: err.suggestion,
313
+ exitCode: EXIT_CODES.STALE_CACHE,
314
+ };
315
+ }
316
+ catch (error) {
317
+ if (!(error instanceof CommandError) || !boundNodeMissing(error.message))
318
+ throw error;
319
+ throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.STALE_CACHE);
320
+ }
321
+ finally {
322
+ if (target && target.cdp !== cdp)
323
+ unbindInFrame(target.cdp);
324
+ }
325
+ }
326
+ /**
327
+ * Remove a node bound in a cross-origin frame from that frame's window
328
+ * (through the scoped connection, so the script runs where the bind ran; the
329
+ * session removes the one bound on the top window after every interaction).
330
+ * Not waited for.
331
+ *
332
+ * @param frameConnection - Connection scoped to the node's frame
333
+ */
334
+ function unbindInFrame(frameConnection) {
335
+ void frameConnection
336
+ .send('Runtime.evaluate', { expression: UNBIND_TARGET_SCRIPT })
337
+ .catch((error) => log.debug(`Frame target not unbound: ${getErrorMessage(error)}`));
141
338
  }
142
339
  /**
143
340
  * Report the user's selector instead of the internal placeholder.
@@ -146,9 +343,28 @@ export async function resolveScriptTarget(cdp, params) {
146
343
  * @param selector - Selector the user gave (or the cached query's selector)
147
344
  * @returns Result with the placeholder replaced
148
345
  */
149
- export function withUserSelector(result, selector) {
346
+ function withUserSelector(result, selector) {
150
347
  if (result.selector !== BOUND_TARGET_SELECTOR)
151
348
  return result;
152
349
  return { ...result, ...(selector !== undefined && { selector }) };
153
350
  }
351
+ /**
352
+ * Error for an element a command could not find.
353
+ *
354
+ * @param target - Selector (and index) or backend node id the command was given
355
+ * @param matchCount - Elements the selector matched
356
+ * @returns Stale (87), out of range (81) or not found (83) error
357
+ */
358
+ export function missingElementError(target, matchCount) {
359
+ if (target.backendNodeId !== undefined) {
360
+ const err = staleNodeError();
361
+ return new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.STALE_CACHE);
362
+ }
363
+ if (matchCount > 0) {
364
+ const err = indexOutOfRangeError(target.index ?? 0, matchCount - 1);
365
+ return new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
366
+ }
367
+ const err = noNodesFoundError(target.selector);
368
+ return new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.RESOURCE_NOT_FOUND);
369
+ }
154
370
  //# sourceMappingURL=targetNode.js.map
@@ -0,0 +1,25 @@
1
+ /**
2
+ * `bdg dom wait`: wait in the page until elements appear, become visible,
3
+ * contain a text or are gone, and/or the page has loaded.
4
+ *
5
+ * The page watches itself (a MutationObserver plus a short poll for style
6
+ * and frame changes the observer cannot see) and answers as soon as what it
7
+ * shows changes; the condition is decided here ({@link isWaitConditionMet}).
8
+ * Each page call lasts at most {@link WAIT_SLICE_MS}, so a navigation during
9
+ * the wait (the page's context is destroyed) only costs a retry on the new
10
+ * document. `--gone` is confirmed by a second snapshot of the same settled
11
+ * document, so the empty document right after a navigation does not count.
12
+ */
13
+ import type { DomWaitCommand, DomWaitData } from '../../ipc/protocol/commands.js';
14
+ import type { CDPSender } from '../../telemetry/objectExpander.js';
15
+ /**
16
+ * Wait until the page meets the condition.
17
+ *
18
+ * @param cdp - CDP connection
19
+ * @param params - Condition and timeout
20
+ * @returns What the page showed when it was met, and how long it took
21
+ * @throws CommandError (81) for an invalid selector, (102) when the timeout passed;
22
+ * CDP errors other than a lost page context (a closed connection) as they are
23
+ */
24
+ export declare function waitForCondition(cdp: CDPSender, params: DomWaitCommand): Promise<DomWaitData>;
25
+ //# sourceMappingURL=wait.d.ts.map