@orkestrel/browser 0.0.26 → 0.0.28

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
- 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, BROWSER_WAIT_EVENTS, BrowserConnectionError, BrowserElementError, BrowserError, BrowserResultLimitError, BrowserToolset, BrowserTransition, createBrowserReading, filterBrowserOutline, normalizeBrowserName, parseBrowserReference, renderBrowserOutline, validateBrowserTimeout } from "../core/index.js";
1
+ import { attempt, isInteger, isJSONValue, isObject, isString } from "@orkestrel/contract";
2
+ import { BROWSER_DEFAULT_TIMEOUT_MS, BROWSER_INTERACTIVE_ROLES, BROWSER_OUTLINE_LIMIT, BROWSER_REFERENCE_PREFIX, BROWSER_RESULT_LIMIT, BROWSER_WAIT_EVENTS, BrowserError, BrowserTransition, createBrowserReading, describeBrowserRefusal, filterBrowserOutline, normalizeBrowserName, parseBrowserReference, renderBrowserElement, renderBrowserOutline, validateBrowserTimeout } from "../core/index.js";
3
3
  import { Emitter } from "@orkestrel/emitter";
4
4
  //#region src/browser/constants.ts
5
5
  /**
@@ -204,7 +204,7 @@ function isBrowserDocument(value) {
204
204
  * @param node - The root whose rendered markup is captured; its owner document supplies `URL`
205
205
  * and `title`
206
206
  * @returns The capture's `url`, `title`, and `html`
207
- * @throws Thrown as a `BrowserResultLimitError` when the serialized capture is longer than
207
+ * @throws Thrown as a `BrowserError` when the serialized capture is longer than
208
208
  * `BROWSER_RESULT_LIMIT` characters, with `length` and `limit` in its context.
209
209
  */
