browser-debugger-cli 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/.claude/skills/bdg/SKILL.md +100 -186
  2. package/README.md +5 -4
  3. package/dist/commands/cdp.d.ts +22 -1
  4. package/dist/commands/cdp.js +100 -43
  5. package/dist/commands/console.d.ts +12 -0
  6. package/dist/commands/console.js +67 -13
  7. package/dist/commands/dom/DomElementResolver.d.ts +3 -1
  8. package/dist/commands/dom/DomElementResolver.js +10 -3
  9. package/dist/commands/dom/a11y.d.ts +1 -1
  10. package/dist/commands/dom/a11y.js +23 -22
  11. package/dist/commands/dom/eval.d.ts +4 -2
  12. package/dist/commands/dom/eval.js +31 -7
  13. package/dist/commands/dom/form.js +10 -9
  14. package/dist/commands/dom/formInteraction.js +9 -8
  15. package/dist/commands/dom/get.js +32 -14
  16. package/dist/commands/dom/helpers/index.d.ts +1 -1
  17. package/dist/commands/dom/helpers/index.js +1 -1
  18. package/dist/commands/dom/helpers/query.d.ts +27 -3
  19. package/dist/commands/dom/helpers/query.js +152 -64
  20. package/dist/commands/dom/helpers/screenshot.js +13 -13
  21. package/dist/commands/dom/index.js +10 -3
  22. package/dist/commands/dom/query.d.ts +20 -2
  23. package/dist/commands/dom/query.js +39 -6
  24. package/dist/commands/dom/screenshot.js +3 -1
  25. package/dist/commands/dom/semanticUtils.d.ts +3 -2
  26. package/dist/commands/dom/semanticUtils.js +40 -9
  27. package/dist/commands/helpJson.d.ts +82 -19
  28. package/dist/commands/helpJson.js +112 -41
  29. package/dist/commands/helpTopic.d.ts +16 -1
  30. package/dist/commands/helpTopic.js +59 -1
  31. package/dist/commands/installSkill.d.ts +15 -5
  32. package/dist/commands/installSkill.js +86 -16
  33. package/dist/commands/network/list.js +65 -12
  34. package/dist/commands/optionBehaviors.d.ts +25 -2
  35. package/dist/commands/optionBehaviors.js +81 -46
  36. package/dist/commands/peek.js +3 -0
  37. package/dist/commands/shared/CommandRunner.js +13 -13
  38. package/dist/commands/shared/daemonErrorHandler.d.ts +5 -2
  39. package/dist/commands/shared/daemonErrorHandler.js +21 -10
  40. package/dist/commands/shared/dataFetcher.d.ts +14 -4
  41. package/dist/commands/shared/dataFetcher.js +20 -4
  42. package/dist/commands/shared/followMode.d.ts +9 -1
  43. package/dist/commands/shared/followMode.js +22 -4
  44. package/dist/commands/shared/handleValidationError.js +3 -3
  45. package/dist/commands/shared/optionTypes.d.ts +17 -3
  46. package/dist/commands/shared/outputFile.js +6 -1
  47. package/dist/commands/shared/startHelpers.js +3 -3
  48. package/dist/commands/start.d.ts +7 -5
  49. package/dist/commands/start.js +65 -21
  50. package/dist/commands/stop.d.ts +11 -0
  51. package/dist/commands/stop.js +24 -1
  52. package/dist/commands.js +1 -1
  53. package/dist/connection/cdp.d.ts +7 -0
  54. package/dist/connection/cdp.js +9 -0
  55. package/dist/connection/chromeIdentity.d.ts +8 -2
  56. package/dist/connection/chromeIdentity.js +85 -13
  57. package/dist/connection/launcher.js +3 -2
  58. package/dist/constants.d.ts +29 -1
  59. package/dist/constants.js +35 -1
  60. package/dist/daemon/SessionController.js +8 -1
  61. package/dist/daemon/launcher.d.ts +3 -2
  62. package/dist/daemon/launcher.js +47 -3
  63. package/dist/daemon/session/Session.d.ts +5 -1
  64. package/dist/daemon/session/Session.js +42 -3
  65. package/dist/daemon/session/TelemetryStore.d.ts +15 -1
  66. package/dist/daemon/session/TelemetryStore.js +19 -1
  67. package/dist/daemon/session/commandRegistry.js +52 -18
  68. package/dist/daemon/session/interactions.d.ts +2 -1
  69. package/dist/daemon/session/interactions.js +13 -1
  70. package/dist/daemon/session/matchedStylesReset.d.ts +26 -0
  71. package/dist/daemon/session/matchedStylesReset.js +46 -0
  72. package/dist/daemon/session/plugins.js +17 -2
  73. package/dist/daemon/session/teardown.js +1 -1
  74. package/dist/daemon/session/triggeredRequests.d.ts +0 -5
  75. package/dist/daemon/session/triggeredRequests.js +13 -7
  76. package/dist/daemon.js +2385 -1229
  77. package/dist/errors/messages.d.ts +62 -11
  78. package/dist/errors/messages.js +119 -22
  79. package/dist/index.js +14995 -9866
  80. package/dist/ipc/client.d.ts +18 -2
  81. package/dist/ipc/client.js +26 -5
  82. package/dist/ipc/protocol/auditTypes.d.ts +8 -2
  83. package/dist/ipc/protocol/commands.d.ts +16 -0
  84. package/dist/ipc/protocol/domTypes.d.ts +12 -0
  85. package/dist/ipc/protocol/inspectTypes.d.ts +7 -2
  86. package/dist/ipc/session/types.d.ts +7 -1
  87. package/dist/program.d.ts +14 -0
  88. package/dist/program.js +53 -0
  89. package/dist/runtime/dom/actionEffects.d.ts +5 -1
  90. package/dist/runtime/dom/actionEffects.js +26 -14
  91. package/dist/runtime/dom/audit.js +3 -2
  92. package/dist/runtime/dom/auditModel.js +6 -1
  93. package/dist/runtime/dom/auditScripts.d.ts +9 -3
  94. package/dist/runtime/dom/auditScripts.js +41 -5
  95. package/dist/runtime/dom/elementGeometry.d.ts +33 -3
  96. package/dist/runtime/dom/elementGeometry.js +44 -19
  97. package/dist/runtime/dom/elementInfo.d.ts +76 -18
  98. package/dist/runtime/dom/elementInfo.js +190 -40
  99. package/dist/runtime/dom/evalHelpers.d.ts +12 -2
  100. package/dist/runtime/dom/evalHelpers.js +67 -7
  101. package/dist/runtime/dom/formDiscovery.d.ts +6 -2
  102. package/dist/runtime/dom/formDiscovery.js +20 -3
  103. package/dist/runtime/dom/formFillHelpers/fill.js +7 -11
  104. package/dist/runtime/dom/formFillHelpers/pressKey.js +2 -2
  105. package/dist/runtime/dom/formFillHelpers/shared.d.ts +16 -10
  106. package/dist/runtime/dom/formFillHelpers/shared.js +19 -52
  107. package/dist/runtime/dom/formSubmitHelpers.js +4 -3
  108. package/dist/runtime/dom/frameLayout.js +1 -0
  109. package/dist/runtime/dom/frameScopedConnection.d.ts +7 -0
  110. package/dist/runtime/dom/frameScopedConnection.js +2 -2
  111. package/dist/runtime/dom/inspect.d.ts +17 -3
  112. package/dist/runtime/dom/inspect.js +45 -32
  113. package/dist/runtime/dom/inspectAllStyles.js +1 -0
  114. package/dist/runtime/dom/inspectHints.d.ts +1 -1
  115. package/dist/runtime/dom/inspectModel.d.ts +5 -4
  116. package/dist/runtime/dom/inspectModel.js +7 -3
  117. package/dist/runtime/dom/inspectPaintModel.d.ts +2 -0
  118. package/dist/runtime/dom/inspectPaintModel.js +3 -1
  119. package/dist/runtime/dom/inspectRules.d.ts +29 -3
  120. package/dist/runtime/dom/inspectRules.js +205 -11
  121. package/dist/runtime/dom/inspectScripts.d.ts +29 -2
  122. package/dist/runtime/dom/inspectScripts.js +49 -10
  123. package/dist/runtime/dom/layout.d.ts +0 -2
  124. package/dist/runtime/dom/layout.js +10 -9
  125. package/dist/runtime/dom/reactEventHelpers.d.ts +17 -4
  126. package/dist/runtime/dom/reactEventHelpers.js +71 -28
  127. package/dist/runtime/dom/targetNode.d.ts +27 -10
  128. package/dist/runtime/dom/targetNode.js +283 -16
  129. package/dist/runtime/dom/wait.js +2 -1
  130. package/dist/runtime/page/bdgWorld.d.ts +57 -0
  131. package/dist/runtime/page/bdgWorld.js +180 -0
  132. package/dist/runtime/page/replacedBuiltins.d.ts +28 -0
  133. package/dist/runtime/page/replacedBuiltins.js +136 -0
  134. package/dist/session/QueryCacheManager.d.ts +4 -1
  135. package/dist/session/QueryCacheManager.js +5 -2
  136. package/dist/session/chrome.d.ts +4 -1
  137. package/dist/session/chrome.js +7 -1
  138. package/dist/session/cleanup/staleSession.d.ts +21 -4
  139. package/dist/session/cleanup/staleSession.js +79 -9
  140. package/dist/session/cleanup/userCommands.d.ts +4 -1
  141. package/dist/session/cleanup/userCommands.js +10 -5
  142. package/dist/session/daemonSocket.d.ts +10 -0
  143. package/dist/session/daemonSocket.js +22 -0
  144. package/dist/session/lastSession.d.ts +6 -3
  145. package/dist/session/lastSession.js +11 -5
  146. package/dist/session/paths.d.ts +3 -1
  147. package/dist/session/paths.js +5 -5
  148. package/dist/session/portClaims.js +4 -3
  149. package/dist/session/sessionList.d.ts +13 -5
  150. package/dist/session/sessionList.js +31 -7
  151. package/dist/telemetry/a11y.d.ts +15 -1
  152. package/dist/telemetry/a11y.js +85 -2
  153. package/dist/telemetry/console.d.ts +2 -1
  154. package/dist/telemetry/console.js +30 -21
  155. package/dist/telemetry/har/builder.js +1 -1
  156. package/dist/telemetry/network.d.ts +13 -16
  157. package/dist/telemetry/network.js +30 -52
  158. package/dist/telemetry/networkRetention.d.ts +83 -0
  159. package/dist/telemetry/networkRetention.js +117 -0
  160. package/dist/telemetry/pageCrash.d.ts +26 -0
  161. package/dist/telemetry/pageCrash.js +53 -0
  162. package/dist/types.d.ts +42 -0
  163. package/dist/ui/OutputBuilder.d.ts +10 -0
  164. package/dist/ui/OutputBuilder.js +12 -0
  165. package/dist/ui/formatters/a11y.d.ts +5 -7
  166. package/dist/ui/formatters/a11y.js +7 -61
  167. package/dist/ui/formatters/audit.js +14 -5
  168. package/dist/ui/formatters/cdp.d.ts +138 -0
  169. package/dist/ui/formatters/cdp.js +131 -0
  170. package/dist/ui/formatters/console/chronological.js +7 -5
  171. package/dist/ui/formatters/console/follow.d.ts +5 -2
  172. package/dist/ui/formatters/console/follow.js +7 -4
  173. package/dist/ui/formatters/console/json.d.ts +4 -7
  174. package/dist/ui/formatters/console/json.js +16 -14
  175. package/dist/ui/formatters/console/shared.d.ts +47 -2
  176. package/dist/ui/formatters/console/shared.js +33 -0
  177. package/dist/ui/formatters/console/summarize.d.ts +9 -2
  178. package/dist/ui/formatters/console/summarize.js +57 -11
  179. package/dist/ui/formatters/console.d.ts +3 -2
  180. package/dist/ui/formatters/console.js +8 -10
  181. package/dist/ui/formatters/details.js +4 -2
  182. package/dist/ui/formatters/dom.d.ts +14 -5
  183. package/dist/ui/formatters/dom.js +30 -13
  184. package/dist/ui/formatters/helpFormatters.js +1 -1
  185. package/dist/ui/formatters/inspect.js +9 -3
  186. package/dist/ui/formatters/installSkill.d.ts +9 -1
  187. package/dist/ui/formatters/installSkill.js +32 -6
  188. package/dist/ui/formatters/layout.js +4 -2
  189. package/dist/ui/formatters/longValues.d.ts +14 -0
  190. package/dist/ui/formatters/longValues.js +23 -0
  191. package/dist/ui/formatters/networkList.d.ts +8 -2
  192. package/dist/ui/formatters/networkList.js +11 -3
  193. package/dist/ui/formatters/preview.d.ts +6 -1
  194. package/dist/ui/formatters/preview.js +67 -15
  195. package/dist/ui/formatters/sessions.d.ts +2 -2
  196. package/dist/ui/formatters/sessions.js +9 -2
  197. package/dist/ui/formatters/status.js +7 -0
  198. package/dist/ui/formatters/triggeredRequests.js +2 -1
  199. package/dist/ui/logging/logger.d.ts +1 -1
  200. package/dist/ui/messages/chrome.d.ts +20 -1
  201. package/dist/ui/messages/chrome.js +29 -3
  202. package/dist/ui/messages/commands.d.ts +153 -12
  203. package/dist/ui/messages/commands.js +198 -15
  204. package/dist/ui/messages/consoleMessages.d.ts +24 -0
  205. package/dist/ui/messages/consoleMessages.js +32 -0
  206. package/dist/ui/messages/networkMessages.d.ts +24 -0
  207. package/dist/ui/messages/networkMessages.js +45 -0
  208. package/dist/ui/messages/preview.d.ts +6 -0
  209. package/dist/ui/messages/preview.js +9 -1
  210. package/dist/ui/messages/session.d.ts +13 -2
  211. package/dist/ui/messages/session.js +22 -3
  212. package/dist/utils/directories.d.ts +34 -0
  213. package/dist/utils/directories.js +88 -0
  214. package/dist/utils/display.d.ts +16 -0
  215. package/dist/utils/display.js +42 -0
  216. package/dist/utils/exitCodes.d.ts +1 -0
  217. package/dist/utils/exitCodes.js +6 -0
  218. package/dist/utils/http.d.ts +9 -2
  219. package/dist/utils/http.js +4 -3
  220. package/dist/utils/process.d.ts +12 -0
  221. package/dist/utils/process.js +25 -0
  222. package/dist/utils/strings.d.ts +19 -0
  223. package/dist/utils/strings.js +16 -0
  224. package/package.json +2 -2
