browser-debugger-cli 0.13.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 (112) hide show
  1. package/.claude/skills/bdg/SKILL.md +100 -186
  2. package/README.md +4 -4
  3. package/dist/commands/console.js +5 -1
  4. package/dist/commands/dom/a11y.d.ts +1 -1
  5. package/dist/commands/dom/a11y.js +20 -20
  6. package/dist/commands/dom/eval.d.ts +2 -1
  7. package/dist/commands/dom/eval.js +21 -3
  8. package/dist/commands/dom/formInteraction.js +1 -1
  9. package/dist/commands/dom/get.js +25 -7
  10. package/dist/commands/dom/index.js +7 -2
  11. package/dist/commands/dom/query.d.ts +2 -1
  12. package/dist/commands/dom/query.js +5 -3
  13. package/dist/commands/dom/screenshot.js +1 -0
  14. package/dist/commands/helpJson.js +1 -1
  15. package/dist/commands/network/list.js +46 -3
  16. package/dist/commands/optionBehaviors.d.ts +25 -2
  17. package/dist/commands/optionBehaviors.js +55 -42
  18. package/dist/commands/peek.js +3 -0
  19. package/dist/commands/shared/CommandRunner.js +13 -13
  20. package/dist/commands/shared/daemonErrorHandler.js +2 -2
  21. package/dist/commands/shared/dataFetcher.d.ts +4 -2
  22. package/dist/commands/shared/dataFetcher.js +11 -3
  23. package/dist/commands/shared/handleValidationError.js +3 -3
  24. package/dist/commands/shared/optionTypes.d.ts +14 -3
  25. package/dist/commands/shared/startHelpers.js +3 -3
  26. package/dist/connection/chromeIdentity.d.ts +8 -2
  27. package/dist/connection/chromeIdentity.js +85 -13
  28. package/dist/constants.d.ts +29 -1
  29. package/dist/constants.js +35 -1
  30. package/dist/daemon/SessionController.js +2 -0
  31. package/dist/daemon/session/Session.d.ts +2 -1
  32. package/dist/daemon/session/Session.js +10 -2
  33. package/dist/daemon/session/TelemetryStore.d.ts +7 -0
  34. package/dist/daemon/session/TelemetryStore.js +6 -0
  35. package/dist/daemon/session/commandRegistry.js +23 -5
  36. package/dist/daemon/session/matchedStylesReset.d.ts +26 -0
  37. package/dist/daemon/session/matchedStylesReset.js +46 -0
  38. package/dist/daemon/session/plugins.js +1 -0
  39. package/dist/daemon/session/triggeredRequests.d.ts +0 -5
  40. package/dist/daemon/session/triggeredRequests.js +13 -7
  41. package/dist/daemon.js +742 -460
  42. package/dist/errors/messages.d.ts +8 -0
  43. package/dist/errors/messages.js +10 -0
  44. package/dist/index.js +710 -518
  45. package/dist/ipc/protocol/commands.d.ts +4 -0
  46. package/dist/ipc/protocol/inspectTypes.d.ts +5 -2
  47. package/dist/ipc/session/types.d.ts +5 -1
  48. package/dist/program.d.ts +14 -0
  49. package/dist/program.js +53 -0
  50. package/dist/runtime/dom/elementGeometry.d.ts +23 -0
  51. package/dist/runtime/dom/elementGeometry.js +17 -15
  52. package/dist/runtime/dom/elementInfo.d.ts +6 -4
  53. package/dist/runtime/dom/elementInfo.js +7 -4
  54. package/dist/runtime/dom/frameScopedConnection.d.ts +7 -0
  55. package/dist/runtime/dom/frameScopedConnection.js +2 -2
  56. package/dist/runtime/dom/inspect.d.ts +17 -3
  57. package/dist/runtime/dom/inspect.js +40 -26
  58. package/dist/runtime/dom/inspectModel.d.ts +3 -3
  59. package/dist/runtime/dom/inspectRules.d.ts +29 -3
  60. package/dist/runtime/dom/inspectRules.js +205 -11
  61. package/dist/runtime/dom/layout.d.ts +0 -2
  62. package/dist/runtime/dom/layout.js +1 -2
  63. package/dist/runtime/dom/reactEventHelpers.d.ts +4 -1
  64. package/dist/runtime/dom/reactEventHelpers.js +9 -2
  65. package/dist/runtime/dom/targetNode.d.ts +10 -6
  66. package/dist/runtime/dom/targetNode.js +15 -8
  67. package/dist/telemetry/a11y.d.ts +15 -1
  68. package/dist/telemetry/a11y.js +83 -0
  69. package/dist/telemetry/har/builder.js +1 -1
  70. package/dist/telemetry/network.d.ts +13 -16
  71. package/dist/telemetry/network.js +30 -52
  72. package/dist/telemetry/networkRetention.d.ts +83 -0
  73. package/dist/telemetry/networkRetention.js +117 -0
  74. package/dist/types.d.ts +26 -0
  75. package/dist/ui/OutputBuilder.d.ts +10 -0
  76. package/dist/ui/OutputBuilder.js +12 -0
  77. package/dist/ui/formatters/a11y.d.ts +5 -7
  78. package/dist/ui/formatters/a11y.js +7 -61
  79. package/dist/ui/formatters/console/chronological.js +4 -4
  80. package/dist/ui/formatters/console/follow.d.ts +4 -2
  81. package/dist/ui/formatters/console/follow.js +6 -3
  82. package/dist/ui/formatters/console/json.d.ts +3 -6
  83. package/dist/ui/formatters/console/json.js +9 -13
  84. package/dist/ui/formatters/console/shared.d.ts +17 -2
  85. package/dist/ui/formatters/console/shared.js +17 -0
  86. package/dist/ui/formatters/console/summarize.d.ts +2 -2
  87. package/dist/ui/formatters/console/summarize.js +22 -7
  88. package/dist/ui/formatters/console.d.ts +1 -1
  89. package/dist/ui/formatters/console.js +1 -5
  90. package/dist/ui/formatters/details.js +1 -1
  91. package/dist/ui/formatters/dom.d.ts +13 -4
  92. package/dist/ui/formatters/dom.js +25 -7
  93. package/dist/ui/formatters/layout.js +2 -1
  94. package/dist/ui/formatters/longValues.d.ts +14 -0
  95. package/dist/ui/formatters/longValues.js +23 -0
  96. package/dist/ui/formatters/networkList.d.ts +8 -2
  97. package/dist/ui/formatters/networkList.js +11 -2
  98. package/dist/ui/formatters/preview.d.ts +4 -1
  99. package/dist/ui/formatters/preview.js +55 -13
  100. package/dist/ui/formatters/status.js +7 -0
  101. package/dist/ui/formatters/triggeredRequests.js +2 -1
  102. package/dist/ui/messages/chrome.d.ts +20 -1
  103. package/dist/ui/messages/chrome.js +29 -3
  104. package/dist/ui/messages/commands.d.ts +29 -8
  105. package/dist/ui/messages/commands.js +36 -8
  106. package/dist/ui/messages/networkMessages.d.ts +24 -0
  107. package/dist/ui/messages/networkMessages.js +45 -0
  108. package/dist/utils/http.d.ts +9 -2
  109. package/dist/utils/http.js +4 -3
  110. package/dist/utils/strings.d.ts +19 -0
  111. package/dist/utils/strings.js +16 -0
  112. package/package.json +2 -2