210
210
  function readBrowserCapture(node) {
@@ -448,7 +448,7 @@ function readBrowserCapture(node) {
448
448
  for (const input of root?.querySelectorAll("input[type=\"password\" i],input[type=\"hidden\" i]") ?? []) input.remove();
449
449
  capture.html = root?.outerHTML ?? "";
450
450
  const length = JSON.stringify(capture).length;
451
- if (length > BROWSER_RESULT_LIMIT) throw new BrowserResultLimitError("Document capture exceeds BROWSER_RESULT_LIMIT", {
451
+ if (length > BROWSER_RESULT_LIMIT) throw new BrowserError("RESULT_LIMIT", "Document capture exceeds BROWSER_RESULT_LIMIT", {
452
452
  length,
453
453
  limit: BROWSER_RESULT_LIMIT
454
454
  });
@@ -1021,7 +1021,7 @@ function readBrowserStates(element, role) {
1021
1021
  * prototype setter and dispatch `input` and `change`, and `fill` refuses `UNKNOWN` an element
1022
1022
  * that is not a text control and a read-only one; `submit` runs `form.requestSubmit()` and
1023
1023
  * observes the outcome through a `submit` listener registered across the call. Each action
1024
- * refuses with a `BrowserElementError` naming the reason: `DISABLED` for a disabled control,
1024
+ * refuses with a `BrowserError` naming the reason: `DISABLED` for a disabled control,
1025
1025
  * `HIDDEN` for an element that does not render, and `UNTRUSTED` for what an untrusted event
1026
1026
  * cannot do, which is a click or submission that opens another browsing context, a click that
1027
1027
  * opens a file chooser, and typing into a contenteditable element. A click that activates a
@@ -1033,12 +1033,6 @@ function readBrowserStates(element, role) {
1033
1033
  * capture, and a dropped reference keeps its last capture. Every action on an element removed
1034
1034
  * from its document, collected, or held across a navigation reports `GONE`.
1035
1035
  *
1036
- * @example
1037
- * ```ts
1038
- * const view = createBrowserDOMView({ document: frame.contentDocument })
1039
- * await view.elements.outline()
1040
- * await view.elements.element('e1')?.click()
1041
- * ```
1042
1036
  */
1043
1037
  var BrowserDOMElement = class {
1044
1038
  #input;
@@ -1059,7 +1053,10 @@ var BrowserDOMElement = class {
1059
1053
  const label = node instanceof view.HTMLLabelElement ? node : node.closest("label");
1060
1054
  const control = label !== null && matchesBrowserActivation(node, label) ? label.control : null;
1061
1055
  if (control !== null && control !== node) {
1062
- if (control.matches(":disabled")) throw new BrowserElementError(this.reference, "DISABLED", "labels a disabled control");
1056
+ if (control.matches(":disabled")) throw new BrowserError("ELEMENT", describeBrowserRefusal({ subject: `Element ${renderBrowserElement(this)}` }, "DISABLED", "labels a disabled control"), {
1057
+ reference: this.reference,
1058
+ reason: "DISABLED"
1059
+ });
1063
1060
  this.#refuse(control);
1064
1061
  }
1065
1062
  this.#refuse(node);
@@ -1075,12 +1072,21 @@ var BrowserDOMElement = class {
1075
1072
  }
1076
1073
  async fill(value, options) {
1077
1074
  const [node, view] = this.#actionable(options);
1078
- if (node instanceof view.HTMLElement && node.isContentEditable) throw new BrowserElementError(this.reference, "UNTRUSTED", "is contenteditable, which an untrusted event cannot type into");
1075
+ if (node instanceof view.HTMLElement && node.isContentEditable) throw new BrowserError("ELEMENT", describeBrowserRefusal({ subject: `Element ${renderBrowserElement(this)}` }, "UNTRUSTED", "is contenteditable, which an untrusted event cannot type into"), {
1076
+ reference: this.reference,
1077
+ reason: "UNTRUSTED"
1078
+ });
1079
1079
  const input = node instanceof view.HTMLInputElement ? node : void 0;
1080
1080
  const control = input ?? (node instanceof view.HTMLTextAreaElement ? node : void 0);
1081
1081
  const setter = Reflect.getOwnPropertyDescriptor(input === void 0 ? view.HTMLTextAreaElement.prototype : view.HTMLInputElement.prototype, "value")?.set;
1082
- if (control === void 0 || input !== void 0 && !BROWSER_TYPED_INPUTS.has(input.type)) throw new BrowserElementError(this.reference, "UNKNOWN", "is not a text control");
1083
- if (control.readOnly || setter === void 0) throw new BrowserElementError(this.reference, "UNKNOWN", "is not editable");
1082
+ if (control === void 0 || input !== void 0 && !BROWSER_TYPED_INPUTS.has(input.type)) throw new BrowserError("ELEMENT", describeBrowserRefusal({ subject: `Element ${renderBrowserElement(this)}` }, "UNKNOWN", "is not a text control"), {
1083
+ reference: this.reference,
1084
+ reason: "UNKNOWN"
1085
+ });
1086
+ if (control.readOnly || setter === void 0) throw new BrowserError("ELEMENT", describeBrowserRefusal({ subject: `Element ${renderBrowserElement(this)}` }, "UNKNOWN", "is not editable"), {
1087
+ reference: this.reference,
1088
+ reason: "UNKNOWN"
1089
+ });
1084
1090
  control.focus();
1085
1091
  Reflect.apply(setter, control, [value]);
1086
1092
  control.dispatchEvent(new view.InputEvent("input", {
@@ -1093,10 +1099,16 @@ var BrowserDOMElement = class {
1093
1099
  }
1094
1100
  async select(values, options) {
1095
1101
  const [node, view] = this.#actionable(options);
1096
- if (!(node instanceof view.HTMLSelectElement)) throw new BrowserElementError(this.reference, "UNKNOWN", "is not a select control");
1102
+ if (!(node instanceof view.HTMLSelectElement)) throw new BrowserError("ELEMENT", describeBrowserRefusal({ subject: `Element ${renderBrowserElement(this)}` }, "UNKNOWN", "is not a select control"), {
1103
+ reference: this.reference,
1104
+ reason: "UNKNOWN"
1105
+ });
1097
1106
  const choices = Array.from(node.options);
1098
1107
  const selected = values.map((value) => choices.find((option) => option.value === value) ?? choices.find((option) => option.label === value));
1099
- if (values.find((_value, index) => selected[index] === void 0) !== void 0) throw new BrowserElementError(this.reference, "UNKNOWN", "has no such option");
1108
+ if (values.find((_value, index) => selected[index] === void 0) !== void 0) throw new BrowserError("ELEMENT", describeBrowserRefusal({ subject: `Element ${renderBrowserElement(this)}` }, "UNKNOWN", "has no such option"), {
1109
+ reference: this.reference,
1110
+ reason: "UNKNOWN"
1111
+ });
1100
1112
  for (const option of choices) option.selected = selected.includes(option);
1101
1113
  node.dispatchEvent(new view.Event("input", {
1102
1114
  bubbles: true,
@@ -1118,11 +1130,20 @@ var BrowserDOMElement = class {
1118
1130
  async submit(options) {
1119
1131
  const [node, view] = this.#current(options);
1120
1132
  const owner = node instanceof view.HTMLFormElement ? node : Reflect.get(node, "form");
1121
- if (!(owner instanceof view.HTMLFormElement)) throw new BrowserElementError(this.reference, "UNKNOWN", "is not in a form");
1133
+ if (!(owner instanceof view.HTMLFormElement)) throw new BrowserError("ELEMENT", describeBrowserRefusal({ subject: `Element ${renderBrowserElement(this)}` }, "UNKNOWN", "is not in a form"), {
1134
+ reference: this.reference,
1135
+ reason: "UNKNOWN"
1136
+ });
1122
1137
  const type = Reflect.get(node, "type");
1123
1138
  const submitter = node instanceof view.HTMLButtonElement && type === "submit" || node instanceof view.HTMLInputElement && (type === "submit" || type === "image") ? node : void 0;
1124
- if (submitter?.matches(":disabled") === true) throw new BrowserElementError(this.reference, "DISABLED");
1125
- if (matchesBrowserPopup(submitter ?? owner)) throw new BrowserElementError(this.reference, "UNTRUSTED", "submits into another browsing context, which an untrusted submission cannot open");
1139
+ if (submitter?.matches(":disabled") === true) throw new BrowserError("ELEMENT", describeBrowserRefusal({ subject: `Element ${renderBrowserElement(this)}` }, "DISABLED"), {
1140
+ reference: this.reference,
1141
+ reason: "DISABLED"
1142
+ });
1143
+ if (matchesBrowserPopup(submitter ?? owner)) throw new BrowserError("ELEMENT", describeBrowserRefusal({ subject: `Element ${renderBrowserElement(this)}` }, "UNTRUSTED", "submits into another browsing context, which an untrusted submission cannot open"), {
1144
+ reference: this.reference,
1145
+ reason: "UNTRUSTED"
1146
+ });
1126
1147
  let submitted = false;
1127
1148
  const release = new AbortController();
1128
1149
  owner.addEventListener("submit", () => {
@@ -1135,11 +1156,11 @@ var BrowserDOMElement = class {
1135
1156
  }
1136
1157
  if (submitted) return;
1137
1158
  const invalid = Array.from(owner.elements).find((control) => control.localName !== "fieldset" && control.matches(":invalid"));
1138
- if (invalid === void 0) throw new BrowserError("did not submit", "BROWSER_DOCUMENT_SUBMIT", { reference: this.reference });
1159
+ if (invalid === void 0) throw new BrowserError("DOCUMENT_SUBMIT", "did not submit", { reference: this.reference });
1139
1160
  const field = computeBrowserName(invalid) || (invalid.getAttribute("name") ?? invalid.localName);
1140
1161
  const reason = Reflect.get(invalid, "validationMessage");
1141
1162
  const message = isString(reason) ? reason : "";
1142
- throw new BrowserError(`did not submit: ${field} — ${message}`, "BROWSER_DOCUMENT_SUBMIT", {
1163
+ throw new BrowserError("DOCUMENT_SUBMIT", `did not submit: ${field} — ${message}`, {
1143
1164
  reference: this.reference,
1144
1165
  field,
1145
1166
  message
@@ -1149,19 +1170,34 @@ var BrowserDOMElement = class {
1149
1170
  options?.signal?.throwIfAborted();
1150
1171
  const node = this.#input.node.deref();
1151
1172
  const view = node?.ownerDocument.defaultView ?? null;
1152
- if (!this.#input.current() || node === void 0 || !node.isConnected || view === null) throw new BrowserElementError(this.reference, "GONE");
1173
+ if (!this.#input.current() || node === void 0 || !node.isConnected || view === null) throw new BrowserError("ELEMENT", describeBrowserRefusal({ subject: `Element ${renderBrowserElement(this)}` }, "GONE"), {
1174
+ reference: this.reference,
1175
+ reason: "GONE"
1176
+ });
1153
1177
  return [node, view];
1154
1178
  }
1155
1179
  #actionable(options) {
1156
1180
  const current = this.#current(options);
1157
1181
  const [node] = current;
1158
- if (!node.checkVisibility()) throw new BrowserElementError(this.reference, "HIDDEN");
1159
- if (node.matches(":disabled")) throw new BrowserElementError(this.reference, "DISABLED");
1182
+ if (!node.checkVisibility()) throw new BrowserError("ELEMENT", describeBrowserRefusal({ subject: `Element ${renderBrowserElement(this)}` }, "HIDDEN"), {
1183
+ reference: this.reference,
1184
+ reason: "HIDDEN"
1185
+ });
1186
+ if (node.matches(":disabled")) throw new BrowserError("ELEMENT", describeBrowserRefusal({ subject: `Element ${renderBrowserElement(this)}` }, "DISABLED"), {
1187
+ reference: this.reference,
1188
+ reason: "DISABLED"
1189
+ });
1160
1190
  return current;
1161
1191
  }
1162
1192
  #refuse(target) {
1163
- if (matchesBrowserPopup(target)) throw new BrowserElementError(this.reference, "UNTRUSTED", "opens another browsing context, which an untrusted click cannot do");
1164
- if (target.localName === "input" && Reflect.get(target, "type") === "file") throw new BrowserElementError(this.reference, "UNTRUSTED", "opens a file chooser, which an untrusted click cannot do");
1193
+ if (matchesBrowserPopup(target)) throw new BrowserError("ELEMENT", describeBrowserRefusal({ subject: `Element ${renderBrowserElement(this)}` }, "UNTRUSTED", "opens another browsing context, which an untrusted click cannot do"), {
1194
+ reference: this.reference,
1195
+ reason: "UNTRUSTED"
1196
+ });
1197
+ if (target.localName === "input" && Reflect.get(target, "type") === "file") throw new BrowserError("ELEMENT", describeBrowserRefusal({ subject: `Element ${renderBrowserElement(this)}` }, "UNTRUSTED", "opens a file chooser, which an untrusted click cannot do"), {
1198
+ reference: this.reference,
1199
+ reason: "UNTRUSTED"
1200
+ });
1165
1201
  }
1166
1202
  };
1167
1203
  //#endregion
@@ -1179,7 +1215,7 @@ var BrowserDOMElement = class {
1179
1215
  * again after every subscription, and no check runs once the deadline, counted from `start`, has
1180
1216
  * passed, which the wait reads again after `roots` returns. The wait settles one time: on the
1181
1217
  * check's first value, on the signal's abort with its reason, at the deadline with
1182
- * `BROWSER_WAIT_TIMEOUT`, on a `pagehide` in the first root's window with `GONE`, or with whatever
1218
+ * `TIMEOUT`, on a `pagehide` in the first root's window with `GONE`, or with whatever
1183
1219
  * the check or the roots function throws. A signal that aborts while `check` or `roots` runs
1184
1220
  * settles the wait with its reason, and wins over a value that same check returns. Settlement
1185
1221
  * disconnects every observer and removes every listener synchronously, so a mutation queued
@@ -1300,10 +1336,16 @@ var BrowserDOMWait = class {
1300
1336
  return this.#wait.start + this.#wait.timeout - (now === void 0 ? performance.now() : now());
1301
1337
  }
1302
1338
  #expire() {
1303
- this.#fail(new BrowserError(`${this.#wait.subject} timed out`, "BROWSER_WAIT_TIMEOUT", { timeout: this.#wait.timeout }));
1339
+ this.#fail(new BrowserError("TIMEOUT", `${this.#wait.subject} timed out`, {
1340
+ operation: "wait",
1341
+ timeout: this.#wait.timeout
1342
+ }));
1304
1343
  }
1305
1344
  #unload() {
1306
- this.#fail(new BrowserElementError({ subject: "document" }, "GONE"));
1345
+ this.#fail(new BrowserError("ELEMENT", describeBrowserRefusal({ subject: "document" }, "GONE"), {
1346
+ subject: "document",
1347
+ reason: "GONE"
1348
+ }));
1307
1349
  }
1308
1350
  #succeed(value) {
1309
1351
  this.#stop();
@@ -1358,12 +1400,6 @@ var BrowserDOMWait = class {
1358
1400
  * at the next observed mutation or `load`, so a match inserted there with no other observed
1359
1401
  * change is not seen before the deadline.
1360
1402
  *
1361
- * @example
1362
- * ```ts
1363
- * const view = createBrowserDOMView({ document: frame.contentDocument })
1364
- * const outline = await view.elements.outline()
1365
- * const [save] = await view.elements.find({ role: 'button', name: 'save' })
1366
- * ```
1367
1403
  */
1368
1404
  var BrowserDOMElementManager = class {
1369
1405
  #input;
@@ -1373,17 +1409,23 @@ var BrowserDOMElementManager = class {
1373
1409
  #epochs = /* @__PURE__ */ new WeakMap();
1374
1410
  #watched = /* @__PURE__ */ new WeakSet();
1375
1411
  #count = 0;
1412
+ #positions = /* @__PURE__ */ new WeakMap();
1376
1413
  constructor(input) {
1377
1414
  this.#input = input;
1378
1415
  }
1379
1416
  async outline(options) {
1380
1417
  this.#input.signal.throwIfAborted();
1381
1418
  const limit = options?.limit ?? BROWSER_OUTLINE_LIMIT;
1382
- if (!isInteger(limit) || limit < 0) throw new BrowserError("Outline limit must be a nonnegative integer", void 0, { limit });
1419
+ if (!isInteger(limit) || limit < 0) throw new BrowserError("ARGUMENT", "Outline limit must be a nonnegative integer", { limit });
1383
1420
  options?.signal?.throwIfAborted();
1384
1421
  const document = this.#input.document();
1422
+ const epoch = this.#epoch(new WeakRef(document));
1385
1423
  const rows = this.#capture(this.#root(document, options?.within), options?.within);
1386
- return renderBrowserOutline(document.URL, document.title, rows, limit, options?.search);
1424
+ if (document !== this.#input.document() || epoch !== this.#epoch(new WeakRef(document))) throw new BrowserError("ELEMENT", describeBrowserRefusal({ subject: "outline" }, "GONE"), {
1425
+ subject: "outline",
1426
+ reason: "GONE"
1427
+ });
1428
+ return renderBrowserOutline(document.URL, document.title, rows, limit, options?.secrets);
1387
1429
  }
1388
1430
  async find(query, options) {
1389
1431
  this.#input.signal.throwIfAborted();
@@ -1428,7 +1470,7 @@ var BrowserDOMElementManager = class {
1428
1470
  const css = query.css;
1429
1471
  if (css === void 0) return matches;
1430
1472
  const selected = attempt(() => Array.from(root.querySelectorAll(css)));
1431
- if (!selected.success) throw new BrowserError(`CSS query is invalid: ${css}`, "BROWSER_ELEMENT_QUERY", { css });
1473
+ if (!selected.success) throw new BrowserError("ELEMENT_QUERY", `CSS query is invalid: ${css}`, { css });
1432
1474
  const bound = selected.value.map((element) => {
1433
1475
  const role = computeBrowserRole(element) ?? "generic";
1434
1476
  return this.#bind(element, role, computeBrowserName(element, role));
@@ -1438,13 +1480,17 @@ var BrowserDOMElementManager = class {
1438
1480
  }
1439
1481
  #capture(root, within) {
1440
1482
  const rows = [];
1483
+ this.#positions = /* @__PURE__ */ new WeakMap();
1441
1484
  if (within === void 0 || !matchesBrowserOmitted(root)) this.#walk(root, rows, false);
1442
1485
  return rows;
1443
1486
  }
1444
1487
  #root(document, within) {
1445
1488
  if (within === void 0) return document.documentElement;
1446
1489
  const node = this.#records.get(parseBrowserReference(within) ?? "")?.node.deref();
1447
- if (node === void 0 || !node.isConnected || node.ownerDocument.defaultView === null) throw new BrowserElementError(within, "GONE");
1490
+ if (node === void 0 || !node.isConnected || node.ownerDocument.defaultView === null) throw new BrowserError("ELEMENT", describeBrowserRefusal(within, "GONE"), {
1491
+ reference: within,
1492
+ reason: "GONE"
1493
+ });
1448
1494
  return node;
1449
1495
  }
1450
1496
  #roots(within) {
@@ -1471,7 +1517,7 @@ var BrowserDOMElementManager = class {
1471
1517
  if (!quiet && (!blank || text !== "") && (parent === null || !matchesBrowserInvisible(parent))) {
1472
1518
  const container = readBrowserBlock(parent, root);
1473
1519
  if (!blank) {
1474
- if (block !== void 0 && block !== container) text = this.#flush(rows, text);
1520
+ if (block !== void 0 && block !== container) text = this.#flush(rows, text, block);
1475
1521
  text += data;
1476
1522
  block = container;
1477
1523
  } else if (container === block) text += data;
@@ -1486,7 +1532,7 @@ var BrowserDOMElementManager = class {
1486
1532
  continue;
1487
1533
  }
1488
1534
  if (node instanceof view.HTMLIFrameElement) {
1489
- text = this.#flush(rows, text);
1535
+ text = this.#flush(rows, text, block);
1490
1536
  const child = node.contentDocument;
1491
1537
  if (invisible) {
1492
1538
  node = skipBrowserSubtree(walker);
@@ -1498,7 +1544,7 @@ var BrowserDOMElementManager = class {
1498
1544
  continue;
1499
1545
  }
1500
1546
  if (node instanceof view.HTMLSlotElement) {
1501
- text = this.#flush(rows, text);
1547
+ text = this.#flush(rows, text, block);
1502
1548
  for (const assigned of node.assignedNodes({ flatten: true })) if (assigned instanceof view.Element) this.#walk(assigned, rows, quiet);
1503
1549
  else if (!quiet) this.#flush(rows, assigned.textContent ?? "");
1504
1550
  node = skipBrowserSubtree(walker);
@@ -1506,10 +1552,28 @@ var BrowserDOMElementManager = class {
1506
1552
  }
1507
1553
  const tool = node.localName === "form" ? node.getAttribute("toolname") ?? "" : "";
1508
1554
  const role = tool === "" ? computeBrowserRole(node) : "form";
1509
- if (!invisible && role !== void 0 && (BROWSER_INTERACTIVE_ROLES.has(role) || role === "heading" || tool !== "")) {
1510
- text = this.#flush(rows, text);
1511
- rows.push(this.#row(node, role, view, String(rows.length)));
1512
- if (silenced === void 0 && BROWSER_CONTENT_NAMED_ROLES.has(role)) silenced = node;
1555
+ if (!invisible && role !== void 0 && (BROWSER_INTERACTIVE_ROLES.has(role) || [
1556
+ "heading",
1557
+ "image",
1558
+ "img",
1559
+ "listitem",
1560
+ "row",
1561
+ "cell",
1562
+ "gridcell",
1563
+ "columnheader",
1564
+ "rowheader"
1565
+ ].includes(role) || tool !== "")) {
1566
+ text = this.#flush(rows, text, block);
1567
+ const row = this.#row(node, role, view, String(rows.length));
1568
+ rows.push(row);
1569
+ this.#positions.set(node, row.id);
1570
+ if (silenced === void 0 && BROWSER_CONTENT_NAMED_ROLES.has(role) && ![
1571
+ "row",
1572
+ "cell",
1573
+ "gridcell",
1574
+ "columnheader",
1575
+ "rowheader"
1576
+ ].includes(role)) silenced = node;
1513
1577
  if (node.localName === "textarea") {
1514
1578
  node = skipBrowserSubtree(walker);
1515
1579
  continue;
@@ -1517,14 +1581,14 @@ var BrowserDOMElementManager = class {
1517
1581
  }
1518
1582
  const shadow = node.shadowRoot;
1519
1583
  if (shadow !== null) {
1520
- text = this.#flush(rows, text);
1584
+ text = this.#flush(rows, text, block);
1521
1585
  this.#walk(shadow, rows, muted || silenced !== void 0);
1522
1586
  node = skipBrowserSubtree(walker);
1523
1587
  continue;
1524
1588
  }
1525
1589
  node = walker.nextNode();
1526
1590
  }
1527
- this.#flush(rows, text);
1591
+ this.#flush(rows, text, block);
1528
1592
  }
1529
1593
  #watch(document) {
1530
1594
  const view = document.defaultView;
@@ -1546,14 +1610,14 @@ var BrowserDOMElementManager = class {
1546
1610
  const target = document.deref();
1547
1611
  return target === void 0 ? -1 : this.#input.navigation() + (this.#epochs.get(target) ?? 0);
1548
1612
  }
1549
- #flush(rows, text) {
1550
- if (normalizeBrowserName(text) !== "") this.#text(rows, text);
1613
+ #flush(rows, text, parent) {
1614
+ if (normalizeBrowserName(text) !== "") this.#text(rows, text, parent);
1551
1615
  return "";
1552
1616
  }
1553
- #text(rows, text) {
1617
+ #text(rows, text, parent) {
1554
1618
  rows.push({
1555
1619
  id: String(rows.length),
1556
- parent: void 0,
1620
+ parent: parent === void 0 ? void 0 : this.#parent(parent),
1557
1621
  children: [],
1558
1622
  backend: void 0,
1559
1623
  frame: void 0,
@@ -1570,7 +1634,7 @@ var BrowserDOMElementManager = class {
1570
1634
  #row(element, role, view, id) {
1571
1635
  const name = computeBrowserName(element, role);
1572
1636
  const tool = element.localName === "form" ? element.getAttribute("toolname") : null;
1573
- const reference = role === "heading" ? void 0 : this.#bind(element, role, name).reference;
1637
+ const reference = BROWSER_INTERACTIVE_ROLES.has(role) || tool !== null ? this.#bind(element, role, name).reference : void 0;
1574
1638
  const input = element instanceof view.HTMLInputElement ? element : void 0;
1575
1639
  const checked = input !== void 0 && (input.type === "checkbox" || input.type === "radio") ? input.checked : element.getAttribute("aria-checked") === "true";
1576
1640
  const root = element.getRootNode();
@@ -1578,10 +1642,11 @@ var BrowserDOMElementManager = class {
1578
1642
  let value;
1579
1643
  if (element instanceof view.HTMLSelectElement) value = Array.from(element.selectedOptions, (option) => option.label).join(", ");
1580
1644
  else if (element instanceof view.HTMLTextAreaElement) value = element.value;
1645
+ else if (input?.type === "password") value = "•".repeat(input.value.length);
1581
1646
  else if (input !== void 0 && input.type !== "password" && (BROWSER_TYPED_INPUTS.has(input.type) || input.type === "range")) value = input.value;
1582
1647
  return {
1583
1648
  id: reference ?? id,
1584
- parent: void 0,
1649
+ parent: this.#parent(element.parentNode),
1585
1650
  children: [],
1586
1651
  backend: void 0,
1587
1652
  frame: void 0,
@@ -1591,6 +1656,9 @@ var BrowserDOMElementManager = class {
1591
1656
  description: void 0,
1592
1657
  value,
1593
1658
  properties: {
1659
+ level: role === "heading" ? Number(element.getAttribute("aria-level") ?? element.localName.slice(1)) : void 0,
1660
+ url: element instanceof view.HTMLAnchorElement ? element.href : void 0,
1661
+ password: input?.type === "password",
1594
1662
  checked,
1595
1663
  disabled: element.matches(":disabled"),
1596
1664
  focused,
@@ -1601,6 +1669,13 @@ var BrowserDOMElementManager = class {
1601
1669
  reference
1602
1670
  };
1603
1671
  }
1672
+ #parent(node) {
1673
+ while (node !== null) {
1674
+ const id = this.#positions.get(node);
1675
+ if (id !== void 0) return id;
1676
+ node = node.parentNode;
1677
+ }
1678
+ }
1604
1679
  #bind(element, role, name) {
1605
1680
  const known = this.#identities.get(element);
1606
1681
  const existing = known === void 0 ? void 0 : this.#records.get(known);
@@ -1654,7 +1729,7 @@ var BrowserDOMElementManager = class {
1654
1729
  * fires none of those events, so a reading there stays current across it. A cross-document
1655
1730
  * navigation also clears every element reference. `destroy()` releases the listeners and every
1656
1731
  * reference, and fails every pending wait with a `BrowserError` coded
1657
- * `BROWSER_DOCUMENT_DESTROYED`; after it, `title()`, `read()`, `wait()`, and the element manager's
1732
+ * `DOCUMENT_DESTROYED`; after it, `title()`, `read()`, `wait()`, and the element manager's
1658
1733
  * `outline()`, `find()`, and `wait()` reject with that error, and a second `destroy()` does
1659
1734
  * nothing more. `trusted` is `false`: no action grants user activation, and
1660
1735
  * every event the view's actions dispatch themselves carries `isTrusted` `false`. The `submit`
@@ -1678,8 +1753,8 @@ var BrowserDOMView = class {
1678
1753
  #listeners = new AbortController();
1679
1754
  constructor(options) {
1680
1755
  const document = options.document;
1681
- if (!isBrowserDocument(document) || document.defaultView === null) throw new BrowserError("Browser DOM view requires a document attached to a window", "BROWSER_DOCUMENT");
1682
- if (document === globalThis.document && options.own !== true) throw new BrowserError("Browser DOM view drives globalThis.document only with own: true, because an action that navigates the realm it runs in cannot return", "BROWSER_DOCUMENT_OWN");
1756
+ if (!isBrowserDocument(document) || document.defaultView === null) throw new BrowserError("DOCUMENT", "Browser DOM view requires a document attached to a window");
1757
+ if (document === globalThis.document && options.own !== true) throw new BrowserError("DOCUMENT_OWN", "Browser DOM view drives globalThis.document only with own: true, because an action that navigates the realm it runs in cannot return");
1683
1758
  this.#window = document.defaultView;
1684
1759
  this.#document = document;
1685
1760
  this.#elements = new BrowserDOMElementManager({
@@ -1727,7 +1802,7 @@ var BrowserDOMView = class {
1727
1802
  }).execute();
1728
1803
  }
1729
1804
  destroy() {
1730
- this.#release.abort(new BrowserError("Browser DOM view was destroyed", "BROWSER_DOCUMENT_DESTROYED"));
1805
+ this.#release.abort(new BrowserError("DOCUMENT_DESTROYED", "Browser DOM view was destroyed"));
1731
1806
  this.#listeners.abort();
1732
1807
  this.#elements.clear();
1733
1808
  }
@@ -1763,7 +1838,7 @@ var BrowserDOMView = class {
1763
1838
  * Provides a raw CDP text transport over the browser's native `WebSocket`.
1764
1839
  *
1765
1840
  * @remarks
1766
- * `start()` resolves on the socket's `open` event and rejects with a `BrowserConnectionError`
1841
+ * `start()` resolves on the socket's `open` event and rejects with a `BrowserError`
1767
1842
  * carrying `url` for a URL whose scheme is not `ws:` or `wss:`, for a URL the `WebSocket`
1768
1843
  * constructor refuses (one with a fragment), for a socket that fails or closes before it opens,
1769
1844
  * and at the `timeout` deadline. Concurrent and repeated `start()` and `close()`
@@ -1810,7 +1885,7 @@ var SocketCDPTransport = class {
1810
1885
  }
1811
1886
  async send(data) {
1812
1887
  const socket = this.#socket;
1813
- if (socket === void 0 || socket.readyState !== WebSocket.OPEN) throw new BrowserConnectionError("Socket CDP transport is not open", { url: this.#url });
1888
+ if (socket === void 0 || socket.readyState !== WebSocket.OPEN) throw new BrowserError("CONNECTION", "Socket CDP transport is not open", { url: this.#url });
1814
1889
  socket.send(data);
1815
1890
  }
1816
1891
  async close() {
@@ -1818,15 +1893,15 @@ var SocketCDPTransport = class {
1818
1893
  }
1819
1894
  async #start() {
1820
1895
  const parsed = attempt(() => new URL(this.#url));
1821
- if (!parsed.success) throw new BrowserConnectionError(`Socket CDP URL is invalid: ${this.#url}`, {
1896
+ if (!parsed.success) throw new BrowserError("CONNECTION", `Socket CDP URL is invalid: ${this.#url}`, {
1822
1897
  url: this.#url,
1823
- error: parsed.error
1898
+ error: isJSONValue(parsed.error) ? parsed.error : String(parsed.error)
1824
1899
  });
1825
- if (parsed.value.protocol !== "ws:" && parsed.value.protocol !== "wss:") throw new BrowserConnectionError(`Socket CDP connection requires a ws: or wss: URL: ${this.#url}`, { url: this.#url });
1900
+ if (parsed.value.protocol !== "ws:" && parsed.value.protocol !== "wss:") throw new BrowserError("CONNECTION", `Socket CDP connection requires a ws: or wss: URL: ${this.#url}`, { url: this.#url });
1826
1901
  const created = attempt(() => new WebSocket(this.#url));
1827
- if (!created.success) throw new BrowserConnectionError(`Socket CDP connection to ${this.#url} could not open`, {
1902
+ if (!created.success) throw new BrowserError("CONNECTION", `Socket CDP connection to ${this.#url} could not open`, {
1828
1903
  url: this.#url,
1829
- error: created.error
1904
+ error: isJSONValue(created.error) ? created.error : String(created.error)
1830
1905
  });
1831
1906
  const socket = created.value;
1832
1907
  this.#socket = socket;
@@ -1883,7 +1958,7 @@ var SocketCDPTransport = class {
1883
1958
  });
1884
1959
  }
1885
1960
  #refuse(reason) {
1886
- return new BrowserConnectionError(`Socket CDP connection to ${this.#url} ${reason}`, { url: this.#url });
1961
+ return new BrowserError("CONNECTION", `Socket CDP connection to ${this.#url} ${reason}`, { url: this.#url });
1887
1962
  }
1888
1963
  };
1889
1964
  //#endregion
@@ -1894,8 +1969,8 @@ var SocketCDPTransport = class {
1894
1969
  * @param options - The driven document, and `own` to admit the realm's own document
1895
1970
  * @returns A {@link BrowserDOMViewInterface} over the document's window
1896
1971
  * @throws Thrown when `document` is not a document attached to a window, with the code
1897
- * `BROWSER_DOCUMENT`, and when it is `globalThis.document` without `own: true`, with the code
1898
- * `BROWSER_DOCUMENT_OWN`.
1972
+ * `DOCUMENT`, and when it is `globalThis.document` without `own: true`, with the code
1973
+ * `DOCUMENT_OWN`.
1899
1974
  *
1900
1975
  * @example
1901
1976
  * ```ts
@@ -1909,55 +1984,6 @@ function createBrowserDOMView(options) {
1909
1984
  return new BrowserDOMView(options);
1910
1985
  }
1911
1986
  /**
1912
- * Creates a `BrowserToolsetInterface` that publishes the five view tools over a DOM document and
1913
- * adopts a source's page tools beside them.
1914
- *
1915
- * @remarks
1916
- * The toolset runs over a view {@link createBrowserDOMView} creates for `document`, so it
1917
- * advertises `look`, `read`, `plain`, `click`, `type`, and `wait`, and a click or type receipt ends with
1918
- * ` (untrusted event)`. `source` supplies page tools, which the toolset adopts during `start()`
1919
- * and again on every `change`; `toolset.native` never includes them, so publishing it to a
1920
- * registry leaves that registry's own tools alone. The view belongs to the toolset: `destroy()`
1921
- * removes the toolset's tools, then destroys the view, which releases its listeners and fails
1922
- * its pending waits, and a construction that throws destroys the view before it rethrows.
1923
- * `journeys` adds `record`, `save`, `journeys`, `edit`, and `replay` at construction; a replay
1924
- * runs the steps the document placement executes.
1925
- *
1926
- * @param options - The driven document, `own`, the page-tool source, the manager, the bound, the
1927
- * journey stores, and the emitter hooks
1928
- * @returns A {@link BrowserToolsetInterface} whose `view` drives the document
1929
- * @throws Thrown when `document` is not a document attached to a window, with the code
1930
- * `BROWSER_DOCUMENT`, when it is `globalThis.document` without `own: true`, with the code
1931
- * `BROWSER_DOCUMENT_OWN`, and when `limit` is not a positive integer.
1932
- *
1933
- * @example
1934
- * ```ts
1935
- * const toolset = createDocumentToolset({ document: frame.contentDocument, source: bridge })
1936
- * await toolset.start()
1937
- * await toolset.tools.execute({ id: '1', name: 'look', arguments: { search: 'form' } })
1938
- * ```
1939
- */
1940
- function createDocumentToolset(options) {
1941
- const view = createBrowserDOMView({
1942
- document: options.document,
1943
- ...options.own === void 0 ? {} : { own: options.own }
1944
- });
1945
- try {
1946
- return new BrowserToolset(view, {
1947
- ...options.on === void 0 ? {} : { on: options.on },
1948
- ...options.error === void 0 ? {} : { error: options.error },
1949
- ...options.tools === void 0 ? {} : { tools: options.tools },
1950
- ...options.source === void 0 ? {} : { source: options.source },
1951
- ...options.limit === void 0 ? {} : { limit: options.limit },
1952
- ...options.journeys === void 0 ? {} : { journeys: options.journeys },
1953
- release: view.destroy.bind(view)
1954
- });
1955
- } catch (error) {
1956
- view.destroy();
1957
- throw error;
1958
- }
1959
- }
1960
- /**
1961
1987
  * Creates a CDP transport over the browser's native `WebSocket`.
1962
1988
  *
1963
1989
  * @param options - The CDP WebSocket debugger URL, the opening deadline, and emitter hooks
@@ -1975,6 +2001,6 @@ function createSocketCDPTransport(options) {
1975
2001
  return new SocketCDPTransport(options);
1976
2002
  }
1977
2003
  //#endregion
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 };
2004
+ 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, BrowserDOMView, BrowserDOMWait, SocketCDPTransport, collectBrowserRoots, computeBrowserAlternative, computeBrowserName, computeBrowserRole, computeBrowserText, createBrowserDOMView, createSocketCDPTransport, isBrowserDocument, listenBrowserNavigation, matchesBrowserActivation, matchesBrowserBlock, matchesBrowserHidden, matchesBrowserInvisible, matchesBrowserOmitted, matchesBrowserPopup, readBrowserBlock, readBrowserCapture, readBrowserContent, readBrowserParent, readBrowserStates, readBrowserToken, skipBrowserSubtree };
1979
2005
 
1980
2006
  //# sourceMappingURL=index.js.map