@@ -9,16 +9,87 @@
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 { COMPOSED_JS, FLAT_TEXT_JS } from './elementInfo.js';
14
+ import { ActionScriptError, throwIfInvalidSelector } from './formFillHelpers/shared.js';
13
15
  import { frameScopedConnection } from './frameScopedConnection.js';
16
+ import { evaluateInBdgWorld } from '../page/bdgWorld.js';
17
+ import { findReplacedBuiltins } from '../page/replacedBuiltins.js';
14
18
  import { createLogger } from '../../ui/logging/index.js';
19
+ import { brokenByReplacedBuiltinsSuggestion, replacedBuiltinsWarning, } from '../../ui/messages/commands.js';
15
20
  import { getErrorMessage } from '../../utils/errors.js';
16
21
  import { EXIT_CODES } from '../../utils/exitCodes.js';
17
22
  import { parseSelectorFilters, } from '../../utils/selectorFilters.js';
18
23
  /** Selector placeholder that makes page scripts use the bound node. */
19
24
  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';
25
+ /** Removes the node bound for index-based commands, and matches bound for a selector, from the window they were stored on */
26
+ export const UNBIND_TARGET_SCRIPT = 'delete window.__bdgTarget; delete window.__bdgMatches';
27
+ /**
28
+ * Built-ins the selector search ({@link DEEP_QUERY_JS}) cannot do without:
29
+ * when the page replaced one, the search runs in bdg's world instead.
30
+ * Helpers that libraries replace with working versions (Prototype.js's
31
+ * `Array.prototype.map`) are left to {@link SCRIPT_BUILTINS}.
32
+ */
33
+ const SELECTION_BUILTINS = [
34
+ 'Element.prototype.querySelectorAll',
35
+ 'Element.prototype.querySelector',
36
+ 'Document.prototype.querySelectorAll',
37
+ 'Document.prototype.querySelector',
38
+ 'DocumentFragment.prototype.querySelectorAll',
39
+ 'Element.prototype.matches',
40
+ 'Element.prototype.closest',
41
+ 'Node.prototype.getRootNode',
42
+ 'Node.prototype.compareDocumentPosition',
43
+ 'Node.prototype.contains',
44
+ 'NodeList.prototype[Symbol.iterator]',
45
+ 'Array.prototype[Symbol.iterator]',
46
+ 'Array.prototype.push',
47
+ 'Set',
48
+ 'Map',
49
+ ];
50
+ /** Other built-ins the interaction scripts use */
51
+ const SCRIPT_BUILTINS = [
52
+ 'Array.from',
53
+ 'Array.prototype.map',
54
+ 'Array.prototype.filter',
55
+ 'Array.prototype.forEach',
56
+ 'Array.prototype.some',
57
+ 'Array.prototype.find',
58
+ 'Array.prototype.includes',
59
+ 'Object.keys',
60
+ 'Object.assign',
61
+ 'Object.getOwnPropertyDescriptor',
62
+ 'Function.prototype.call',
63
+ 'JSON.stringify',
64
+ 'JSON.parse',
65
+ ];
66
+ /** DOM built-ins the interaction scripts act through (anti-bot scripts often make them throw) */
67
+ const DOM_ACTION_BUILTINS = [
68
+ 'Element.prototype.getBoundingClientRect',
69
+ 'Element.prototype.getClientRects',
70
+ 'Element.prototype.scrollIntoView',
71
+ 'window.getComputedStyle',
72
+ 'Document.prototype.elementFromPoint',
73
+ 'Document.prototype.createEvent',
74
+ 'EventTarget.prototype.dispatchEvent',
75
+ 'HTMLElement.prototype.focus',
76
+ 'HTMLElement.prototype.blur',
77
+ 'HTMLElement.prototype.click',
78
+ 'Event',
79
+ 'MouseEvent',
80
+ 'HTMLInputElement.prototype.value',
81
+ 'HTMLTextAreaElement.prototype.value',
82
+ 'HTMLSelectElement.prototype.value',
83
+ ];
84
+ /** Matches bound for a selector when the page replaced the selector search built-ins (enough for --index) */
85
+ const MATCH_BIND_LIMIT = 100;
86
+ /** 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) */
87
+ const BIND_MATCH_FUNCTION = `function (selector, i) {
88
+ const w = window.top;
89
+ if (!w.__bdgMatches || w.__bdgMatches.selector !== selector) return false;
90
+ w.__bdgMatches.nodes[i] = this;
91
+ return true;
92
+ }`;
22
93
  const log = createLogger('dom');
