Duibonduil commited on
Commit
7f0e4cb
·
verified ·
1 Parent(s): ea7486e

Upload buildDomTree.js

Browse files
examples/tools/browsers/buildDomTree.js ADDED
@@ -0,0 +1,1056 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Derived from browser_use https://github.com/browser-use/browser-use/blob/main/browser_use/dom/buildDomTree.js
2
+ (
3
+ args = {
4
+ doHighlightElements: true,
5
+ focusHighlightIndex: -1,
6
+ viewportExpansion: 0,
7
+ debugMode: false,
8
+ }
9
+ ) => {
10
+ const { doHighlightElements, focusHighlightIndex, viewportExpansion, debugMode } = args;
11
+ let highlightIndex = 0; // Reset highlight index
12
+
13
+ // Add timing stack to handle recursion
14
+ const TIMING_STACK = {
15
+ nodeProcessing: [],
16
+ treeTraversal: [],
17
+ highlighting: [],
18
+ current: null
19
+ };
20
+
21
+ function pushTiming(type) {
22
+ TIMING_STACK[type] = TIMING_STACK[type] || [];
23
+ TIMING_STACK[type].push(performance.now());
24
+ }
25
+
26
+ function popTiming(type) {
27
+ const start = TIMING_STACK[type].pop();
28
+ const duration = performance.now() - start;
29
+ return duration;
30
+ }
31
+
32
+ // Only initialize performance tracking if in debug mode
33
+ const PERF_METRICS = debugMode ? {
34
+ buildDomTreeCalls: 0,
35
+ timings: {
36
+ buildDomTree: 0,
37
+ highlightElement: 0,
38
+ isInteractiveElement: 0,
39
+ isElementVisible: 0,
40
+ isTopElement: 0,
41
+ isInExpandedViewport: 0,
42
+ isTextNodeVisible: 0,
43
+ getEffectiveScroll: 0,
44
+ },
45
+ cacheMetrics: {
46
+ boundingRectCacheHits: 0,
47
+ boundingRectCacheMisses: 0,
48
+ computedStyleCacheHits: 0,
49
+ computedStyleCacheMisses: 0,
50
+ getBoundingClientRectTime: 0,
51
+ getComputedStyleTime: 0,
52
+ boundingRectHitRate: 0,
53
+ computedStyleHitRate: 0,
54
+ overallHitRate: 0,
55
+ },
56
+ nodeMetrics: {
57
+ totalNodes: 0,
58
+ processedNodes: 0,
59
+ skippedNodes: 0,
60
+ },
61
+ buildDomTreeBreakdown: {
62
+ totalTime: 0,
63
+ totalSelfTime: 0,
64
+ buildDomTreeCalls: 0,
65
+ domOperations: {
66
+ getBoundingClientRect: 0,
67
+ getComputedStyle: 0,
68
+ },
69
+ domOperationCounts: {
70
+ getBoundingClientRect: 0,
71
+ getComputedStyle: 0,
72
+ }
73
+ }
74
+ } : null;
75
+
76
+ // Simple timing helper that only runs in debug mode
77
+ function measureTime(fn) {
78
+ if (!debugMode) return fn;
79
+ return function (...args) {
80
+ const start = performance.now();
81
+ const result = fn.apply(this, args);
82
+ const duration = performance.now() - start;
83
+ return result;
84
+ };
85
+ }
86
+
87
+ // Helper to measure DOM operations
88
+ function measureDomOperation(operation, name) {
89
+ if (!debugMode) return operation();
90
+
91
+ const start = performance.now();
92
+ const result = operation();
93
+ const duration = performance.now() - start;
94
+
95
+ if (PERF_METRICS && name in PERF_METRICS.buildDomTreeBreakdown.domOperations) {
96
+ PERF_METRICS.buildDomTreeBreakdown.domOperations[name] += duration;
97
+ PERF_METRICS.buildDomTreeBreakdown.domOperationCounts[name]++;
98
+ }
99
+
100
+ return result;
101
+ }
102
+
103
+ // Add caching mechanisms at the top level
104
+ const DOM_CACHE = {
105
+ boundingRects: new WeakMap(),
106
+ computedStyles: new WeakMap(),
107
+ clearCache: () => {
108
+ DOM_CACHE.boundingRects = new WeakMap();
109
+ DOM_CACHE.computedStyles = new WeakMap();
110
+ }
111
+ };
112
+
113
+ // Cache helper functions
114
+ function getCachedBoundingRect(element) {
115
+ if (!element) return null;
116
+
117
+ if (DOM_CACHE.boundingRects.has(element)) {
118
+ if (debugMode && PERF_METRICS) {
119
+ PERF_METRICS.cacheMetrics.boundingRectCacheHits++;
120
+ }
121
+ return DOM_CACHE.boundingRects.get(element);
122
+ }
123
+
124
+ if (debugMode && PERF_METRICS) {
125
+ PERF_METRICS.cacheMetrics.boundingRectCacheMisses++;
126
+ }
127
+
128
+ let rect;
129
+ if (debugMode) {
130
+ const start = performance.now();
131
+ rect = element.getBoundingClientRect();
132
+ const duration = performance.now() - start;
133
+ if (PERF_METRICS) {
134
+ PERF_METRICS.buildDomTreeBreakdown.domOperations.getBoundingClientRect += duration;
135
+ PERF_METRICS.buildDomTreeBreakdown.domOperationCounts.getBoundingClientRect++;
136
+ }
137
+ } else {
138
+ rect = element.getBoundingClientRect();
139
+ }
140
+
141
+ if (rect) {
142
+ DOM_CACHE.boundingRects.set(element, rect);
143
+ }
144
+ return rect;
145
+ }
146
+
147
+ function getCachedComputedStyle(element) {
148
+ if (!element) return null;
149
+
150
+ if (DOM_CACHE.computedStyles.has(element)) {
151
+ if (debugMode && PERF_METRICS) {
152
+ PERF_METRICS.cacheMetrics.computedStyleCacheHits++;
153
+ }
154
+ return DOM_CACHE.computedStyles.get(element);
155
+ }
156
+
157
+ if (debugMode && PERF_METRICS) {
158
+ PERF_METRICS.cacheMetrics.computedStyleCacheMisses++;
159
+ }
160
+
161
+ let style;
162
+ if (debugMode) {
163
+ const start = performance.now();
164
+ style = window.getComputedStyle(element);
165
+ const duration = performance.now() - start;
166
+ if (PERF_METRICS) {
167
+ PERF_METRICS.buildDomTreeBreakdown.domOperations.getComputedStyle += duration;
168
+ PERF_METRICS.buildDomTreeBreakdown.domOperationCounts.getComputedStyle++;
169
+ }
170
+ } else {
171
+ style = window.getComputedStyle(element);
172
+ }
173
+
174
+ if (style) {
175
+ DOM_CACHE.computedStyles.set(element, style);
176
+ }
177
+ return style;
178
+ }
179
+
180
+ /**
181
+ * Hash map of DOM nodes indexed by their highlight index.
182
+ *
183
+ * @type {Object<string, any>}
184
+ */
185
+ const DOM_HASH_MAP = {};
186
+
187
+ const ID = { current: 0 };
188
+
189
+ const HIGHLIGHT_CONTAINER_ID = "playwright-highlight-container";
190
+
191
+ /**
192
+ * Highlights an element in the DOM and returns the index of the next element.
193
+ */
194
+ function highlightElement(element, index, parentIframe = null) {
195
+ if (!element) return index;
196
+
197
+ try {
198
+ // Create or get highlight container
199
+ let container = document.getElementById(HIGHLIGHT_CONTAINER_ID);
200
+ if (!container) {
201
+ container = document.createElement("div");
202
+ container.id = HIGHLIGHT_CONTAINER_ID;
203
+ container.style.position = "fixed";
204
+ container.style.pointerEvents = "none";
205
+ container.style.top = "0";
206
+ container.style.left = "0";
207
+ container.style.width = "100%";
208
+ container.style.height = "100%";
209
+ container.style.zIndex = "2147483647";
210
+ document.body.appendChild(container);
211
+ }
212
+
213
+ // Get element position
214
+ const rect = measureDomOperation(
215
+ () => element.getBoundingClientRect(),
216
+ 'getBoundingClientRect'
217
+ );
218
+
219
+ if (!rect) return index;
220
+
221
+ // Generate a color based on the index
222
+ const colors = [
223
+ "#FF0000",
224
+ "#00FF00",
225
+ "#0000FF",
226
+ "#FFA500",
227
+ "#800080",
228
+ "#008080",
229
+ "#FF69B4",
230
+ "#4B0082",
231
+ "#FF4500",
232
+ "#2E8B57",
233
+ "#DC143C",
234
+ "#4682B4",
235
+ ];
236
+ const colorIndex = index % colors.length;
237
+ const baseColor = colors[colorIndex];
238
+ const backgroundColor = baseColor + "1A"; // 10% opacity version of the color
239
+
240
+ // Create highlight overlay
241
+ const overlay = document.createElement("div");
242
+ overlay.style.position = "fixed";
243
+ overlay.style.border = `2px solid ${baseColor}`;
244
+ overlay.style.backgroundColor = backgroundColor;
245
+ overlay.style.pointerEvents = "none";
246
+ overlay.style.boxSizing = "border-box";
247
+
248
+ // Get element position
249
+ let iframeOffset = { x: 0, y: 0 };
250
+
251
+ // If element is in an iframe, calculate iframe offset
252
+ if (parentIframe) {
253
+ const iframeRect = parentIframe.getBoundingClientRect();
254
+ iframeOffset.x = iframeRect.left;
255
+ iframeOffset.y = iframeRect.top;
256
+ }
257
+
258
+ // Calculate position
259
+ const top = rect.top + iframeOffset.y;
260
+ const left = rect.left + iframeOffset.x;
261
+
262
+ overlay.style.top = `${top}px`;
263
+ overlay.style.left = `${left}px`;
264
+ overlay.style.width = `${rect.width}px`;
265
+ overlay.style.height = `${rect.height}px`;
266
+
267
+ // Create and position label
268
+ const label = document.createElement("div");
269
+ label.className = "playwright-highlight-label";
270
+ label.style.position = "fixed";
271
+ label.style.background = baseColor;
272
+ label.style.color = "white";
273
+ label.style.padding = "1px 4px";
274
+ label.style.borderRadius = "4px";
275
+ label.style.fontSize = `${Math.min(12, Math.max(8, rect.height / 2))}px`;
276
+ label.textContent = index;
277
+
278
+ const labelWidth = 20;
279
+ const labelHeight = 16;
280
+
281
+ let labelTop = top + 2;
282
+ let labelLeft = left + rect.width - labelWidth - 2;
283
+
284
+ if (rect.width < labelWidth + 4 || rect.height < labelHeight + 4) {
285
+ labelTop = top - labelHeight - 2;
286
+ labelLeft = left + rect.width - labelWidth;
287
+ }
288
+
289
+ label.style.top = `${labelTop}px`;
290
+ label.style.left = `${labelLeft}px`;
291
+
292
+ // Add to container
293
+ container.appendChild(overlay);
294
+ container.appendChild(label);
295
+
296
+ // Update positions on scroll
297
+ const updatePositions = () => {
298
+ const newRect = element.getBoundingClientRect();
299
+ let newIframeOffset = { x: 0, y: 0 };
300
+
301
+ if (parentIframe) {
302
+ const iframeRect = parentIframe.getBoundingClientRect();
303
+ newIframeOffset.x = iframeRect.left;
304
+ newIframeOffset.y = iframeRect.top;
305
+ }
306
+
307
+ const newTop = newRect.top + newIframeOffset.y;
308
+ const newLeft = newRect.left + newIframeOffset.x;
309
+
310
+ overlay.style.top = `${newTop}px`;
311
+ overlay.style.left = `${newLeft}px`;
312
+ overlay.style.width = `${newRect.width}px`;
313
+ overlay.style.height = `${newRect.height}px`;
314
+
315
+ let newLabelTop = newTop + 2;
316
+ let newLabelLeft = newLeft + newRect.width - labelWidth - 2;
317
+
318
+ if (newRect.width < labelWidth + 4 || newRect.height < labelHeight + 4) {
319
+ newLabelTop = newTop - labelHeight - 2;
320
+ newLabelLeft = newLeft + newRect.width - labelWidth;
321
+ }
322
+
323
+ label.style.top = `${newLabelTop}px`;
324
+ label.style.left = `${newLabelLeft}px`;
325
+ };
326
+
327
+ window.addEventListener('scroll', updatePositions);
328
+ window.addEventListener('resize', updatePositions);
329
+
330
+ return index + 1;
331
+ } finally {
332
+ popTiming('highlighting');
333
+ }
334
+ }
335
+
336
+ /**
337
+ * Returns an XPath tree string for an element.
338
+ */
339
+ function getXPathTree(element, stopAtBoundary = true) {
340
+ const segments = [];
341
+ let currentElement = element;
342
+
343
+ while (currentElement && currentElement.nodeType === Node.ELEMENT_NODE) {
344
+ // Stop if we hit a shadow root or iframe
345
+ if (
346
+ stopAtBoundary &&
347
+ (currentElement.parentNode instanceof ShadowRoot ||
348
+ currentElement.parentNode instanceof HTMLIFrameElement)
349
+ ) {
350
+ break;
351
+ }
352
+
353
+ let index = 0;
354
+ let sibling = currentElement.previousSibling;
355
+ while (sibling) {
356
+ if (
357
+ sibling.nodeType === Node.ELEMENT_NODE &&
358
+ sibling.nodeName === currentElement.nodeName
359
+ ) {
360
+ index++;
361
+ }
362
+ sibling = sibling.previousSibling;
363
+ }
364
+
365
+ const tagName = currentElement.nodeName.toLowerCase();
366
+ const xpathIndex = index > 0 ? `[${index + 1}]` : "";
367
+ segments.unshift(`${tagName}${xpathIndex}`);
368
+
369
+ currentElement = currentElement.parentNode;
370
+ }
371
+
372
+ return segments.join("/");
373
+ }
374
+
375
+ /**
376
+ * Checks if a text node is visible.
377
+ */
378
+ function isTextNodeVisible(textNode) {
379
+ try {
380
+ const range = document.createRange();
381
+ range.selectNodeContents(textNode);
382
+ const rect = range.getBoundingClientRect();
383
+
384
+ // Simple size check
385
+ if (rect.width === 0 || rect.height === 0) {
386
+ return false;
387
+ }
388
+
389
+ // Simple viewport check without scroll calculations
390
+ const isInViewport = !(
391
+ rect.bottom < -viewportExpansion ||
392
+ rect.top > window.innerHeight + viewportExpansion ||
393
+ rect.right < -viewportExpansion ||
394
+ rect.left > window.innerWidth + viewportExpansion
395
+ );
396
+
397
+ // Check parent visibility
398
+ const parentElement = textNode.parentElement;
399
+ if (!parentElement) return false;
400
+
401
+ try {
402
+ return isInViewport && parentElement.checkVisibility({
403
+ checkOpacity: true,
404
+ checkVisibilityCSS: true,
405
+ });
406
+ } catch (e) {
407
+ // Fallback if checkVisibility is not supported
408
+ const style = window.getComputedStyle(parentElement);
409
+ return isInViewport &&
410
+ style.display !== 'none' &&
411
+ style.visibility !== 'hidden' &&
412
+ style.opacity !== '0';
413
+ }
414
+ } catch (e) {
415
+ console.warn('Error checking text node visibility:', e);
416
+ return false;
417
+ }
418
+ }
419
+
420
+ // Helper function to check if element is accepted
421
+ function isElementAccepted(element) {
422
+ if (!element || !element.tagName) return false;
423
+
424
+ // Always accept body and common container elements
425
+ const alwaysAccept = new Set([
426
+ "body", "div", "main", "article", "section", "nav", "header", "footer"
427
+ ]);
428
+ const tagName = element.tagName.toLowerCase();
429
+
430
+ if (alwaysAccept.has(tagName)) return true;
431
+
432
+ const leafElementDenyList = new Set([
433
+ "svg",
434
+ "script",
435
+ "style",
436
+ "link",
437
+ "meta",
438
+ "noscript",
439
+ "template",
440
+ ]);
441
+
442
+ return !leafElementDenyList.has(tagName);
443
+ }
444
+
445
+ /**
446
+ * Checks if an element is visible.
447
+ */
448
+ function isElementVisible(element) {
449
+ const style = getCachedComputedStyle(element);
450
+ return (
451
+ element.offsetWidth > 0 &&
452
+ element.offsetHeight > 0 &&
453
+ style.visibility !== "hidden" &&
454
+ style.display !== "none"
455
+ );
456
+ }
457
+
458
+ /**
459
+ * Checks if an element is interactive.
460
+ */
461
+ function isInteractiveElement(element) {
462
+ if (!element || element.nodeType !== Node.ELEMENT_NODE) {
463
+ return false;
464
+ }
465
+
466
+ // Special handling for cookie banner elements
467
+ const isCookieBannerElement =
468
+ (typeof element.closest === 'function') && (
469
+ element.closest('[id*="onetrust"]') ||
470
+ element.closest('[class*="onetrust"]') ||
471
+ element.closest('[data-nosnippet="true"]') ||
472
+ element.closest('[aria-label*="cookie"]')
473
+ );
474
+
475
+ if (isCookieBannerElement) {
476
+ // Check if it's a button or interactive element within the banner
477
+ if (
478
+ element.tagName.toLowerCase() === 'button' ||
479
+ element.getAttribute('role') === 'button' ||
480
+ element.onclick ||
481
+ element.getAttribute('onclick') ||
482
+ (element.classList && (
483
+ element.classList.contains('ot-sdk-button') ||
484
+ element.classList.contains('accept-button') ||
485
+ element.classList.contains('reject-button')
486
+ )) ||
487
+ element.getAttribute('aria-label')?.toLowerCase().includes('accept') ||
488
+ element.getAttribute('aria-label')?.toLowerCase().includes('reject')
489
+ ) {
490
+ return true;
491
+ }
492
+ }
493
+
494
+ // Base interactive elements and roles
495
+ const interactiveElements = new Set([
496
+ "a", "button", "details", "embed", "input", "menu", "menuitem",
497
+ "object", "select", "textarea", "canvas", "summary", "dialog",
498
+ "banner"
499
+ ]);
500
+
501
+ const interactiveRoles = new Set(['button-icon', 'dialog', 'button-text-icon-only', 'treeitem', 'alert', 'grid', 'progressbar', 'radio', 'checkbox', 'menuitem', 'option', 'switch', 'dropdown', 'scrollbar', 'combobox', 'a-button-text', 'button', 'region', 'textbox', 'tabpanel', 'tab', 'click', 'button-text', 'spinbutton', 'a-button-inner', 'link', 'menu', 'slider', 'listbox', 'a-dropdown-button', 'button-icon-only', 'searchbox', 'menuitemradio', 'tooltip', 'tree', 'menuitemcheckbox']);
502
+
503
+ const tagName = element.tagName.toLowerCase();
504
+ const role = element.getAttribute("role");
505
+ const ariaRole = element.getAttribute("aria-role");
506
+ const tabIndex = element.getAttribute("tabindex");
507
+
508
+ // Add check for specific class
509
+ const hasAddressInputClass = element.classList && (
510
+ element.classList.contains("address-input__container__input") ||
511
+ element.classList.contains("nav-btn") ||
512
+ element.classList.contains("pull-left")
513
+ );
514
+
515
+ // Added enhancement to capture dropdown interactive elements
516
+ if (element.classList && (
517
+ element.classList.contains('dropdown-toggle') ||
518
+ element.getAttribute('data-toggle') === 'dropdown' ||
519
+ element.getAttribute('aria-haspopup') === 'true'
520
+ )) {
521
+ return true;
522
+ }
523
+
524
+ // Basic role/attribute checks
525
+ const hasInteractiveRole =
526
+ hasAddressInputClass ||
527
+ interactiveElements.has(tagName) ||
528
+ interactiveRoles.has(role) ||
529
+ interactiveRoles.has(ariaRole) ||
530
+ (tabIndex !== null &&
531
+ tabIndex !== "-1" &&
532
+ element.parentElement?.tagName.toLowerCase() !== "body") ||
533
+ element.getAttribute("data-action") === "a-dropdown-select" ||
534
+ element.getAttribute("data-action") === "a-dropdown-button";
535
+
536
+ if (hasInteractiveRole) return true;
537
+
538
+ // Additional checks for cookie banners and consent UI
539
+ const isCookieBanner =
540
+ element.id?.toLowerCase().includes('cookie') ||
541
+ element.id?.toLowerCase().includes('consent') ||
542
+ element.id?.toLowerCase().includes('notice') ||
543
+ (element.classList && (
544
+ element.classList.contains('otCenterRounded') ||
545
+ element.classList.contains('ot-sdk-container')
546
+ )) ||
547
+ element.getAttribute('data-nosnippet') === 'true' ||
548
+ element.getAttribute('aria-label')?.toLowerCase().includes('cookie') ||
549
+ element.getAttribute('aria-label')?.toLowerCase().includes('consent') ||
550
+ (element.tagName.toLowerCase() === 'div' && (
551
+ element.id?.includes('onetrust') ||
552
+ (element.classList && (
553
+ element.classList.contains('onetrust') ||
554
+ element.classList.contains('cookie') ||
555
+ element.classList.contains('consent')
556
+ ))
557
+ ));
558
+
559
+ if (isCookieBanner) return true;
560
+
561
+ // Additional check for buttons in cookie banners
562
+ const isInCookieBanner = typeof element.closest === 'function' && element.closest(
563
+ '[id*="cookie"],[id*="consent"],[class*="cookie"],[class*="consent"],[id*="onetrust"]'
564
+ );
565
+
566
+ if (isInCookieBanner && (
567
+ element.tagName.toLowerCase() === 'button' ||
568
+ element.getAttribute('role') === 'button' ||
569
+ (element.classList && element.classList.contains('button')) ||
570
+ element.onclick ||
571
+ element.getAttribute('onclick')
572
+ )) {
573
+ return true;
574
+ }
575
+
576
+ // Get computed style
577
+ const style = window.getComputedStyle(element);
578
+
579
+ // Check for event listeners
580
+ const hasClickHandler =
581
+ element.onclick !== null ||
582
+ element.getAttribute("onclick") !== null ||
583
+ element.hasAttribute("ng-click") ||
584
+ element.hasAttribute("@click") ||
585
+ element.hasAttribute("v-on:click");
586
+
587
+ // Helper function to safely get event listeners
588
+ function getEventListeners(el) {
589
+ try {
590
+ return window.getEventListeners?.(el) || {};
591
+ } catch (e) {
592
+ const listeners = {};
593
+ const eventTypes = [
594
+ "click",
595
+ "mousedown",
596
+ "mouseup",
597
+ "touchstart",
598
+ "touchend",
599
+ "keydown",
600
+ "keyup",
601
+ "focus",
602
+ "blur",
603
+ ];
604
+
605
+ for (const type of eventTypes) {
606
+ const handler = el[`on${type}`];
607
+ if (handler) {
608
+ listeners[type] = [{ listener: handler, useCapture: false }];
609
+ }
610
+ }
611
+ return listeners;
612
+ }
613
+ }
614
+
615
+ // Check for click-related events
616
+ const listeners = getEventListeners(element);
617
+ const hasClickListeners =
618
+ listeners &&
619
+ (listeners.click?.length > 0 ||
620
+ listeners.mousedown?.length > 0 ||
621
+ listeners.mouseup?.length > 0 ||
622
+ listeners.touchstart?.length > 0 ||
623
+ listeners.touchend?.length > 0);
624
+
625
+ // Check for ARIA properties
626
+ const hasAriaProps =
627
+ element.hasAttribute("aria-expanded") ||
628
+ element.hasAttribute("aria-pressed") ||
629
+ element.hasAttribute("aria-selected") ||
630
+ element.hasAttribute("aria-checked");
631
+
632
+ const isContentEditable = element.getAttribute("contenteditable") === "true" ||
633
+ element.isContentEditable ||
634
+ element.id === "tinymce" ||
635
+ element.classList.contains("mce-content-body") ||
636
+ (element.tagName.toLowerCase() === "body" && element.getAttribute("data-id")?.startsWith("mce_"));
637
+
638
+ // Check if element is draggable
639
+ const isDraggable =
640
+ element.draggable || element.getAttribute("draggable") === "true";
641
+
642
+ return (
643
+ hasAriaProps ||
644
+ hasClickHandler ||
645
+ hasClickListeners ||
646
+ isDraggable ||
647
+ isContentEditable
648
+ );
649
+ }
650
+
651
+ /**
652
+ * Checks if an element is the topmost element at its position.
653
+ */
654
+ function isTopElement(element) {
655
+ const rect = getCachedBoundingRect(element);
656
+
657
+ // If element is not in viewport, consider it top
658
+ const isInViewport = (
659
+ rect.left < window.innerWidth &&
660
+ rect.right > 0 &&
661
+ rect.top < window.innerHeight &&
662
+ rect.bottom > 0
663
+ );
664
+
665
+ if (!isInViewport) {
666
+ return true;
667
+ }
668
+
669
+ // Find the correct document context and root element
670
+ let doc = element.ownerDocument;
671
+
672
+ // If we're in an iframe, elements are considered top by default
673
+ if (doc !== window.document) {
674
+ return true;
675
+ }
676
+
677
+ // For shadow DOM, we need to check within its own root context
678
+ const shadowRoot = element.getRootNode();
679
+ if (shadowRoot instanceof ShadowRoot) {
680
+ const centerX = rect.left + rect.width / 2;
681
+ const centerY = rect.top + rect.height / 2;
682
+
683
+ try {
684
+ const topEl = measureDomOperation(
685
+ () => shadowRoot.elementFromPoint(centerX, centerY),
686
+ 'elementFromPoint'
687
+ );
688
+ if (!topEl) return false;
689
+
690
+ let current = topEl;
691
+ while (current && current !== shadowRoot) {
692
+ if (current === element) return true;
693
+ current = current.parentElement;
694
+ }
695
+ return false;
696
+ } catch (e) {
697
+ return true;
698
+ }
699
+ }
700
+
701
+ // For elements in viewport, check if they're topmost
702
+ const centerX = rect.left + rect.width / 2;
703
+ const centerY = rect.top + rect.height / 2;
704
+
705
+ try {
706
+ const topEl = document.elementFromPoint(centerX, centerY);
707
+ if (!topEl) return false;
708
+
709
+ let current = topEl;
710
+ while (current && current !== document.documentElement) {
711
+ if (current === element) return true;
712
+ current = current.parentElement;
713
+ }
714
+ return false;
715
+ } catch (e) {
716
+ return true;
717
+ }
718
+ }
719
+
720
+ /**
721
+ * Checks if an element is within the expanded viewport.
722
+ */
723
+ function isInExpandedViewport(element, viewportExpansion) {
724
+ if (viewportExpansion === -1) {
725
+ return true;
726
+ }
727
+
728
+ const rect = getCachedBoundingRect(element);
729
+
730
+ // Simple viewport check without scroll calculations
731
+ return !(
732
+ rect.bottom < -viewportExpansion ||
733
+ rect.top > window.innerHeight + viewportExpansion ||
734
+ rect.right < -viewportExpansion ||
735
+ rect.left > window.innerWidth + viewportExpansion
736
+ );
737
+ }
738
+
739
+ // Add this new helper function
740
+ function getEffectiveScroll(element) {
741
+ let currentEl = element;
742
+ let scrollX = 0;
743
+ let scrollY = 0;
744
+
745
+ return measureDomOperation(() => {
746
+ while (currentEl && currentEl !== document.documentElement) {
747
+ if (currentEl.scrollLeft || currentEl.scrollTop) {
748
+ scrollX += currentEl.scrollLeft;
749
+ scrollY += currentEl.scrollTop;
750
+ }
751
+ currentEl = currentEl.parentElement;
752
+ }
753
+
754
+ scrollX += window.scrollX;
755
+ scrollY += window.scrollY;
756
+
757
+ return { scrollX, scrollY };
758
+ }, 'scrollOperations');
759
+ }
760
+
761
+ // Add these helper functions at the top level
762
+ function isInteractiveCandidate(element) {
763
+ if (!element || element.nodeType !== Node.ELEMENT_NODE) return false;
764
+
765
+ const tagName = element.tagName.toLowerCase();
766
+
767
+ // Fast-path for common interactive elements
768
+ const interactiveElements = new Set([
769
+ "a", "button", "input", "select", "textarea", "details", "summary"
770
+ ]);
771
+
772
+ if (interactiveElements.has(tagName)) return true;
773
+
774
+ // Quick attribute checks without getting full lists
775
+ const hasQuickInteractiveAttr = element.hasAttribute("onclick") ||
776
+ element.hasAttribute("role") ||
777
+ element.hasAttribute("tabindex") ||
778
+ element.hasAttribute("aria-") ||
779
+ element.hasAttribute("data-action");
780
+
781
+ return hasQuickInteractiveAttr;
782
+ }
783
+
784
+ function quickVisibilityCheck(element) {
785
+ // Fast initial check before expensive getComputedStyle
786
+ return element.offsetWidth > 0 &&
787
+ element.offsetHeight > 0 &&
788
+ !element.hasAttribute("hidden") &&
789
+ element.style.display !== "none" &&
790
+ element.style.visibility !== "hidden";
791
+ }
792
+
793
+ /**
794
+ * Creates a node data object for a given node and its descendants.
795
+ */
796
+ function buildDomTree(node, parentIframe = null) {
797
+ if (debugMode) PERF_METRICS.nodeMetrics.totalNodes++;
798
+
799
+ if (!node || node.id === HIGHLIGHT_CONTAINER_ID) {
800
+ if (debugMode) PERF_METRICS.nodeMetrics.skippedNodes++;
801
+ return null;
802
+ }
803
+
804
+ // Special handling for root node (body)
805
+ if (node === document.body) {
806
+ const nodeData = {
807
+ tagName: 'body',
808
+ attributes: {},
809
+ xpath: '/body',
810
+ children: [],
811
+ };
812
+
813
+ // Process children of body
814
+ for (const child of node.childNodes) {
815
+ const domElement = buildDomTree(child, parentIframe);
816
+ if (domElement) nodeData.children.push(domElement);
817
+ }
818
+
819
+ const id = `${ID.current++}`;
820
+ DOM_HASH_MAP[id] = nodeData;
821
+ if (debugMode) PERF_METRICS.nodeMetrics.processedNodes++;
822
+ return id;
823
+ }
824
+
825
+ // Early bailout for non-element nodes except text
826
+ if (node.nodeType !== Node.ELEMENT_NODE && node.nodeType !== Node.TEXT_NODE) {
827
+ if (debugMode) PERF_METRICS.nodeMetrics.skippedNodes++;
828
+ return null;
829
+ }
830
+
831
+ // Process text nodes
832
+ if (node.nodeType === Node.TEXT_NODE) {
833
+ const textContent = node.textContent.trim();
834
+ if (!textContent) {
835
+ if (debugMode) PERF_METRICS.nodeMetrics.skippedNodes++;
836
+ return null;
837
+ }
838
+
839
+ // Only check visibility for text nodes that might be visible
840
+ const parentElement = node.parentElement;
841
+ if (!parentElement || parentElement.tagName.toLowerCase() === 'script') {
842
+ if (debugMode) PERF_METRICS.nodeMetrics.skippedNodes++;
843
+ return null;
844
+ }
845
+
846
+ const id = `${ID.current++}`;
847
+ DOM_HASH_MAP[id] = {
848
+ type: "TEXT_NODE",
849
+ text: textContent,
850
+ isVisible: isTextNodeVisible(node),
851
+ };
852
+ if (debugMode) PERF_METRICS.nodeMetrics.processedNodes++;
853
+ return id;
854
+ }
855
+
856
+ // Quick checks for element nodes
857
+ if (node.nodeType === Node.ELEMENT_NODE && !isElementAccepted(node)) {
858
+ if (debugMode) PERF_METRICS.nodeMetrics.skippedNodes++;
859
+ return null;
860
+ }
861
+
862
+ // Early viewport check - only filter out elements clearly outside viewport
863
+ if (viewportExpansion !== -1) {
864
+ const rect = getCachedBoundingRect(node);
865
+ const style = getCachedComputedStyle(node);
866
+
867
+ // Skip viewport check for fixed/sticky elements as they may appear anywhere
868
+ const isFixedOrSticky = style && (style.position === 'fixed' || style.position === 'sticky');
869
+
870
+ // Check if element has actual dimensions
871
+ const hasSize = node.offsetWidth > 0 || node.offsetHeight > 0;
872
+
873
+ if (!rect || (!isFixedOrSticky && !hasSize && (
874
+ rect.bottom < -viewportExpansion ||
875
+ rect.top > window.innerHeight + viewportExpansion ||
876
+ rect.right < -viewportExpansion ||
877
+ rect.left > window.innerWidth + viewportExpansion
878
+ ))) {
879
+ if (debugMode) PERF_METRICS.nodeMetrics.skippedNodes++;
880
+ return null;
881
+ }
882
+ }
883
+
884
+ // Process element node
885
+ const nodeData = {
886
+ tagName: node.tagName.toLowerCase(),
887
+ attributes: {},
888
+ xpath: getXPathTree(node, true),
889
+ children: [],
890
+ };
891
+
892
+ // Get attributes for interactive elements or potential text containers
893
+ if (isInteractiveCandidate(node) || node.tagName.toLowerCase() === 'iframe' || node.tagName.toLowerCase() === 'body') {
894
+ const attributeNames = node.getAttributeNames?.() || [];
895
+ for (const name of attributeNames) {
896
+ nodeData.attributes[name] = node.getAttribute(name);
897
+ }
898
+ }
899
+
900
+ // if (isInteractiveCandidate(node)) {
901
+
902
+ // Check interactivity
903
+ if (node.nodeType === Node.ELEMENT_NODE) {
904
+ nodeData.isVisible = isElementVisible(node);
905
+ if (nodeData.isVisible) {
906
+ nodeData.isTopElement = isTopElement(node);
907
+ if (nodeData.isTopElement) {
908
+ nodeData.isInteractive = isInteractiveElement(node);
909
+ if (nodeData.isInteractive) {
910
+ nodeData.isInViewport = true;
911
+ nodeData.highlightIndex = highlightIndex++;
912
+
913
+ if (doHighlightElements) {
914
+ if (focusHighlightIndex >= 0) {
915
+ if (focusHighlightIndex === nodeData.highlightIndex) {
916
+ highlightElement(node, nodeData.highlightIndex, parentIframe);
917
+ }
918
+ } else {
919
+ highlightElement(node, nodeData.highlightIndex, parentIframe);
920
+ }
921
+ }
922
+ }
923
+ }
924
+ }
925
+ }
926
+
927
+ // Process children, with special handling for iframes and rich text editors
928
+ if (node.tagName) {
929
+ const tagName = node.tagName.toLowerCase();
930
+
931
+ // Handle iframes
932
+ if (tagName === "iframe") {
933
+ try {
934
+ const iframeDoc = node.contentDocument || node.contentWindow?.document;
935
+ if (iframeDoc) {
936
+ for (const child of iframeDoc.childNodes) {
937
+ const domElement = buildDomTree(child, node);
938
+ if (domElement) nodeData.children.push(domElement);
939
+ }
940
+ }
941
+ } catch (e) {
942
+ console.warn("Unable to access iframe:", e);
943
+ }
944
+ }
945
+ // Handle rich text editors and contenteditable elements
946
+ else if (
947
+ node.isContentEditable ||
948
+ node.getAttribute("contenteditable") === "true" ||
949
+ node.id === "tinymce" ||
950
+ node.classList.contains("mce-content-body") ||
951
+ (tagName === "body" && node.getAttribute("data-id")?.startsWith("mce_"))
952
+ ) {
953
+ // Process all child nodes to capture formatted text
954
+ for (const child of node.childNodes) {
955
+ const domElement = buildDomTree(child, parentIframe);
956
+ if (domElement) nodeData.children.push(domElement);
957
+ }
958
+ }
959
+ // Handle shadow DOM
960
+ else if (node.shadowRoot) {
961
+ nodeData.shadowRoot = true;
962
+ for (const child of node.shadowRoot.childNodes) {
963
+ const domElement = buildDomTree(child, parentIframe);
964
+ if (domElement) nodeData.children.push(domElement);
965
+ }
966
+ }
967
+ // Handle regular elements
968
+ else {
969
+ for (const child of node.childNodes) {
970
+ const domElement = buildDomTree(child, parentIframe);
971
+ if (domElement) nodeData.children.push(domElement);
972
+ }
973
+ }
974
+ }
975
+
976
+ // Skip empty anchor tags
977
+ if (nodeData.tagName === 'a' && nodeData.children.length === 0 && !nodeData.attributes.href) {
978
+ if (debugMode) PERF_METRICS.nodeMetrics.skippedNodes++;
979
+ return null;
980
+ }
981
+
982
+ const id = `${ID.current++}`;
983
+ DOM_HASH_MAP[id] = nodeData;
984
+ if (debugMode) PERF_METRICS.nodeMetrics.processedNodes++;
985
+ return id;
986
+ }
987
+
988
+ // After all functions are defined, wrap them with performance measurement
989
+ // Remove buildDomTree from here as we measure it separately
990
+ highlightElement = measureTime(highlightElement);
991
+ isInteractiveElement = measureTime(isInteractiveElement);
992
+ isElementVisible = measureTime(isElementVisible);
993
+ isTopElement = measureTime(isTopElement);
994
+ isInExpandedViewport = measureTime(isInExpandedViewport);
995
+ isTextNodeVisible = measureTime(isTextNodeVisible);
996
+ getEffectiveScroll = measureTime(getEffectiveScroll);
997
+
998
+ const rootId = buildDomTree(document.body);
999
+
1000
+ // Clear the cache before starting
1001
+ DOM_CACHE.clearCache();
1002
+
1003
+ // Only process metrics in debug mode
1004
+ if (debugMode && PERF_METRICS) {
1005
+ // Convert timings to seconds and add useful derived metrics
1006
+ Object.keys(PERF_METRICS.timings).forEach(key => {
1007
+ PERF_METRICS.timings[key] = PERF_METRICS.timings[key] / 1000;
1008
+ });
1009
+
1010
+ Object.keys(PERF_METRICS.buildDomTreeBreakdown).forEach(key => {
1011
+ if (typeof PERF_METRICS.buildDomTreeBreakdown[key] === 'number') {
1012
+ PERF_METRICS.buildDomTreeBreakdown[key] = PERF_METRICS.buildDomTreeBreakdown[key] / 1000;
1013
+ }
1014
+ });
1015
+
1016
+ // Add some useful derived metrics
1017
+ if (PERF_METRICS.buildDomTreeBreakdown.buildDomTreeCalls > 0) {
1018
+ PERF_METRICS.buildDomTreeBreakdown.averageTimePerNode =
1019
+ PERF_METRICS.buildDomTreeBreakdown.totalTime / PERF_METRICS.buildDomTreeBreakdown.buildDomTreeCalls;
1020
+ }
1021
+
1022
+ PERF_METRICS.buildDomTreeBreakdown.timeInChildCalls =
1023
+ PERF_METRICS.buildDomTreeBreakdown.totalTime - PERF_METRICS.buildDomTreeBreakdown.totalSelfTime;
1024
+
1025
+ // Add average time per operation to the metrics
1026
+ Object.keys(PERF_METRICS.buildDomTreeBreakdown.domOperations).forEach(op => {
1027
+ const time = PERF_METRICS.buildDomTreeBreakdown.domOperations[op];
1028
+ const count = PERF_METRICS.buildDomTreeBreakdown.domOperationCounts[op];
1029
+ if (count > 0) {
1030
+ PERF_METRICS.buildDomTreeBreakdown.domOperations[`${op}Average`] = time / count;
1031
+ }
1032
+ });
1033
+
1034
+ // Calculate cache hit rates
1035
+ const boundingRectTotal = PERF_METRICS.cacheMetrics.boundingRectCacheHits + PERF_METRICS.cacheMetrics.boundingRectCacheMisses;
1036
+ const computedStyleTotal = PERF_METRICS.cacheMetrics.computedStyleCacheHits + PERF_METRICS.cacheMetrics.computedStyleCacheMisses;
1037
+
1038
+ if (boundingRectTotal > 0) {
1039
+ PERF_METRICS.cacheMetrics.boundingRectHitRate = PERF_METRICS.cacheMetrics.boundingRectCacheHits / boundingRectTotal;
1040
+ }
1041
+
1042
+ if (computedStyleTotal > 0) {
1043
+ PERF_METRICS.cacheMetrics.computedStyleHitRate = PERF_METRICS.cacheMetrics.computedStyleCacheHits / computedStyleTotal;
1044
+ }
1045
+
1046
+ if ((boundingRectTotal + computedStyleTotal) > 0) {
1047
+ PERF_METRICS.cacheMetrics.overallHitRate =
1048
+ (PERF_METRICS.cacheMetrics.boundingRectCacheHits + PERF_METRICS.cacheMetrics.computedStyleCacheHits) /
1049
+ (boundingRectTotal + computedStyleTotal);
1050
+ }
1051
+ }
1052
+
1053
+ return debugMode ?
1054
+ { rootId, map: DOM_HASH_MAP, perfMetrics: PERF_METRICS } :
1055
+ { rootId, map: DOM_HASH_MAP };
1056
+ };