@orkestrel/browser 0.0.21 → 0.0.23

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.
@@ -1,5 +1,5 @@
1
1
  import { attempt, isInteger, isObject, isString } from "@orkestrel/contract";
2
- import { BROWSER_DEFAULT_TIMEOUT_MS, BROWSER_INTERACTIVE_ROLES, BROWSER_OUTLINE_LIMIT, BROWSER_REFERENCE_PREFIX, BROWSER_RESULT_LIMIT, BrowserConnectionError, BrowserElementError, BrowserError, BrowserResultLimitError, BrowserToolset, BrowserTransition, createBrowserReading, filterBrowserOutline, normalizeBrowserName, parseBrowserReference, renderBrowserOutline, validateBrowserTimeout } from "../core/index.js";
2
+ import { BROWSER_DEFAULT_TIMEOUT_MS, BROWSER_INTERACTIVE_ROLES, BROWSER_OUTLINE_LIMIT, BROWSER_REFERENCE_PREFIX, BROWSER_RESULT_LIMIT, BROWSER_WAIT_EVENTS, BrowserConnectionError, BrowserElementError, BrowserError, BrowserResultLimitError, BrowserToolset, BrowserTransition, createBrowserReading, filterBrowserOutline, normalizeBrowserName, parseBrowserReference, renderBrowserOutline, validateBrowserTimeout } from "../core/index.js";
3
3
  import { Emitter } from "@orkestrel/emitter";
4
4
  //#region src/browser/constants.ts
5
5
  /**
@@ -129,6 +129,35 @@ var BROWSER_CONTEXT_TARGETS = Object.freeze(/* @__PURE__ */ new Set([
129
129
  "_parent",
130
130
  "_top"
131
131
  ]));
132
+ /**
133
+ * Names the roles whose ARIA expansion state the DOM outline reads.
134
+ *
135
+ * @remarks
136
+ * The role set follows the accessibility-tree reading from Chromium 141.0.7390.37.
137
+ */
138
+ var BROWSER_EXPANDED_ROLES = Object.freeze(/* @__PURE__ */ new Set([
139
+ "button",
140
+ "link",
141
+ "checkbox",
142
+ "switch",
143
+ "menuitem",
144
+ "menuitemcheckbox",
145
+ "menuitemradio",
146
+ "tab",
147
+ "treeitem",
148
+ "combobox"
149
+ ]));
150
+ /**
151
+ * Names the roles whose selection state the DOM outline reads.
152
+ *
153
+ * @remarks
154
+ * The role set follows the accessibility-tree reading from Chromium 141.0.7390.37.
155
+ */
156
+ var BROWSER_SELECTED_ROLES = Object.freeze(/* @__PURE__ */ new Set([
157
+ "option",
158
+ "tab",
159
+ "treeitem"
160
+ ]));
132
161
  /** Names the `input` type states whose value an untrusted `fill` sets as typed text. */