23
94
  /**
24
95
  * Page-side matching of filters ({@link SelectorFilter}) and scoped steps
@@ -32,9 +103,13 @@ const log = createLogger('dom');
32
103
  * the text nodes of hidden ones (display or visibility; not those of
33
104
  * `<script>`, `<style>` or `<noscript>`), so text filters match hidden
34
105
  * 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.
106
+ * use their value. Text is read in the flat tree, as `bdg dom query` shows
107
+ * it: the text a web component's open shadow root renders, slotted content
108
+ * in place of its slots. A visible component, slot or element holding either
109
+ * ({@link COMPOSED_JS}) is read with {@link FLAT_TEXT_JS}, including the
110
+ * selects and editable regions `innerText` reads. Whitespace is collapsed;
111
+ * filter texts arrive normalized (`has-text` lowercased). `:visible` is
112
+ * checked before the text filters, which read the text.
38
113
  *
39
114
  * Steps and `:has()` match CSS relative to an element (`:scope > css`). A
40
115
  * descendant step also searches the open shadow roots under the element (the
@@ -42,14 +117,15 @@ const log = createLogger('dom');
42
117
  * element's own tree.
43
118
  */
44
119
  export const FILTER_MATCHING_JS = `(shadowRoots) => {
120
+ const composed = ${COMPOSED_JS};
121
+ const flatText = ${FLAT_TEXT_JS};
45
122
  const skipped = /^(script|style|noscript|template)$/;
46
123
  const hiddenText = (el) => {
47
- const walker = el.ownerDocument.createTreeWalker(el, 5, {
48
- acceptNode: (node) => (node.nodeType === 1 && skipped.test(node.localName) ? 2 : 1)
49
- });
124
+ const nodes = el.localName === 'slot' ? el.assignedNodes({ flatten: true }) : (el.shadowRoot || el).childNodes;
50
125
  let text = '';
51
- for (let node = walker.nextNode(); node; node = walker.nextNode()) {
126
+ for (const node of nodes) {
52
127
  if (node.nodeType === 3) text += node.data;
128
+ else if (node.nodeType === 1 && !skipped.test(node.localName)) text += hiddenText(node);
53
129
  }
54
130
  return text;
55
131
  };
@@ -57,7 +133,8 @@ export const FILTER_MATCHING_JS = `(shadowRoots) => {
57
133
  if (el.localName === 'input' && /^(submit|button|reset)$/i.test(el.type)) return el.value;
58
134
  const rendered = typeof el.innerText === 'string' &&
59
135
  (typeof el.checkVisibility !== 'function' || el.checkVisibility({ visibilityProperty: true }));
60
- return (rendered ? el.innerText : hiddenText(el)).replace(/\\s+/g, ' ').trim();
136
+ const text = !rendered ? hiddenText(el) : composed(el) ? flatText(el, Infinity, true) : el.innerText;
137
+ return text.replace(/\\s+/g, ' ').trim();
61
138
  };
