@orkestrel/browser 0.0.26 → 0.0.27
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.
- package/README.md +23 -13
- package/dist/bin/main.js +11 -9
- package/dist/bin/main.js.map +1 -1
- package/dist/src/browser/index.d.ts +6 -184
- package/dist/src/browser/index.js +145 -119
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +2324 -1991
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +672 -1366
- package/dist/src/core/index.d.ts +672 -1366
- package/dist/src/core/index.js +2287 -1941
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +202 -211
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +65 -166
- package/dist/src/server/index.d.ts +65 -166
- package/dist/src/server/index.js +201 -205
- package/dist/src/server/index.js.map +1 -1
- package/package.json +7 -6
|
@@ -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,
|
|
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 `
|
|
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
|
|
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 `
|
|
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
|
|
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
|
|
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
|
|
1083
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1125
|
-
|
|
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",
|
|
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}`,
|
|
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
|
|
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
|
|
1159
|
-
|
|
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
|
|
1164
|
-
|
|
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
|
-
* `
|
|
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`,
|
|
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
|
|
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",
|
|
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
|
-
|
|
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}`,
|
|
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
|
|
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) ||
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
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
|
|
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:
|
|
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
|
-
* `
|
|
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"
|
|
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"
|
|
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"
|
|
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 `
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
* `
|
|
1898
|
-
* `
|
|
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,
|
|
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
|