@@ -37,24 +37,218 @@ export function trackStyleSheets(cdp) {
37
37
  export function styleSheetHeaders(cdp) {
38
38
  return headersByConnection.get(cdp)?.values() ?? [];
39
39
  }
40
+ /** Time allowed for the matched rules behind the default hints; a read within it clears the slow mark */
41
+ export const HINTS_BUDGET_MS = 1000;
42
+ /** Time allowed for them with --rules or --why */
43
+ export const RULES_BUDGET_MS = 5000;
44
+ /** Answers that came faster are not kept: reading again is cheap and always current */
45
+ const KEEP_ANSWERS_SLOWER_THAN_MS = 300;
46
+ /**
47
+ * How long a kept answer is reused. Page state such as `:checked`, `:hover`
48
+ * or `:focus` changes without any CDP event, so answers are kept only briefly
49
+ * (and dropped by every command that may change the page, see {@link resetMatchedStyles}).
50
+ */
51
+ const KEPT_ANSWER_TTL_MS = 5000;
52
+ /** Answers kept per document (one can be several MB on CSS-heavy pages) */
53
+ const MAX_KEPT_ANSWERS = 4;
54
+ /** Stylesheet events: kept answers are dropped and the slow mark cleared */
55
+ const STYLESHEET_EVENTS = ['CSS.styleSheetAdded', 'CSS.styleSheetChanged', 'CSS.styleSheetRemoved'];
56
+ /** Other events after which kept answers may be stale */
57
+ const STYLE_CHANGE_EVENTS = [
58
+ 'CSS.mediaQueryResultChanged',
59
+ 'DOM.attributeModified',
60
+ 'DOM.attributeRemoved',
61
+ 'DOM.inlineStyleInvalidated',
62
+ 'DOM.childNodeInserted',
63
+ 'DOM.childNodeRemoved',
64
+ 'DOM.childNodeCountUpdated',
65
+ 'DOM.pseudoElementAdded',
66
+ 'DOM.pseudoElementRemoved',
67
+ ];
68
+ /** Matched-styles state per connection */
69
+ const documentStylesByConnection = new WeakMap();
70
+ /**
71
+ * The matched-styles state of a connection, tracked from its first use: a
72
+ * new document (`DOM.documentUpdated`) starts afresh (a request still running
73
+ * for the old one no longer holds back new ones), stylesheet changes drop the
74
+ * kept answers and the slow mark, style and DOM changes the kept answers.
75
+ * Events of other sessions (iframes) are ignored.
76
+ *
77
+ * @param cdp - CDP connection
78
+ * @returns State
79
+ */
80
+ function documentStyles(cdp) {
81
+ const existing = documentStylesByConnection.get(cdp);
82
+ if (existing)
83
+ return existing;
84
+ const state = { requests: new Map(), slow: false, document: 0 };
85
+ documentStylesByConnection.set(cdp, state);
86
+ cdp.on('DOM.documentUpdated', (_params, sessionId) => {
87
+ if (sessionId)
88
+ return;
89
+ state.document++;
90
+ state.requests.clear();
91
+ state.running = undefined;
92
+ state.slow = false;
93
+ });
94
+ for (const event of STYLESHEET_EVENTS) {
95
+ cdp.on(event, (_params, sessionId) => {
96
+ if (sessionId)
97
+ return;
98
+ state.requests.clear();
99
+ state.slow = false;
100
+ });
101
+ }
102
+ for (const event of STYLE_CHANGE_EVENTS) {
103
+ cdp.on(event, (_params, sessionId) => {
104
+ if (!sessionId)
105
+ state.requests.clear();
106
+ });
107
+ }
108
+ return state;
109
+ }
110
+ /**
111
+ * Forget the kept answers of a connection (requests still running stay
112
+ * shared). For commands that may change the page in ways CDP reports no
113
+ * event for: clicks, typing, hovering, scripts, emulation.
114
+ *
115
+ * @param cdp - CDP connection
116
+ */
117
+ export function resetMatchedStyles(cdp) {
118
+ documentStylesByConnection.get(cdp)?.requests.clear();
119
+ }
120
+ /**
121
+ * Record an answer: a fast one clears the slow mark and is dropped, a slow
122
+ * one is kept for {@link KEPT_ANSWER_TTL_MS}, a failed one is dropped.
123
+ *
124
+ * @param state - Document state
125
+ * @param nodeId - Node id of the element
126
+ * @param request - The request
127
+ * @param tookMs - How long Chrome took
128
+ */
129
+ function settleRequest(state, nodeId, request, tookMs) {
130
+ if (state.running === request.promise)
131
+ state.running = undefined;
132
+ if (request.document !== state.document)
133
+ return;
134
+ const answered = request.answer !== 'failed';
135
+ if (answered && tookMs <= HINTS_BUDGET_MS)
136
+ state.slow = false;
137
+ if (answered && tookMs > KEEP_ANSWERS_SLOWER_THAN_MS) {
138
+ request.expiresAt = Date.now() + KEPT_ANSWER_TTL_MS;
139
+ }
140
+ else if (state.requests.get(nodeId) === request) {
141
+ state.requests.delete(nodeId);
142
+ }
143
+ }
144
+ /**
145
+ * Send a matched-styles request and share it while it runs, dropping the
146
+ * least recently used entries beyond {@link MAX_KEPT_ANSWERS}.
147
+ *
148
+ * @param cdp - CDP connection
149
+ * @param state - Document state
150
+ * @param nodeId - Node id of the element
151
+ * @returns The request
152
+ */
153
+ function sendMatchedRequest(cdp, state, nodeId) {
154
+ const sentAt = Date.now();
155
+ const request = {
156
+ document: state.document,
157
+ promise: cdp
158
+ .send('CSS.getMatchedStylesForNode', { nodeId })
159
+ .then((response) => response)
160
+ .catch((error) => {
161
+ log.debug(`CSS.getMatchedStylesForNode failed: ${String(error)}`);
162
+ return 'failed';
163
+ }),
164
+ };
165
+ state.running = request.promise;
166
+ void request.promise.then((answer) => {
167
+ request.answer = answer;
168
+ settleRequest(state, nodeId, request, Date.now() - sentAt);
169
+ });
170
+ state.requests.set(nodeId, request);
171
+ for (const oldest of state.requests.keys()) {
172
+ if (state.requests.size <= MAX_KEPT_ANSWERS)
173
+ break;
174
+ state.requests.delete(oldest);
175
+ }
176
+ return request;
177
+ }
178
+ /**
179
+ * The element's request still running or kept answer, unless expired;
180
+ * marked as the most recently used.
181
+ *
182
+ * @param state - Document state
183
+ * @param nodeId - Node id of the element
184
+ * @returns The request, or undefined
185
+ */
186
+ function keptRequest(state, nodeId) {
187
+ const kept = state.requests.get(nodeId);
188
+ if (!kept)
189
+ return undefined;
190
+ state.requests.delete(nodeId);
191
+ if (kept.expiresAt !== undefined && kept.expiresAt <= Date.now())
192
+ return undefined;
193
+ state.requests.set(nodeId, kept);
194
+ return kept;
195
+ }
196
+ /**
197
+ * The element's request, sent once Chrome has answered the one it is
198
+ * working on (another element's, or this one's sent by a concurrent call).
199
+ *
200
+ * @param cdp - CDP connection
201
+ * @param state - Document state
202
+ * @param nodeId - Node id of the element
203
+ * @param deadline - When to give up waiting
204
+ * @returns The request, or undefined when the running one outlasted the deadline
205
+ */
206
+ async function requestWhenFree(cdp, state, nodeId, deadline) {
207
+ while (state.running) {
208
+ const kept = state.requests.get(nodeId);
209
+ if (kept)
210
+ return kept;
211
+ if ((await raceTimeout(state.running, deadline - Date.now())) === undefined)
212
+ return undefined;
213
+ }
214
+ return state.requests.get(nodeId) ?? sendMatchedRequest(cdp, state, nodeId);
215
+ }
40
216
  /**
41
217
  * The rules that match an element, with its inline style and what its
42
- * ancestors pass down, or why they are missing.
218
+ * ancestors pass down, or why they are missing. A request still running for
219
+ * the element is shared; a slow answer (over {@link KEEP_ANSWERS_SLOWER_THAN_MS})
220
+ * is reused for {@link KEPT_ANSWER_TTL_MS} unless the document, its
221
+ * stylesheets or its DOM change or a command may have changed the page.
222
+ * Another element's request is sent only after the one Chrome is working on,
223
+ * within the budget. When the request this call sent or shared outlasts the
224
+ * budget, the document is marked slow: `skipWhenSlow` reads then return
225
+ * `skipped` at once (unless the answer is kept) until a read is fast again,
226
+ * a stylesheet changes or the page navigates.
43
227
  *
44
228
  * @param cdp - CDP connection
45
229
  * @param nodeId - Node id of the element
46
230
  * @param budgetMs - Time allowed
47
- * @returns Matched styles, `timeout` (longer than the budget) or `failed` (CDP error)
231
+ * @param options - `skipWhenSlow`: do not wait on a document marked slow (the default hints)
232
+ * @returns Matched styles, `timeout` (longer than the budget), `failed` (CDP error)
233
+ * or `skipped` (slow document)
48
234
  */
49
- export async function matchedStyles(cdp, nodeId, budgetMs) {
50
- const request = cdp
51
- .send('CSS.getMatchedStylesForNode', { nodeId })
52
- .then((response) => response)
53
- .catch((error) => {
54
- log.debug(`CSS.getMatchedStylesForNode failed: ${String(error)}`);
55
- return 'failed';
56
- });
57
- return (await raceTimeout(request, budgetMs)) ?? 'timeout';
235
+ export async function matchedStyles(cdp, nodeId, budgetMs, options = {}) {
236
+ const state = documentStyles(cdp);
237
+ const kept = keptRequest(state, nodeId);
238
+ if (kept?.answer)
239
+ return kept.answer;
240
+ if (options.skipWhenSlow && state.slow)
241
+ return 'skipped';
242
+ const deadline = Date.now() + budgetMs;
243
+ const request = kept ?? (await requestWhenFree(cdp, state, nodeId, deadline));
244
+ if (!request)
245
+ return 'timeout';
246
+ const answer = await raceTimeout(request.promise, deadline - Date.now());
247
+ if (answer)
248
+ return answer;
249
+ if (request.document === state.document)
250
+ state.slow = true;
251
+ return 'timeout';
58
252
  }
59
253
  /**
60
254
  * File name of a URL, without query or hash; the host for a site's root.
@@ -11,8 +11,6 @@ import type { CDPConnection } from '../../connection/cdp.js';
11
11
  import type { DomLayoutCommand } from '../../ipc/protocol/commands.js';
12
12
  import type { LayoutComputedStyle, LayoutResult, PageLayout } from '../../ipc/protocol/domTypes.js';
13
13
  import { type ElementGeometry } from './elementGeometry.js';
14
- /** Matches measured per command (the rest are counted as omitted) */
15
- export declare const LAYOUT_ELEMENT_LIMIT = 100;
16
14
  /**
17
15
  * Page-side end of a visible span kept clear of an overlay scrollbar along
18
16
  * an edge (16 CSS px wide): overlay scrollbars (macOS, mobile) show for about a
@@ -7,6 +7,7 @@
7
7
  * shows. The measurements are classified outside the page
8
8
  * ({@link classifyViewportPosition}).
9
9
  */
10
+ import { LAYOUT_ELEMENT_LIMIT } from '../../constants.js';
10
11
  import { CommandError } from '../../errors/index.js';
11
12
  import { operationFailedError } from '../../errors/messages.js';
12
13
  import { ELEMENT_GEOMETRY_JS, FRAME_OFFSET_JS, VIEWPORT_SIZE_JS, classifyViewportPosition, } from './elementGeometry.js';
@@ -20,8 +21,6 @@ import { createLogger } from '../../ui/logging/index.js';
20
21
  import { getErrorMessage } from '../../utils/errors.js';
21
22
  import { EXIT_CODES } from '../../utils/exitCodes.js';
22
23
  const log = createLogger('dom');
23
- /** Matches measured per command (the rest are counted as omitted) */
24
- export const LAYOUT_ELEMENT_LIMIT = 100;
25
24
  /**
26
25
  * Page-side end of a visible span kept clear of an overlay scrollbar along
27
26
  * an edge (16 CSS px wide): overlay scrollbars (macOS, mobile) show for about a
@@ -107,7 +107,10 @@ export declare const FILL_READ_BACK_SCRIPT: string;
107
107
  * dialog or bubble can swallow input while the page looks normal.
108
108
  * Slotted text hit-tests as its shadow host, so an element in a shadow root
109
109
  * that shows slotted content (a button labelled through a `<slot>`) is
110
- * topmost where its host is hit.
110
+ * topmost where its host is hit. An element that is not topmost because an
111
+ * ancestor in the flat tree clips it away (a collapsed
112
+ * `height: 0; overflow: hidden` accordion, {@link ANCESTOR_CLIP_JS}) is
113
+ * reported as hidden by it, not as covered.
111
114
  */
112
115
  export declare const CLICK_ELEMENT_SCRIPT: string;
113
116
  /**
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { FILL_REFUSALS, LABEL_WITHOUT_CONTROL, NAME_QUERY_PLACEHOLDER, VIA_LABEL_SUFFIX, } from '../../errors/messages.js';
9
9
  import { REVEAL_SNAPSHOT_JS } from './actionEffectsScripts.js';
10
+ import { ANCESTOR_CLIP_JS } from './elementGeometry.js';
10
11
  import { DISABLED_CAUSE_JS, ELEMENT_DESCRIPTION_JS, ELEMENT_IDENTITY_JS, } from './elementInfo.js';
11
12
  import { FIND_ELEMENTS_JS, LABEL_CONTROL_JS } from './targetNode.js';
12
13
  /**
@@ -484,7 +485,10 @@ export const FILL_READ_BACK_SCRIPT = `(() => {
484
485
  * dialog or bubble can swallow input while the page looks normal.
485
486
  * Slotted text hit-tests as its shadow host, so an element in a shadow root
486
487
  * that shows slotted content (a button labelled through a `<slot>`) is
487
- * topmost where its host is hit.
488
+ * topmost where its host is hit. An element that is not topmost because an
489
+ * ancestor in the flat tree clips it away (a collapsed
490
+ * `height: 0; overflow: hidden` accordion, {@link ANCESTOR_CLIP_JS}) is
491
+ * reported as hidden by it, not as covered.
488
492
  */
489
493
  export const CLICK_ELEMENT_SCRIPT = `
490
494
  (function(selector, parts, index, action) {
@@ -639,7 +643,10 @@ export const CLICK_ELEMENT_SCRIPT = `
639
643
  else if (el.closest('[inert]')) obstruction = 'inert (the page made it non-interactive)';
640
644
  else if (style.pointerEvents === 'none') obstruction = 'not clickable (pointer-events: none)';
641
645
  else if (!hasSize) obstruction = 'zero-size';
642
- else if (!hittable) obstruction = 'covered by another element' + coveredBy();
646
+ else if (!hittable) {
647
+ const collapsed = (${ANCESTOR_CLIP_JS})(el, rect, describe).collapsed;
648
+ obstruction = collapsed ? 'hidden (' + collapsed + ')' : 'covered by another element' + coveredBy();
649
+ }
643
650
 
644
651
  if (action === 'hover') (${REVEAL_SNAPSHOT_JS})(el);
645
652
 
@@ -26,16 +26,20 @@ export declare const UNBIND_TARGET_SCRIPT = "delete window.__bdgTarget; delete w
26
26
  * the text nodes of hidden ones (display or visibility; not those of
27
27
  * `<script>`, `<style>` or `<noscript>`), so text filters match hidden
28
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.
29
+ * use their value. Text is read in the flat tree, as `bdg dom query` shows
30
+ * it: the text a web component's open shadow root renders, slotted content
31
+ * in place of its slots. A visible component, slot or element holding either
32
+ * ({@link COMPOSED_JS}) is read with {@link FLAT_TEXT_JS}, including the
33
+ * selects and editable regions `innerText` reads. Whitespace is collapsed;
34
+ * filter texts arrive normalized (`has-text` lowercased). `:visible` is
35
+ * checked before the text filters, which read the text.
32
36
  *
33
37
  * Steps and `:has()` match CSS relative to an element (`:scope > css`). A
34
38
  * descendant step also searches the open shadow roots under the element (the
35
39
  * whole step CSS inside one shadow tree); a child step stays in the
36
40
  * element's own tree.
37
41
  */
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}";
42
+ export declare const FILTER_MATCHING_JS = "(shadowRoots) => {\n const composed = (node) => {\n const composes = (n) => Boolean(n.shadowRoot) || n.localName === 'slot';\n if (composes(node)) return true;\n const walker = node.ownerDocument.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);\n while (walker.nextNode()) if (composes(walker.currentNode)) return true;\n return false;\n};\n const flatText = (el, limit, fields) => {\n const composed = (node) => {\n const composes = (n) => Boolean(n.shadowRoot) || n.localName === 'slot';\n if (composes(node)) return true;\n const walker = node.ownerDocument.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);\n while (walker.nextNode()) if (composes(walker.currentNode)) return true;\n return false;\n};\n const view = el.ownerDocument.defaultView;\n let text = '';\n const nodesOf = (node) =>\n node.localName === 'slot' ? node.assignedNodes({ flatten: true }) : Array.from((node.shadowRoot || node).childNodes);\n const read = (node, visible) => {\n if (text.length >= limit) return;\n if (node.nodeType === 3 && visible) text += node.data.replace(/\\s+/g, ' ');\n if (node.nodeType !== 1) return;\n if (/^(input|textarea)$/.test(node.localName)) return;\n if (!fields && (node.localName === 'select' || node.isContentEditable)) return;\n const style = view.getComputedStyle(node);\n if (node.checkVisibility && !node.checkVisibility() && style.display !== 'contents') return;\n if (node.localName === 'br') text += '\\n';\n const apart = style.display.startsWith('inline') || style.display === 'contents' ? '' : '\\n';\n text += apart;\n const own = node.textContent || '';\n if (composed(node)) nodesOf(node).forEach((child) => read(child, style.visibility === 'visible'));\n else if (own.length > limit - text.length) text += own.slice(0, limit - text.length);\n else text += typeof node.innerText === 'string' ? node.innerText : own;\n text += apart;\n };\n const visible = view.getComputedStyle(el).visibility === 'visible';\n nodesOf(el).forEach((child) => read(child, visible));\n return text.slice(0, limit);\n};\n const skipped = /^(script|style|noscript|template)$/;\n const hiddenText = (el) => {\n const nodes = el.localName === 'slot' ? el.assignedNodes({ flatten: true }) : (el.shadowRoot || el).childNodes;\n let text = '';\n for (const node of nodes) {\n if (node.nodeType === 3) text += node.data;\n else if (node.nodeType === 1 && !skipped.test(node.localName)) text += hiddenText(node);\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 const text = !rendered ? hiddenText(el) : composed(el) ? flatText(el, Infinity, true) : el.innerText;\n return text.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}";
39
43
  /**
40
44
  * Page-side selector search that also reaches into open shadow roots and
41
45
  * same-origin iframes (recursively), like a user sees the page.
@@ -49,7 +53,7 @@ export declare const FILTER_MATCHING_JS = "(shadowRoots) => {\n const skipped =
49
53
  * reached. Cross-origin iframes are separate processes and cannot be
50
54
  * searched. Throws a `SyntaxError` for an invalid selector.
51
55
  */
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}";
56
+ 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 composed = (node) => {\n const composes = (n) => Boolean(n.shadowRoot) || n.localName === 'slot';\n if (composes(node)) return true;\n const walker = node.ownerDocument.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);\n while (walker.nextNode()) if (composes(walker.currentNode)) return true;\n return false;\n};\n const flatText = (el, limit, fields) => {\n const composed = (node) => {\n const composes = (n) => Boolean(n.shadowRoot) || n.localName === 'slot';\n if (composes(node)) return true;\n const walker = node.ownerDocument.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);\n while (walker.nextNode()) if (composes(walker.currentNode)) return true;\n return false;\n};\n const view = el.ownerDocument.defaultView;\n let text = '';\n const nodesOf = (node) =>\n node.localName === 'slot' ? node.assignedNodes({ flatten: true }) : Array.from((node.shadowRoot || node).childNodes);\n const read = (node, visible) => {\n if (text.length >= limit) return;\n if (node.nodeType === 3 && visible) text += node.data.replace(/\\s+/g, ' ');\n if (node.nodeType !== 1) return;\n if (/^(input|textarea)$/.test(node.localName)) return;\n if (!fields && (node.localName === 'select' || node.isContentEditable)) return;\n const style = view.getComputedStyle(node);\n if (node.checkVisibility && !node.checkVisibility() && style.display !== 'contents') return;\n if (node.localName === 'br') text += '\\n';\n const apart = style.display.startsWith('inline') || style.display === 'contents' ? '' : '\\n';\n text += apart;\n const own = node.textContent || '';\n if (composed(node)) nodesOf(node).forEach((child) => read(child, style.visibility === 'visible'));\n else if (own.length > limit - text.length) text += own.slice(0, limit - text.length);\n else text += typeof node.innerText === 'string' ? node.innerText : own;\n text += apart;\n };\n const visible = view.getComputedStyle(el).visibility === 'visible';\n nodesOf(el).forEach((child) => read(child, visible));\n return text.slice(0, limit);\n};\n const skipped = /^(script|style|noscript|template)$/;\n const hiddenText = (el) => {\n const nodes = el.localName === 'slot' ? el.assignedNodes({ flatten: true }) : (el.shadowRoot || el).childNodes;\n let text = '';\n for (const node of nodes) {\n if (node.nodeType === 3) text += node.data;\n else if (node.nodeType === 1 && !skipped.test(node.localName)) text += hiddenText(node);\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 const text = !rendered ? hiddenText(el) : composed(el) ? flatText(el, Infinity, true) : el.innerText;\n return text.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}";
53
57
  /**
54
58
  * Page-side element lookup shared by the interaction scripts.
55
59
  *
@@ -58,7 +62,7 @@ export declare const DEEP_QUERY_JS = "function (selector, parts) {\n const css
58
62
  * built-ins ({@link bindMatches}), otherwise all matches of the selector
59
63
  * ({@link DEEP_QUERY_JS}).
60
64
  */
61
- export declare const FIND_ELEMENTS_JS = "function (selector, parts) {\n if (selector === '__bdg_bound_target__') {\n const el = window.__bdgTarget;\n return el && el.isConnected ? [el] : [];\n }\n const bound = window.__bdgMatches;\n if (bound && bound.selector === selector) {\n const connected = [];\n for (let i = 0; i < bound.nodes.length; i++) {\n if (bound.nodes[i] && bound.nodes[i].isConnected) connected[connected.length] = bound.nodes[i];\n }\n return connected;\n }\n return (function (selector, parts) {\n const css = parts ? parts.map((part) => part.css).join(', ') : selector;\n const found = [];\n const roots = [];\n const visit = (root) => {\n roots.push(root);\n for (const match of root.querySelectorAll(css)) found.push(match);\n for (const el of root.querySelectorAll('*')) {\n if (el.shadowRoot) visit(el.shadowRoot);\n if (el.tagName === 'IFRAME' || el.tagName === 'FRAME') {\n let frameDocument = null;\n try { frameDocument = el.contentDocument; } catch (e) { frameDocument = null; }\n if (frameDocument) visit(frameDocument);\n }\n }\n };\n visit(document);\n if (!parts) return found;\n const shadowRoots = roots.filter((root) => root.host);\n const { passesAll, descend } = ((shadowRoots) => {\n const skipped = /^(script|style|noscript|template)$/;\n const hiddenText = (el) => {\n const walker = el.ownerDocument.createTreeWalker(el, 5, {\n acceptNode: (node) => (node.nodeType === 1 && skipped.test(node.localName) ? 2 : 1)\n });\n let text = '';\n for (let node = walker.nextNode(); node; node = walker.nextNode()) {\n if (node.nodeType === 3) text += node.data;\n }\n return text;\n };\n const textOf = (el) => {\n if (el.localName === 'input' && /^(submit|button|reset)$/i.test(el.type)) return el.value;\n const rendered = typeof el.innerText === 'string' &&\n (typeof el.checkVisibility !== 'function' || el.checkVisibility({ visibilityProperty: true }));\n return (rendered ? el.innerText : hiddenText(el)).replace(/\\s+/g, ' ').trim();\n };\n const passes = (el, filter) => {\n if (filter.kind === 'visible') {\n if (typeof el.checkVisibility === 'function' && !el.checkVisibility()) return false;\n const rect = el.getBoundingClientRect();\n return rect.width > 0 && rect.height > 0 && el.ownerDocument.defaultView.getComputedStyle(el).visibility === 'visible';\n }\n if (filter.kind === 'has') return filter.selectors.some((chain) => descend(el, chain).length > 0);\n const text = textOf(el);\n return filter.kind === 'text-is' ? text === filter.text : text.toLowerCase().includes(filter.text);\n };\n const passesAll = (el, filters) =>\n filters.every((filter) => filter.kind !== 'visible' || passes(el, filter)) &&\n filters.every((filter) => filter.kind === 'visible' || passes(el, filter));\n const isUnder = (scope, node) => {\n for (let current = node; current; current = current.getRootNode().host) {\n if (scope === current || scope.contains(current)) return true;\n }\n return false;\n };\n const stepMatches = (scope, step) => {\n const matches = [...scope.querySelectorAll(':scope ' + step.combinator + ' ' + step.css)];\n if (step.combinator !== ' ') return matches;\n for (const root of shadowRoots) {\n if (isUnder(scope, root.host)) matches.push(...root.querySelectorAll(step.css));\n }\n return matches;\n };\n const descend = (scope, steps) => {\n let current = [scope];\n for (const step of steps) {\n const next = new Set();\n for (const el of current) {\n for (const match of stepMatches(el, step)) {\n if (passesAll(match, step.filters)) next.add(match);\n }\n }\n current = [...next];\n }\n return current;\n };\n return { passesAll: passesAll, descend: descend };\n})(shadowRoots);\n const result = new Set();\n for (const el of found) {\n for (const part of parts) {\n if (!el.matches(part.css) || !passesAll(el, part.filters)) continue;\n for (const match of descend(el, part.steps || [])) result.add(match);\n }\n }\n const rootOrder = new Map(roots.map((root, i) => [root, i]));\n const rootIndex = (el) => rootOrder.get(el.getRootNode()) ?? roots.length;\n return [...result].sort((a, b) =>\n rootIndex(a) - rootIndex(b) || (a.compareDocumentPosition(b) & 4 ? -1 : a === b ? 0 : 1)\n );\n})(selector, parts);\n}";
65
+ export declare const FIND_ELEMENTS_JS = "function (selector, parts) {\n if (selector === '__bdg_bound_target__') {\n const el = window.__bdgTarget;\n return el && el.isConnected ? [el] : [];\n }\n const bound = window.__bdgMatches;\n if (bound && bound.selector === selector) {\n const connected = [];\n for (let i = 0; i < bound.nodes.length; i++) {\n if (bound.nodes[i] && bound.nodes[i].isConnected) connected[connected.length] = bound.nodes[i];\n }\n return connected;\n }\n return (function (selector, parts) {\n const css = parts ? parts.map((part) => part.css).join(', ') : selector;\n const found = [];\n const roots = [];\n const visit = (root) => {\n roots.push(root);\n for (const match of root.querySelectorAll(css)) found.push(match);\n for (const el of root.querySelectorAll('*')) {\n if (el.shadowRoot) visit(el.shadowRoot);\n if (el.tagName === 'IFRAME' || el.tagName === 'FRAME') {\n let frameDocument = null;\n try { frameDocument = el.contentDocument; } catch (e) { frameDocument = null; }\n if (frameDocument) visit(frameDocument);\n }\n }\n };\n visit(document);\n if (!parts) return found;\n const shadowRoots = roots.filter((root) => root.host);\n const { passesAll, descend } = ((shadowRoots) => {\n const composed = (node) => {\n const composes = (n) => Boolean(n.shadowRoot) || n.localName === 'slot';\n if (composes(node)) return true;\n const walker = node.ownerDocument.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);\n while (walker.nextNode()) if (composes(walker.currentNode)) return true;\n return false;\n};\n const flatText = (el, limit, fields) => {\n const composed = (node) => {\n const composes = (n) => Boolean(n.shadowRoot) || n.localName === 'slot';\n if (composes(node)) return true;\n const walker = node.ownerDocument.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);\n while (walker.nextNode()) if (composes(walker.currentNode)) return true;\n return false;\n};\n const view = el.ownerDocument.defaultView;\n let text = '';\n const nodesOf = (node) =>\n node.localName === 'slot' ? node.assignedNodes({ flatten: true }) : Array.from((node.shadowRoot || node).childNodes);\n const read = (node, visible) => {\n if (text.length >= limit) return;\n if (node.nodeType === 3 && visible) text += node.data.replace(/\\s+/g, ' ');\n if (node.nodeType !== 1) return;\n if (/^(input|textarea)$/.test(node.localName)) return;\n if (!fields && (node.localName === 'select' || node.isContentEditable)) return;\n const style = view.getComputedStyle(node);\n if (node.checkVisibility && !node.checkVisibility() && style.display !== 'contents') return;\n if (node.localName === 'br') text += '\\n';\n const apart = style.display.startsWith('inline') || style.display === 'contents' ? '' : '\\n';\n text += apart;\n const own = node.textContent || '';\n if (composed(node)) nodesOf(node).forEach((child) => read(child, style.visibility === 'visible'));\n else if (own.length > limit - text.length) text += own.slice(0, limit - text.length);\n else text += typeof node.innerText === 'string' ? node.innerText : own;\n text += apart;\n };\n const visible = view.getComputedStyle(el).visibility === 'visible';\n nodesOf(el).forEach((child) => read(child, visible));\n return text.slice(0, limit);\n};\n const skipped = /^(script|style|noscript|template)$/;\n const hiddenText = (el) => {\n const nodes = el.localName === 'slot' ? el.assignedNodes({ flatten: true }) : (el.shadowRoot || el).childNodes;\n let text = '';\n for (const node of nodes) {\n if (node.nodeType === 3) text += node.data;\n else if (node.nodeType === 1 && !skipped.test(node.localName)) text += hiddenText(node);\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 const text = !rendered ? hiddenText(el) : composed(el) ? flatText(el, Infinity, true) : el.innerText;\n return text.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}";
62
66
  /**
63
67
  * Page-side check of what selectors cannot search in the top document:
64
68
  * iframes whose document cannot be read (cross-origin) and `<object>`/`<embed>`
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import { CommandError } from '../../errors/index.js';
12
12
  import { actionBrokenByPageError, actionScriptFailedError, emptySelectorError, indexOutOfRangeError, noNodesFoundError, staleNodeError, } from '../../errors/messages.js';
13
+ import { COMPOSED_JS, FLAT_TEXT_JS } from './elementInfo.js';
13
14
  import { ActionScriptError, throwIfInvalidSelector } from './formFillHelpers/shared.js';
14
15
  import { frameScopedConnection } from './frameScopedConnection.js';
15
16
  import { evaluateInBdgWorld } from '../page/bdgWorld.js';
@@ -102,9 +103,13 @@ const log = createLogger('dom');
102
103
  * the text nodes of hidden ones (display or visibility; not those of
103
104
  * `<script>`, `<style>` or `<noscript>`), so text filters match hidden
104
105
  * elements like Playwright's and `:visible` decides visibility; button inputs
105
- * use their value. Whitespace is collapsed; filter texts arrive normalized
106
- * (`has-text` lowercased). `:visible` is checked before the text filters,
107
- * 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.
108
113
  *
109
114
  * Steps and `:has()` match CSS relative to an element (`:scope > css`). A
110
115
  * descendant step also searches the open shadow roots under the element (the
@@ -112,14 +117,15 @@ const log = createLogger('dom');
112
117
  * element's own tree.
113
118
  */
114
119
  export const FILTER_MATCHING_JS = `(shadowRoots) => {
120
+ const composed = ${COMPOSED_JS};
121
+ const flatText = ${FLAT_TEXT_JS};
115
122
  const skipped = /^(script|style|noscript|template)$/;
116
123
  const hiddenText = (el) => {
117
- const walker = el.ownerDocument.createTreeWalker(el, 5, {
118
- acceptNode: (node) => (node.nodeType === 1 && skipped.test(node.localName) ? 2 : 1)
119
- });
124
+ const nodes = el.localName === 'slot' ? el.assignedNodes({ flatten: true }) : (el.shadowRoot || el).childNodes;
120
125
  let text = '';
121
- for (let node = walker.nextNode(); node; node = walker.nextNode()) {
126
+ for (const node of nodes) {
122
127
  if (node.nodeType === 3) text += node.data;
128
+ else if (node.nodeType === 1 && !skipped.test(node.localName)) text += hiddenText(node);
123
129
  }
124
130
  return text;
125
131
  };
@@ -127,7 +133,8 @@ export const FILTER_MATCHING_JS = `(shadowRoots) => {
127
133
  if (el.localName === 'input' && /^(submit|button|reset)$/i.test(el.type)) return el.value;
128
134
  const rendered = typeof el.innerText === 'string' &&
129
135
  (typeof el.checkVisibility !== 'function' || el.checkVisibility({ visibilityProperty: true }));
130
- 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();
131
138
  };
132
139
  const passes = (el, filter) => {
133
140
  if (filter.kind === 'visible') {
@@ -1,5 +1,5 @@
1
1
  import type { Protocol } from '../connection/typed-cdp.js';
2
- import type { A11yNode, A11yTree, A11yQueryPattern, A11yQueryResult, NodeRef } from '../types.js';
2
+ import type { A11yNode, A11yTree, A11yQueryPattern, A11yQueryResult, ListedA11yTree, NodeRef } from '../types.js';
3
3
  /**
4
4
  * Builds accessibility tree from raw CDP nodes.
5
5
  *
@@ -14,6 +14,20 @@ import type { A11yNode, A11yTree, A11yQueryPattern, A11yQueryResult, NodeRef } f
14
14
  * @throws Error if no root node found
15
15
  */
16
16
  export declare function buildTreeFromRawNodes(rawNodes: Protocol.Accessibility.AXNode[]): A11yTree;
17
+ /**
18
+ * The nodes `dom a11y tree` lists, depth-first from the root (then nodes not
19
+ * under it, such as frame content): the first `limit` (0 = all) no deeper
20
+ * than `maxDepth`. Noise ({@link isTreeNoise}) is counted as skipped and its
21
+ * children move up a level, so the budget goes to meaningful nodes; nodes cut
22
+ * by the limit or depth are counted as omitted. Every node of the tree is
23
+ * listed, omitted or skipped.
24
+ *
25
+ * @param tree - Accessibility tree
26
+ * @param limit - Nodes to list (0 = all listable ones)
27
+ * @param maxDepth - Deepest level to list (0 = root only; undefined = all)
28
+ * @returns Listed nodes with their depth, the tree size, and how many were cut or skipped
29
+ */
30
+ export declare function listA11yTree(tree: A11yTree, limit: number, maxDepth?: number): ListedA11yTree;
17
31
  /**
18
32
  * Collects the full accessibility tree from the page via IPC.
19
33
  *
@@ -47,6 +47,89 @@ export function buildTreeFromRawNodes(rawNodes) {
47
47
  const root = nodes.get(rootRaw.nodeId);
48
48
  return { root, nodes, count: nodes.size };
49
49
  }
50
+ /** Roles that only lay out their children and say nothing themselves */
51
+ const LAYOUT_ROLES = new Set([
52
+ 'generic',
53
+ 'none',
54
+ 'presentation',
55
+ 'LayoutTable',
56
+ 'LayoutTableRow',
57
+ 'LayoutTableCell',
58
+ ]);
59
+ /**
60
+ * Whether a node is left out of `dom a11y tree`: text boxes, blank text,
61
+ * text that repeats its parent's name, and nameless layout wrappers.
62
+ *
63
+ * @param node - Accessibility node
64
+ * @param parentName - Name of the nearest named ancestor
65
+ * @returns True when the node says nothing of its own
66
+ */
67
+ function isTreeNoise(node, parentName) {
68
+ return (node.role === 'InlineTextBox' ||
69
+ (node.role === 'StaticText' && (node.name === parentName || !node.name?.trim())) ||
70
+ (LAYOUT_ROLES.has(node.role) && !node.name));
71
+ }
72
+ /**
73
+ * The nodes `dom a11y tree` lists, depth-first from the root (then nodes not
74
+ * under it, such as frame content): the first `limit` (0 = all) no deeper
75
+ * than `maxDepth`. Noise ({@link isTreeNoise}) is counted as skipped and its
76
+ * children move up a level, so the budget goes to meaningful nodes; nodes cut
77
+ * by the limit or depth are counted as omitted. Every node of the tree is
78
+ * listed, omitted or skipped.
79
+ *
80
+ * @param tree - Accessibility tree
81
+ * @param limit - Nodes to list (0 = all listable ones)
82
+ * @param maxDepth - Deepest level to list (0 = root only; undefined = all)
83
+ * @returns Listed nodes with their depth, the tree size, and how many were cut or skipped
84
+ */
85
+ export function listA11yTree(tree, limit, maxDepth) {
86
+ const nodes = [];
87
+ const visited = new Set();
88
+ let omitted = 0;
89
+ let skipped = 0;
90
+ const visit = (node, depth, parentName) => {
91
+ if (visited.has(node.nodeId))
92
+ return;
93
+ visited.add(node.nodeId);
94
+ const listable = !isTreeNoise(node, parentName);
95
+ if (listable) {
96
+ const fits = (limit === 0 || nodes.length < limit) && (maxDepth ?? depth) >= depth;
97
+ if (fits)
98
+ nodes.push(listedNode(node, depth));
99
+ else
100
+ omitted++;
101
+ }
102
+ else {
103
+ skipped++;
104
+ }
105
+ for (const childId of node.childIds ?? []) {
106
+ const child = tree.nodes.get(childId);
107
+ if (child)
108
+ visit(child, listable ? depth + 1 : depth, node.name ?? parentName);
109
+ }
110
+ };
111
+ visit(tree.root, 0, undefined);
112
+ for (const node of tree.nodes.values())
113
+ visit(node, 0, undefined);
114
+ return {
115
+ nodes,
116
+ count: tree.count,
117
+ ...(omitted > 0 && { omitted }),
118
+ ...(skipped > 0 && { skipped }),
119
+ };
120
+ }
121
+ /**
122
+ * A node as the tree lists it: its depth instead of child ids (its children
123
+ * follow it, one level deeper).
124
+ *
125
+ * @param node - Accessibility node
126
+ * @param depth - Its level in the listing
127
+ * @returns Listed node
128
+ */
129
+ function listedNode(node, depth) {
130
+ const { childIds: _childIds, ...rest } = node;
131
+ return { ...rest, depth };
132
+ }
50
133
  /**
51
134
  * Ids of a node's children as shown: ignored children are replaced by their
52
135
  * own non-ignored descendants; ids missing from the tree are dropped.
@@ -2,7 +2,7 @@
2
2
  * HAR (HTTP Archive) builder for transforming network telemetry to HAR 1.2 format.
3
3
  */
4
4
  import { createRequire } from 'node:module';
5
- import { skippedBodyReason } from '../network.js';
5
+ import { skippedBodyReason } from '../networkRetention.js';
6
6
  /**
7
7
  * Loads Node builtins on first use: importing `node:http` in an ES module
8
8
  * reads all its exports, which loads undici and zlib (about 8 ms of every CLI
@@ -1,19 +1,6 @@
1
1
  import type { CDPConnection } from '../connection/cdp.js';
2
2
  import type { NetworkRequest, WebSocketConnection, CleanupFunction } from '../types.js';
3
- /**
4
- * Placeholder stored instead of a response body that was not fetched.
5
- *
6
- * @param reason - Why the body was skipped
7
- * @returns Placeholder text shown by `bdg details`
8
- */
9
- export declare function skippedBodyPlaceholder(reason: string): string;
10
- /**
11
- * Extract the reason from a skipped-body placeholder.
12
- *
13
- * @param body - Stored response body
14
- * @returns Reason if `body` is a placeholder, otherwise undefined
15
- */
16
- export declare function skippedBodyReason(body: string | undefined): string | undefined;
3
+ import { type NetworkEvictions } from './networkRetention.js';
17
4
  /**
18
5
  * A request that has started but not finished or failed yet.
19
6
  */
@@ -41,6 +28,12 @@ export interface NetworkCollectionOptions {
41
28
  networkInclude?: string[];
42
29
  networkExclude?: string[];
43
30
  maxBodySize?: number;
31
+ /** Finished requests kept at most; past it the oldest are dropped (default {@link MAX_NETWORK_REQUESTS}) */
32
+ maxRequests?: number;
33
+ /** Total size of stored response bodies; past it the oldest are evicted (default {@link MAX_TOTAL_BODY_BYTES}) */
34
+ maxTotalBodyBytes?: number;
35
+ /** Counters of dropped requests and evicted bodies; otherwise the collector keeps private ones */
36
+ evictions?: NetworkEvictions | undefined;
44
37
  getCurrentNavigationId?: (() => number) | undefined;
45
38
  }
46
39
  /**
@@ -56,8 +49,12 @@ export interface NetworkCollectionOptions {
56
49
  *
57
50
  * @remarks
58
51
  * - Chrome buffer limits: 50MB total, 10MB per resource, 1MB POST data (with fallback)
59
- * - Stale requests (incomplete after 60s) are removed from tracking but NOT added to output
60
- * - Request limit of 10,000 prevents memory issues in long-running sessions
52
+ * - The newest 10,000 finished requests are kept: past that the oldest finished
53
+ * ones are dropped (counted in `evictions.requestsDropped`); requests in flight
54
+ * are tracked separately and never dropped mid-flight
55
+ * - Stored response bodies total at most 100MB: past that the oldest bodies are
56
+ * replaced by a placeholder (counted in `evictions.bodiesEvicted`), their
57
+ * request metadata stays
61
58
  * - Response bodies are automatically skipped for images, fonts, CSS, and source maps (see DEFAULT_SKIP_BODY_PATTERNS)
62
59
  * - Response bodies larger than 5MB are skipped with a placeholder message
63
60
  * - By default, common tracking/analytics domains are filtered out (use includeAll to disable)