62
139
  const passes = (el, filter) => {
63
140
  if (filter.kind === 'visible') {
@@ -151,13 +228,23 @@ export const DEEP_QUERY_JS = `function (selector, parts) {
151
228
  * Page-side element lookup shared by the interaction scripts.
152
229
  *
153
230
  * Returns the bound node (if still in the page) for the placeholder selector,
154
- * otherwise all matches of the selector ({@link DEEP_QUERY_JS}).
231
+ * the matches bound for the selector when the page replaced the search
232
+ * built-ins ({@link bindMatches}), otherwise all matches of the selector
233
+ * ({@link DEEP_QUERY_JS}).
155
234
  */
156
235
  export const FIND_ELEMENTS_JS = `function (selector, parts) {
157
236
  if (selector === '${BOUND_TARGET_SELECTOR}') {
158
237
  const el = window.__bdgTarget;
159
238
  return el && el.isConnected ? [el] : [];
160
239
  }
240
+ const bound = window.__bdgMatches;
241
+ if (bound && bound.selector === selector) {
242
+ const connected = [];
243
+ for (let i = 0; i < bound.nodes.length; i++) {
244
+ if (bound.nodes[i] && bound.nodes[i].isConnected) connected[connected.length] = bound.nodes[i];
245
+ }
246
+ return connected;
247
+ }
161
248
  return (${DEEP_QUERY_JS})(selector, parts);
162
249
  }`;
163
250
  /**
@@ -269,13 +356,124 @@ async function bindTargetNode(cdp, backendNodeId) {
269
356
  */
270
357
  async function resolveScriptTarget(cdp, params) {
271
358
  if (params.backendNodeId === undefined) {
359
+ const selector = params.selector ?? '';
360
+ const replaced = await replacedBuiltins(cdp);
361
+ const searchReplaced = replaced.some((name) => SELECTION_BUILTINS.includes(name));
362
+ const bound = searchReplaced && (await bindMatches(cdp, selector, params.index ?? 0));
272
363
  return {
273
- selector: params.selector ?? '',
364
+ selector,
274
365
  ...(params.index !== undefined && { index: params.index }),
275
366
  cdp,
367
+ ...(replaced.length > 0 && { replacedBuiltins: replaced }),
368
+ ...(bound && { boundInBdgWorld: true }),
276
369
  };
277
370
  }
278
- return { selector: BOUND_TARGET_SELECTOR, cdp: await bindTargetNode(cdp, params.backendNodeId) };
371
+ const scriptCdp = await bindTargetNode(cdp, params.backendNodeId);
372
+ const replaced = await replacedBuiltins(scriptCdp);
373
+ return {
374
+ selector: BOUND_TARGET_SELECTOR,
375
+ cdp: scriptCdp,
376
+ ...(replaced.length > 0 && { replacedBuiltins: replaced }),
377
+ };
378
+ }
379
+ /**
380
+ * The built-ins the interaction scripts use that the page replaced.
381
+ *
382
+ * @param cdp - CDP connection
383
+ * @returns Their dotted names (empty when all are the browser's, or the check failed)
384
+ */
385
+ function replacedBuiltins(cdp) {
386
+ return findReplacedBuiltins(cdp, [
387
+ ...SELECTION_BUILTINS,
388
+ ...SCRIPT_BUILTINS,
389
+ ...DOM_ACTION_BUILTINS,
390
+ ]);
391
+ }
392
+ /**
393
+ * Search the selector in bdg's own world, where the page's replacements do
394
+ * not apply, and hand the matches (the first {@link MATCH_BIND_LIMIT}, or up
395
+ * to the index asked for) to the page scripts, which run in the page's world.
396
+ * When a match cannot be handed over (or the search fails other than on an
397
+ * invalid selector), nothing is bound and the scripts search themselves.
398
+ *
399
+ * @param cdp - CDP connection
400
+ * @param selector - Selector as the user gave it
401
+ * @param index - Match the action is for
402
+ * @returns Whether the matches were bound
403
+ * @throws CommandError (81) for an invalid selector
404
+ */
405
+ async function bindMatches(cdp, selector, index) {
406
+ const objectGroup = `bdg-bind-${Date.now()}`;
407
+ const limit = Math.max(MATCH_BIND_LIMIT, index + 1);
408
+ try {
409
+ const found = await evaluateInBdgWorld(cdp, {
410
+ expression: `(${DEEP_QUERY_JS})(${selectorArgsJS(selector)}).slice(0, ${limit})`,
411
+ objectGroup,
412
+ });
413
+ if (found.exceptionDetails)
414
+ throwIfInvalidSelector(found.exceptionDetails, selector);
415
+ const arrayId = found.result.objectId;
416
+ if (found.exceptionDetails || !arrayId)
417
+ return false;
418
+ const { result } = (await cdp.send('Runtime.getProperties', {
419
+ objectId: arrayId,
420
+ ownProperties: true,
421
+ }));
422
+ const matches = result
423
+ .filter((property) => /^\d+$/.test(property.name))
424
+ .map((property) => ({ i: Number(property.name), objectId: property.value?.objectId }));
425
+ await cdp.send('Runtime.evaluate', {
426
+ expression: `window.__bdgMatches = { selector: ${JSON.stringify(selector)}, nodes: [] }`,
427
+ });
428
+ const bound = await Promise.all(matches.map(({ i, objectId }) => objectId ? bindMatch(cdp, selector, i, objectId) : Promise.resolve(false)));
429
+ if (bound.every(Boolean))
430
+ return true;
431
+ await cdp.send('Runtime.evaluate', { expression: 'delete window.__bdgMatches' });
432
+ return false;
433
+ }
434
+ catch (error) {
435
+ if (error instanceof CommandError)
436
+ throw error;
437
+ log.debug(`Matches not bound: ${getErrorMessage(error)}`);
438
+ return false;
439
+ }
440
+ finally {
441
+ await cdp
442
+ .send('Runtime.releaseObjectGroup', { objectGroup })
443
+ .catch((error) => log.debug(`Matches not released: ${getErrorMessage(error)}`));
444
+ }
445
+ }
446
+ /**
447
+ * Hand one match from bdg's world to the page's world as match `i`.
448
+ *
449
+ * @param cdp - CDP connection
450
+ * @param selector - Selector the matches are for
451
+ * @param i - Its position among the matches
452
+ * @param objectId - The match in bdg's world
453
+ * @returns Whether it was handed over
454
+ */
455
+ async function bindMatch(cdp, selector, i, objectId) {
456
+ try {
457
+ const { node } = (await cdp.send('DOM.describeNode', {
458
+ objectId,
459
+ }));
460
+ const resolved = (await cdp.send('DOM.resolveNode', {
461
+ backendNodeId: node.backendNodeId,
462
+ }));
463
+ if (!resolved.object.objectId)
464
+ return false;
465
+ const stored = (await cdp.send('Runtime.callFunctionOn', {
466
+ objectId: resolved.object.objectId,
467
+ functionDeclaration: BIND_MATCH_FUNCTION,
468
+ arguments: [{ value: selector }, { value: i }],
469
+ returnByValue: true,
470
+ }));
471
+ return stored.result.value === true;
472
+ }
473
+ catch (error) {
474
+ log.debug(`Match ${i} not bound: ${getErrorMessage(error)}`);
475
+ return false;
476
+ }
279
477
  }
280
478
  /**
281
479
  * Whether an error is about the bound node: the placeholder only appears in
@@ -291,19 +489,28 @@ function boundNodeMissing(text) {
291
489
  * Run an interaction on the element a request targets. Results and errors
292
490
  * never show the internal placeholder: a bound node the page scripts could
293
491
  * not find is reported as stale (87), and results carry the user's selector.
492
+ * On a page that replaced the selector search, a result warns that the
493
+ * element was found in bdg's world; on a page that replaced built-ins the
494
+ * scripts use, a failure adds that they may be the cause, and a script that
495
+ * threw is reported as broken by the page (90), naming them.
294
496
  *
295
497
  * @param cdp - CDP connection
296
498
  * @param params - Request with a selector (and optional index) or a backend node id
297
499
  * @param work - The interaction, given the script target
298
500
  * @returns The interaction's result
299
- * @throws CommandError (87) when the bound node left the page during the action
501
+ * @throws CommandError (87) when the bound node left the page during the
502
+ * action, (90) when its script threw on a page that replaced built-ins
300
503
  */
301
504
  export async function onScriptTarget(cdp, params, work) {
302
505
  const err = staleNodeError();
303
506
  let target;
304
507
  try {
305
508
  target = await resolveScriptTarget(cdp, params);
306
- const result = withUserSelector(await work(target), params.selector);
509
+ const replaced = target.replacedBuiltins ?? [];
510
+ const result = withReplacedBuiltins(withUserSelector(await work(target), params.selector), target.boundInBdgWorld ? replaced : []);
511
+ if (result.error && replaced.length > 0) {
512
+ return { ...result, suggestion: withBrokenHint(result.suggestion, replaced) };
513
+ }
307
514
  if (!boundNodeMissing(result.error))
308
515
  return result;
309
516
  return {
@@ -314,6 +521,16 @@ export async function onScriptTarget(cdp, params, work) {
314
521
  };
315
522
  }
316
523
  catch (error) {
524
+ const replaced = target?.replacedBuiltins ?? [];
525
+ if (error instanceof ActionScriptError) {
526
+ throw actionScriptFailure(error, replaced, params.selector ?? '');
527
+ }
528
+ if (error instanceof CommandError && replaced.length > 0) {
529
+ const suggestion = error.metadata['suggestion'];
530
+ throw new CommandError(error.message, {
531
+ suggestion: withBrokenHint(typeof suggestion === 'string' ? suggestion : undefined, replaced),
532
+ }, error.exitCode);
533
+ }
317
534
  if (!(error instanceof CommandError) || !boundNodeMissing(error.message))
318
535
  throw error;
319
536
  throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.STALE_CACHE);
@@ -323,6 +540,24 @@ export async function onScriptTarget(cdp, params, work) {
323
540
  unbindInFrame(target.cdp);
324
541
  }
325
542
  }
543
+ /**
544
+ * The error for an action script that threw: on a page that replaced
545
+ * built-ins the scripts use, that the page broke it (90, naming them);
546
+ * otherwise what it threw (110), with the user's selector.
547
+ *
548
+ * @param error - What the action reported
549
+ * @param replaced - Built-ins the page replaced
550
+ * @param selector - Selector the user gave (or the cached query's selector)
551
+ * @returns Error to throw
552
+ */
553
+ function actionScriptFailure(error, replaced, selector) {
554
+ if (replaced.length > 0) {
555
+ const err = actionBrokenByPageError(error.action, error.exception, replaced);
556
+ return new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.RESOURCE_CONFLICT);
557
+ }
558
+ const err = actionScriptFailedError(error.action, error.exception, selector);
559
+ return new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.SOFTWARE_ERROR);
560
+ }
326
561
  /**
327
562
  * Remove a node bound in a cross-origin frame from that frame's window
328
563
  * (through the scoped connection, so the script runs where the bind ran; the
@@ -336,6 +571,38 @@ function unbindInFrame(frameConnection) {
336
571
  .send('Runtime.evaluate', { expression: UNBIND_TARGET_SCRIPT })
337
572
  .catch((error) => log.debug(`Frame target not unbound: ${getErrorMessage(error)}`));
338
573
  }
574
+ /**
575
+ * A failed action's suggestion with the hint that the page's replaced
576
+ * built-ins may have broken it.
577
+ *
578
+ * @param suggestion - The action's own suggestion
579
+ * @param replaced - Built-ins the page replaced
580
+ * @returns Both, the action's first
581
+ */
582
+ function withBrokenHint(suggestion, replaced) {
583
+ const hint = brokenByReplacedBuiltinsSuggestion(replaced);
584
+ return suggestion ? `${suggestion}. ${hint}` : hint;
585
+ }
586
+ /**
587
+ * Warn that the page replaced built-ins the page scripts use: they run in
588
+ * the page's world, so the action may misbehave (the element itself was
589
+ * found in bdg's world). The warning names the first few; `replacedBuiltins`
590
+ * lists them all.
591
+ *
592
+ * @param result - Script result
593
+ * @param replaced - Built-ins the page replaced
594
+ * @returns Result with the warning added to any it has
595
+ */
596
+ function withReplacedBuiltins(result, replaced) {
597
+ if (!replaced || replaced.length === 0)
598
+ return result;
599
+ const warning = replacedBuiltinsWarning(replaced);
600
+ return {
601
+ ...result,
602
+ warning: result.warning ? `${result.warning}; ${warning}` : warning,
603
+ replacedBuiltins: replaced,
604
+ };
605
+ }
339
606
  /**
340
607
  * Report the user's selector instead of the internal placeholder.
341
608
  *
@@ -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
@@ -0,0 +1,180 @@
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 { createLogger } from '../../ui/logging/index.js';
17
+ import { getErrorMessage } from '../../utils/errors.js';
18
+ const log = createLogger('cdp');
19
+ /** Name of bdg's isolated world (shown in DevTools' context selector) */
20
+ export const BDG_WORLD_NAME = 'bdg';
21
+ /** Errors of a context that is gone (navigation, reload) */
22
+ const CONTEXT_GONE = /Cannot find context with specified id|Execution context was destroyed/;
23
+ /** The world's context id per connection, until the page navigates */
24
+ const worlds = new WeakMap();
25
+ /**
26
+ * The execution context id of bdg's world in the top frame, created on first
27
+ * use and forgotten when the top frame navigates or its contexts are cleared.
28
+ *
29
+ * @param cdp - Connection to the page
30
+ * @returns Context id, or null when no world can be made on this connection
31
+ */
32
+ function worldContext(cdp) {
33
+ const known = worlds.get(cdp);
34
+ if (known)
35
+ return known;
36
+ const created = createWorld(cdp);
37
+ worlds.set(cdp, created);
38
+ return created;
39
+ }
40
+ /**
41
+ * Create the world in the top frame and forget it when the page changes.
42
+ *
43
+ * @param cdp - Connection to the page
44
+ * @returns Context id, or null when it cannot be created
45
+ */
46
+ async function createWorld(cdp) {
47
+ const { on } = cdp;
48
+ if (!on)
49
+ return null;
50
+ try {
51
+ const { frameTree } = (await cdp.send('Page.getFrameTree'));
52
+ const { executionContextId } = (await cdp.send('Page.createIsolatedWorld', {
53
+ frameId: frameTree.frame.id,
54
+ worldName: BDG_WORLD_NAME,
55
+ grantUniveralAccess: false,
56
+ }));
57
+ const forget = () => {
58
+ if (worlds.get(cdp) === created)
59
+ worlds.delete(cdp);
60
+ stopNavigated();
61
+ stopCleared();
62
+ };
63
+ const created = worlds.get(cdp);
64
+ const stopNavigated = on.call(cdp, 'Page.frameNavigated', (params) => {
65
+ if (params.frame.parentId === undefined)
66
+ forget();
67
+ });
68
+ const stopCleared = on.call(cdp, 'Runtime.executionContextsCleared', forget);
69
+ return executionContextId;
70
+ }
71
+ catch (error) {
72
+ log.debug(`bdg's isolated world not available: ${getErrorMessage(error)}`);
73
+ worlds.delete(cdp);
74
+ return null;
75
+ }
76
+ }
77
+ /**
78
+ * Send a command with the world's context, once more with a new world when
79
+ * the context is gone, and in the main world when there is none.
80
+ *
81
+ * @param cdp - Connection to the page
82
+ * @param method - CDP method
83
+ * @param params - Its parameters
84
+ * @param withContext - The parameters with the world's context id
85
+ * @returns The command's result
86
+ */
87
+ async function sendInWorld(cdp, method, params, withContext) {
88
+ for (let attempt = 0; attempt < 2; attempt++) {
89
+ const contextId = await worldContext(cdp);
90
+ if (contextId === null)
91
+ break;
92
+ try {
93
+ return await cdp.send(method, withContext(contextId));
94
+ }
95
+ catch (error) {
96
+ if (!CONTEXT_GONE.test(getErrorMessage(error)))
97
+ throw error;
98
+ worlds.delete(cdp);
99
+ }
100
+ }
101
+ return cdp.send(method, params);
102
+ }
103
+ /**
104
+ * `Runtime.evaluate` in bdg's world of the top frame. A call that names its
105
+ * own context is sent as it is.
106
+ *
107
+ * @param cdp - Connection to the page
108
+ * @param params - Evaluate parameters
109
+ * @returns Evaluate response
110
+ */
111
+ export async function evaluateInBdgWorld(cdp, params) {
112
+ if (params.contextId !== undefined || params.uniqueContextId !== undefined) {
113
+ return (await cdp.send('Runtime.evaluate', { ...params }));
114
+ }
115
+ return (await sendInWorld(cdp, 'Runtime.evaluate', { ...params }, (contextId) => ({
116
+ ...params,
117
+ contextId,
118
+ })));
119
+ }
120
+ /**
121
+ * `DOM.resolveNode` into bdg's world of the top frame, so functions called on
122
+ * the node run there. A node the world cannot use (one of a cross-origin
123
+ * frame, or one no longer in the page) is resolved in its own frame's main
124
+ * world, as before.
125
+ *
126
+ * @param cdp - Connection to the page
127
+ * @param params - Resolve parameters
128
+ * @returns Resolve response
129
+ */
130
+ export async function resolveNodeInBdgWorld(cdp, params) {
131
+ if (params.executionContextId !== undefined) {
132
+ return (await cdp.send('DOM.resolveNode', { ...params }));
133
+ }
134
+ try {
135
+ const resolved = (await sendInWorld(cdp, 'DOM.resolveNode', { ...params }, (executionContextId) => ({ ...params, executionContextId })));
136
+ if (await usableInWorld(cdp, resolved.object.objectId))
137
+ return resolved;
138
+ }
139
+ catch (error) {
140
+ log.debug(`Node resolved in the page's world: ${getErrorMessage(error)}`);
141
+ }
142
+ return (await cdp.send('DOM.resolveNode', { ...params }));
143
+ }
144
+ /**
145
+ * Whether a node resolved into bdg's world can be used there: a node of a
146
+ * cross-origin frame resolves, but every access to it throws.
147
+ *
148
+ * @param cdp - Connection to the page
149
+ * @param objectId - The node in bdg's world
150
+ * @returns True when it can be read
151
+ */
152
+ async function usableInWorld(cdp, objectId) {
153
+ if (!objectId)
154
+ return false;
155
+ const probe = (await cdp.send('Runtime.callFunctionOn', {
156
+ objectId,
157
+ functionDeclaration: 'function () { try { return this.isConnected === true; } catch (e) { return false; } }',
158
+ returnByValue: true,
159
+ }));
160
+ return probe.result.value === true;
161
+ }
162
+ /**
163
+ * Send a CDP command for bdg's own scripts: `Runtime.evaluate` and
164
+ * `DOM.resolveNode` go to bdg's world, other methods as they are.
165
+ *
166
+ * @param cdp - Connection to the page
167
+ * @param method - CDP method
168
+ * @param params - Its parameters
169
+ * @returns The command's result
170
+ */
171
+ export function sendForBdgScript(cdp, method, params) {
172
+ if (method === 'Runtime.evaluate') {
173
+ return evaluateInBdgWorld(cdp, params);
174
+ }
175
+ if (method === 'DOM.resolveNode') {
176
+ return resolveNodeInBdgWorld(cdp, params);
177
+ }
178
+ return cdp.send(method, params);
179
+ }
180
+ //# sourceMappingURL=bdgWorld.js.map