133
162
  var BROWSER_TYPED_INPUTS = Object.freeze(/* @__PURE__ */ new Set([
134
163
  "date",
@@ -163,15 +192,16 @@ function isBrowserDocument(value) {
163
192
  return isObject(value) && Reflect.get(value, "nodeType") === 9 && isObject(Reflect.get(value, "defaultView"));
164
193
  }
165
194
  /**
166
- * Reads the URL, title, and serialized markup a DOM reading is built from, refusing a capture
195
+ * Reads the URL, title, and rendered markup a DOM reading is built from, refusing a capture
167
196
  * over the result limit before any parse.
168
197
  *
169
198
  * @remarks
170
199
  * The measure is the length of `JSON.stringify({ url, title, html })`, the form the CDP capture
171
200
  * guard compares with `BROWSER_RESULT_LIMIT`, so a character JSON escapes counts as its escape
172
201
  * sequence. A reading a caller builds with `createBrowserReading` passes through no limit.
202
+ * {@link BrowserReadingInput} defines the inert capture, lowering, redaction, and limits.
173
203
  *
174
- * @param node - The element whose `outerHTML` is captured; its owner document supplies `URL`
204
+ * @param node - The root whose rendered markup is captured; its owner document supplies `URL`
175
205
  * and `title`
176
206
  * @returns The capture's `url`, `title`, and `html`
177
207
  * @throws Thrown as a `BrowserResultLimitError` when the serialized capture is longer than
@@ -182,8 +212,241 @@ function readBrowserCapture(node) {
182
212
  const capture = {
183
213
  url: document.URL,
184
214
  title: document.title,
185
- html: node.outerHTML
215
+ html: ""
186
216
  };
217
+ const view = document.defaultView;
218
+ const rendered = view !== null && node.isConnected;
219
+ const inert = document.implementation.createHTMLDocument("");
220
+ let root = inert.importNode(node, true);
221
+ const pending = [{
222
+ live: node,
223
+ twin: root,
224
+ visited: false
225
+ }];
226
+ if (rendered) {
227
+ let child = node;
228
+ for (let parent = node; parent?.ownerDocument === document; parent = readBrowserParent(parent)) {
229
+ const style = view.getComputedStyle(parent);
230
+ const outside = parent !== node && (style.contentVisibility === "hidden" || [
231
+ "canvas",
232
+ "video",
233
+ "audio"
234
+ ].includes(parent.localName) || parent instanceof view.HTMLInputElement || parent instanceof view.HTMLTextAreaElement || parent instanceof view.HTMLOptionElement || parent.namespaceURI === "http://www.w3.org/2000/svg" && parent.localName === "switch" && child !== (Array.from(parent.children).find((candidate) => candidate.getClientRects().length > 0) ?? parent.firstElementChild) || (parent instanceof view.HTMLSelectElement || parent instanceof view.HTMLOptGroupElement) && !(node instanceof view.HTMLOptionElement || node instanceof view.HTMLOptGroupElement) || [
235
+ "script",
236
+ "style",
237
+ "template",
238
+ "frame",
239
+ "frameset",
240
+ "iframe",
241
+ "object",
242
+ "embed",
243
+ "applet",
244
+ "noscript",
245
+ "meta",
246
+ "link",
247
+ "base",
248
+ "math"
249
+ ].includes(parent.localName) || parent.localName === "details" && !parent.hasAttribute("open") && child !== parent.querySelector(":scope > summary"));
250
+ const unassigned = child.parentElement?.shadowRoot !== null && child.parentElement?.shadowRoot !== void 0 && child.assignedSlot === null;
251
+ if (style.display === "none" || outside || unassigned) {
252
+ root = void 0;
253
+ pending.length = 0;
254
+ break;
255
+ }
256
+ child = parent;
257
+ }
258
+ }
259
+ while (pending.length > 0) {
260
+ const entry = pending.pop();
261
+ if (entry === void 0) continue;
262
+ const { live, twin, visited } = entry;
263
+ const tag = live.localName;
264
+ const input = tag === "input" ? live.getAttribute("type")?.toLowerCase() ?? "text" : "";
265
+ if ([
266
+ "script",
267
+ "style",
268
+ "template",
269
+ "frame",
270
+ "frameset",
271
+ "iframe",
272
+ "object",
273
+ "embed",
274
+ "applet",
275
+ "noscript",
276
+ "meta",
277
+ "link",
278
+ "base",
279
+ "math"
280
+ ].includes(tag) || tag === "input" && ["password", "hidden"].includes(input)) {
281
+ twin.remove();
282
+ if (twin === root) root = void 0;
283
+ continue;
284
+ }
285
+ const style = entry.style ?? (rendered ? view.getComputedStyle(live) : void 0);
286
+ const invisible = style?.visibility === "hidden" || style?.visibility === "collapse";
287
+ if (style?.display === "none" || invisible && !live.hasChildNodes()) {
288
+ twin.remove();
289
+ if (twin === root) root = void 0;
290
+ continue;
291
+ }
292
+ if (!visited) {
293
+ pending.push({
294
+ live,
295
+ twin,
296
+ style,
297
+ visited: true
298
+ });
299
+ if (rendered) {
300
+ twin.removeAttribute("hidden");
301
+ twin.removeAttribute("aria-hidden");
302
+ }
303
+ if (rendered && (live instanceof view.HTMLAnchorElement || live instanceof view.HTMLAreaElement) && live.hasAttribute("href")) twin.setAttribute("href", live.href);
304
+ if (style?.contentVisibility === "hidden" || [
305
+ "canvas",
306
+ "video",
307
+ "audio"
308
+ ].includes(tag)) {
309
+ twin.replaceChildren();
310
+ continue;
311
+ }
312
+ if (rendered && (live instanceof view.HTMLInputElement || live instanceof view.HTMLTextAreaElement || live instanceof view.HTMLSelectElement || live instanceof view.HTMLOptionElement || live instanceof view.HTMLOptGroupElement)) continue;
313
+ const summary = tag === "details" && !live.hasAttribute("open") ? live.querySelector(":scope > summary") : void 0;
314
+ const conditional = tag === "switch" && live.namespaceURI === "http://www.w3.org/2000/svg";
315
+ const branch = rendered && conditional ? Array.from(live.children).find((candidate) => candidate.getClientRects().length > 0) ?? live.firstElementChild : void 0;
316
+ let element = live.lastElementChild;
317
+ let counterpart = twin.lastElementChild;
318
+ let child = live.lastChild;
319
+ let mirror = twin.lastChild;
320
+ while (child !== null && mirror !== null) {
321
+ const previous = child.previousSibling;
322
+ const previousMirror = mirror.previousSibling;
323
+ const previousElement = element?.previousElementSibling ?? null;
324
+ const previousCounterpart = counterpart?.previousElementSibling ?? null;
325
+ let omitted = rendered && (summary !== void 0 && child !== summary || conditional && child !== branch);
326
+ if (rendered && live.shadowRoot !== null) {
327
+ const slot = child instanceof view.Element || child instanceof view.Text ? child.assignedSlot : null;
328
+ if (slot === null) omitted = true;
329
+ else for (let parent = slot; parent !== null && parent !== live; parent = readBrowserParent(parent)) if (view.getComputedStyle(parent).display === "none") omitted = true;
330
+ }
331
+ if (omitted || child.nodeType === 3 && invisible) mirror.parentNode?.removeChild(mirror);
332
+ else if (child.nodeType === 1) {
333
+ if (element !== null && counterpart !== null) pending.push({
334
+ live: element,
335
+ twin: counterpart,
336
+ visited: false
337
+ });
338
+ }
339
+ if (child.nodeType === 1) {
340
+ element = previousElement;
341
+ counterpart = previousCounterpart;
342
+ }
343
+ child = previous;
344
+ mirror = previousMirror;
345
+ }
346
+ continue;
347
+ }
348
+ if (!rendered || style?.contentVisibility === "hidden") continue;
349
+ if ([
350
+ "div",
351
+ "summary",
352
+ "details"
353
+ ].includes(tag) && !style?.display.startsWith("inline") && style?.display !== "contents" && twin.hasChildNodes()) {
354
+ twin.prepend(inert.createElement("br"));
355
+ twin.append(inert.createElement("br"));
356
+ }
357
+ let fragments;
358
+ let lines;
359
+ let container = false;
360
+ if (live instanceof view.HTMLInputElement) {
361
+ lines = [];
362
+ if ([
363
+ "text",
364
+ "email",
365
+ "search",
366
+ "tel",
367
+ "url",
368
+ "number"
369
+ ].includes(live.type)) {
370
+ if (live.type !== "number" || !live.validity.badInput) {
371
+ const placeholder = view.getComputedStyle(live, "::placeholder");
372
+ lines = [live.value || (live.matches(":placeholder-shown") && placeholder.visibility === "visible" && placeholder.opacity !== "0" && placeholder.color !== "rgba(0, 0, 0, 0)" ? live.placeholder : "")];
373
+ }
374
+ } else if ([
375
+ "submit",
376
+ "reset",
377
+ "button"
378
+ ].includes(live.type)) lines = [live.value];
379
+ else if (live.type === "file" && live.files?.length === 1) lines = Array.from(live.files ?? [], (file) => file.name);
380
+ else if (live.type === "image" && live.getAttribute("aria-hidden") !== "true") lines = [live.getAttribute("aria-label")?.trim() || live.alt];
381
+ } else if (live instanceof view.HTMLTextAreaElement) {
382
+ const placeholder = view.getComputedStyle(live, "::placeholder");
383
+ lines = (live.value || (live.matches(":placeholder-shown") && placeholder.visibility === "visible" && placeholder.opacity !== "0" && placeholder.color !== "rgba(0, 0, 0, 0)" ? live.placeholder : "")).split(/\r?\n/);
384
+ } else if (live instanceof view.HTMLSelectElement || live instanceof view.HTMLOptionElement || live instanceof view.HTMLOptGroupElement) {
385
+ lines = [];
386
+ const select = live instanceof view.HTMLSelectElement ? live : live.closest("select");
387
+ if (select !== null) {
388
+ if (!select.multiple && select.size <= 1) {
389
+ const option = select.selectedOptions[0];
390
+ if (option !== void 0 && (live === select || live === option || live.contains(option))) lines.push(option.label);
391
+ } else {
392
+ const box = select.getBoundingClientRect();
393
+ const top = box.top + select.clientTop;
394
+ const bottom = top + select.clientHeight;
395
+ const rows = live === select ? select.querySelectorAll("option,optgroup") : [live, ...live.querySelectorAll("option")];
396
+ for (const row of rows) {
397
+ const rowStyle = view.getComputedStyle(row);
398
+ const groupStyle = row.parentElement instanceof view.HTMLOptGroupElement ? view.getComputedStyle(row.parentElement) : void 0;
399
+ if (rowStyle.display === "none" || rowStyle.visibility !== "visible" || groupStyle?.display === "none") continue;
400
+ const rect = row.getBoundingClientRect();
401
+ if ((row instanceof view.HTMLOptGroupElement ? Array.from(row.querySelectorAll("option")).find((option) => option.getClientRects().length > 0)?.getBoundingClientRect().top ?? rect.bottom : rect.bottom) > top && rect.top < bottom && rect.right > box.left + select.clientLeft && rect.left < box.left + select.clientLeft + select.clientWidth && (row instanceof view.HTMLOptionElement || row instanceof view.HTMLOptGroupElement)) lines.push(row.label);
402
+ }
403
+ }
404
+ }
405
+ } else if (tag === "svg") {
406
+ if (twin.parentElement?.closest("svg, foreignObject")?.localName === "svg") continue;
407
+ lines = [];
408
+ fragments = [];
409
+ for (const content of twin.querySelectorAll("text, foreignObject")) {
410
+ if (content.parentElement?.closest("svg, foreignObject")?.localName === "foreignObject") continue;
411
+ if (content.localName === "text" && content.parentElement?.closest("text")?.closest("svg") === content.closest("svg")) continue;
412
+ if (content.localName === "foreignObject") fragments.push(...content.childNodes);
413
+ else if (content.textContent) fragments.push(inert.createTextNode(content.textContent));
414
+ }
415
+ if (fragments.length === 0) fragments = void 0;
416
+ if (!invisible && fragments === void 0 && live.getAttribute("aria-hidden") !== "true") lines.push(live.getAttribute("aria-label")?.trim() || live.querySelector(":scope > title")?.textContent || "");
417
+ } else if ([
418
+ "form",
419
+ "dialog",
420
+ "button"
421
+ ].includes(tag)) {
422
+ container = true;
423
+ if (live instanceof view.HTMLButtonElement && !invisible && live.innerText.trim() === "") {
424
+ const name = live.getAttribute("aria-label")?.trim();
425
+ if (name) {
426
+ lines = [name];
427
+ container = false;
428
+ }
429
+ }
430
+ }
431
+ if (lines === void 0 && !container) continue;
432
+ if (invisible && !container && tag !== "svg") lines = [];
433
+ const block = !style?.display.startsWith("inline") && style?.display !== "contents";
434
+ const carrier = inert.createElement(block ? "div" : "span");
435
+ if (container) carrier.append(...twin.childNodes);
436
+ else if (fragments !== void 0) for (const fragment of fragments) carrier.append(fragment, inert.createElement("br"));
437
+ else for (const [index, line] of (lines ?? []).entries()) {
438
+ if (index > 0) carrier.append(inert.createElement("br"));
439
+ carrier.append(inert.createTextNode(line));
440
+ }
441
+ if (carrier.hasChildNodes()) {
442
+ carrier.prepend(inert.createElement("br"));
443
+ carrier.append(inert.createElement("br"));
444
+ }
445
+ twin.replaceWith(carrier);
446
+ if (twin === root) root = carrier;
447
+ }
448
+ for (const input of root?.querySelectorAll("input[type=\"password\" i],input[type=\"hidden\" i]") ?? []) input.remove();
449
+ capture.html = root?.outerHTML ?? "";
187
450
  const length = JSON.stringify(capture).length;
188
451
  if (length > BROWSER_RESULT_LIMIT) throw new BrowserResultLimitError("Document capture exceeds BROWSER_RESULT_LIMIT", {
189
452
  length,
@@ -695,6 +958,59 @@ function listenBrowserNavigation(window, listener, signal) {
695
958
  }
696
959
  window.addEventListener("pagehide", listener, options);
697
960
  }
961
+ /**
962
+ * Reads a lowercased ARIA token, preserving whitespace and treating empty or undefined tokens as absent.
963
+ *
964
+ * @param element - The element carrying the attribute
965
+ * @param attribute - The ARIA attribute name
966
+ * @returns The lowercased token, or `undefined` for a missing, empty, or `undefined` attribute
967
+ *
968
+ * @example
969
+ * ```ts
970
+ * const button = document.createElement('button')
971
+ * button.setAttribute('aria-pressed', 'TRUE')
972
+ * readBrowserToken(button, 'aria-pressed') // 'true'
973
+ * ```
974
+ */
975
+ function readBrowserToken(element, attribute) {
976
+ const token = element.getAttribute(attribute)?.toLowerCase();
977
+ return token === "" || token === "undefined" ? void 0 : token;
978
+ }
979
+ /**
980
+ * Reads the pressed, expanded, and selected states the DOM outline assigns to an element's role.
981
+ *
982
+ * @remarks
983
+ * Only buttons carry pressed state. Expansion follows `BROWSER_EXPANDED_ROLES`, with native
984
+ * single selects always collapsed. Selection follows `BROWSER_SELECTED_ROLES`: an ARIA token
985
+ * overrides native option selectedness, and other elements without a token omit selection. Tokens are not
986
+ * trimmed; values other than `false` are true, except the pressed token `mixed`.
987
+ *
988
+ * @param element - The element whose attributes and native state are read
989
+ * @param role - Its computed accessibility role
990
+ * @returns Present states, including false, with pressed represented as a tristate string
991
+ *
992
+ * @example
993
+ * ```ts
994
+ * const button = document.createElement('button')
995
+ * button.setAttribute('aria-pressed', 'mixed')
996
+ * readBrowserStates(button, 'button') // { pressed: 'mixed' }
997
+ * ```
998
+ */
999
+ function readBrowserStates(element, role) {
1000
+ const states = {};
1001
+ const pressed = readBrowserToken(element, "aria-pressed");
1002
+ const expanded = readBrowserToken(element, "aria-expanded");
1003
+ const selected = readBrowserToken(element, "aria-selected");
1004
+ const view = element.ownerDocument.defaultView;
1005
+ if (role === "button" && pressed !== void 0) states["pressed"] = pressed === "false" || pressed === "mixed" ? pressed : "true";
1006
+ if (role === "combobox" && view !== null && element instanceof view.HTMLSelectElement && !element.multiple && element.size <= 1) states["expanded"] = false;
1007
+ else if (BROWSER_EXPANDED_ROLES.has(role) && expanded !== void 0) states["expanded"] = expanded !== "false";
1008
+ if (BROWSER_SELECTED_ROLES.has(role)) {
1009
+ if (selected !== void 0) states["selected"] = selected !== "false";
1010
+ else if (view !== null && element instanceof view.HTMLOptionElement) states["selected"] = element.selected;
1011
+ }
1012
+ return states;
1013
+ }
698
1014
  //#endregion
699
1015
  //#region src/browser/elements/BrowserDOMElement.ts
700
1016
  /**
@@ -851,14 +1167,15 @@ var BrowserDOMElement = class {
851
1167
  //#endregion
852
1168
  //#region src/browser/BrowserDOMWait.ts
853
1169
  /**
854
- * Parks one condition on DOM mutations until it holds, with one deadline and no other timer.
1170
+ * Parks one condition on DOM mutations and finished transitions and animations until it holds, with one deadline and coalesced wake tasks.
855
1171
  *
856
1172
  * @remarks
857
1173
  * The wait observes every root `roots` returns, each through a `MutationObserver` of that root's
858
- * realm, and re-reads the roots after every mutation batch and every capture-phase `load` in an
859
- * observed root: a same-origin frame or an open shadow root that appears joins the observation,
1174
+ * realm, and re-reads the roots after every mutation batch, every capture-phase `load`, and
1175
+ * the task after a `BROWSER_WAIT_EVENTS` event in an observed root: a same-origin
1176
+ * frame or an open shadow root that appears joins the observation,
860
1177
  * and a root that departed (a replaced frame document, a removed shadow root) leaves it, with its
861
- * observer disconnected and its `load` listener removed. The check runs before the wait parks and
1178
+ * observer disconnected and its listeners removed. The check runs before the wait parks and
862
1179
  * again after every subscription, and no check runs once the deadline, counted from `start`, has
863
1180
  * passed, which the wait reads again after `roots` returns. The wait settles one time: on the
864
1181
  * check's first value, on the signal's abort with its reason, at the deadline with
@@ -870,9 +1187,9 @@ var BrowserDOMElement = class {
870
1187
  * observer, listener, or further check behind.
871
1188
  *
872
1189
  * A shadow root attached after the wait began is a declared limit: attaching one fires no
873
- * mutation record and no event, so the wait discovers it at the next mutation or `load` it
874
- * observes, and a match inserted into it with no other observed change is not seen before the
875
- * deadline. The wait never polls to close that gap.
1190
+ * mutation record and no event, so the wait discovers it at the next mutation batch, `load`,
1191
+ * or `BROWSER_WAIT_EVENTS` event it observes, and a match inserted into it with no other
1192
+ * observed change is not seen before the deadline. The wait never polls to close that gap.
876
1193
  *
877
1194
  * @example
878
1195
  * ```ts
@@ -892,7 +1209,10 @@ var BrowserDOMWait = class {
892
1209
  #observers = /* @__PURE__ */ new Map();
893
1210
  #main;
894
1211
  #timer;
1212
+ #task;
895
1213
  #recheckHandler = this.#recheck.bind(this);
1214
+ #scheduleHandler = this.#schedule.bind(this);
1215
+ #wakeHandler = this.#wake.bind(this);
896
1216
  #unloadHandler = this.#unload.bind(this);
897
1217
  constructor(wait) {
898
1218
  this.#wait = wait;
@@ -911,6 +1231,14 @@ var BrowserDOMWait = class {
911
1231
  this.#recheck();
912
1232
  return await this.#settled.promise;
913
1233
  }
1234
+ #schedule() {
1235
+ if (this.#release.signal.aborted || this.#task !== void 0) return;
1236
+ this.#task = setTimeout(this.#wakeHandler, 0);
1237
+ }
1238
+ #wake() {
1239
+ this.#task = void 0;
1240
+ this.#recheck();
1241
+ }
914
1242
  #recheck() {
915
1243
  if (this.#release.signal.aborted) return;
916
1244
  if (this.#remaining() <= 0) {
@@ -954,6 +1282,10 @@ var BrowserDOMWait = class {
954
1282
  capture: true,
955
1283
  signal: release.signal
956
1284
  });
1285
+ for (const name of BROWSER_WAIT_EVENTS) root.addEventListener(name, this.#scheduleHandler, {
1286
+ capture: true,
1287
+ signal: release.signal
1288
+ });
957
1289
  this.#observers.set(root, {
958
1290
  observer,
959
1291
  release
@@ -983,6 +1315,8 @@ var BrowserDOMWait = class {
983
1315
  }
984
1316
  #stop() {
985
1317
  clearTimeout(this.#timer);
1318
+ clearTimeout(this.#task);
1319
+ this.#task = void 0;
986
1320
  this.#release.abort();
987
1321
  for (const entry of this.#observers.values()) {
988
1322
  entry.observer.disconnect();
@@ -1009,8 +1343,10 @@ var BrowserDOMWait = class {
1009
1343
  * wrapper reports that latest capture, also after the manager drops the reference. A reference
1010
1344
  * and a reading from any walked document record that document's epoch, which advances on its own
1011
1345
  * navigation events and on the view's; that document's `pagehide` drops its references. A form
1012
- * carrying a `toolname` attribute renders `[tool=NAME]` after its role and name. A `select` row
1013
- * is followed by one `option` row per option, named by its label. Whitespace between two texts of
1346
+ * carrying a `toolname` attribute renders `[tool=NAME]` after its role and name. A row carries
1347
+ * `focused` when its element is the active element of its own document or shadow root. Rows carry
1348
+ * pressed, expanded, and selected states through the Chromium mapping in `readBrowserStates`.
1349
+ * A `select` row is followed by one `option` row per option, named by its label. Whitespace between two texts of
1014
1350
  * one block renders as one space, as a rendered `br` between them does, and texts with neither
1015
1351
  * between them stay joined. A CSS query
1016
1352
  * searches the document the view drives, not its child documents or shadow trees. After the
@@ -1047,7 +1383,7 @@ var BrowserDOMElementManager = class {
1047
1383
  options?.signal?.throwIfAborted();
1048
1384
  const document = this.#input.document();
1049
1385
  const rows = this.#capture(this.#root(document, options?.within), options?.within);
1050
- return renderBrowserOutline(document.URL, document.title, rows, limit);
1386
+ return renderBrowserOutline(document.URL, document.title, rows, limit, options?.search);
1051
1387
  }
1052
1388
  async find(query, options) {
1053
1389
  this.#input.signal.throwIfAborted();
@@ -1237,6 +1573,8 @@ var BrowserDOMElementManager = class {
1237
1573
  const reference = role === "heading" ? void 0 : this.#bind(element, role, name).reference;
1238
1574
  const input = element instanceof view.HTMLInputElement ? element : void 0;
1239
1575
  const checked = input !== void 0 && (input.type === "checkbox" || input.type === "radio") ? input.checked : element.getAttribute("aria-checked") === "true";
1576
+ const root = element.getRootNode();
1577
+ const focused = (root instanceof view.Document || root instanceof view.ShadowRoot) && root.activeElement === element;
1240
1578
  let value;
1241
1579
  if (element instanceof view.HTMLSelectElement) value = Array.from(element.selectedOptions, (option) => option.label).join(", ");
1242
1580
  else if (element instanceof view.HTMLTextAreaElement) value = element.value;
@@ -1254,7 +1592,9 @@ var BrowserDOMElementManager = class {
1254
1592
  value,
1255
1593
  properties: {
1256
1594
  checked,
1257
- disabled: element.matches(":disabled")
1595
+ disabled: element.matches(":disabled"),
1596
+ focused,
1597
+ ...readBrowserStates(element, role)
1258
1598
  },
1259
1599
  ...tool === null || tool === "" ? {} : { tool },
1260
1600
  session: "",
@@ -1379,7 +1719,7 @@ var BrowserDOMView = class {
1379
1719
  const document = this.#current();
1380
1720
  await new BrowserDOMWait({
1381
1721
  roots: collectBrowserRoots.bind(void 0, document),
1382
- check: this.#contains.bind(this, document, text),
1722
+ check: this.#contains.bind(this, document, text, options?.absent === true),
1383
1723
  timeout,
1384
1724
  start,
1385
1725
  signal: options?.signal === void 0 ? this.#release.signal : AbortSignal.any([options.signal, this.#release.signal]),
@@ -1408,8 +1748,8 @@ var BrowserDOMView = class {
1408
1748
  this.#epoch += 1;
1409
1749
  if (event === void 0 || event.type === "pagehide") this.#elements.clear();
1410
1750
  }
1411
- #contains(document, text) {
1412
- return (document.body?.innerText ?? "").includes(text) ? true : void 0;
1751
+ #contains(document, text, absent) {
1752
+ return (document.body?.innerText ?? "").includes(text) !== absent ? true : void 0;
1413
1753
  }
1414
1754
  #listen() {
1415
1755
  this.#listeners.abort();
@@ -1574,7 +1914,7 @@ function createBrowserDOMView(options) {
1574
1914
  *
1575
1915
  * @remarks
1576
1916
  * The toolset runs over a view {@link createBrowserDOMView} creates for `document`, so it
1577
- * advertises `look`, `read`, `click`, `type`, and `wait`, and a click or type receipt ends with
1917
+ * advertises `look`, `read`, `plain`, `click`, `type`, and `wait`, and a click or type receipt ends with
1578
1918
  * ` (untrusted event)`. `source` supplies page tools, which the toolset adopts during `start()`
1579
1919
  * and again on every `change`; `toolset.native` never includes them, so publishing it to a
1580
1920
  * registry leaves that registry's own tools alone. The view belongs to the toolset: `destroy()`
@@ -1594,7 +1934,7 @@ function createBrowserDOMView(options) {
1594
1934
  * ```ts
1595
1935
  * const toolset = createDocumentToolset({ document: frame.contentDocument, source: bridge })
1596
1936
  * await toolset.start()
1597
- * await toolset.tools.execute({ id: '1', name: 'look', arguments: { what: 'form' } })
1937
+ * await toolset.tools.execute({ id: '1', name: 'look', arguments: { search: 'form' } })
1598
1938
  * ```
1599
1939
  */
1600
1940
  function createDocumentToolset(options) {
@@ -1635,6 +1975,6 @@ function createSocketCDPTransport(options) {
1635
1975
  return new SocketCDPTransport(options);
1636
1976
  }
1637
1977
  //#endregion
1638
- export { BROWSER_CONTENT_NAMED_ROLES, BROWSER_CONTEXT_TARGETS, BROWSER_DOCUMENT_TIMEOUT_MS, BROWSER_IMPLICIT_ROLES, BROWSER_INTERACTIVE_CONTENT, BROWSER_TYPED_INPUTS, BrowserDOMElement, BrowserDOMElementManager, BrowserDOMView, BrowserDOMWait, SocketCDPTransport, collectBrowserRoots, computeBrowserAlternative, computeBrowserName, computeBrowserRole, computeBrowserText, createBrowserDOMView, createDocumentToolset, createSocketCDPTransport, isBrowserDocument, listenBrowserNavigation, matchesBrowserActivation, matchesBrowserBlock, matchesBrowserHidden, matchesBrowserInvisible, matchesBrowserOmitted, matchesBrowserPopup, readBrowserBlock, readBrowserCapture, readBrowserContent, readBrowserParent, skipBrowserSubtree };
1978
+ export { BROWSER_CONTENT_NAMED_ROLES, BROWSER_CONTEXT_TARGETS, BROWSER_DOCUMENT_TIMEOUT_MS, BROWSER_EXPANDED_ROLES, BROWSER_IMPLICIT_ROLES, BROWSER_INTERACTIVE_CONTENT, BROWSER_SELECTED_ROLES, BROWSER_TYPED_INPUTS, BrowserDOMElement, BrowserDOMElementManager, BrowserDOMView, BrowserDOMWait, SocketCDPTransport, collectBrowserRoots, computeBrowserAlternative, computeBrowserName, computeBrowserRole, computeBrowserText, createBrowserDOMView, createDocumentToolset, createSocketCDPTransport, isBrowserDocument, listenBrowserNavigation, matchesBrowserActivation, matchesBrowserBlock, matchesBrowserHidden, matchesBrowserInvisible, matchesBrowserOmitted, matchesBrowserPopup, readBrowserBlock, readBrowserCapture, readBrowserContent, readBrowserParent, readBrowserStates, readBrowserToken, skipBrowserSubtree };
1639
1979
 
1640
1980
  //# sourceMappingURL=index.js.map