@orkestrel/browser 0.0.20 → 0.0.22
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 +6 -3
- package/dist/src/browser/index.d.ts +77 -19
- package/dist/src/browser/index.js +361 -21
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +963 -173
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +354 -78
- package/dist/src/core/index.d.ts +354 -78
- package/dist/src/core/index.js +957 -175
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +36 -11
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +2 -2
- package/dist/src/server/index.d.ts +2 -2
- package/dist/src/server/index.js +36 -11
- package/dist/src/server/index.js.map +1 -1
- package/package.json +5 -5
|
@@ -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
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
859
|
-
*
|
|
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
|
|
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
|
|
874
|
-
* observes, and a match inserted into it with no other
|
|
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
|
|
1013
|
-
*
|
|
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: {
|
|
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
|