@page-agent/page-controller 0.0.6
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/LICENSE +21 -0
- package/dist/lib/PageController.d.ts +255 -0
- package/dist/lib/page-controller.js +1901 -0
- package/dist/lib/page-controller.js.map +1 -0
- package/package.json +40 -0
|
@@ -0,0 +1,1901 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
+
async function waitFor(seconds) {
|
|
4
|
+
await new Promise((resolve) => setTimeout(resolve, seconds * 1e3));
|
|
5
|
+
}
|
|
6
|
+
__name(waitFor, "waitFor");
|
|
7
|
+
async function movePointerToElement(element) {
|
|
8
|
+
const rect = element.getBoundingClientRect();
|
|
9
|
+
const x = rect.left + rect.width / 2;
|
|
10
|
+
const y = rect.top + rect.height / 2;
|
|
11
|
+
window.dispatchEvent(new CustomEvent("PageAgent::MovePointerTo", { detail: { x, y } }));
|
|
12
|
+
await waitFor(0.3);
|
|
13
|
+
}
|
|
14
|
+
__name(movePointerToElement, "movePointerToElement");
|
|
15
|
+
function getElementByIndex(selectorMap, index) {
|
|
16
|
+
const interactiveNode = selectorMap.get(index);
|
|
17
|
+
if (!interactiveNode) {
|
|
18
|
+
throw new Error(`No interactive element found at index ${index}`);
|
|
19
|
+
}
|
|
20
|
+
const element = interactiveNode.ref;
|
|
21
|
+
if (!element) {
|
|
22
|
+
throw new Error(`Element at index ${index} does not have a reference`);
|
|
23
|
+
}
|
|
24
|
+
if (!(element instanceof HTMLElement)) {
|
|
25
|
+
throw new Error(`Element at index ${index} is not an HTMLElement`);
|
|
26
|
+
}
|
|
27
|
+
return element;
|
|
28
|
+
}
|
|
29
|
+
__name(getElementByIndex, "getElementByIndex");
|
|
30
|
+
let lastClickedElement = null;
|
|
31
|
+
function blurLastClickedElement() {
|
|
32
|
+
if (lastClickedElement) {
|
|
33
|
+
lastClickedElement.blur();
|
|
34
|
+
lastClickedElement.dispatchEvent(
|
|
35
|
+
new MouseEvent("mouseout", { bubbles: true, cancelable: true })
|
|
36
|
+
);
|
|
37
|
+
lastClickedElement = null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
__name(blurLastClickedElement, "blurLastClickedElement");
|
|
41
|
+
async function clickElement(element) {
|
|
42
|
+
blurLastClickedElement();
|
|
43
|
+
lastClickedElement = element;
|
|
44
|
+
await scrollIntoViewIfNeeded(element);
|
|
45
|
+
await movePointerToElement(element);
|
|
46
|
+
window.dispatchEvent(new CustomEvent("PageAgent::ClickPointer"));
|
|
47
|
+
await waitFor(0.1);
|
|
48
|
+
element.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true, cancelable: true }));
|
|
49
|
+
element.dispatchEvent(new MouseEvent("mouseover", { bubbles: true, cancelable: true }));
|
|
50
|
+
element.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, cancelable: true }));
|
|
51
|
+
element.focus();
|
|
52
|
+
element.dispatchEvent(new MouseEvent("mouseup", { bubbles: true, cancelable: true }));
|
|
53
|
+
element.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
|
|
54
|
+
await waitFor(0.1);
|
|
55
|
+
}
|
|
56
|
+
__name(clickElement, "clickElement");
|
|
57
|
+
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
|
58
|
+
window.HTMLInputElement.prototype,
|
|
59
|
+
"value"
|
|
60
|
+
).set;
|
|
61
|
+
const nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(
|
|
62
|
+
window.HTMLTextAreaElement.prototype,
|
|
63
|
+
"value"
|
|
64
|
+
).set;
|
|
65
|
+
async function inputTextElement(element, text) {
|
|
66
|
+
if (!(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement)) {
|
|
67
|
+
throw new Error("Element is not an input or textarea");
|
|
68
|
+
}
|
|
69
|
+
await clickElement(element);
|
|
70
|
+
if (element instanceof HTMLTextAreaElement) {
|
|
71
|
+
nativeTextAreaValueSetter.call(element, text);
|
|
72
|
+
} else {
|
|
73
|
+
nativeInputValueSetter.call(element, text);
|
|
74
|
+
}
|
|
75
|
+
const inputEvent = new Event("input", { bubbles: true });
|
|
76
|
+
element.dispatchEvent(inputEvent);
|
|
77
|
+
await waitFor(0.1);
|
|
78
|
+
blurLastClickedElement();
|
|
79
|
+
}
|
|
80
|
+
__name(inputTextElement, "inputTextElement");
|
|
81
|
+
async function selectOptionElement(selectElement, optionText) {
|
|
82
|
+
if (!(selectElement instanceof HTMLSelectElement)) {
|
|
83
|
+
throw new Error("Element is not a select element");
|
|
84
|
+
}
|
|
85
|
+
const options = Array.from(selectElement.options);
|
|
86
|
+
const option = options.find((opt) => opt.textContent?.trim() === optionText.trim());
|
|
87
|
+
if (!option) {
|
|
88
|
+
throw new Error(`Option with text "${optionText}" not found in select element`);
|
|
89
|
+
}
|
|
90
|
+
selectElement.value = option.value;
|
|
91
|
+
selectElement.dispatchEvent(new Event("change", { bubbles: true }));
|
|
92
|
+
await waitFor(0.1);
|
|
93
|
+
}
|
|
94
|
+
__name(selectOptionElement, "selectOptionElement");
|
|
95
|
+
async function scrollIntoViewIfNeeded(element) {
|
|
96
|
+
const el = element;
|
|
97
|
+
if (el.scrollIntoViewIfNeeded) {
|
|
98
|
+
el.scrollIntoViewIfNeeded();
|
|
99
|
+
} else {
|
|
100
|
+
el.scrollIntoView({ behavior: "auto", block: "center", inline: "nearest" });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
__name(scrollIntoViewIfNeeded, "scrollIntoViewIfNeeded");
|
|
104
|
+
async function scrollVertically(down, scroll_amount, element) {
|
|
105
|
+
if (element) {
|
|
106
|
+
const targetElement = element;
|
|
107
|
+
console.log(
|
|
108
|
+
"[SCROLL DEBUG] Starting direct container scroll for element:",
|
|
109
|
+
targetElement.tagName
|
|
110
|
+
);
|
|
111
|
+
let currentElement = targetElement;
|
|
112
|
+
let scrollSuccess = false;
|
|
113
|
+
let scrolledElement = null;
|
|
114
|
+
let scrollDelta = 0;
|
|
115
|
+
let attempts = 0;
|
|
116
|
+
const dy2 = scroll_amount;
|
|
117
|
+
while (currentElement && attempts < 10) {
|
|
118
|
+
const computedStyle = window.getComputedStyle(currentElement);
|
|
119
|
+
const hasScrollableY = /(auto|scroll|overlay)/.test(computedStyle.overflowY);
|
|
120
|
+
const canScrollVertically = currentElement.scrollHeight > currentElement.clientHeight;
|
|
121
|
+
console.log(
|
|
122
|
+
"[SCROLL DEBUG] Checking element:",
|
|
123
|
+
currentElement.tagName,
|
|
124
|
+
"hasScrollableY:",
|
|
125
|
+
hasScrollableY,
|
|
126
|
+
"canScrollVertically:",
|
|
127
|
+
canScrollVertically,
|
|
128
|
+
"scrollHeight:",
|
|
129
|
+
currentElement.scrollHeight,
|
|
130
|
+
"clientHeight:",
|
|
131
|
+
currentElement.clientHeight
|
|
132
|
+
);
|
|
133
|
+
if (hasScrollableY && canScrollVertically) {
|
|
134
|
+
const beforeScroll = currentElement.scrollTop;
|
|
135
|
+
const maxScroll = currentElement.scrollHeight - currentElement.clientHeight;
|
|
136
|
+
let scrollAmount = dy2 / 3;
|
|
137
|
+
if (scrollAmount > 0) {
|
|
138
|
+
scrollAmount = Math.min(scrollAmount, maxScroll - beforeScroll);
|
|
139
|
+
} else {
|
|
140
|
+
scrollAmount = Math.max(scrollAmount, -beforeScroll);
|
|
141
|
+
}
|
|
142
|
+
currentElement.scrollTop = beforeScroll + scrollAmount;
|
|
143
|
+
const afterScroll = currentElement.scrollTop;
|
|
144
|
+
const actualScrollDelta = afterScroll - beforeScroll;
|
|
145
|
+
console.log(
|
|
146
|
+
"[SCROLL DEBUG] Scroll attempt:",
|
|
147
|
+
currentElement.tagName,
|
|
148
|
+
"before:",
|
|
149
|
+
beforeScroll,
|
|
150
|
+
"after:",
|
|
151
|
+
afterScroll,
|
|
152
|
+
"delta:",
|
|
153
|
+
actualScrollDelta
|
|
154
|
+
);
|
|
155
|
+
if (Math.abs(actualScrollDelta) > 0.5) {
|
|
156
|
+
scrollSuccess = true;
|
|
157
|
+
scrolledElement = currentElement;
|
|
158
|
+
scrollDelta = actualScrollDelta;
|
|
159
|
+
console.log(
|
|
160
|
+
"[SCROLL DEBUG] Successfully scrolled container:",
|
|
161
|
+
currentElement.tagName,
|
|
162
|
+
"delta:",
|
|
163
|
+
actualScrollDelta
|
|
164
|
+
);
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (currentElement === document.body || currentElement === document.documentElement) {
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
currentElement = currentElement.parentElement;
|
|
172
|
+
attempts++;
|
|
173
|
+
}
|
|
174
|
+
if (scrollSuccess) {
|
|
175
|
+
return `Scrolled container (${scrolledElement?.tagName}) by ${scrollDelta}px`;
|
|
176
|
+
} else {
|
|
177
|
+
return `No scrollable container found for element (${targetElement.tagName})`;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const dy = scroll_amount;
|
|
181
|
+
const bigEnough = /* @__PURE__ */ __name((el2) => el2.clientHeight >= window.innerHeight * 0.5, "bigEnough");
|
|
182
|
+
const canScroll = /* @__PURE__ */ __name((el2) => el2 && /(auto|scroll|overlay)/.test(getComputedStyle(el2).overflowY) && el2.scrollHeight > el2.clientHeight && bigEnough(el2), "canScroll");
|
|
183
|
+
let el = document.activeElement;
|
|
184
|
+
while (el && !canScroll(el) && el !== document.body) el = el.parentElement;
|
|
185
|
+
el = canScroll(el) ? el : Array.from(document.querySelectorAll("*")).find(canScroll) || document.scrollingElement || document.documentElement;
|
|
186
|
+
if (el === document.scrollingElement || el === document.documentElement || el === document.body) {
|
|
187
|
+
window.scrollBy(0, dy);
|
|
188
|
+
return `✅ Scrolled page by ${dy}px.`;
|
|
189
|
+
} else {
|
|
190
|
+
el.scrollBy({ top: dy, behavior: "smooth" });
|
|
191
|
+
await waitFor(0.1);
|
|
192
|
+
return `✅ Scrolled container (${el.tagName}) by ${dy}px.`;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
__name(scrollVertically, "scrollVertically");
|
|
196
|
+
async function scrollHorizontally(right, scroll_amount, element) {
|
|
197
|
+
if (element) {
|
|
198
|
+
const targetElement = element;
|
|
199
|
+
console.log(
|
|
200
|
+
"[SCROLL DEBUG] Starting direct container scroll for element:",
|
|
201
|
+
targetElement.tagName
|
|
202
|
+
);
|
|
203
|
+
let currentElement = targetElement;
|
|
204
|
+
let scrollSuccess = false;
|
|
205
|
+
let scrolledElement = null;
|
|
206
|
+
let scrollDelta = 0;
|
|
207
|
+
let attempts = 0;
|
|
208
|
+
const dx2 = right ? scroll_amount : -scroll_amount;
|
|
209
|
+
while (currentElement && attempts < 10) {
|
|
210
|
+
const computedStyle = window.getComputedStyle(currentElement);
|
|
211
|
+
const hasScrollableX = /(auto|scroll|overlay)/.test(computedStyle.overflowX);
|
|
212
|
+
const canScrollHorizontally = currentElement.scrollWidth > currentElement.clientWidth;
|
|
213
|
+
console.log(
|
|
214
|
+
"[SCROLL DEBUG] Checking element:",
|
|
215
|
+
currentElement.tagName,
|
|
216
|
+
"hasScrollableX:",
|
|
217
|
+
hasScrollableX,
|
|
218
|
+
"canScrollHorizontally:",
|
|
219
|
+
canScrollHorizontally,
|
|
220
|
+
"scrollWidth:",
|
|
221
|
+
currentElement.scrollWidth,
|
|
222
|
+
"clientWidth:",
|
|
223
|
+
currentElement.clientWidth
|
|
224
|
+
);
|
|
225
|
+
if (hasScrollableX && canScrollHorizontally) {
|
|
226
|
+
const beforeScroll = currentElement.scrollLeft;
|
|
227
|
+
const maxScroll = currentElement.scrollWidth - currentElement.clientWidth;
|
|
228
|
+
let scrollAmount = dx2 / 3;
|
|
229
|
+
if (scrollAmount > 0) {
|
|
230
|
+
scrollAmount = Math.min(scrollAmount, maxScroll - beforeScroll);
|
|
231
|
+
} else {
|
|
232
|
+
scrollAmount = Math.max(scrollAmount, -beforeScroll);
|
|
233
|
+
}
|
|
234
|
+
currentElement.scrollLeft = beforeScroll + scrollAmount;
|
|
235
|
+
const afterScroll = currentElement.scrollLeft;
|
|
236
|
+
const actualScrollDelta = afterScroll - beforeScroll;
|
|
237
|
+
console.log(
|
|
238
|
+
"[SCROLL DEBUG] Scroll attempt:",
|
|
239
|
+
currentElement.tagName,
|
|
240
|
+
"before:",
|
|
241
|
+
beforeScroll,
|
|
242
|
+
"after:",
|
|
243
|
+
afterScroll,
|
|
244
|
+
"delta:",
|
|
245
|
+
actualScrollDelta
|
|
246
|
+
);
|
|
247
|
+
if (Math.abs(actualScrollDelta) > 0.5) {
|
|
248
|
+
scrollSuccess = true;
|
|
249
|
+
scrolledElement = currentElement;
|
|
250
|
+
scrollDelta = actualScrollDelta;
|
|
251
|
+
console.log(
|
|
252
|
+
"[SCROLL DEBUG] Successfully scrolled container:",
|
|
253
|
+
currentElement.tagName,
|
|
254
|
+
"delta:",
|
|
255
|
+
actualScrollDelta
|
|
256
|
+
);
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (currentElement === document.body || currentElement === document.documentElement) {
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
currentElement = currentElement.parentElement;
|
|
264
|
+
attempts++;
|
|
265
|
+
}
|
|
266
|
+
if (scrollSuccess) {
|
|
267
|
+
return `Scrolled container (${scrolledElement?.tagName}) horizontally by ${scrollDelta}px`;
|
|
268
|
+
} else {
|
|
269
|
+
return `No horizontally scrollable container found for element (${targetElement.tagName})`;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const dx = right ? scroll_amount : -scroll_amount;
|
|
273
|
+
const bigEnough = /* @__PURE__ */ __name((el2) => el2.clientWidth >= window.innerWidth * 0.5, "bigEnough");
|
|
274
|
+
const canScroll = /* @__PURE__ */ __name((el2) => el2 && /(auto|scroll|overlay)/.test(getComputedStyle(el2).overflowX) && el2.scrollWidth > el2.clientWidth && bigEnough(el2), "canScroll");
|
|
275
|
+
let el = document.activeElement;
|
|
276
|
+
while (el && !canScroll(el) && el !== document.body) el = el.parentElement;
|
|
277
|
+
el = canScroll(el) ? el : Array.from(document.querySelectorAll("*")).find(canScroll) || document.scrollingElement || document.documentElement;
|
|
278
|
+
if (el === document.scrollingElement || el === document.documentElement || el === document.body) {
|
|
279
|
+
window.scrollBy(dx, 0);
|
|
280
|
+
return `✅ Scrolled page horizontally by ${dx}px`;
|
|
281
|
+
} else {
|
|
282
|
+
el.scrollBy({ left: dx, behavior: "smooth" });
|
|
283
|
+
await waitFor(0.1);
|
|
284
|
+
return `✅ Scrolled container (${el.tagName}) horizontally by ${dx}px`;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
__name(scrollHorizontally, "scrollHorizontally");
|
|
288
|
+
const VIEWPORT_EXPANSION = -1;
|
|
289
|
+
const domTree = /* @__PURE__ */ __name((args = {
|
|
290
|
+
doHighlightElements: true,
|
|
291
|
+
focusHighlightIndex: -1,
|
|
292
|
+
viewportExpansion: 0,
|
|
293
|
+
debugMode: false,
|
|
294
|
+
/**
|
|
295
|
+
* @edit
|
|
296
|
+
*/
|
|
297
|
+
/** @type {Element[]} */
|
|
298
|
+
interactiveBlacklist: [],
|
|
299
|
+
/** @type {Element[]} */
|
|
300
|
+
interactiveWhitelist: [],
|
|
301
|
+
highlightOpacity: 0.1,
|
|
302
|
+
highlightLabelOpacity: 0.5
|
|
303
|
+
}) => {
|
|
304
|
+
const { interactiveBlacklist, interactiveWhitelist, highlightOpacity, highlightLabelOpacity } = args;
|
|
305
|
+
const { doHighlightElements, focusHighlightIndex, viewportExpansion, debugMode } = args;
|
|
306
|
+
let highlightIndex = 0;
|
|
307
|
+
const extraData = /* @__PURE__ */ new WeakMap();
|
|
308
|
+
function addExtraData(element, data) {
|
|
309
|
+
if (!element || element.nodeType !== Node.ELEMENT_NODE) return;
|
|
310
|
+
extraData.set(element, { ...extraData.get(element), ...data });
|
|
311
|
+
}
|
|
312
|
+
__name(addExtraData, "addExtraData");
|
|
313
|
+
const DOM_CACHE = {
|
|
314
|
+
boundingRects: /* @__PURE__ */ new WeakMap(),
|
|
315
|
+
clientRects: /* @__PURE__ */ new WeakMap(),
|
|
316
|
+
computedStyles: /* @__PURE__ */ new WeakMap(),
|
|
317
|
+
clearCache: /* @__PURE__ */ __name(() => {
|
|
318
|
+
DOM_CACHE.boundingRects = /* @__PURE__ */ new WeakMap();
|
|
319
|
+
DOM_CACHE.clientRects = /* @__PURE__ */ new WeakMap();
|
|
320
|
+
DOM_CACHE.computedStyles = /* @__PURE__ */ new WeakMap();
|
|
321
|
+
}, "clearCache")
|
|
322
|
+
};
|
|
323
|
+
function getCachedBoundingRect(element) {
|
|
324
|
+
if (!element) return null;
|
|
325
|
+
if (DOM_CACHE.boundingRects.has(element)) {
|
|
326
|
+
return DOM_CACHE.boundingRects.get(element);
|
|
327
|
+
}
|
|
328
|
+
const rect = element.getBoundingClientRect();
|
|
329
|
+
if (rect) {
|
|
330
|
+
DOM_CACHE.boundingRects.set(element, rect);
|
|
331
|
+
}
|
|
332
|
+
return rect;
|
|
333
|
+
}
|
|
334
|
+
__name(getCachedBoundingRect, "getCachedBoundingRect");
|
|
335
|
+
function getCachedComputedStyle(element) {
|
|
336
|
+
if (!element) return null;
|
|
337
|
+
if (DOM_CACHE.computedStyles.has(element)) {
|
|
338
|
+
return DOM_CACHE.computedStyles.get(element);
|
|
339
|
+
}
|
|
340
|
+
const style = window.getComputedStyle(element);
|
|
341
|
+
if (style) {
|
|
342
|
+
DOM_CACHE.computedStyles.set(element, style);
|
|
343
|
+
}
|
|
344
|
+
return style;
|
|
345
|
+
}
|
|
346
|
+
__name(getCachedComputedStyle, "getCachedComputedStyle");
|
|
347
|
+
function getCachedClientRects(element) {
|
|
348
|
+
if (!element) return null;
|
|
349
|
+
if (DOM_CACHE.clientRects.has(element)) {
|
|
350
|
+
return DOM_CACHE.clientRects.get(element);
|
|
351
|
+
}
|
|
352
|
+
const rects = element.getClientRects();
|
|
353
|
+
if (rects) {
|
|
354
|
+
DOM_CACHE.clientRects.set(element, rects);
|
|
355
|
+
}
|
|
356
|
+
return rects;
|
|
357
|
+
}
|
|
358
|
+
__name(getCachedClientRects, "getCachedClientRects");
|
|
359
|
+
const DOM_HASH_MAP = {};
|
|
360
|
+
const ID = { current: 0 };
|
|
361
|
+
const HIGHLIGHT_CONTAINER_ID = "playwright-highlight-container";
|
|
362
|
+
function highlightElement(element, index, parentIframe = null) {
|
|
363
|
+
if (!element) return index;
|
|
364
|
+
const overlays = [];
|
|
365
|
+
let label = null;
|
|
366
|
+
let labelWidth = 20;
|
|
367
|
+
let labelHeight = 16;
|
|
368
|
+
let cleanupFn = null;
|
|
369
|
+
try {
|
|
370
|
+
let container = document.getElementById(HIGHLIGHT_CONTAINER_ID);
|
|
371
|
+
if (!container) {
|
|
372
|
+
container = document.createElement("div");
|
|
373
|
+
container.id = HIGHLIGHT_CONTAINER_ID;
|
|
374
|
+
container.style.position = "fixed";
|
|
375
|
+
container.style.pointerEvents = "none";
|
|
376
|
+
container.style.top = "0";
|
|
377
|
+
container.style.left = "0";
|
|
378
|
+
container.style.width = "100%";
|
|
379
|
+
container.style.height = "100%";
|
|
380
|
+
container.style.zIndex = "2147483640";
|
|
381
|
+
container.style.backgroundColor = "transparent";
|
|
382
|
+
document.body.appendChild(container);
|
|
383
|
+
}
|
|
384
|
+
const rects = element.getClientRects();
|
|
385
|
+
if (!rects || rects.length === 0) return index;
|
|
386
|
+
const colors = [
|
|
387
|
+
"#FF0000",
|
|
388
|
+
"#00FF00",
|
|
389
|
+
"#0000FF",
|
|
390
|
+
"#FFA500",
|
|
391
|
+
"#800080",
|
|
392
|
+
"#008080",
|
|
393
|
+
"#FF69B4",
|
|
394
|
+
"#4B0082",
|
|
395
|
+
"#FF4500",
|
|
396
|
+
"#2E8B57",
|
|
397
|
+
"#DC143C",
|
|
398
|
+
"#4682B4"
|
|
399
|
+
];
|
|
400
|
+
const colorIndex = index % colors.length;
|
|
401
|
+
let baseColor = colors[colorIndex];
|
|
402
|
+
const backgroundColor = baseColor + Math.floor(highlightOpacity * 255).toString(16).padStart(2, "0");
|
|
403
|
+
baseColor = baseColor + Math.floor(highlightLabelOpacity * 255).toString(16).padStart(2, "0");
|
|
404
|
+
let iframeOffset = { x: 0, y: 0 };
|
|
405
|
+
if (parentIframe) {
|
|
406
|
+
const iframeRect = parentIframe.getBoundingClientRect();
|
|
407
|
+
iframeOffset.x = iframeRect.left;
|
|
408
|
+
iframeOffset.y = iframeRect.top;
|
|
409
|
+
}
|
|
410
|
+
const fragment = document.createDocumentFragment();
|
|
411
|
+
for (const rect of rects) {
|
|
412
|
+
if (rect.width === 0 || rect.height === 0) continue;
|
|
413
|
+
const overlay = document.createElement("div");
|
|
414
|
+
overlay.style.position = "fixed";
|
|
415
|
+
overlay.style.border = `2px solid ${baseColor}`;
|
|
416
|
+
overlay.style.backgroundColor = backgroundColor;
|
|
417
|
+
overlay.style.pointerEvents = "none";
|
|
418
|
+
overlay.style.boxSizing = "border-box";
|
|
419
|
+
const top = rect.top + iframeOffset.y;
|
|
420
|
+
const left = rect.left + iframeOffset.x;
|
|
421
|
+
overlay.style.top = `${top}px`;
|
|
422
|
+
overlay.style.left = `${left}px`;
|
|
423
|
+
overlay.style.width = `${rect.width}px`;
|
|
424
|
+
overlay.style.height = `${rect.height}px`;
|
|
425
|
+
fragment.appendChild(overlay);
|
|
426
|
+
overlays.push({ element: overlay, initialRect: rect });
|
|
427
|
+
}
|
|
428
|
+
const firstRect = rects[0];
|
|
429
|
+
label = document.createElement("div");
|
|
430
|
+
label.className = "playwright-highlight-label";
|
|
431
|
+
label.style.position = "fixed";
|
|
432
|
+
label.style.background = baseColor;
|
|
433
|
+
label.style.color = "white";
|
|
434
|
+
label.style.padding = "1px 4px";
|
|
435
|
+
label.style.borderRadius = "4px";
|
|
436
|
+
label.style.fontSize = `${Math.min(12, Math.max(8, firstRect.height / 2))}px`;
|
|
437
|
+
label.textContent = index.toString();
|
|
438
|
+
labelWidth = label.offsetWidth > 0 ? label.offsetWidth : labelWidth;
|
|
439
|
+
labelHeight = label.offsetHeight > 0 ? label.offsetHeight : labelHeight;
|
|
440
|
+
const firstRectTop = firstRect.top + iframeOffset.y;
|
|
441
|
+
const firstRectLeft = firstRect.left + iframeOffset.x;
|
|
442
|
+
let labelTop = firstRectTop + 2;
|
|
443
|
+
let labelLeft = firstRectLeft + firstRect.width - labelWidth - 2;
|
|
444
|
+
if (firstRect.width < labelWidth + 4 || firstRect.height < labelHeight + 4) {
|
|
445
|
+
labelTop = firstRectTop - labelHeight - 2;
|
|
446
|
+
labelLeft = firstRectLeft + firstRect.width - labelWidth;
|
|
447
|
+
if (labelLeft < iframeOffset.x) labelLeft = firstRectLeft;
|
|
448
|
+
}
|
|
449
|
+
labelTop = Math.max(0, Math.min(labelTop, window.innerHeight - labelHeight));
|
|
450
|
+
labelLeft = Math.max(0, Math.min(labelLeft, window.innerWidth - labelWidth));
|
|
451
|
+
label.style.top = `${labelTop}px`;
|
|
452
|
+
label.style.left = `${labelLeft}px`;
|
|
453
|
+
fragment.appendChild(label);
|
|
454
|
+
const updatePositions = /* @__PURE__ */ __name(() => {
|
|
455
|
+
const newRects = element.getClientRects();
|
|
456
|
+
let newIframeOffset = { x: 0, y: 0 };
|
|
457
|
+
if (parentIframe) {
|
|
458
|
+
const iframeRect = parentIframe.getBoundingClientRect();
|
|
459
|
+
newIframeOffset.x = iframeRect.left;
|
|
460
|
+
newIframeOffset.y = iframeRect.top;
|
|
461
|
+
}
|
|
462
|
+
overlays.forEach((overlayData, i) => {
|
|
463
|
+
if (i < newRects.length) {
|
|
464
|
+
const newRect = newRects[i];
|
|
465
|
+
const newTop = newRect.top + newIframeOffset.y;
|
|
466
|
+
const newLeft = newRect.left + newIframeOffset.x;
|
|
467
|
+
overlayData.element.style.top = `${newTop}px`;
|
|
468
|
+
overlayData.element.style.left = `${newLeft}px`;
|
|
469
|
+
overlayData.element.style.width = `${newRect.width}px`;
|
|
470
|
+
overlayData.element.style.height = `${newRect.height}px`;
|
|
471
|
+
overlayData.element.style.display = newRect.width === 0 || newRect.height === 0 ? "none" : "block";
|
|
472
|
+
} else {
|
|
473
|
+
overlayData.element.style.display = "none";
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
if (newRects.length < overlays.length) {
|
|
477
|
+
for (let i = newRects.length; i < overlays.length; i++) {
|
|
478
|
+
overlays[i].element.style.display = "none";
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (label && newRects.length > 0) {
|
|
482
|
+
const firstNewRect = newRects[0];
|
|
483
|
+
const firstNewRectTop = firstNewRect.top + newIframeOffset.y;
|
|
484
|
+
const firstNewRectLeft = firstNewRect.left + newIframeOffset.x;
|
|
485
|
+
let newLabelTop = firstNewRectTop + 2;
|
|
486
|
+
let newLabelLeft = firstNewRectLeft + firstNewRect.width - labelWidth - 2;
|
|
487
|
+
if (firstNewRect.width < labelWidth + 4 || firstNewRect.height < labelHeight + 4) {
|
|
488
|
+
newLabelTop = firstNewRectTop - labelHeight - 2;
|
|
489
|
+
newLabelLeft = firstNewRectLeft + firstNewRect.width - labelWidth;
|
|
490
|
+
if (newLabelLeft < newIframeOffset.x) newLabelLeft = firstNewRectLeft;
|
|
491
|
+
}
|
|
492
|
+
newLabelTop = Math.max(0, Math.min(newLabelTop, window.innerHeight - labelHeight));
|
|
493
|
+
newLabelLeft = Math.max(0, Math.min(newLabelLeft, window.innerWidth - labelWidth));
|
|
494
|
+
label.style.top = `${newLabelTop}px`;
|
|
495
|
+
label.style.left = `${newLabelLeft}px`;
|
|
496
|
+
label.style.display = "block";
|
|
497
|
+
} else if (label) {
|
|
498
|
+
label.style.display = "none";
|
|
499
|
+
}
|
|
500
|
+
}, "updatePositions");
|
|
501
|
+
const throttleFunction = /* @__PURE__ */ __name((func, delay) => {
|
|
502
|
+
let lastCall = 0;
|
|
503
|
+
return (...args2) => {
|
|
504
|
+
const now = performance.now();
|
|
505
|
+
if (now - lastCall < delay) return;
|
|
506
|
+
lastCall = now;
|
|
507
|
+
return func(...args2);
|
|
508
|
+
};
|
|
509
|
+
}, "throttleFunction");
|
|
510
|
+
const throttledUpdatePositions = throttleFunction(updatePositions, 16);
|
|
511
|
+
window.addEventListener("scroll", throttledUpdatePositions, true);
|
|
512
|
+
window.addEventListener("resize", throttledUpdatePositions);
|
|
513
|
+
cleanupFn = /* @__PURE__ */ __name(() => {
|
|
514
|
+
window.removeEventListener("scroll", throttledUpdatePositions, true);
|
|
515
|
+
window.removeEventListener("resize", throttledUpdatePositions);
|
|
516
|
+
overlays.forEach((overlay) => overlay.element.remove());
|
|
517
|
+
if (label) label.remove();
|
|
518
|
+
}, "cleanupFn");
|
|
519
|
+
container.appendChild(fragment);
|
|
520
|
+
return index + 1;
|
|
521
|
+
} finally {
|
|
522
|
+
if (cleanupFn) {
|
|
523
|
+
(window._highlightCleanupFunctions = window._highlightCleanupFunctions || []).push(
|
|
524
|
+
cleanupFn
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
__name(highlightElement, "highlightElement");
|
|
530
|
+
function isScrollableElement(element) {
|
|
531
|
+
if (!element || element.nodeType !== Node.ELEMENT_NODE) {
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
const style = getCachedComputedStyle(element);
|
|
535
|
+
if (!style) return null;
|
|
536
|
+
const display = style.display;
|
|
537
|
+
if (display === "inline" || display === "inline-block") {
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
const overflowX = style.overflowX;
|
|
541
|
+
const overflowY = style.overflowY;
|
|
542
|
+
const scrollableX = overflowX === "auto" || overflowX === "scroll";
|
|
543
|
+
const scrollableY = overflowY === "auto" || overflowY === "scroll";
|
|
544
|
+
if (!scrollableX && !scrollableY) {
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
const scrollWidth = element.scrollWidth - element.clientWidth;
|
|
548
|
+
const scrollHeight = element.scrollHeight - element.clientHeight;
|
|
549
|
+
const threshold = 4;
|
|
550
|
+
if (scrollWidth < threshold && scrollHeight < threshold) {
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
if (!scrollableY && scrollWidth < threshold) {
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
if (!scrollableX && scrollHeight < threshold) {
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
const distanceToTop = element.scrollTop;
|
|
560
|
+
const distanceToLeft = element.scrollLeft;
|
|
561
|
+
const distanceToRight = element.scrollWidth - element.clientWidth - element.scrollLeft;
|
|
562
|
+
const distanceToBottom = element.scrollHeight - element.clientHeight - element.scrollTop;
|
|
563
|
+
const scrollData = {
|
|
564
|
+
top: distanceToTop,
|
|
565
|
+
right: distanceToRight,
|
|
566
|
+
bottom: distanceToBottom,
|
|
567
|
+
left: distanceToLeft
|
|
568
|
+
};
|
|
569
|
+
addExtraData(element, {
|
|
570
|
+
scrollable: true,
|
|
571
|
+
scrollData
|
|
572
|
+
});
|
|
573
|
+
return scrollData;
|
|
574
|
+
}
|
|
575
|
+
__name(isScrollableElement, "isScrollableElement");
|
|
576
|
+
function isTextNodeVisible(textNode) {
|
|
577
|
+
try {
|
|
578
|
+
if (viewportExpansion === -1) {
|
|
579
|
+
const parentElement2 = textNode.parentElement;
|
|
580
|
+
if (!parentElement2) return false;
|
|
581
|
+
try {
|
|
582
|
+
return parentElement2.checkVisibility({
|
|
583
|
+
checkOpacity: true,
|
|
584
|
+
checkVisibilityCSS: true
|
|
585
|
+
});
|
|
586
|
+
} catch (e) {
|
|
587
|
+
const style = window.getComputedStyle(parentElement2);
|
|
588
|
+
return style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0";
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
const range = document.createRange();
|
|
592
|
+
range.selectNodeContents(textNode);
|
|
593
|
+
const rects = range.getClientRects();
|
|
594
|
+
if (!rects || rects.length === 0) {
|
|
595
|
+
return false;
|
|
596
|
+
}
|
|
597
|
+
let isAnyRectVisible = false;
|
|
598
|
+
let isAnyRectInViewport = false;
|
|
599
|
+
for (const rect of rects) {
|
|
600
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
601
|
+
isAnyRectVisible = true;
|
|
602
|
+
if (!(rect.bottom < -viewportExpansion || rect.top > window.innerHeight + viewportExpansion || rect.right < -viewportExpansion || rect.left > window.innerWidth + viewportExpansion)) {
|
|
603
|
+
isAnyRectInViewport = true;
|
|
604
|
+
break;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
if (!isAnyRectVisible || !isAnyRectInViewport) {
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
const parentElement = textNode.parentElement;
|
|
612
|
+
if (!parentElement) return false;
|
|
613
|
+
try {
|
|
614
|
+
return parentElement.checkVisibility({
|
|
615
|
+
checkOpacity: true,
|
|
616
|
+
checkVisibilityCSS: true
|
|
617
|
+
});
|
|
618
|
+
} catch (e) {
|
|
619
|
+
const style = window.getComputedStyle(parentElement);
|
|
620
|
+
return style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0";
|
|
621
|
+
}
|
|
622
|
+
} catch (e) {
|
|
623
|
+
console.warn("Error checking text node visibility:", e);
|
|
624
|
+
return false;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
__name(isTextNodeVisible, "isTextNodeVisible");
|
|
628
|
+
function isElementAccepted(element) {
|
|
629
|
+
if (!element || !element.tagName) return false;
|
|
630
|
+
const alwaysAccept = /* @__PURE__ */ new Set([
|
|
631
|
+
"body",
|
|
632
|
+
"div",
|
|
633
|
+
"main",
|
|
634
|
+
"article",
|
|
635
|
+
"section",
|
|
636
|
+
"nav",
|
|
637
|
+
"header",
|
|
638
|
+
"footer"
|
|
639
|
+
]);
|
|
640
|
+
const tagName = element.tagName.toLowerCase();
|
|
641
|
+
if (alwaysAccept.has(tagName)) return true;
|
|
642
|
+
const leafElementDenyList = /* @__PURE__ */ new Set([
|
|
643
|
+
"svg",
|
|
644
|
+
"script",
|
|
645
|
+
"style",
|
|
646
|
+
"link",
|
|
647
|
+
"meta",
|
|
648
|
+
"noscript",
|
|
649
|
+
"template"
|
|
650
|
+
]);
|
|
651
|
+
return !leafElementDenyList.has(tagName);
|
|
652
|
+
}
|
|
653
|
+
__name(isElementAccepted, "isElementAccepted");
|
|
654
|
+
function isElementVisible(element) {
|
|
655
|
+
const style = getCachedComputedStyle(element);
|
|
656
|
+
return element.offsetWidth > 0 && element.offsetHeight > 0 && style?.visibility !== "hidden" && style?.display !== "none";
|
|
657
|
+
}
|
|
658
|
+
__name(isElementVisible, "isElementVisible");
|
|
659
|
+
function isInteractiveElement(element) {
|
|
660
|
+
if (!element || element.nodeType !== Node.ELEMENT_NODE) {
|
|
661
|
+
return false;
|
|
662
|
+
}
|
|
663
|
+
if (interactiveBlacklist.includes(element)) {
|
|
664
|
+
return false;
|
|
665
|
+
}
|
|
666
|
+
if (interactiveWhitelist.includes(element)) {
|
|
667
|
+
return true;
|
|
668
|
+
}
|
|
669
|
+
const tagName = element.tagName.toLowerCase();
|
|
670
|
+
const style = getCachedComputedStyle(element);
|
|
671
|
+
const interactiveCursors = /* @__PURE__ */ new Set([
|
|
672
|
+
"pointer",
|
|
673
|
+
// Link/clickable elements
|
|
674
|
+
"move",
|
|
675
|
+
// Movable elements
|
|
676
|
+
"text",
|
|
677
|
+
// Text selection
|
|
678
|
+
"grab",
|
|
679
|
+
// Grabbable elements
|
|
680
|
+
"grabbing",
|
|
681
|
+
// Currently grabbing
|
|
682
|
+
"cell",
|
|
683
|
+
// Table cell selection
|
|
684
|
+
"copy",
|
|
685
|
+
// Copy operation
|
|
686
|
+
"alias",
|
|
687
|
+
// Alias creation
|
|
688
|
+
"all-scroll",
|
|
689
|
+
// Scrollable content
|
|
690
|
+
"col-resize",
|
|
691
|
+
// Column resize
|
|
692
|
+
"context-menu",
|
|
693
|
+
// Context menu available
|
|
694
|
+
"crosshair",
|
|
695
|
+
// Precise selection
|
|
696
|
+
"e-resize",
|
|
697
|
+
// East resize
|
|
698
|
+
"ew-resize",
|
|
699
|
+
// East-west resize
|
|
700
|
+
"help",
|
|
701
|
+
// Help available
|
|
702
|
+
"n-resize",
|
|
703
|
+
// North resize
|
|
704
|
+
"ne-resize",
|
|
705
|
+
// Northeast resize
|
|
706
|
+
"nesw-resize",
|
|
707
|
+
// Northeast-southwest resize
|
|
708
|
+
"ns-resize",
|
|
709
|
+
// North-south resize
|
|
710
|
+
"nw-resize",
|
|
711
|
+
// Northwest resize
|
|
712
|
+
"nwse-resize",
|
|
713
|
+
// Northwest-southeast resize
|
|
714
|
+
"row-resize",
|
|
715
|
+
// Row resize
|
|
716
|
+
"s-resize",
|
|
717
|
+
// South resize
|
|
718
|
+
"se-resize",
|
|
719
|
+
// Southeast resize
|
|
720
|
+
"sw-resize",
|
|
721
|
+
// Southwest resize
|
|
722
|
+
"vertical-text",
|
|
723
|
+
// Vertical text selection
|
|
724
|
+
"w-resize",
|
|
725
|
+
// West resize
|
|
726
|
+
"zoom-in",
|
|
727
|
+
// Zoom in
|
|
728
|
+
"zoom-out"
|
|
729
|
+
// Zoom out
|
|
730
|
+
]);
|
|
731
|
+
const nonInteractiveCursors = /* @__PURE__ */ new Set([
|
|
732
|
+
"not-allowed",
|
|
733
|
+
// Action not allowed
|
|
734
|
+
"no-drop",
|
|
735
|
+
// Drop not allowed
|
|
736
|
+
"wait",
|
|
737
|
+
// Processing
|
|
738
|
+
"progress",
|
|
739
|
+
// In progress
|
|
740
|
+
"initial",
|
|
741
|
+
// Initial value
|
|
742
|
+
"inherit"
|
|
743
|
+
// Inherited value
|
|
744
|
+
//? Let's just include all potentially clickable elements that are not specifically blocked
|
|
745
|
+
// 'none', // No cursor
|
|
746
|
+
// 'default', // Default cursor
|
|
747
|
+
// 'auto', // Browser default
|
|
748
|
+
]);
|
|
749
|
+
function doesElementHaveInteractivePointer(element2) {
|
|
750
|
+
if (element2.tagName.toLowerCase() === "html") return false;
|
|
751
|
+
if (style?.cursor && interactiveCursors.has(style.cursor)) return true;
|
|
752
|
+
return false;
|
|
753
|
+
}
|
|
754
|
+
__name(doesElementHaveInteractivePointer, "doesElementHaveInteractivePointer");
|
|
755
|
+
let isInteractiveCursor = doesElementHaveInteractivePointer(element);
|
|
756
|
+
if (isInteractiveCursor) {
|
|
757
|
+
return true;
|
|
758
|
+
}
|
|
759
|
+
const interactiveElements = /* @__PURE__ */ new Set([
|
|
760
|
+
"a",
|
|
761
|
+
// Links
|
|
762
|
+
"button",
|
|
763
|
+
// Buttons
|
|
764
|
+
"input",
|
|
765
|
+
// All input types (text, checkbox, radio, etc.)
|
|
766
|
+
"select",
|
|
767
|
+
// Dropdown menus
|
|
768
|
+
"textarea",
|
|
769
|
+
// Text areas
|
|
770
|
+
"details",
|
|
771
|
+
// Expandable details
|
|
772
|
+
"summary",
|
|
773
|
+
// Summary element (clickable part of details)
|
|
774
|
+
"label",
|
|
775
|
+
// Form labels (often clickable)
|
|
776
|
+
"option",
|
|
777
|
+
// Select options
|
|
778
|
+
"optgroup",
|
|
779
|
+
// Option groups
|
|
780
|
+
"fieldset",
|
|
781
|
+
// Form fieldsets (can be interactive with legend)
|
|
782
|
+
"legend"
|
|
783
|
+
// Fieldset legends
|
|
784
|
+
]);
|
|
785
|
+
const explicitDisableTags = /* @__PURE__ */ new Set([
|
|
786
|
+
"disabled",
|
|
787
|
+
// Standard disabled attribute
|
|
788
|
+
// 'aria-disabled', // ARIA disabled state
|
|
789
|
+
"readonly"
|
|
790
|
+
// Read-only state
|
|
791
|
+
// 'aria-readonly', // ARIA read-only state
|
|
792
|
+
// 'aria-hidden', // Hidden from accessibility
|
|
793
|
+
// 'hidden', // Hidden attribute
|
|
794
|
+
// 'inert', // Inert attribute
|
|
795
|
+
// 'aria-inert', // ARIA inert state
|
|
796
|
+
// 'tabindex="-1"', // Removed from tab order
|
|
797
|
+
// 'aria-hidden="true"' // Hidden from screen readers
|
|
798
|
+
]);
|
|
799
|
+
if (interactiveElements.has(tagName)) {
|
|
800
|
+
if (style?.cursor && nonInteractiveCursors.has(style.cursor)) {
|
|
801
|
+
return false;
|
|
802
|
+
}
|
|
803
|
+
for (const disableTag of explicitDisableTags) {
|
|
804
|
+
if (element.hasAttribute(disableTag) || element.getAttribute(disableTag) === "true" || element.getAttribute(disableTag) === "") {
|
|
805
|
+
return false;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
if (element.disabled) {
|
|
809
|
+
return false;
|
|
810
|
+
}
|
|
811
|
+
if (element.readOnly) {
|
|
812
|
+
return false;
|
|
813
|
+
}
|
|
814
|
+
if (element.inert) {
|
|
815
|
+
return false;
|
|
816
|
+
}
|
|
817
|
+
return true;
|
|
818
|
+
}
|
|
819
|
+
const role = element.getAttribute("role");
|
|
820
|
+
const ariaRole = element.getAttribute("aria-role");
|
|
821
|
+
if (element.getAttribute("contenteditable") === "true" || element.isContentEditable) {
|
|
822
|
+
return true;
|
|
823
|
+
}
|
|
824
|
+
if (element.classList && (element.classList.contains("button") || element.classList.contains("dropdown-toggle") || element.getAttribute("data-index") || element.getAttribute("data-toggle") === "dropdown" || element.getAttribute("aria-haspopup") === "true")) {
|
|
825
|
+
return true;
|
|
826
|
+
}
|
|
827
|
+
const interactiveRoles = /* @__PURE__ */ new Set([
|
|
828
|
+
"button",
|
|
829
|
+
// Directly clickable element
|
|
830
|
+
// 'link', // Clickable link
|
|
831
|
+
"menu",
|
|
832
|
+
// Menu container (ARIA menus)
|
|
833
|
+
"menubar",
|
|
834
|
+
// Menu bar container
|
|
835
|
+
"menuitem",
|
|
836
|
+
// Clickable menu item
|
|
837
|
+
"menuitemradio",
|
|
838
|
+
// Radio-style menu item (selectable)
|
|
839
|
+
"menuitemcheckbox",
|
|
840
|
+
// Checkbox-style menu item (toggleable)
|
|
841
|
+
"radio",
|
|
842
|
+
// Radio button (selectable)
|
|
843
|
+
"checkbox",
|
|
844
|
+
// Checkbox (toggleable)
|
|
845
|
+
"tab",
|
|
846
|
+
// Tab (clickable to switch content)
|
|
847
|
+
"switch",
|
|
848
|
+
// Toggle switch (clickable to change state)
|
|
849
|
+
"slider",
|
|
850
|
+
// Slider control (draggable)
|
|
851
|
+
"spinbutton",
|
|
852
|
+
// Number input with up/down controls
|
|
853
|
+
"combobox",
|
|
854
|
+
// Dropdown with text input
|
|
855
|
+
"searchbox",
|
|
856
|
+
// Search input field
|
|
857
|
+
"textbox",
|
|
858
|
+
// Text input field
|
|
859
|
+
"listbox",
|
|
860
|
+
// Selectable list
|
|
861
|
+
"option",
|
|
862
|
+
// Selectable option in a list
|
|
863
|
+
"scrollbar"
|
|
864
|
+
// Scrollable control
|
|
865
|
+
]);
|
|
866
|
+
const hasInteractiveRole = interactiveElements.has(tagName) || role && interactiveRoles.has(role) || ariaRole && interactiveRoles.has(ariaRole);
|
|
867
|
+
if (hasInteractiveRole) return true;
|
|
868
|
+
try {
|
|
869
|
+
if (typeof getEventListeners === "function") {
|
|
870
|
+
const listeners = getEventListeners(element);
|
|
871
|
+
const mouseEvents = ["click", "mousedown", "mouseup", "dblclick"];
|
|
872
|
+
for (const eventType of mouseEvents) {
|
|
873
|
+
if (listeners[eventType] && listeners[eventType].length > 0) {
|
|
874
|
+
return true;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
const getEventListenersForNode = element?.ownerDocument?.defaultView?.getEventListenersForNode || window.getEventListenersForNode;
|
|
879
|
+
if (typeof getEventListenersForNode === "function") {
|
|
880
|
+
const listeners = getEventListenersForNode(element);
|
|
881
|
+
const interactionEvents = [
|
|
882
|
+
"click",
|
|
883
|
+
"mousedown",
|
|
884
|
+
"mouseup",
|
|
885
|
+
"keydown",
|
|
886
|
+
"keyup",
|
|
887
|
+
"submit",
|
|
888
|
+
"change",
|
|
889
|
+
"input",
|
|
890
|
+
"focus",
|
|
891
|
+
"blur"
|
|
892
|
+
];
|
|
893
|
+
for (const eventType of interactionEvents) {
|
|
894
|
+
for (const listener of listeners) {
|
|
895
|
+
if (listener.type === eventType) {
|
|
896
|
+
return true;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
const commonMouseAttrs = ["onclick", "onmousedown", "onmouseup", "ondblclick"];
|
|
902
|
+
for (const attr of commonMouseAttrs) {
|
|
903
|
+
if (element.hasAttribute(attr) || typeof element[attr] === "function") {
|
|
904
|
+
return true;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
} catch (e) {
|
|
908
|
+
}
|
|
909
|
+
if (isScrollableElement(element)) {
|
|
910
|
+
return true;
|
|
911
|
+
}
|
|
912
|
+
return false;
|
|
913
|
+
}
|
|
914
|
+
__name(isInteractiveElement, "isInteractiveElement");
|
|
915
|
+
function isTopElement(element) {
|
|
916
|
+
if (viewportExpansion === -1) {
|
|
917
|
+
return true;
|
|
918
|
+
}
|
|
919
|
+
const rects = getCachedClientRects(element);
|
|
920
|
+
if (!rects || rects.length === 0) {
|
|
921
|
+
return false;
|
|
922
|
+
}
|
|
923
|
+
let isAnyRectInViewport = false;
|
|
924
|
+
for (const rect2 of rects) {
|
|
925
|
+
if (rect2.width > 0 && rect2.height > 0 && !// Only check non-empty rects
|
|
926
|
+
(rect2.bottom < -viewportExpansion || rect2.top > window.innerHeight + viewportExpansion || rect2.right < -viewportExpansion || rect2.left > window.innerWidth + viewportExpansion)) {
|
|
927
|
+
isAnyRectInViewport = true;
|
|
928
|
+
break;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
if (!isAnyRectInViewport) {
|
|
932
|
+
return false;
|
|
933
|
+
}
|
|
934
|
+
let doc = element.ownerDocument;
|
|
935
|
+
if (doc !== window.document) {
|
|
936
|
+
return true;
|
|
937
|
+
}
|
|
938
|
+
let rect = Array.from(rects).find((r) => r.width > 0 && r.height > 0);
|
|
939
|
+
if (!rect) {
|
|
940
|
+
return false;
|
|
941
|
+
}
|
|
942
|
+
const shadowRoot = element.getRootNode();
|
|
943
|
+
if (shadowRoot instanceof ShadowRoot) {
|
|
944
|
+
const centerX = rect.left + rect.width / 2;
|
|
945
|
+
const centerY = rect.top + rect.height / 2;
|
|
946
|
+
try {
|
|
947
|
+
const topEl = shadowRoot.elementFromPoint(centerX, centerY);
|
|
948
|
+
if (!topEl) return false;
|
|
949
|
+
let current = topEl;
|
|
950
|
+
while (current && current !== shadowRoot) {
|
|
951
|
+
if (current === element) return true;
|
|
952
|
+
current = current.parentElement;
|
|
953
|
+
}
|
|
954
|
+
return false;
|
|
955
|
+
} catch (e) {
|
|
956
|
+
return true;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
const margin = 5;
|
|
960
|
+
const checkPoints = [
|
|
961
|
+
// Initially only this was used, but it was not enough
|
|
962
|
+
{ x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 },
|
|
963
|
+
{ x: rect.left + margin, y: rect.top + margin },
|
|
964
|
+
// top left
|
|
965
|
+
// { x: rect.right - margin, y: rect.top + margin }, // top right
|
|
966
|
+
// { x: rect.left + margin, y: rect.bottom - margin }, // bottom left
|
|
967
|
+
{ x: rect.right - margin, y: rect.bottom - margin }
|
|
968
|
+
// bottom right
|
|
969
|
+
];
|
|
970
|
+
return checkPoints.some(({ x, y }) => {
|
|
971
|
+
try {
|
|
972
|
+
const topEl = document.elementFromPoint(x, y);
|
|
973
|
+
if (!topEl) return false;
|
|
974
|
+
let current = topEl;
|
|
975
|
+
while (current && current !== document.documentElement) {
|
|
976
|
+
if (current === element) return true;
|
|
977
|
+
current = current.parentElement;
|
|
978
|
+
}
|
|
979
|
+
return false;
|
|
980
|
+
} catch (e) {
|
|
981
|
+
return true;
|
|
982
|
+
}
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
__name(isTopElement, "isTopElement");
|
|
986
|
+
function isInExpandedViewport(element, viewportExpansion2) {
|
|
987
|
+
if (viewportExpansion2 === -1) {
|
|
988
|
+
return true;
|
|
989
|
+
}
|
|
990
|
+
const rects = element.getClientRects();
|
|
991
|
+
if (!rects || rects.length === 0) {
|
|
992
|
+
const boundingRect = getCachedBoundingRect(element);
|
|
993
|
+
if (!boundingRect || boundingRect.width === 0 || boundingRect.height === 0) {
|
|
994
|
+
return false;
|
|
995
|
+
}
|
|
996
|
+
return !(boundingRect.bottom < -viewportExpansion2 || boundingRect.top > window.innerHeight + viewportExpansion2 || boundingRect.right < -viewportExpansion2 || boundingRect.left > window.innerWidth + viewportExpansion2);
|
|
997
|
+
}
|
|
998
|
+
for (const rect of rects) {
|
|
999
|
+
if (rect.width === 0 || rect.height === 0) continue;
|
|
1000
|
+
if (!(rect.bottom < -viewportExpansion2 || rect.top > window.innerHeight + viewportExpansion2 || rect.right < -viewportExpansion2 || rect.left > window.innerWidth + viewportExpansion2)) {
|
|
1001
|
+
return true;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
return false;
|
|
1005
|
+
}
|
|
1006
|
+
__name(isInExpandedViewport, "isInExpandedViewport");
|
|
1007
|
+
function isInteractiveCandidate(element) {
|
|
1008
|
+
if (!element || element.nodeType !== Node.ELEMENT_NODE) return false;
|
|
1009
|
+
const tagName = element.tagName.toLowerCase();
|
|
1010
|
+
const interactiveElements = /* @__PURE__ */ new Set([
|
|
1011
|
+
"a",
|
|
1012
|
+
"button",
|
|
1013
|
+
"input",
|
|
1014
|
+
"select",
|
|
1015
|
+
"textarea",
|
|
1016
|
+
"details",
|
|
1017
|
+
"summary",
|
|
1018
|
+
"label"
|
|
1019
|
+
]);
|
|
1020
|
+
if (interactiveElements.has(tagName)) return true;
|
|
1021
|
+
const hasQuickInteractiveAttr = element.hasAttribute("onclick") || element.hasAttribute("role") || element.hasAttribute("tabindex") || element.hasAttribute("aria-") || element.hasAttribute("data-action") || element.getAttribute("contenteditable") === "true";
|
|
1022
|
+
return hasQuickInteractiveAttr;
|
|
1023
|
+
}
|
|
1024
|
+
__name(isInteractiveCandidate, "isInteractiveCandidate");
|
|
1025
|
+
const DISTINCT_INTERACTIVE_TAGS = /* @__PURE__ */ new Set([
|
|
1026
|
+
"a",
|
|
1027
|
+
"button",
|
|
1028
|
+
"input",
|
|
1029
|
+
"select",
|
|
1030
|
+
"textarea",
|
|
1031
|
+
"summary",
|
|
1032
|
+
"details",
|
|
1033
|
+
"label",
|
|
1034
|
+
"option"
|
|
1035
|
+
]);
|
|
1036
|
+
const INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
|
|
1037
|
+
"button",
|
|
1038
|
+
"link",
|
|
1039
|
+
"menuitem",
|
|
1040
|
+
"menuitemradio",
|
|
1041
|
+
"menuitemcheckbox",
|
|
1042
|
+
"radio",
|
|
1043
|
+
"checkbox",
|
|
1044
|
+
"tab",
|
|
1045
|
+
"switch",
|
|
1046
|
+
"slider",
|
|
1047
|
+
"spinbutton",
|
|
1048
|
+
"combobox",
|
|
1049
|
+
"searchbox",
|
|
1050
|
+
"textbox",
|
|
1051
|
+
"listbox",
|
|
1052
|
+
"option",
|
|
1053
|
+
"scrollbar"
|
|
1054
|
+
]);
|
|
1055
|
+
function isHeuristicallyInteractive(element) {
|
|
1056
|
+
if (!element || element.nodeType !== Node.ELEMENT_NODE) return false;
|
|
1057
|
+
if (!isElementVisible(element)) return false;
|
|
1058
|
+
const hasInteractiveAttributes = element.hasAttribute("role") || element.hasAttribute("tabindex") || element.hasAttribute("onclick") || typeof element.onclick === "function";
|
|
1059
|
+
const hasInteractiveClass = /\b(btn|clickable|menu|item|entry|link)\b/i.test(
|
|
1060
|
+
element.className || ""
|
|
1061
|
+
);
|
|
1062
|
+
const isInKnownContainer = Boolean(
|
|
1063
|
+
element.closest('button,a,[role="button"],.menu,.dropdown,.list,.toolbar')
|
|
1064
|
+
);
|
|
1065
|
+
const hasVisibleChildren = [...element.children].some(isElementVisible);
|
|
1066
|
+
const isParentBody = element.parentElement && element.parentElement.isSameNode(document.body);
|
|
1067
|
+
return (isInteractiveElement(element) || hasInteractiveAttributes || hasInteractiveClass) && hasVisibleChildren && isInKnownContainer && !isParentBody;
|
|
1068
|
+
}
|
|
1069
|
+
__name(isHeuristicallyInteractive, "isHeuristicallyInteractive");
|
|
1070
|
+
function isElementDistinctInteraction(element) {
|
|
1071
|
+
if (!element || element.nodeType !== Node.ELEMENT_NODE) {
|
|
1072
|
+
return false;
|
|
1073
|
+
}
|
|
1074
|
+
const tagName = element.tagName.toLowerCase();
|
|
1075
|
+
const role = element.getAttribute("role");
|
|
1076
|
+
if (tagName === "iframe") {
|
|
1077
|
+
return true;
|
|
1078
|
+
}
|
|
1079
|
+
if (DISTINCT_INTERACTIVE_TAGS.has(tagName)) {
|
|
1080
|
+
return true;
|
|
1081
|
+
}
|
|
1082
|
+
if (role && INTERACTIVE_ROLES.has(role)) {
|
|
1083
|
+
return true;
|
|
1084
|
+
}
|
|
1085
|
+
if (element.isContentEditable || element.getAttribute("contenteditable") === "true") {
|
|
1086
|
+
return true;
|
|
1087
|
+
}
|
|
1088
|
+
if (element.hasAttribute("data-testid") || element.hasAttribute("data-cy") || element.hasAttribute("data-test")) {
|
|
1089
|
+
return true;
|
|
1090
|
+
}
|
|
1091
|
+
if (element.hasAttribute("onclick") || typeof element.onclick === "function") {
|
|
1092
|
+
return true;
|
|
1093
|
+
}
|
|
1094
|
+
try {
|
|
1095
|
+
const getEventListenersForNode = element?.ownerDocument?.defaultView?.getEventListenersForNode || window.getEventListenersForNode;
|
|
1096
|
+
if (typeof getEventListenersForNode === "function") {
|
|
1097
|
+
const listeners = getEventListenersForNode(element);
|
|
1098
|
+
const interactionEvents = [
|
|
1099
|
+
"click",
|
|
1100
|
+
"mousedown",
|
|
1101
|
+
"mouseup",
|
|
1102
|
+
"keydown",
|
|
1103
|
+
"keyup",
|
|
1104
|
+
"submit",
|
|
1105
|
+
"change",
|
|
1106
|
+
"input",
|
|
1107
|
+
"focus",
|
|
1108
|
+
"blur"
|
|
1109
|
+
];
|
|
1110
|
+
for (const eventType of interactionEvents) {
|
|
1111
|
+
for (const listener of listeners) {
|
|
1112
|
+
if (listener.type === eventType) {
|
|
1113
|
+
return true;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
const commonEventAttrs = [
|
|
1119
|
+
"onmousedown",
|
|
1120
|
+
"onmouseup",
|
|
1121
|
+
"onkeydown",
|
|
1122
|
+
"onkeyup",
|
|
1123
|
+
"onsubmit",
|
|
1124
|
+
"onchange",
|
|
1125
|
+
"oninput",
|
|
1126
|
+
"onfocus",
|
|
1127
|
+
"onblur"
|
|
1128
|
+
];
|
|
1129
|
+
if (commonEventAttrs.some((attr) => element.hasAttribute(attr))) {
|
|
1130
|
+
return true;
|
|
1131
|
+
}
|
|
1132
|
+
} catch (e) {
|
|
1133
|
+
}
|
|
1134
|
+
if (isHeuristicallyInteractive(element)) {
|
|
1135
|
+
return true;
|
|
1136
|
+
}
|
|
1137
|
+
return false;
|
|
1138
|
+
}
|
|
1139
|
+
__name(isElementDistinctInteraction, "isElementDistinctInteraction");
|
|
1140
|
+
function handleHighlighting(nodeData, node, parentIframe, isParentHighlighted) {
|
|
1141
|
+
if (!nodeData.isInteractive) return false;
|
|
1142
|
+
let shouldHighlight = false;
|
|
1143
|
+
if (!isParentHighlighted) {
|
|
1144
|
+
shouldHighlight = true;
|
|
1145
|
+
} else {
|
|
1146
|
+
if (isElementDistinctInteraction(node)) {
|
|
1147
|
+
shouldHighlight = true;
|
|
1148
|
+
} else {
|
|
1149
|
+
shouldHighlight = false;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
if (shouldHighlight) {
|
|
1153
|
+
nodeData.isInViewport = isInExpandedViewport(node, viewportExpansion);
|
|
1154
|
+
if (nodeData.isInViewport || viewportExpansion === -1) {
|
|
1155
|
+
nodeData.highlightIndex = highlightIndex++;
|
|
1156
|
+
if (doHighlightElements) {
|
|
1157
|
+
if (focusHighlightIndex >= 0) {
|
|
1158
|
+
if (focusHighlightIndex === nodeData.highlightIndex) {
|
|
1159
|
+
highlightElement(node, nodeData.highlightIndex, parentIframe);
|
|
1160
|
+
}
|
|
1161
|
+
} else {
|
|
1162
|
+
highlightElement(node, nodeData.highlightIndex, parentIframe);
|
|
1163
|
+
}
|
|
1164
|
+
return true;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
return false;
|
|
1169
|
+
}
|
|
1170
|
+
__name(handleHighlighting, "handleHighlighting");
|
|
1171
|
+
function buildDomTree(node, parentIframe = null, isParentHighlighted = false) {
|
|
1172
|
+
if (!node || node.id === HIGHLIGHT_CONTAINER_ID || node.nodeType !== Node.ELEMENT_NODE && node.nodeType !== Node.TEXT_NODE) {
|
|
1173
|
+
return null;
|
|
1174
|
+
}
|
|
1175
|
+
if (!node || node.id === HIGHLIGHT_CONTAINER_ID) {
|
|
1176
|
+
return null;
|
|
1177
|
+
}
|
|
1178
|
+
if (node.dataset?.browserUseIgnore === "true") {
|
|
1179
|
+
return true;
|
|
1180
|
+
}
|
|
1181
|
+
if (node === document.body) {
|
|
1182
|
+
const nodeData2 = {
|
|
1183
|
+
tagName: "body",
|
|
1184
|
+
attributes: {},
|
|
1185
|
+
xpath: "/body",
|
|
1186
|
+
children: []
|
|
1187
|
+
};
|
|
1188
|
+
for (const child of node.childNodes) {
|
|
1189
|
+
const domElement = buildDomTree(child, parentIframe, false);
|
|
1190
|
+
if (domElement) nodeData2.children.push(domElement);
|
|
1191
|
+
}
|
|
1192
|
+
const id2 = `${ID.current++}`;
|
|
1193
|
+
DOM_HASH_MAP[id2] = nodeData2;
|
|
1194
|
+
return id2;
|
|
1195
|
+
}
|
|
1196
|
+
if (node.nodeType !== Node.ELEMENT_NODE && node.nodeType !== Node.TEXT_NODE) {
|
|
1197
|
+
return null;
|
|
1198
|
+
}
|
|
1199
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
1200
|
+
const textContent = node.textContent?.trim();
|
|
1201
|
+
if (!textContent) {
|
|
1202
|
+
return null;
|
|
1203
|
+
}
|
|
1204
|
+
const parentElement = node.parentElement;
|
|
1205
|
+
if (!parentElement || parentElement.tagName.toLowerCase() === "script") {
|
|
1206
|
+
return null;
|
|
1207
|
+
}
|
|
1208
|
+
const id2 = `${ID.current++}`;
|
|
1209
|
+
DOM_HASH_MAP[id2] = {
|
|
1210
|
+
type: "TEXT_NODE",
|
|
1211
|
+
text: textContent,
|
|
1212
|
+
isVisible: isTextNodeVisible(node)
|
|
1213
|
+
};
|
|
1214
|
+
return id2;
|
|
1215
|
+
}
|
|
1216
|
+
if (node.nodeType === Node.ELEMENT_NODE && !isElementAccepted(node)) {
|
|
1217
|
+
return null;
|
|
1218
|
+
}
|
|
1219
|
+
if (viewportExpansion !== -1 && !node.shadowRoot) {
|
|
1220
|
+
const rect = getCachedBoundingRect(node);
|
|
1221
|
+
const style = getCachedComputedStyle(node);
|
|
1222
|
+
const isFixedOrSticky = style && (style.position === "fixed" || style.position === "sticky");
|
|
1223
|
+
const hasSize = node.offsetWidth > 0 || node.offsetHeight > 0;
|
|
1224
|
+
if (!rect || !isFixedOrSticky && !hasSize && (rect.bottom < -viewportExpansion || rect.top > window.innerHeight + viewportExpansion || rect.right < -viewportExpansion || rect.left > window.innerWidth + viewportExpansion)) {
|
|
1225
|
+
return null;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
const nodeData = {
|
|
1229
|
+
tagName: node.tagName.toLowerCase(),
|
|
1230
|
+
attributes: {},
|
|
1231
|
+
/**
|
|
1232
|
+
* @edit no need for xpath
|
|
1233
|
+
*/
|
|
1234
|
+
// xpath: getXPathTree(node, true),
|
|
1235
|
+
children: []
|
|
1236
|
+
};
|
|
1237
|
+
if (isInteractiveCandidate(node) || node.tagName.toLowerCase() === "iframe" || node.tagName.toLowerCase() === "body") {
|
|
1238
|
+
const attributeNames = node.getAttributeNames?.() || [];
|
|
1239
|
+
for (const name of attributeNames) {
|
|
1240
|
+
const value = node.getAttribute(name);
|
|
1241
|
+
nodeData.attributes[name] = value;
|
|
1242
|
+
}
|
|
1243
|
+
if (node.tagName.toLowerCase() === "input" && (node.type === "checkbox" || node.type === "radio")) {
|
|
1244
|
+
nodeData.attributes.checked = node.checked ? "true" : "false";
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
let nodeWasHighlighted = false;
|
|
1248
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
1249
|
+
nodeData.isVisible = isElementVisible(node);
|
|
1250
|
+
if (nodeData.isVisible) {
|
|
1251
|
+
nodeData.isTopElement = isTopElement(node);
|
|
1252
|
+
const role = node.getAttribute("role");
|
|
1253
|
+
const isMenuContainer = role === "menu" || role === "menubar" || role === "listbox";
|
|
1254
|
+
if (nodeData.isTopElement || isMenuContainer) {
|
|
1255
|
+
nodeData.isInteractive = isInteractiveElement(node);
|
|
1256
|
+
nodeWasHighlighted = handleHighlighting(nodeData, node, parentIframe, isParentHighlighted);
|
|
1257
|
+
nodeData.ref = node;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
if (node.tagName) {
|
|
1262
|
+
const tagName = node.tagName.toLowerCase();
|
|
1263
|
+
if (tagName === "iframe") {
|
|
1264
|
+
try {
|
|
1265
|
+
const iframeDoc = node.contentDocument || node.contentWindow?.document;
|
|
1266
|
+
if (iframeDoc) {
|
|
1267
|
+
for (const child of iframeDoc.childNodes) {
|
|
1268
|
+
const domElement = buildDomTree(child, node, false);
|
|
1269
|
+
if (domElement) nodeData.children.push(domElement);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
} catch (e) {
|
|
1273
|
+
console.warn("Unable to access iframe:", e);
|
|
1274
|
+
}
|
|
1275
|
+
} else if (node.isContentEditable || node.getAttribute("contenteditable") === "true" || node.id === "tinymce" || node.classList.contains("mce-content-body") || tagName === "body" && node.getAttribute("data-id")?.startsWith("mce_")) {
|
|
1276
|
+
for (const child of node.childNodes) {
|
|
1277
|
+
const domElement = buildDomTree(child, parentIframe, nodeWasHighlighted);
|
|
1278
|
+
if (domElement) nodeData.children.push(domElement);
|
|
1279
|
+
}
|
|
1280
|
+
} else {
|
|
1281
|
+
if (node.shadowRoot) {
|
|
1282
|
+
nodeData.shadowRoot = true;
|
|
1283
|
+
for (const child of node.shadowRoot.childNodes) {
|
|
1284
|
+
const domElement = buildDomTree(child, parentIframe, nodeWasHighlighted);
|
|
1285
|
+
if (domElement) nodeData.children.push(domElement);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
for (const child of node.childNodes) {
|
|
1289
|
+
const passHighlightStatusToChild = nodeWasHighlighted || isParentHighlighted;
|
|
1290
|
+
const domElement = buildDomTree(child, parentIframe, passHighlightStatusToChild);
|
|
1291
|
+
if (domElement) nodeData.children.push(domElement);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
if (nodeData.tagName === "a" && nodeData.children.length === 0 && !nodeData.attributes.href) {
|
|
1296
|
+
const rect = getCachedBoundingRect(node);
|
|
1297
|
+
const hasSize = rect && rect.width > 0 && rect.height > 0 || node.offsetWidth > 0 || node.offsetHeight > 0;
|
|
1298
|
+
if (!hasSize) {
|
|
1299
|
+
return null;
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
nodeData.extra = extraData.get(node) || null;
|
|
1303
|
+
const id = `${ID.current++}`;
|
|
1304
|
+
DOM_HASH_MAP[id] = nodeData;
|
|
1305
|
+
return id;
|
|
1306
|
+
}
|
|
1307
|
+
__name(buildDomTree, "buildDomTree");
|
|
1308
|
+
const rootId = buildDomTree(document.body);
|
|
1309
|
+
DOM_CACHE.clearCache();
|
|
1310
|
+
return { rootId, map: DOM_HASH_MAP };
|
|
1311
|
+
}, "domTree");
|
|
1312
|
+
const newElementsCache = /* @__PURE__ */ new WeakMap();
|
|
1313
|
+
function getFlatTree(config) {
|
|
1314
|
+
const interactiveBlacklist = [];
|
|
1315
|
+
for (const item of config.interactiveBlacklist || []) {
|
|
1316
|
+
if (typeof item === "function") {
|
|
1317
|
+
interactiveBlacklist.push(item());
|
|
1318
|
+
} else {
|
|
1319
|
+
interactiveBlacklist.push(item);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
const interactiveWhitelist = [];
|
|
1323
|
+
for (const item of config.interactiveWhitelist || []) {
|
|
1324
|
+
if (typeof item === "function") {
|
|
1325
|
+
interactiveWhitelist.push(item());
|
|
1326
|
+
} else {
|
|
1327
|
+
interactiveWhitelist.push(item);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
const elements = domTree({
|
|
1331
|
+
doHighlightElements: true,
|
|
1332
|
+
debugMode: true,
|
|
1333
|
+
focusHighlightIndex: -1,
|
|
1334
|
+
viewportExpansion: VIEWPORT_EXPANSION,
|
|
1335
|
+
interactiveBlacklist,
|
|
1336
|
+
interactiveWhitelist,
|
|
1337
|
+
highlightOpacity: config.highlightOpacity ?? 0,
|
|
1338
|
+
highlightLabelOpacity: config.highlightLabelOpacity ?? 0.1
|
|
1339
|
+
});
|
|
1340
|
+
const currentUrl = window.location.href;
|
|
1341
|
+
for (const nodeId in elements.map) {
|
|
1342
|
+
const node = elements.map[nodeId];
|
|
1343
|
+
if (node.isInteractive && node.ref) {
|
|
1344
|
+
const ref = node.ref;
|
|
1345
|
+
if (!newElementsCache.has(ref)) {
|
|
1346
|
+
newElementsCache.set(ref, currentUrl);
|
|
1347
|
+
node.isNew = true;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
return elements;
|
|
1352
|
+
}
|
|
1353
|
+
__name(getFlatTree, "getFlatTree");
|
|
1354
|
+
function flatTreeToString(flatTree, include_attributes) {
|
|
1355
|
+
const DEFAULT_INCLUDE_ATTRIBUTES = [
|
|
1356
|
+
"title",
|
|
1357
|
+
"type",
|
|
1358
|
+
"checked",
|
|
1359
|
+
"name",
|
|
1360
|
+
"role",
|
|
1361
|
+
"value",
|
|
1362
|
+
"placeholder",
|
|
1363
|
+
"data-date-format",
|
|
1364
|
+
"alt",
|
|
1365
|
+
"aria-label",
|
|
1366
|
+
"aria-expanded",
|
|
1367
|
+
"data-state",
|
|
1368
|
+
"aria-checked",
|
|
1369
|
+
// @edit added for better form handling
|
|
1370
|
+
"id",
|
|
1371
|
+
"for",
|
|
1372
|
+
// for jump check
|
|
1373
|
+
"target",
|
|
1374
|
+
// absolute 定位的下拉菜单
|
|
1375
|
+
"aria-haspopup",
|
|
1376
|
+
"aria-controls",
|
|
1377
|
+
"aria-owns"
|
|
1378
|
+
];
|
|
1379
|
+
const includeAttrs = [...include_attributes || [], ...DEFAULT_INCLUDE_ATTRIBUTES];
|
|
1380
|
+
const capTextLength = /* @__PURE__ */ __name((text, maxLength) => {
|
|
1381
|
+
if (text.length > maxLength) {
|
|
1382
|
+
return text.substring(0, maxLength) + "...";
|
|
1383
|
+
}
|
|
1384
|
+
return text;
|
|
1385
|
+
}, "capTextLength");
|
|
1386
|
+
const buildTreeNode = /* @__PURE__ */ __name((nodeId) => {
|
|
1387
|
+
const node = flatTree.map[nodeId];
|
|
1388
|
+
if (!node) return null;
|
|
1389
|
+
if (node.type === "TEXT_NODE") {
|
|
1390
|
+
const textNode = node;
|
|
1391
|
+
return {
|
|
1392
|
+
type: "text",
|
|
1393
|
+
text: textNode.text,
|
|
1394
|
+
isVisible: textNode.isVisible,
|
|
1395
|
+
parent: null,
|
|
1396
|
+
children: []
|
|
1397
|
+
};
|
|
1398
|
+
} else {
|
|
1399
|
+
const elementNode = node;
|
|
1400
|
+
const children = [];
|
|
1401
|
+
if (elementNode.children) {
|
|
1402
|
+
for (const childId of elementNode.children) {
|
|
1403
|
+
const child = buildTreeNode(childId);
|
|
1404
|
+
if (child) {
|
|
1405
|
+
child.parent = null;
|
|
1406
|
+
children.push(child);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
return {
|
|
1411
|
+
type: "element",
|
|
1412
|
+
tagName: elementNode.tagName,
|
|
1413
|
+
attributes: elementNode.attributes ?? {},
|
|
1414
|
+
isVisible: elementNode.isVisible ?? false,
|
|
1415
|
+
isInteractive: elementNode.isInteractive ?? false,
|
|
1416
|
+
isTopElement: elementNode.isTopElement ?? false,
|
|
1417
|
+
isNew: elementNode.isNew ?? false,
|
|
1418
|
+
highlightIndex: elementNode.highlightIndex,
|
|
1419
|
+
parent: null,
|
|
1420
|
+
children,
|
|
1421
|
+
extra: elementNode.extra ?? {}
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
|
+
}, "buildTreeNode");
|
|
1425
|
+
const setParentReferences = /* @__PURE__ */ __name((node, parent = null) => {
|
|
1426
|
+
node.parent = parent;
|
|
1427
|
+
for (const child of node.children) {
|
|
1428
|
+
setParentReferences(child, node);
|
|
1429
|
+
}
|
|
1430
|
+
}, "setParentReferences");
|
|
1431
|
+
const rootNode = buildTreeNode(flatTree.rootId);
|
|
1432
|
+
if (!rootNode) return "";
|
|
1433
|
+
setParentReferences(rootNode);
|
|
1434
|
+
const hasParentWithHighlightIndex = /* @__PURE__ */ __name((node) => {
|
|
1435
|
+
let current = node.parent;
|
|
1436
|
+
while (current) {
|
|
1437
|
+
if (current.type === "element" && current.highlightIndex !== void 0) {
|
|
1438
|
+
return true;
|
|
1439
|
+
}
|
|
1440
|
+
current = current.parent;
|
|
1441
|
+
}
|
|
1442
|
+
return false;
|
|
1443
|
+
}, "hasParentWithHighlightIndex");
|
|
1444
|
+
const processNode = /* @__PURE__ */ __name((node, depth, result22) => {
|
|
1445
|
+
let nextDepth = depth;
|
|
1446
|
+
const depthStr = " ".repeat(depth);
|
|
1447
|
+
if (node.type === "element") {
|
|
1448
|
+
if (node.highlightIndex !== void 0) {
|
|
1449
|
+
nextDepth += 1;
|
|
1450
|
+
const text = getAllTextTillNextClickableElement(node);
|
|
1451
|
+
let attributesHtmlStr = "";
|
|
1452
|
+
if (includeAttrs.length > 0 && node.attributes) {
|
|
1453
|
+
const attributesToInclude = {};
|
|
1454
|
+
for (const key of includeAttrs) {
|
|
1455
|
+
const value = node.attributes[key];
|
|
1456
|
+
if (value && value.trim() !== "") {
|
|
1457
|
+
attributesToInclude[key] = value.trim();
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
const orderedKeys = includeAttrs.filter((key) => key in attributesToInclude);
|
|
1461
|
+
if (orderedKeys.length > 1) {
|
|
1462
|
+
const keysToRemove = /* @__PURE__ */ new Set();
|
|
1463
|
+
const seenValues = {};
|
|
1464
|
+
for (const key of orderedKeys) {
|
|
1465
|
+
const value = attributesToInclude[key];
|
|
1466
|
+
if (value.length > 5) {
|
|
1467
|
+
if (value in seenValues) {
|
|
1468
|
+
keysToRemove.add(key);
|
|
1469
|
+
} else {
|
|
1470
|
+
seenValues[value] = key;
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
for (const key of keysToRemove) {
|
|
1475
|
+
delete attributesToInclude[key];
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
if (attributesToInclude.role === node.tagName) {
|
|
1479
|
+
delete attributesToInclude.role;
|
|
1480
|
+
}
|
|
1481
|
+
const attrsToRemoveIfTextMatches = ["aria-label", "placeholder", "title"];
|
|
1482
|
+
for (const attr of attrsToRemoveIfTextMatches) {
|
|
1483
|
+
if (attributesToInclude[attr] && attributesToInclude[attr].toLowerCase().trim() === text.toLowerCase().trim()) {
|
|
1484
|
+
delete attributesToInclude[attr];
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
if (Object.keys(attributesToInclude).length > 0) {
|
|
1488
|
+
attributesHtmlStr = Object.entries(attributesToInclude).map(([key, value]) => `${key}=${capTextLength(value, 20)}`).join(" ");
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
const highlightIndicator = node.isNew ? `*[${node.highlightIndex}]` : `[${node.highlightIndex}]`;
|
|
1492
|
+
let line = `${depthStr}${highlightIndicator}<${node.tagName ?? ""}`;
|
|
1493
|
+
if (attributesHtmlStr) {
|
|
1494
|
+
line += ` ${attributesHtmlStr}`;
|
|
1495
|
+
}
|
|
1496
|
+
if (node.extra) {
|
|
1497
|
+
if (node.extra.scrollable) {
|
|
1498
|
+
let scrollDataText = "";
|
|
1499
|
+
if (node.extra.scrollData?.left)
|
|
1500
|
+
scrollDataText += `left=${node.extra.scrollData.left}, `;
|
|
1501
|
+
if (node.extra.scrollData?.top) scrollDataText += `top=${node.extra.scrollData.top}, `;
|
|
1502
|
+
if (node.extra.scrollData?.right)
|
|
1503
|
+
scrollDataText += `right=${node.extra.scrollData.right}, `;
|
|
1504
|
+
if (node.extra.scrollData?.bottom)
|
|
1505
|
+
scrollDataText += `bottom=${node.extra.scrollData.bottom}`;
|
|
1506
|
+
line += ` data-scrollable="${scrollDataText}"`;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
if (text) {
|
|
1510
|
+
const trimmedText = text.trim();
|
|
1511
|
+
if (!attributesHtmlStr) {
|
|
1512
|
+
line += " ";
|
|
1513
|
+
}
|
|
1514
|
+
line += `>${trimmedText}`;
|
|
1515
|
+
} else if (!attributesHtmlStr) {
|
|
1516
|
+
line += " ";
|
|
1517
|
+
}
|
|
1518
|
+
line += " />";
|
|
1519
|
+
result22.push(line);
|
|
1520
|
+
}
|
|
1521
|
+
for (const child of node.children) {
|
|
1522
|
+
processNode(child, nextDepth, result22);
|
|
1523
|
+
}
|
|
1524
|
+
} else if (node.type === "text") {
|
|
1525
|
+
if (hasParentWithHighlightIndex(node)) {
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
if (node.parent && node.parent.type === "element" && node.parent.isVisible && node.parent.isTopElement) {
|
|
1529
|
+
result22.push(`${depthStr}${node.text ?? ""}`);
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
}, "processNode");
|
|
1533
|
+
const result2 = [];
|
|
1534
|
+
processNode(rootNode, 0, result2);
|
|
1535
|
+
return result2.join("\n");
|
|
1536
|
+
}
|
|
1537
|
+
__name(flatTreeToString, "flatTreeToString");
|
|
1538
|
+
const getAllTextTillNextClickableElement = /* @__PURE__ */ __name((node, maxDepth = -1) => {
|
|
1539
|
+
const textParts = [];
|
|
1540
|
+
const collectText = /* @__PURE__ */ __name((currentNode, currentDepth) => {
|
|
1541
|
+
if (maxDepth !== -1 && currentDepth > maxDepth) {
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
if (currentNode.type === "element" && currentNode !== node && currentNode.highlightIndex !== void 0) {
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
if (currentNode.type === "text" && currentNode.text) {
|
|
1548
|
+
textParts.push(currentNode.text);
|
|
1549
|
+
} else if (currentNode.type === "element") {
|
|
1550
|
+
for (const child of currentNode.children) {
|
|
1551
|
+
collectText(child, currentDepth + 1);
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
}, "collectText");
|
|
1555
|
+
collectText(node, 0);
|
|
1556
|
+
return textParts.join("\n").trim();
|
|
1557
|
+
}, "getAllTextTillNextClickableElement");
|
|
1558
|
+
function getSelectorMap(flatTree) {
|
|
1559
|
+
const selectorMap = /* @__PURE__ */ new Map();
|
|
1560
|
+
const keys = Object.keys(flatTree.map);
|
|
1561
|
+
for (const key of keys) {
|
|
1562
|
+
const node = flatTree.map[key];
|
|
1563
|
+
if (node.isInteractive && typeof node.highlightIndex === "number") {
|
|
1564
|
+
selectorMap.set(node.highlightIndex, node);
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
return selectorMap;
|
|
1568
|
+
}
|
|
1569
|
+
__name(getSelectorMap, "getSelectorMap");
|
|
1570
|
+
function getElementTextMap(simplifiedHTML) {
|
|
1571
|
+
const lines = simplifiedHTML.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
1572
|
+
const elementTextMap = /* @__PURE__ */ new Map();
|
|
1573
|
+
for (const line of lines) {
|
|
1574
|
+
const regex = /^\[(\d+)\]<[^>]+>([^<]*)/;
|
|
1575
|
+
const match = regex.exec(line);
|
|
1576
|
+
if (match) {
|
|
1577
|
+
const index = parseInt(match[1], 10);
|
|
1578
|
+
elementTextMap.set(index, line);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
return elementTextMap;
|
|
1582
|
+
}
|
|
1583
|
+
__name(getElementTextMap, "getElementTextMap");
|
|
1584
|
+
function cleanUpHighlights() {
|
|
1585
|
+
const cleanupFunctions = window._highlightCleanupFunctions || [];
|
|
1586
|
+
for (const cleanup of cleanupFunctions) {
|
|
1587
|
+
if (typeof cleanup === "function") {
|
|
1588
|
+
cleanup();
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
window._highlightCleanupFunctions = [];
|
|
1592
|
+
}
|
|
1593
|
+
__name(cleanUpHighlights, "cleanUpHighlights");
|
|
1594
|
+
window.addEventListener("popstate", () => {
|
|
1595
|
+
cleanUpHighlights();
|
|
1596
|
+
});
|
|
1597
|
+
window.addEventListener("hashchange", () => {
|
|
1598
|
+
cleanUpHighlights();
|
|
1599
|
+
});
|
|
1600
|
+
window.addEventListener("beforeunload", () => {
|
|
1601
|
+
cleanUpHighlights();
|
|
1602
|
+
});
|
|
1603
|
+
const navigation = window.navigation;
|
|
1604
|
+
if (navigation && typeof navigation.addEventListener === "function") {
|
|
1605
|
+
navigation.addEventListener("navigate", () => {
|
|
1606
|
+
cleanUpHighlights();
|
|
1607
|
+
});
|
|
1608
|
+
} else {
|
|
1609
|
+
let currentUrl = window.location.href;
|
|
1610
|
+
setInterval(() => {
|
|
1611
|
+
if (window.location.href !== currentUrl) {
|
|
1612
|
+
currentUrl = window.location.href;
|
|
1613
|
+
cleanUpHighlights();
|
|
1614
|
+
}
|
|
1615
|
+
}, 500);
|
|
1616
|
+
}
|
|
1617
|
+
function getPageInfo() {
|
|
1618
|
+
const viewport_width = window.innerWidth;
|
|
1619
|
+
const viewport_height = window.innerHeight;
|
|
1620
|
+
const page_width = Math.max(document.documentElement.scrollWidth, document.body.scrollWidth || 0);
|
|
1621
|
+
const page_height = Math.max(
|
|
1622
|
+
document.documentElement.scrollHeight,
|
|
1623
|
+
document.body.scrollHeight || 0
|
|
1624
|
+
);
|
|
1625
|
+
const scroll_x = window.scrollX || window.pageXOffset || document.documentElement.scrollLeft || 0;
|
|
1626
|
+
const scroll_y = window.scrollY || window.pageYOffset || document.documentElement.scrollTop || 0;
|
|
1627
|
+
const pixels_below = Math.max(0, page_height - (window.innerHeight + scroll_y));
|
|
1628
|
+
const pixels_right = Math.max(0, page_width - (window.innerWidth + scroll_x));
|
|
1629
|
+
return {
|
|
1630
|
+
// Current viewport dimensions
|
|
1631
|
+
viewport_width,
|
|
1632
|
+
viewport_height,
|
|
1633
|
+
// Total page dimensions
|
|
1634
|
+
page_width,
|
|
1635
|
+
page_height,
|
|
1636
|
+
// Current scroll position
|
|
1637
|
+
scroll_x,
|
|
1638
|
+
scroll_y,
|
|
1639
|
+
pixels_above: scroll_y,
|
|
1640
|
+
pixels_below,
|
|
1641
|
+
pages_above: viewport_height > 0 ? scroll_y / viewport_height : 0,
|
|
1642
|
+
pages_below: viewport_height > 0 ? pixels_below / viewport_height : 0,
|
|
1643
|
+
total_pages: viewport_height > 0 ? page_height / viewport_height : 0,
|
|
1644
|
+
current_page_position: scroll_y / Math.max(1, page_height - viewport_height),
|
|
1645
|
+
pixels_left: scroll_x,
|
|
1646
|
+
pixels_right
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
__name(getPageInfo, "getPageInfo");
|
|
1650
|
+
function patchReact(pageController) {
|
|
1651
|
+
const reactRootElements = document.querySelectorAll(
|
|
1652
|
+
'[data-reactroot], [data-reactid], [data-react-checksum], #root, #app, [id^="root-"], [id^="app-"], #adex-wrapper, #adex-root'
|
|
1653
|
+
);
|
|
1654
|
+
for (const element of reactRootElements) {
|
|
1655
|
+
element.setAttribute("data-page-agent-not-interactive", "true");
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
__name(patchReact, "patchReact");
|
|
1659
|
+
const _PageController = class _PageController extends EventTarget {
|
|
1660
|
+
config;
|
|
1661
|
+
/** Corresponds to eval_page in browser-use */
|
|
1662
|
+
flatTree = null;
|
|
1663
|
+
/**
|
|
1664
|
+
* All highlighted index-mapped interactive elements
|
|
1665
|
+
* Corresponds to DOMState.selector_map in browser-use
|
|
1666
|
+
*/
|
|
1667
|
+
selectorMap = /* @__PURE__ */ new Map();
|
|
1668
|
+
/** Index -> element text description mapping */
|
|
1669
|
+
elementTextMap = /* @__PURE__ */ new Map();
|
|
1670
|
+
/**
|
|
1671
|
+
* Simplified HTML for LLM consumption.
|
|
1672
|
+
* Corresponds to clickable_elements_to_string in browser-use
|
|
1673
|
+
*/
|
|
1674
|
+
simplifiedHTML = "<EMPTY>";
|
|
1675
|
+
/** last time the tree was updated */
|
|
1676
|
+
lastTimeUpdate = 0;
|
|
1677
|
+
constructor(config = {}) {
|
|
1678
|
+
super();
|
|
1679
|
+
this.config = config;
|
|
1680
|
+
patchReact();
|
|
1681
|
+
}
|
|
1682
|
+
// ======= State Queries =======
|
|
1683
|
+
/**
|
|
1684
|
+
* Get current page URL
|
|
1685
|
+
*/
|
|
1686
|
+
async getCurrentUrl() {
|
|
1687
|
+
return window.location.href;
|
|
1688
|
+
}
|
|
1689
|
+
/**
|
|
1690
|
+
* Get current page title
|
|
1691
|
+
*/
|
|
1692
|
+
async getPageTitle() {
|
|
1693
|
+
return document.title;
|
|
1694
|
+
}
|
|
1695
|
+
/**
|
|
1696
|
+
* Get page scroll and size info
|
|
1697
|
+
*/
|
|
1698
|
+
async getPageInfo() {
|
|
1699
|
+
return getPageInfo();
|
|
1700
|
+
}
|
|
1701
|
+
/**
|
|
1702
|
+
* Get the simplified HTML representation of the page.
|
|
1703
|
+
* This is used by LLM to understand the page structure.
|
|
1704
|
+
*/
|
|
1705
|
+
async getSimplifiedHTML() {
|
|
1706
|
+
return this.simplifiedHTML;
|
|
1707
|
+
}
|
|
1708
|
+
/**
|
|
1709
|
+
* Get text description for an element by index
|
|
1710
|
+
*/
|
|
1711
|
+
async getElementText(index) {
|
|
1712
|
+
return this.elementTextMap.get(index);
|
|
1713
|
+
}
|
|
1714
|
+
/**
|
|
1715
|
+
* Get total number of indexed interactive elements
|
|
1716
|
+
*/
|
|
1717
|
+
async getElementCount() {
|
|
1718
|
+
return this.selectorMap.size;
|
|
1719
|
+
}
|
|
1720
|
+
/**
|
|
1721
|
+
* Get last tree update timestamp
|
|
1722
|
+
*/
|
|
1723
|
+
async getLastUpdateTime() {
|
|
1724
|
+
return this.lastTimeUpdate;
|
|
1725
|
+
}
|
|
1726
|
+
/**
|
|
1727
|
+
* Get the viewport expansion setting
|
|
1728
|
+
*/
|
|
1729
|
+
async getViewportExpansion() {
|
|
1730
|
+
return this.config.viewportExpansion ?? VIEWPORT_EXPANSION;
|
|
1731
|
+
}
|
|
1732
|
+
// ======= DOM Tree Operations =======
|
|
1733
|
+
/**
|
|
1734
|
+
* Update DOM tree, returns simplified HTML for LLM.
|
|
1735
|
+
* This is the main method to refresh the page state.
|
|
1736
|
+
*/
|
|
1737
|
+
async updateTree() {
|
|
1738
|
+
this.dispatchEvent(new Event("beforeUpdate"));
|
|
1739
|
+
this.lastTimeUpdate = Date.now();
|
|
1740
|
+
cleanUpHighlights();
|
|
1741
|
+
const blacklist = [
|
|
1742
|
+
...this.config.interactiveBlacklist || [],
|
|
1743
|
+
...document.querySelectorAll("[data-page-agent-not-interactive]").values()
|
|
1744
|
+
];
|
|
1745
|
+
this.flatTree = getFlatTree({
|
|
1746
|
+
...this.config,
|
|
1747
|
+
interactiveBlacklist: blacklist
|
|
1748
|
+
});
|
|
1749
|
+
this.simplifiedHTML = flatTreeToString(this.flatTree, this.config.include_attributes);
|
|
1750
|
+
this.selectorMap.clear();
|
|
1751
|
+
this.selectorMap = getSelectorMap(this.flatTree);
|
|
1752
|
+
this.elementTextMap.clear();
|
|
1753
|
+
this.elementTextMap = getElementTextMap(this.simplifiedHTML);
|
|
1754
|
+
this.dispatchEvent(new Event("afterUpdate"));
|
|
1755
|
+
return this.simplifiedHTML;
|
|
1756
|
+
}
|
|
1757
|
+
/**
|
|
1758
|
+
* Clean up all element highlights
|
|
1759
|
+
*/
|
|
1760
|
+
async cleanUpHighlights() {
|
|
1761
|
+
cleanUpHighlights();
|
|
1762
|
+
}
|
|
1763
|
+
// ======= Element Actions =======
|
|
1764
|
+
/**
|
|
1765
|
+
* Click element by index
|
|
1766
|
+
*/
|
|
1767
|
+
async clickElement(index) {
|
|
1768
|
+
try {
|
|
1769
|
+
const element = getElementByIndex(this.selectorMap, index);
|
|
1770
|
+
const elemText = this.elementTextMap.get(index);
|
|
1771
|
+
await clickElement(element);
|
|
1772
|
+
if (element instanceof HTMLAnchorElement && element.target === "_blank") {
|
|
1773
|
+
return {
|
|
1774
|
+
success: true,
|
|
1775
|
+
message: `✅ Clicked element (${elemText ?? index}). ⚠️ Link opens in a new tab. You are not capable of reading new tabs.`
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1778
|
+
return {
|
|
1779
|
+
success: true,
|
|
1780
|
+
message: `✅ Clicked element (${elemText ?? index}).`
|
|
1781
|
+
};
|
|
1782
|
+
} catch (error) {
|
|
1783
|
+
return {
|
|
1784
|
+
success: false,
|
|
1785
|
+
message: `❌ Failed to click element: ${error}`
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
/**
|
|
1790
|
+
* Input text into element by index
|
|
1791
|
+
*/
|
|
1792
|
+
async inputText(index, text) {
|
|
1793
|
+
try {
|
|
1794
|
+
const element = getElementByIndex(this.selectorMap, index);
|
|
1795
|
+
const elemText = this.elementTextMap.get(index);
|
|
1796
|
+
await inputTextElement(element, text);
|
|
1797
|
+
return {
|
|
1798
|
+
success: true,
|
|
1799
|
+
message: `✅ Input text (${text}) into element (${elemText ?? index}).`
|
|
1800
|
+
};
|
|
1801
|
+
} catch (error) {
|
|
1802
|
+
return {
|
|
1803
|
+
success: false,
|
|
1804
|
+
message: `❌ Failed to input text: ${error}`
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
/**
|
|
1809
|
+
* Select dropdown option by index and option text
|
|
1810
|
+
*/
|
|
1811
|
+
async selectOption(index, optionText) {
|
|
1812
|
+
try {
|
|
1813
|
+
const element = getElementByIndex(this.selectorMap, index);
|
|
1814
|
+
const elemText = this.elementTextMap.get(index);
|
|
1815
|
+
await selectOptionElement(element, optionText);
|
|
1816
|
+
return {
|
|
1817
|
+
success: true,
|
|
1818
|
+
message: `✅ Selected option (${optionText}) in element (${elemText ?? index}).`
|
|
1819
|
+
};
|
|
1820
|
+
} catch (error) {
|
|
1821
|
+
return {
|
|
1822
|
+
success: false,
|
|
1823
|
+
message: `❌ Failed to select option: ${error}`
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
/**
|
|
1828
|
+
* Scroll vertically
|
|
1829
|
+
*/
|
|
1830
|
+
async scroll(options) {
|
|
1831
|
+
try {
|
|
1832
|
+
const { down, numPages, pixels, index } = options;
|
|
1833
|
+
const scrollAmount = pixels ?? numPages * (down ? 1 : -1) * window.innerHeight;
|
|
1834
|
+
const element = index !== void 0 ? getElementByIndex(this.selectorMap, index) : null;
|
|
1835
|
+
const message = await scrollVertically(down, scrollAmount, element);
|
|
1836
|
+
return {
|
|
1837
|
+
success: true,
|
|
1838
|
+
message
|
|
1839
|
+
};
|
|
1840
|
+
} catch (error) {
|
|
1841
|
+
return {
|
|
1842
|
+
success: false,
|
|
1843
|
+
message: `❌ Failed to scroll: ${error}`
|
|
1844
|
+
};
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
/**
|
|
1848
|
+
* Scroll horizontally
|
|
1849
|
+
*/
|
|
1850
|
+
async scrollHorizontally(options) {
|
|
1851
|
+
try {
|
|
1852
|
+
const { right, pixels, index } = options;
|
|
1853
|
+
const scrollAmount = pixels * (right ? 1 : -1);
|
|
1854
|
+
const element = index !== void 0 ? getElementByIndex(this.selectorMap, index) : null;
|
|
1855
|
+
const message = await scrollHorizontally(right, scrollAmount, element);
|
|
1856
|
+
return {
|
|
1857
|
+
success: true,
|
|
1858
|
+
message
|
|
1859
|
+
};
|
|
1860
|
+
} catch (error) {
|
|
1861
|
+
return {
|
|
1862
|
+
success: false,
|
|
1863
|
+
message: `❌ Failed to scroll horizontally: ${error}`
|
|
1864
|
+
};
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
/**
|
|
1868
|
+
* Execute arbitrary JavaScript on the page
|
|
1869
|
+
*/
|
|
1870
|
+
async executeJavascript(script) {
|
|
1871
|
+
try {
|
|
1872
|
+
const asyncFunction = eval(`(async () => { ${script} })`);
|
|
1873
|
+
const result = await asyncFunction();
|
|
1874
|
+
return {
|
|
1875
|
+
success: true,
|
|
1876
|
+
message: `✅ Executed JavaScript. Result: ${result}`
|
|
1877
|
+
};
|
|
1878
|
+
} catch (error) {
|
|
1879
|
+
return {
|
|
1880
|
+
success: false,
|
|
1881
|
+
message: `❌ Error executing JavaScript: ${error}`
|
|
1882
|
+
};
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
/**
|
|
1886
|
+
* Dispose and clean up resources
|
|
1887
|
+
*/
|
|
1888
|
+
dispose() {
|
|
1889
|
+
cleanUpHighlights();
|
|
1890
|
+
this.flatTree = null;
|
|
1891
|
+
this.selectorMap.clear();
|
|
1892
|
+
this.elementTextMap.clear();
|
|
1893
|
+
this.simplifiedHTML = "<EMPTY>";
|
|
1894
|
+
}
|
|
1895
|
+
};
|
|
1896
|
+
__name(_PageController, "PageController");
|
|
1897
|
+
let PageController = _PageController;
|
|
1898
|
+
export {
|
|
1899
|
+
PageController
|
|
1900
|
+
};
|
|
1901
|
+
//# sourceMappingURL=page-controller.js.map
|