@yielded/agent-platform-cloudflare 0.1.0-beta.167 → 0.1.0-beta.168
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/dist/{Alarm-B0ODr1NE.mjs → Alarm-DxODgAZZ.mjs} +259 -159
- package/dist/Alarm-DxODgAZZ.mjs.map +1 -0
- package/dist/Alarm.d.mts +27 -36
- package/dist/Alarm.mjs +2 -2
- package/dist/BrowserCredentials-C5IWfi7f.d.mts +91 -0
- package/dist/BrowserCredentials.d.mts +2 -91
- package/dist/BrowserRestCapture.mjs +3 -3
- package/dist/BrowserRestCapture.mjs.map +1 -1
- package/dist/BrowserRestCrawl.mjs +6 -6
- package/dist/BrowserRestCrawl.mjs.map +1 -1
- package/dist/{BrowserSession-RfIzxGCH.mjs → BrowserSession-BfNmyayH.mjs} +5 -5
- package/dist/BrowserSession-BfNmyayH.mjs.map +1 -0
- package/dist/{BrowserSession-B969Xg-2.d.mts → BrowserSession-Bx0V1-6_.d.mts} +6 -3
- package/dist/BrowserSession.d.mts +1 -1
- package/dist/BrowserSession.mjs +2 -2
- package/dist/BrowserUse-AstK0jKX.mjs +1467 -0
- package/dist/BrowserUse-AstK0jKX.mjs.map +1 -0
- package/dist/BrowserUse.d.mts +83 -0
- package/dist/BrowserUse.mjs +2 -0
- package/dist/CloudflareBindings.d.mts +8 -1
- package/dist/CloudflareBindings.mjs +2 -2
- package/dist/CloudflareBindings.mjs.map +1 -1
- package/dist/{CloudflareBrowser-5an6m-Qb.mjs → CloudflareBrowser-Vt8qTdYW.mjs} +11 -13
- package/dist/CloudflareBrowser-Vt8qTdYW.mjs.map +1 -0
- package/dist/CloudflareBrowser.mjs +1 -1
- package/dist/CloudflareCodeMode.mjs +1 -1
- package/dist/CloudflareCodeMode.mjs.map +1 -1
- package/dist/{CloudflareConfig-qDrRLofL.d.mts → CloudflareConfig-C9MvPbX8.d.mts} +9 -11
- package/dist/CloudflareConfig.d.mts +1 -1
- package/dist/CloudflareConfig.mjs +9 -12
- package/dist/CloudflareConfig.mjs.map +1 -1
- package/dist/CloudflareMemory.d.mts +3 -25
- package/dist/CloudflareMemory.mjs +33 -31
- package/dist/CloudflareMemory.mjs.map +1 -1
- package/dist/CloudflareScheduling.mjs +8 -15
- package/dist/CloudflareScheduling.mjs.map +1 -1
- package/dist/CloudflareSubscriptions.mjs +9 -16
- package/dist/CloudflareSubscriptions.mjs.map +1 -1
- package/dist/CloudflareThreadClient-CZm0TvUe.mjs +588 -0
- package/dist/CloudflareThreadClient-CZm0TvUe.mjs.map +1 -0
- package/dist/CloudflareThreadClient-QsVzLGGZ.d.mts +2033 -0
- package/dist/CloudflareThreadClient.d.mts +2 -1796
- package/dist/CloudflareThreadClient.mjs +2 -382
- package/dist/{InteractiveBrowser-BbG3kGA9.mjs → InteractiveBrowser-LdKzQeLR.mjs} +18 -19
- package/dist/InteractiveBrowser-LdKzQeLR.mjs.map +1 -0
- package/dist/InteractiveBrowser.mjs +1 -1
- package/dist/{ThreadObject-BcfXbzSO.d.mts → ThreadObject-7s5XnlH-.d.mts} +103 -79
- package/dist/{ThreadObject-DHjrnlX4.mjs → ThreadObject-C0vrcDrw.mjs} +265 -66
- package/dist/ThreadObject-C0vrcDrw.mjs.map +1 -0
- package/dist/ThreadObject.d.mts +2 -2
- package/dist/ThreadObject.mjs +2 -3
- package/dist/WakeScheduler.d.mts +6 -5
- package/dist/WakeScheduler.mjs +15 -11
- package/dist/WakeScheduler.mjs.map +1 -1
- package/dist/index.d.mts +7 -6
- package/dist/index.mjs +7 -6
- package/dist/page-function-CmUCcyNP.mjs +11 -0
- package/dist/page-function-CmUCcyNP.mjs.map +1 -0
- package/dist/{prepared-admission-DMX5p9dC.mjs → prepared-admission-3UUq0zth.mjs} +3 -3
- package/dist/{prepared-admission-DMX5p9dC.mjs.map → prepared-admission-3UUq0zth.mjs.map} +1 -1
- package/package.json +1 -1
- package/src/Alarm.ts +510 -279
- package/src/BrowserRestCapture.ts +2 -2
- package/src/BrowserRestCrawl.ts +10 -14
- package/src/BrowserSession.ts +10 -2
- package/src/BrowserUse.ts +1541 -0
- package/src/CloudflareBindings.ts +13 -4
- package/src/CloudflareCodeMode.ts +1 -1
- package/src/CloudflareConfig.ts +9 -12
- package/src/CloudflareMemory.ts +21 -28
- package/src/CloudflareScheduling.ts +7 -15
- package/src/CloudflareSubscriptions.ts +11 -20
- package/src/CloudflareThreadClient.ts +289 -9
- package/src/InteractiveBrowser.ts +325 -323
- package/src/ThreadObject.ts +172 -52
- package/src/WakeScheduler.ts +29 -18
- package/src/index.ts +1 -0
- package/src/internal/browser-dom.ts +845 -0
- package/src/internal/browser-file-selection.ts +8 -1
- package/src/internal/browser-jev-dom.ts +344 -0
- package/src/internal/browser-quick-action.ts +7 -8
- package/src/internal/browser-session-lifecycle.ts +2 -5
- package/src/internal/crypto.ts +41 -0
- package/src/internal/due-queue.ts +5 -69
- package/src/internal/layers.ts +100 -17
- package/src/internal/live-text-protocol.ts +51 -0
- package/src/internal/live-text.ts +256 -0
- package/src/internal/page-function.ts +12 -0
- package/src/internal/prepared-admission.ts +1 -1
- package/src/internal/progress-wait.ts +19 -24
- package/src/internal/transport.ts +1 -1
- package/dist/Alarm-B0ODr1NE.mjs.map +0 -1
- package/dist/BrowserSession-RfIzxGCH.mjs.map +0 -1
- package/dist/CloudflareBrowser-5an6m-Qb.mjs.map +0 -1
- package/dist/CloudflareThreadClient.mjs.map +0 -1
- package/dist/InteractiveBrowser-BbG3kGA9.mjs.map +0 -1
- package/dist/ThreadObject-DHjrnlX4.mjs.map +0 -1
|
@@ -0,0 +1,845 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
|
|
3
|
+
import type { Control } from "@yielded/agent/browser-use";
|
|
4
|
+
|
|
5
|
+
import { pageFunction } from "./page-function.ts";
|
|
6
|
+
|
|
7
|
+
/** Executor-owned input settling, matching jev-ultrafast's frame/option readiness predicate. */
|
|
8
|
+
export const settleInputDom = pageFunction(
|
|
9
|
+
(ref: string | undefined) =>
|
|
10
|
+
new Promise<void>((resolve) => {
|
|
11
|
+
const field: Element | undefined =
|
|
12
|
+
ref === undefined
|
|
13
|
+
? undefined
|
|
14
|
+
: Reflect.get(globalThis, "@effect-agent/native-browser")?.get(ref);
|
|
15
|
+
|
|
16
|
+
const autocomplete = field?.getAttribute("role") === "combobox";
|
|
17
|
+
let frames = 0;
|
|
18
|
+
let stopped = false;
|
|
19
|
+
let animation = 0;
|
|
20
|
+
|
|
21
|
+
const finish = () => {
|
|
22
|
+
if (stopped) return;
|
|
23
|
+
stopped = true;
|
|
24
|
+
clearTimeout(deadline);
|
|
25
|
+
cancelAnimationFrame(animation);
|
|
26
|
+
resolve();
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const deadline = setTimeout(finish, autocomplete ? 200 : 50);
|
|
30
|
+
|
|
31
|
+
const ready = () => {
|
|
32
|
+
if (stopped) return;
|
|
33
|
+
|
|
34
|
+
const ids = (field?.getAttribute("aria-controls") || field?.getAttribute("aria-owns") || "")
|
|
35
|
+
.split(/\s+/)
|
|
36
|
+
.filter(Boolean);
|
|
37
|
+
|
|
38
|
+
const roots = ids.length ? ids.map((id) => document.getElementById(id)) : [document];
|
|
39
|
+
|
|
40
|
+
const options = roots.flatMap((root) => [
|
|
41
|
+
...(root?.querySelectorAll('[role="option"]') ?? []),
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
if (
|
|
45
|
+
++frames >= 2 &&
|
|
46
|
+
(!autocomplete ||
|
|
47
|
+
options.some((element) => {
|
|
48
|
+
const rect = element.getBoundingClientRect();
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
rect.width &&
|
|
52
|
+
rect.height &&
|
|
53
|
+
rect.bottom > 0 &&
|
|
54
|
+
rect.top < innerHeight &&
|
|
55
|
+
element.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true })
|
|
56
|
+
);
|
|
57
|
+
}))
|
|
58
|
+
)
|
|
59
|
+
finish();
|
|
60
|
+
else animation = requestAnimationFrame(ready);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
animation = requestAnimationFrame(ready);
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
/** Read-only executor settling: 100 ms without a DOM mutation, bounded by one second. */
|
|
68
|
+
export const settleDom = pageFunction(
|
|
69
|
+
() =>
|
|
70
|
+
new Promise<boolean>((resolve) => {
|
|
71
|
+
let quiet: ReturnType<typeof setTimeout>;
|
|
72
|
+
|
|
73
|
+
const observer = new MutationObserver(() => {
|
|
74
|
+
clearTimeout(quiet);
|
|
75
|
+
quiet = setTimeout(() => finish(true), 100);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const finish = (settled: boolean) => {
|
|
79
|
+
observer.disconnect();
|
|
80
|
+
clearTimeout(quiet);
|
|
81
|
+
clearTimeout(deadline);
|
|
82
|
+
resolve(settled);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const deadline = setTimeout(() => finish(false), 1_000);
|
|
86
|
+
|
|
87
|
+
observer.observe(document, {
|
|
88
|
+
subtree: true,
|
|
89
|
+
childList: true,
|
|
90
|
+
attributes: true,
|
|
91
|
+
characterData: true,
|
|
92
|
+
});
|
|
93
|
+
quiet = setTimeout(() => finish(true), 100);
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
/** Executed in the isolated page realm; the page cannot forge the reference registry. */
|
|
98
|
+
export const inspectDom = pageFunction(
|
|
99
|
+
(
|
|
100
|
+
selector: string | undefined,
|
|
101
|
+
prefix: string,
|
|
102
|
+
maximum: number,
|
|
103
|
+
optionFilter: string | undefined,
|
|
104
|
+
viewportOnly = false,
|
|
105
|
+
) => {
|
|
106
|
+
const registry = new Map<string, Element>();
|
|
107
|
+
|
|
108
|
+
const nameChecks = new Map<
|
|
109
|
+
string,
|
|
110
|
+
{ name: string; originalName: string; verify: () => boolean }
|
|
111
|
+
>();
|
|
112
|
+
|
|
113
|
+
Reflect.set(globalThis, "@effect-agent/native-browser", registry);
|
|
114
|
+
Reflect.set(globalThis, "@effect-agent/native-browser-name-checks", nameChecks);
|
|
115
|
+
const roots: Array<Document | ShadowRoot> = [document];
|
|
116
|
+
const visited = new Set<Node>();
|
|
117
|
+
let discoveredElements = 0;
|
|
118
|
+
const candidates: Array<HTMLElement> = [];
|
|
119
|
+
let scanned = 0;
|
|
120
|
+
let truncated = false;
|
|
121
|
+
let text = "";
|
|
122
|
+
|
|
123
|
+
const controlSelector =
|
|
124
|
+
'button,a[href],input,textarea,select,label,[contenteditable="true"],[role="button"],[role="link"],[role="checkbox"],[role="radio"],[role="switch"],[role="tab"],[role="option"],[role="combobox"],[role="textbox"],[role="menuitem"],[role="scrollbar"]';
|
|
125
|
+
|
|
126
|
+
const filter: NodeFilter = {
|
|
127
|
+
acceptNode(node) {
|
|
128
|
+
if (!(node instanceof Element)) return NodeFilter.FILTER_ACCEPT;
|
|
129
|
+
const style = getComputedStyle(node);
|
|
130
|
+
|
|
131
|
+
return node.matches('script,style,template,option,optgroup,[inert],[aria-hidden="true"]') ||
|
|
132
|
+
style.display === "none" ||
|
|
133
|
+
style.opacity === "0" ||
|
|
134
|
+
style.contentVisibility === "hidden"
|
|
135
|
+
? NodeFilter.FILTER_REJECT
|
|
136
|
+
: NodeFilter.FILTER_ACCEPT;
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
for (let index = 0; index < roots.length && !truncated; index++) {
|
|
141
|
+
const root = roots[index];
|
|
142
|
+
|
|
143
|
+
if (root === undefined) break;
|
|
144
|
+
|
|
145
|
+
// Narrow inspection starts at matching roots, so unrelated page content
|
|
146
|
+
// cannot exhaust its budget. Shadow-host discovery has a separate bound.
|
|
147
|
+
if (selector !== undefined) {
|
|
148
|
+
const discovery = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, filter);
|
|
149
|
+
let node = discovery.nextNode();
|
|
150
|
+
|
|
151
|
+
while (node !== null) {
|
|
152
|
+
if (++discoveredElements > 10_000) {
|
|
153
|
+
truncated = true;
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
if (node instanceof Element && node.shadowRoot !== null) roots.push(node.shadowRoot);
|
|
157
|
+
node = discovery.nextNode();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const matches = selector === undefined ? [root] : Array.from(root.querySelectorAll(selector));
|
|
161
|
+
const scopes = matches.slice(0, 256);
|
|
162
|
+
const nodes: Array<Element> = [];
|
|
163
|
+
|
|
164
|
+
truncated ||= matches.length > scopes.length;
|
|
165
|
+
for (const scope of scopes) {
|
|
166
|
+
if (scope instanceof Element && filter.acceptNode(scope) === NodeFilter.FILTER_REJECT)
|
|
167
|
+
continue;
|
|
168
|
+
|
|
169
|
+
const walker = document.createTreeWalker(
|
|
170
|
+
scope,
|
|
171
|
+
NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT,
|
|
172
|
+
filter,
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
let node: Node | null = scope instanceof Element ? scope : walker.nextNode();
|
|
176
|
+
|
|
177
|
+
while (node !== null) {
|
|
178
|
+
if (visited.has(node)) {
|
|
179
|
+
node = walker.nextNode();
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
visited.add(node);
|
|
183
|
+
if (++scanned > 10_000) {
|
|
184
|
+
truncated = true;
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
if (node instanceof Element) {
|
|
188
|
+
nodes.push(node);
|
|
189
|
+
if (selector === undefined && node.shadowRoot !== null) roots.push(node.shadowRoot);
|
|
190
|
+
} else if (node instanceof Text) {
|
|
191
|
+
const parent = node.parentElement;
|
|
192
|
+
|
|
193
|
+
// Select options have their own bounded lookup; collapsed option text
|
|
194
|
+
// must not bury the current form's values and validation messages.
|
|
195
|
+
if (
|
|
196
|
+
parent instanceof HTMLElement &&
|
|
197
|
+
parent.closest('select,script,style,[inert],[aria-hidden="true"]') === null &&
|
|
198
|
+
parent.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true })
|
|
199
|
+
) {
|
|
200
|
+
const value = node.textContent?.replace(/\s+/g, " ").trim() ?? "";
|
|
201
|
+
|
|
202
|
+
if (value.length > 0) {
|
|
203
|
+
const room = Math.max(0, 12_000 - text.length);
|
|
204
|
+
|
|
205
|
+
text += `${value.slice(0, room)}\n`;
|
|
206
|
+
truncated ||= value.length >= room;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
node = walker.nextNode();
|
|
211
|
+
}
|
|
212
|
+
if (scanned > 10_000) break;
|
|
213
|
+
}
|
|
214
|
+
for (const node of nodes) {
|
|
215
|
+
if (selector !== undefined && !node.matches(selector) && node.closest(selector) === null)
|
|
216
|
+
continue;
|
|
217
|
+
if (!(node instanceof HTMLElement)) continue;
|
|
218
|
+
|
|
219
|
+
const scrollable =
|
|
220
|
+
node.scrollHeight > node.clientHeight &&
|
|
221
|
+
["auto", "scroll"].includes(getComputedStyle(node).overflowY);
|
|
222
|
+
|
|
223
|
+
if (!node.matches(controlSelector) && !scrollable) continue;
|
|
224
|
+
if (
|
|
225
|
+
!node.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true }) ||
|
|
226
|
+
node.closest('[inert],[aria-hidden="true"]') !== null
|
|
227
|
+
)
|
|
228
|
+
continue;
|
|
229
|
+
const modal = document.querySelector("dialog:modal");
|
|
230
|
+
|
|
231
|
+
if (modal !== null && !modal.contains(node) && node.getRootNode() === document) continue;
|
|
232
|
+
candidates.push(node);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
let retained = candidates;
|
|
237
|
+
|
|
238
|
+
// A popup falls back to off-screen controls only when none of its controls
|
|
239
|
+
// are in view. Do not restore an entire off-screen calendar beside visible days.
|
|
240
|
+
if (viewportOnly || candidates.length > maximum) {
|
|
241
|
+
const visible: Array<HTMLElement> = [];
|
|
242
|
+
const popupControls = new Map<Element, Array<HTMLElement>>();
|
|
243
|
+
const visiblePopups = new Set<Element>();
|
|
244
|
+
const offscreen: Array<HTMLElement> = [];
|
|
245
|
+
|
|
246
|
+
const popupSelector =
|
|
247
|
+
'dialog[open],[role="dialog"],[role="listbox"],[role="menu"],:popover-open';
|
|
248
|
+
|
|
249
|
+
for (const node of candidates) {
|
|
250
|
+
const rect = node.getBoundingClientRect();
|
|
251
|
+
const x = rect.x + rect.width / 2;
|
|
252
|
+
const y = rect.y + rect.height / 2;
|
|
253
|
+
|
|
254
|
+
const inView = x >= 0 && x < innerWidth && y >= 0 && y < innerHeight;
|
|
255
|
+
|
|
256
|
+
if (viewportOnly) {
|
|
257
|
+
let popup = node.closest(popupSelector);
|
|
258
|
+
|
|
259
|
+
if (popup?.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true })) {
|
|
260
|
+
if (!inView) {
|
|
261
|
+
const group = popupControls.get(popup) ?? [];
|
|
262
|
+
|
|
263
|
+
group.push(node);
|
|
264
|
+
popupControls.set(popup, group);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
while (popup !== null) {
|
|
268
|
+
visiblePopups.add(popup);
|
|
269
|
+
popup = popup.parentElement?.closest(popupSelector) ?? null;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
(inView ? visible : offscreen).push(node);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const preferred = [
|
|
277
|
+
...visible,
|
|
278
|
+
...[...popupControls].flatMap(([popup, nodes]) => (visiblePopups.has(popup) ? [] : nodes)),
|
|
279
|
+
];
|
|
280
|
+
|
|
281
|
+
retained = (
|
|
282
|
+
viewportOnly ? (preferred.length > 0 ? preferred : offscreen) : [...visible, ...offscreen]
|
|
283
|
+
).slice(0, maximum);
|
|
284
|
+
truncated ||= candidates.length > retained.length;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const readName = (node: HTMLElement) => {
|
|
288
|
+
const associated = node instanceof HTMLLabelElement ? (node.control ?? node) : node;
|
|
289
|
+
|
|
290
|
+
const nativeField =
|
|
291
|
+
associated instanceof HTMLInputElement ||
|
|
292
|
+
associated instanceof HTMLTextAreaElement ||
|
|
293
|
+
associated instanceof HTMLSelectElement;
|
|
294
|
+
|
|
295
|
+
const labels = nativeField ? associated.labels : null;
|
|
296
|
+
const root = node.getRootNode();
|
|
297
|
+
|
|
298
|
+
const labelledBy = (node.getAttribute("aria-labelledby") ?? "")
|
|
299
|
+
.split(/\s+/)
|
|
300
|
+
.map((id) =>
|
|
301
|
+
root instanceof Document || root instanceof ShadowRoot
|
|
302
|
+
? (root.getElementById(id)?.textContent ?? "")
|
|
303
|
+
: "",
|
|
304
|
+
)
|
|
305
|
+
.join(" ");
|
|
306
|
+
|
|
307
|
+
return (
|
|
308
|
+
node.getAttribute("aria-label") ||
|
|
309
|
+
labelledBy.trim() ||
|
|
310
|
+
(labels === null
|
|
311
|
+
? ""
|
|
312
|
+
: Array.from(labels)
|
|
313
|
+
.map((label) => label.textContent)
|
|
314
|
+
.join(" ")) ||
|
|
315
|
+
(nativeField
|
|
316
|
+
? node.getAttribute("placeholder")
|
|
317
|
+
: node instanceof HTMLElement
|
|
318
|
+
? node.innerText
|
|
319
|
+
: "") ||
|
|
320
|
+
""
|
|
321
|
+
)
|
|
322
|
+
.replace(/\s+/g, " ")
|
|
323
|
+
.trim()
|
|
324
|
+
.slice(0, 300);
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const controls = retained.map((node, index) => {
|
|
328
|
+
const ref = `${prefix}-${index}`;
|
|
329
|
+
const tag = node.tagName.toLowerCase();
|
|
330
|
+
const associated = node instanceof HTMLLabelElement ? (node.control ?? node) : node;
|
|
331
|
+
const type = associated instanceof HTMLInputElement ? associated.type : "";
|
|
332
|
+
|
|
333
|
+
const nativeField =
|
|
334
|
+
associated instanceof HTMLInputElement ||
|
|
335
|
+
associated instanceof HTMLTextAreaElement ||
|
|
336
|
+
associated instanceof HTMLSelectElement;
|
|
337
|
+
|
|
338
|
+
const role = node.getAttribute("role");
|
|
339
|
+
|
|
340
|
+
const kind =
|
|
341
|
+
tag === "label" &&
|
|
342
|
+
associated instanceof HTMLInputElement &&
|
|
343
|
+
["checkbox", "radio"].includes(type)
|
|
344
|
+
? type
|
|
345
|
+
: (role ?? (type === "checkbox" || type === "radio" ? type : tag === "a" ? "link" : tag));
|
|
346
|
+
|
|
347
|
+
const name = readName(node);
|
|
348
|
+
|
|
349
|
+
const allOptions = node instanceof HTMLSelectElement ? Array.from(node.options) : [];
|
|
350
|
+
|
|
351
|
+
const matchingOptions = allOptions.flatMap((option, index) =>
|
|
352
|
+
option.selected ||
|
|
353
|
+
optionFilter === undefined ||
|
|
354
|
+
`${option.label}\n${option.value}`.toLowerCase().includes(optionFilter.toLowerCase())
|
|
355
|
+
? [{ option, index }]
|
|
356
|
+
: [],
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
// Keep selected state even when it falls beyond the bounded lookup window.
|
|
360
|
+
const selectedOptions = matchingOptions.filter(({ option }) => option.selected);
|
|
361
|
+
|
|
362
|
+
const visibleOptions = [
|
|
363
|
+
...selectedOptions.slice(0, 256),
|
|
364
|
+
...matchingOptions
|
|
365
|
+
.filter(({ option }) => !option.selected)
|
|
366
|
+
.slice(0, Math.max(0, 256 - selectedOptions.length)),
|
|
367
|
+
];
|
|
368
|
+
|
|
369
|
+
if (matchingOptions.length > visibleOptions.length) truncated = true;
|
|
370
|
+
|
|
371
|
+
const attributes = Object.fromEntries(
|
|
372
|
+
[
|
|
373
|
+
"id",
|
|
374
|
+
"name",
|
|
375
|
+
"type",
|
|
376
|
+
"role",
|
|
377
|
+
"tabindex",
|
|
378
|
+
"placeholder",
|
|
379
|
+
"aria-label",
|
|
380
|
+
"aria-labelledby",
|
|
381
|
+
"aria-expanded",
|
|
382
|
+
"aria-selected",
|
|
383
|
+
"aria-checked",
|
|
384
|
+
"aria-controls",
|
|
385
|
+
"href",
|
|
386
|
+
].flatMap((key) => {
|
|
387
|
+
const value = node.getAttribute(key);
|
|
388
|
+
|
|
389
|
+
return value === null ? [] : [[key, value.slice(0, 1_024)]];
|
|
390
|
+
}),
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
registry.set(ref, node);
|
|
394
|
+
nameChecks.set(ref, {
|
|
395
|
+
name,
|
|
396
|
+
originalName: name,
|
|
397
|
+
verify: () => readName(node) === name,
|
|
398
|
+
});
|
|
399
|
+
const pointerEvents = getComputedStyle(node).pointerEvents?.slice(0, 64);
|
|
400
|
+
|
|
401
|
+
return {
|
|
402
|
+
ref,
|
|
403
|
+
kind,
|
|
404
|
+
name,
|
|
405
|
+
attributes,
|
|
406
|
+
...(pointerEvents ? { pointerEvents } : {}),
|
|
407
|
+
value:
|
|
408
|
+
nativeField && type !== "password"
|
|
409
|
+
? associated.value.slice(0, 4_096)
|
|
410
|
+
: node.isContentEditable
|
|
411
|
+
? node.innerText.slice(0, 4_096)
|
|
412
|
+
: "",
|
|
413
|
+
options: visibleOptions.map(({ option }) => option.value.slice(0, 4_096)),
|
|
414
|
+
...(node instanceof HTMLSelectElement
|
|
415
|
+
? {
|
|
416
|
+
optionCount: allOptions.length,
|
|
417
|
+
optionDetails: visibleOptions.map(({ option, index }) => ({
|
|
418
|
+
index,
|
|
419
|
+
value: option.value.slice(0, 4096),
|
|
420
|
+
label: option.label.slice(0, 300),
|
|
421
|
+
disabled:
|
|
422
|
+
option.disabled ||
|
|
423
|
+
(option.parentElement instanceof HTMLOptGroupElement &&
|
|
424
|
+
option.parentElement.disabled),
|
|
425
|
+
selected: option.selected,
|
|
426
|
+
})),
|
|
427
|
+
}
|
|
428
|
+
: {}),
|
|
429
|
+
disabled:
|
|
430
|
+
associated.matches(":disabled") || associated.getAttribute("aria-disabled") === "true",
|
|
431
|
+
editable:
|
|
432
|
+
(node instanceof HTMLInputElement &&
|
|
433
|
+
!node.readOnly &&
|
|
434
|
+
!["password", "file", "checkbox", "radio", "hidden", "submit", "button"].includes(
|
|
435
|
+
node.type,
|
|
436
|
+
)) ||
|
|
437
|
+
(node instanceof HTMLTextAreaElement && !node.readOnly) ||
|
|
438
|
+
(node.isContentEditable && node.getAttribute("aria-readonly") !== "true"),
|
|
439
|
+
...(associated instanceof HTMLInputElement && (type === "checkbox" || type === "radio")
|
|
440
|
+
? { checked: associated.checked }
|
|
441
|
+
: {}),
|
|
442
|
+
};
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
const names = new Map<string, number>();
|
|
446
|
+
|
|
447
|
+
for (const control of controls) names.set(control.name, (names.get(control.name) ?? 0) + 1);
|
|
448
|
+
|
|
449
|
+
const explicitLabel = (node: Element) => {
|
|
450
|
+
const root = node.getRootNode();
|
|
451
|
+
|
|
452
|
+
return (
|
|
453
|
+
node.getAttribute("aria-label") ||
|
|
454
|
+
(node.getAttribute("aria-labelledby") ?? "")
|
|
455
|
+
.split(/\s+/)
|
|
456
|
+
.map((id) =>
|
|
457
|
+
root instanceof Document || root instanceof ShadowRoot
|
|
458
|
+
? (root.getElementById(id)?.textContent ?? "")
|
|
459
|
+
: "",
|
|
460
|
+
)
|
|
461
|
+
.join(" ")
|
|
462
|
+
)
|
|
463
|
+
.replace(/\s+/g, " ")
|
|
464
|
+
.trim();
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
for (const [index, control] of controls.entries()) {
|
|
468
|
+
const node = retained[index];
|
|
469
|
+
|
|
470
|
+
if (node === undefined || control.name.length === 0 || (names.get(control.name) ?? 0) < 2)
|
|
471
|
+
continue;
|
|
472
|
+
let branch: Element = node;
|
|
473
|
+
|
|
474
|
+
for (let scope = node.parentElement; scope !== null; scope = scope.parentElement) {
|
|
475
|
+
let source: Element | undefined = explicitLabel(scope) ? scope : undefined;
|
|
476
|
+
let read = () => (source === undefined ? "" : explicitLabel(source));
|
|
477
|
+
|
|
478
|
+
if (source === undefined) {
|
|
479
|
+
for (
|
|
480
|
+
let previous = branch.previousElementSibling;
|
|
481
|
+
previous !== null;
|
|
482
|
+
previous = previous.previousElementSibling
|
|
483
|
+
) {
|
|
484
|
+
if (
|
|
485
|
+
!(previous instanceof HTMLElement) ||
|
|
486
|
+
!previous.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true }) ||
|
|
487
|
+
previous.closest('[inert],[aria-hidden="true"]') !== null ||
|
|
488
|
+
previous.matches(controlSelector) ||
|
|
489
|
+
previous.querySelector(controlSelector) !== null ||
|
|
490
|
+
(previous.childElementCount > 0 &&
|
|
491
|
+
!previous.matches('h1,h2,h3,h4,h5,h6,caption,figcaption,legend,[role="heading"]'))
|
|
492
|
+
)
|
|
493
|
+
continue;
|
|
494
|
+
const caption = previous.innerText.replace(/\s+/g, " ").trim();
|
|
495
|
+
|
|
496
|
+
// Short static leaf text also serves as a caption in unlabelled containers.
|
|
497
|
+
if (caption.length === 0 || caption.length > 120) continue;
|
|
498
|
+
source = previous;
|
|
499
|
+
const captionElement = previous;
|
|
500
|
+
|
|
501
|
+
read = () => captionElement.innerText.replace(/\s+/g, " ").trim();
|
|
502
|
+
break;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
const context = read();
|
|
506
|
+
|
|
507
|
+
if (source !== undefined && context.length > 0) {
|
|
508
|
+
const originalName = control.name;
|
|
509
|
+
const container = scope;
|
|
510
|
+
const caption = source;
|
|
511
|
+
|
|
512
|
+
control.name = `${originalName} (${context})`.slice(0, 300);
|
|
513
|
+
// Keep raw-name and contextual identity checks in the isolated realm.
|
|
514
|
+
// Presentation must not weaken the existing native dispatch guard.
|
|
515
|
+
nameChecks.set(control.ref, {
|
|
516
|
+
name: control.name,
|
|
517
|
+
originalName,
|
|
518
|
+
verify: () =>
|
|
519
|
+
readName(node) === originalName &&
|
|
520
|
+
container.isConnected &&
|
|
521
|
+
container.contains(node) &&
|
|
522
|
+
container.contains(caption) &&
|
|
523
|
+
(caption === container ||
|
|
524
|
+
(caption.compareDocumentPosition(node) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0) &&
|
|
525
|
+
read() === context,
|
|
526
|
+
});
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
branch = scope;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return {
|
|
534
|
+
text: text.slice(0, 12_000),
|
|
535
|
+
controls,
|
|
536
|
+
readyState: document.readyState,
|
|
537
|
+
truncated: truncated || text.length > 12_000,
|
|
538
|
+
};
|
|
539
|
+
},
|
|
540
|
+
);
|
|
541
|
+
|
|
542
|
+
/** Current state after standard DOM scrolling. Pointer actions also require a hit test;
|
|
543
|
+
* keyboard actions verify native focus at dispatch instead. Never dispatches input.
|
|
544
|
+
*/
|
|
545
|
+
export const checkDom = pageFunction(
|
|
546
|
+
(target: Element | string, expected: typeof Control.Type, scroll: boolean, pointer: boolean) => {
|
|
547
|
+
// A ref is looked up in this realm's registry, saving a handle round trip each way.
|
|
548
|
+
const node: unknown =
|
|
549
|
+
typeof target === "string"
|
|
550
|
+
? Reflect.get(globalThis, "@effect-agent/native-browser")?.get(target)
|
|
551
|
+
: target;
|
|
552
|
+
|
|
553
|
+
if (!(node instanceof HTMLElement) || !node.isConnected) return false;
|
|
554
|
+
|
|
555
|
+
const nameCheck:
|
|
556
|
+
| {
|
|
557
|
+
name: string;
|
|
558
|
+
originalName: string;
|
|
559
|
+
kind?: string;
|
|
560
|
+
viewport?: boolean;
|
|
561
|
+
verify: () => boolean;
|
|
562
|
+
}
|
|
563
|
+
| undefined = Reflect.get(globalThis, "@effect-agent/native-browser-name-checks")?.get(
|
|
564
|
+
expected.ref,
|
|
565
|
+
);
|
|
566
|
+
|
|
567
|
+
if (nameCheck !== undefined && (nameCheck.name !== expected.name || !nameCheck.verify()))
|
|
568
|
+
return false;
|
|
569
|
+
// Jev refs require their centers to remain in view. Do not move a visible
|
|
570
|
+
// form or popup just to prepare input; the same native hit test still applies.
|
|
571
|
+
if (scroll && !nameCheck?.viewport)
|
|
572
|
+
node.scrollIntoView({ block: "center", inline: "center", behavior: "instant" });
|
|
573
|
+
const associated = node instanceof HTMLLabelElement ? (node.control ?? node) : node;
|
|
574
|
+
const type = associated instanceof HTMLInputElement ? associated.type : "";
|
|
575
|
+
|
|
576
|
+
if (
|
|
577
|
+
associated instanceof HTMLSelectElement &&
|
|
578
|
+
expected.optionDetails !== undefined &&
|
|
579
|
+
((expected.optionCount === undefined
|
|
580
|
+
? expected.optionDetails.length
|
|
581
|
+
: expected.optionCount) !== associated.options.length ||
|
|
582
|
+
expected.optionDetails.some((option, index) => {
|
|
583
|
+
const current = associated.options.item(option.index ?? index);
|
|
584
|
+
|
|
585
|
+
return (
|
|
586
|
+
current === null ||
|
|
587
|
+
current.value !== option.value ||
|
|
588
|
+
current.label.slice(0, 300) !== option.label ||
|
|
589
|
+
current.selected !== option.selected ||
|
|
590
|
+
(current.disabled ||
|
|
591
|
+
(current.parentElement instanceof HTMLOptGroupElement &&
|
|
592
|
+
current.parentElement.disabled)) !== option.disabled
|
|
593
|
+
);
|
|
594
|
+
}))
|
|
595
|
+
)
|
|
596
|
+
return false;
|
|
597
|
+
if (node.isContentEditable && node.innerText.slice(0, 4_096) !== expected.value) return false;
|
|
598
|
+
|
|
599
|
+
const kind =
|
|
600
|
+
nameCheck?.kind ??
|
|
601
|
+
(node instanceof HTMLLabelElement &&
|
|
602
|
+
associated instanceof HTMLInputElement &&
|
|
603
|
+
["checkbox", "radio"].includes(type)
|
|
604
|
+
? type
|
|
605
|
+
: (node.getAttribute("role") ??
|
|
606
|
+
(type === "checkbox" || type === "radio"
|
|
607
|
+
? type
|
|
608
|
+
: node.tagName === "A"
|
|
609
|
+
? "link"
|
|
610
|
+
: node.tagName.toLowerCase())));
|
|
611
|
+
|
|
612
|
+
if (
|
|
613
|
+
kind !== expected.kind ||
|
|
614
|
+
associated.matches(":disabled") ||
|
|
615
|
+
associated.getAttribute("aria-disabled") === "true" ||
|
|
616
|
+
node.closest('[inert],[aria-hidden="true"]') !== null ||
|
|
617
|
+
!node.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true })
|
|
618
|
+
)
|
|
619
|
+
return false;
|
|
620
|
+
if (
|
|
621
|
+
(associated instanceof HTMLInputElement ||
|
|
622
|
+
associated instanceof HTMLTextAreaElement ||
|
|
623
|
+
associated instanceof HTMLSelectElement) &&
|
|
624
|
+
type !== "password" &&
|
|
625
|
+
associated.value.slice(0, 4_096) !== expected.value
|
|
626
|
+
)
|
|
627
|
+
return false;
|
|
628
|
+
if (
|
|
629
|
+
!(
|
|
630
|
+
associated instanceof HTMLInputElement ||
|
|
631
|
+
associated instanceof HTMLTextAreaElement ||
|
|
632
|
+
associated instanceof HTMLSelectElement
|
|
633
|
+
) &&
|
|
634
|
+
!node.isContentEditable &&
|
|
635
|
+
!node.hasAttribute("aria-label") &&
|
|
636
|
+
!node.hasAttribute("aria-labelledby") &&
|
|
637
|
+
node.innerText.replace(/\s+/g, " ").trim().slice(0, 300) !==
|
|
638
|
+
(nameCheck?.originalName ?? expected.name)
|
|
639
|
+
)
|
|
640
|
+
return false;
|
|
641
|
+
if (
|
|
642
|
+
expected.checked !== undefined &&
|
|
643
|
+
(!(associated instanceof HTMLInputElement) || associated.checked !== expected.checked)
|
|
644
|
+
)
|
|
645
|
+
return false;
|
|
646
|
+
if (
|
|
647
|
+
Object.entries(expected.attributes ?? {}).some(
|
|
648
|
+
([key, value]) => (node.getAttribute(key) ?? "").slice(0, 1_024) !== value,
|
|
649
|
+
)
|
|
650
|
+
)
|
|
651
|
+
return false;
|
|
652
|
+
const ancestors = new Set<Node>();
|
|
653
|
+
let ancestor: Node | null = node;
|
|
654
|
+
|
|
655
|
+
while (ancestor !== null) {
|
|
656
|
+
ancestors.add(ancestor);
|
|
657
|
+
ancestor = ancestor instanceof ShadowRoot ? ancestor.host : ancestor.parentNode;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const modalSelector =
|
|
661
|
+
'dialog:modal,[role="dialog"][aria-modal="true"],[role="alertdialog"][aria-modal="true"]';
|
|
662
|
+
|
|
663
|
+
const roots = new Set([document, node.getRootNode()]);
|
|
664
|
+
|
|
665
|
+
for (const root of roots) {
|
|
666
|
+
if (!(root instanceof Document || root instanceof ShadowRoot)) continue;
|
|
667
|
+
for (const modal of root.querySelectorAll(modalSelector)) {
|
|
668
|
+
if (
|
|
669
|
+
!(modal instanceof HTMLElement) ||
|
|
670
|
+
ancestors.has(modal) ||
|
|
671
|
+
!modal.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true })
|
|
672
|
+
)
|
|
673
|
+
continue;
|
|
674
|
+
const box = modal.getBoundingClientRect();
|
|
675
|
+
|
|
676
|
+
// An ARIA modal without an on-screen box, such as a collapsed cart drawer, covers
|
|
677
|
+
// nothing; the hit test below still refuses anything an open overlay obstructs.
|
|
678
|
+
if (
|
|
679
|
+
modal.matches("dialog:modal") ||
|
|
680
|
+
(box.width > 0 &&
|
|
681
|
+
box.height > 0 &&
|
|
682
|
+
box.right > 0 &&
|
|
683
|
+
box.bottom > 0 &&
|
|
684
|
+
box.left < innerWidth &&
|
|
685
|
+
box.top < innerHeight)
|
|
686
|
+
)
|
|
687
|
+
return false;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
const rect = node.getBoundingClientRect();
|
|
691
|
+
|
|
692
|
+
const x =
|
|
693
|
+
Math.max(0, rect.left) + (Math.min(innerWidth, rect.right) - Math.max(0, rect.left)) / 2;
|
|
694
|
+
|
|
695
|
+
const y =
|
|
696
|
+
Math.max(0, rect.top) + (Math.min(innerHeight, rect.bottom) - Math.max(0, rect.top)) / 2;
|
|
697
|
+
|
|
698
|
+
let hit = document.elementFromPoint(x, y);
|
|
699
|
+
|
|
700
|
+
while (hit?.shadowRoot !== null && hit?.shadowRoot !== undefined) {
|
|
701
|
+
const next = hit.shadowRoot.elementFromPoint(x, y);
|
|
702
|
+
|
|
703
|
+
if (next === null || next === hit) break;
|
|
704
|
+
hit = next;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Keyboard-only overlays can sit above sibling card content. Require that
|
|
708
|
+
// containing element to remain unobstructed, then verify native focus at dispatch.
|
|
709
|
+
// This never permits a pointer click through the overlay.
|
|
710
|
+
return rect.width > 0 &&
|
|
711
|
+
rect.height > 0 &&
|
|
712
|
+
node.isConnected &&
|
|
713
|
+
(hit === node ||
|
|
714
|
+
(hit !== null &&
|
|
715
|
+
(node.contains(hit) ||
|
|
716
|
+
(!pointer &&
|
|
717
|
+
node.tabIndex >= 0 &&
|
|
718
|
+
getComputedStyle(node).pointerEvents === "none" &&
|
|
719
|
+
node.parentElement?.contains(hit)))))
|
|
720
|
+
? { x, y }
|
|
721
|
+
: false;
|
|
722
|
+
},
|
|
723
|
+
);
|
|
724
|
+
|
|
725
|
+
/** Translate a checked child point and refuse input through hidden/covered frame owners. */
|
|
726
|
+
export const checkFrameDom = pageFunction(
|
|
727
|
+
async (node: Element, point: { x: number; y: number }, scroll: boolean) => {
|
|
728
|
+
if (
|
|
729
|
+
!(node instanceof HTMLElement) ||
|
|
730
|
+
!node.isConnected ||
|
|
731
|
+
node.closest('[inert],[aria-hidden="true"]') !== null ||
|
|
732
|
+
!node.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true })
|
|
733
|
+
)
|
|
734
|
+
return false;
|
|
735
|
+
if (scroll) {
|
|
736
|
+
node.scrollIntoView({ block: "center", inline: "center", behavior: "instant" });
|
|
737
|
+
// Input into an out-of-process frame is routed by the compositor's last frame, not by
|
|
738
|
+
// live layout. Let a frame with the new scroll position render before input follows.
|
|
739
|
+
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
740
|
+
}
|
|
741
|
+
const style = getComputedStyle(node);
|
|
742
|
+
|
|
743
|
+
// The bounding rect already includes translations. Anything that scales, rotates, skews or
|
|
744
|
+
// projects the frame needs a native geometry adapter; never guess.
|
|
745
|
+
let ancestor: Element | null = node;
|
|
746
|
+
|
|
747
|
+
while (ancestor !== null) {
|
|
748
|
+
const current = getComputedStyle(ancestor);
|
|
749
|
+
|
|
750
|
+
const matrix = new DOMMatrixReadOnly(
|
|
751
|
+
current.transform === "none" ? undefined : current.transform,
|
|
752
|
+
);
|
|
753
|
+
|
|
754
|
+
if (
|
|
755
|
+
!(
|
|
756
|
+
matrix.m11 === 1 &&
|
|
757
|
+
matrix.m12 === 0 &&
|
|
758
|
+
matrix.m13 === 0 &&
|
|
759
|
+
matrix.m14 === 0 &&
|
|
760
|
+
matrix.m21 === 0 &&
|
|
761
|
+
matrix.m22 === 1 &&
|
|
762
|
+
matrix.m23 === 0 &&
|
|
763
|
+
matrix.m24 === 0 &&
|
|
764
|
+
matrix.m31 === 0 &&
|
|
765
|
+
matrix.m32 === 0 &&
|
|
766
|
+
matrix.m33 === 1 &&
|
|
767
|
+
matrix.m34 === 0 &&
|
|
768
|
+
matrix.m44 === 1
|
|
769
|
+
) ||
|
|
770
|
+
current.perspective !== "none" ||
|
|
771
|
+
// Individual scale, rotate and motion-path properties apply outside `transform`.
|
|
772
|
+
current.scale !== "none" ||
|
|
773
|
+
current.rotate !== "none" ||
|
|
774
|
+
current.offsetPath !== "none" ||
|
|
775
|
+
(current.zoom !== "1" && current.zoom !== "normal")
|
|
776
|
+
)
|
|
777
|
+
return false;
|
|
778
|
+
ancestor = ancestor.parentElement;
|
|
779
|
+
}
|
|
780
|
+
const rect = node.getBoundingClientRect();
|
|
781
|
+
|
|
782
|
+
const x =
|
|
783
|
+
rect.left + parseFloat(style.borderLeftWidth) + parseFloat(style.paddingLeft) + point.x;
|
|
784
|
+
|
|
785
|
+
const y = rect.top + parseFloat(style.borderTopWidth) + parseFloat(style.paddingTop) + point.y;
|
|
786
|
+
const hit = document.elementFromPoint(x, y);
|
|
787
|
+
|
|
788
|
+
return hit === node ? { x, y } : false;
|
|
789
|
+
},
|
|
790
|
+
);
|
|
791
|
+
|
|
792
|
+
export const waitDom = pageFunction((selector: string, state: string, text: string | undefined) => {
|
|
793
|
+
const roots: Array<Document | ShadowRoot> = [document];
|
|
794
|
+
const expectedText = text?.replace(/\s+/g, " ").trim();
|
|
795
|
+
let scanned = 0;
|
|
796
|
+
|
|
797
|
+
for (let index = 0; index < roots.length; index++) {
|
|
798
|
+
const root = roots[index];
|
|
799
|
+
|
|
800
|
+
if (root === undefined) break;
|
|
801
|
+
|
|
802
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, {
|
|
803
|
+
acceptNode(node) {
|
|
804
|
+
if (!(node instanceof Element)) return NodeFilter.FILTER_ACCEPT;
|
|
805
|
+
const style = getComputedStyle(node);
|
|
806
|
+
|
|
807
|
+
return node.matches('script,style,template,option,optgroup,[inert],[aria-hidden="true"]') ||
|
|
808
|
+
style.display === "none" ||
|
|
809
|
+
style.opacity === "0" ||
|
|
810
|
+
style.contentVisibility === "hidden"
|
|
811
|
+
? NodeFilter.FILTER_REJECT
|
|
812
|
+
: NodeFilter.FILTER_ACCEPT;
|
|
813
|
+
},
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
let node = walker.nextNode();
|
|
817
|
+
|
|
818
|
+
while (node !== null) {
|
|
819
|
+
if (++scanned > 10_000) return false;
|
|
820
|
+
if (node instanceof Element) {
|
|
821
|
+
if (node.shadowRoot !== null) roots.push(node.shadowRoot);
|
|
822
|
+
if (
|
|
823
|
+
node instanceof HTMLElement &&
|
|
824
|
+
node.matches(selector) &&
|
|
825
|
+
node.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true }) &&
|
|
826
|
+
(expectedText === undefined ||
|
|
827
|
+
node.innerText.replace(/\s+/g, " ").trim().includes(expectedText))
|
|
828
|
+
) {
|
|
829
|
+
if (state === "hidden") return false;
|
|
830
|
+
if (
|
|
831
|
+
state === "visible" ||
|
|
832
|
+
(state === "enabled" &&
|
|
833
|
+
!node.matches(":disabled") &&
|
|
834
|
+
node.getAttribute("aria-disabled") !== "true") ||
|
|
835
|
+
(state === "text" && text !== undefined)
|
|
836
|
+
)
|
|
837
|
+
return true;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
node = walker.nextNode();
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
return state === "hidden";
|
|
845
|
+
});
|