aether-mcp-server 2.1.0 → 2.1.1

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.
@@ -0,0 +1,1024 @@
1
+ "use strict";
2
+ /**
3
+ * Consolidated in-page JavaScript scripts.
4
+ *
5
+ * All Runtime.evaluate() scripts that were duplicated across cdp-bridge.ts,
6
+ * cdp-client.ts, and locator-engine.ts now live here as single-source constants.
7
+ *
8
+ * Use these by injecting SHARED_DOM_HELPERS once via
9
+ * Page.addScriptToEvaluateOnNewDocument, then calling individual action scripts
10
+ * that reference the pre-injected helpers.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.LOCATOR_BOOTSTRAP_SCRIPT = exports.EXPORT_STORAGE_SCRIPT = exports.COLLECT_FORMS_SCRIPT = exports.DISMISS_POPUPS_SCRIPT = exports.CAPTCHA_DETECTION_SCRIPT = exports.SHARED_DOM_HELPERS = void 0;
14
+ exports.makeElementCollectionScript = makeElementCollectionScript;
15
+ exports.makeActionabilityCheckScript = makeActionabilityCheckScript;
16
+ exports.makeClickByIdScript = makeClickByIdScript;
17
+ exports.makeActionFactsScript = makeActionFactsScript;
18
+ exports.makeWaitForSelectorScript = makeWaitForSelectorScript;
19
+ exports.makeFocusAndClearScript = makeFocusAndClearScript;
20
+ exports.makeGetElementCenterScript = makeGetElementCenterScript;
21
+ exports.makeWaitForTextScript = makeWaitForTextScript;
22
+ exports.makeFuzzyMatchScript = makeFuzzyMatchScript;
23
+ exports.makeElementAtPointScript = makeElementAtPointScript;
24
+ exports.makeSelectInspectScript = makeSelectInspectScript;
25
+ exports.makeCheckedStateScript = makeCheckedStateScript;
26
+ exports.makeSetValueScript = makeSetValueScript;
27
+ exports.makeSetCheckedScript = makeSetCheckedScript;
28
+ exports.makeAssertionScript = makeAssertionScript;
29
+ exports.makeVerifyUIScript = makeVerifyUIScript;
30
+ exports.makeComputedStyleScript = makeComputedStyleScript;
31
+ exports.makeGetPageTextScript = makeGetPageTextScript;
32
+ // ─── Core DOM Helpers (injected once via Page.addScriptToEvaluateOnNewDocument) ──
33
+ exports.SHARED_DOM_HELPERS = `
34
+ function aetherNorm(value) {
35
+ return String(value == null ? '' : value).trim().replace(/\\s+/g, ' ');
36
+ }
37
+
38
+ function aetherVisible(el) {
39
+ if (!el || el.nodeType !== Node.ELEMENT_NODE) return false;
40
+ const rect = el.getBoundingClientRect();
41
+ if (rect.width <= 0 || rect.height <= 0) return false;
42
+ const style = window.getComputedStyle(el);
43
+ return style.display !== 'none' &&
44
+ style.visibility !== 'hidden' &&
45
+ style.opacity !== '0';
46
+ }
47
+
48
+ function aetherImplicitRole(el) {
49
+ const explicit = (el.getAttribute('role') || '').trim().toLowerCase();
50
+ if (explicit) return explicit.split(/\\s+/)[0];
51
+ const tag = el.tagName.toLowerCase();
52
+ const type = (el.getAttribute('type') || '').toLowerCase();
53
+ switch (tag) {
54
+ case 'a': case 'area':
55
+ return el.hasAttribute('href') ? 'link' : 'generic';
56
+ case 'button': case 'summary': return 'button';
57
+ case 'select':
58
+ return (el.multiple || el.size > 1) ? 'listbox' : 'combobox';
59
+ case 'textarea': return 'textbox';
60
+ case 'progress': return 'progressbar';
61
+ case 'input':
62
+ if (type === 'checkbox') return 'checkbox';
63
+ if (type === 'radio') return 'radio';
64
+ if (type === 'button' || type === 'submit' || type === 'reset' || type === 'image') return 'button';
65
+ if (type === 'range') return 'slider';
66
+ if (type === 'search') return 'searchbox';
67
+ return 'textbox';
68
+ default: return 'generic';
69
+ }
70
+ }
71
+
72
+ function aetherLabelFor(el) {
73
+ if (!el) return '';
74
+ if (el.id) {
75
+ const label = document.querySelector('label[for="' + CSS.escape(el.id) + '"]');
76
+ if (label) return aetherNorm(label.innerText || label.textContent);
77
+ }
78
+ const wrappingLabel = el.closest('label');
79
+ if (wrappingLabel) return aetherNorm(wrappingLabel.innerText || wrappingLabel.textContent);
80
+ const ariaLabelledby = el.getAttribute('aria-labelledby');
81
+ if (ariaLabelledby) {
82
+ const ref = document.getElementById(ariaLabelledby.split(/\\s+/)[0]);
83
+ if (ref) return aetherNorm(ref.innerText || ref.textContent);
84
+ }
85
+ return '';
86
+ }
87
+
88
+ function aetherAccessibleName(el) {
89
+ if (!el) return '';
90
+ const ariaLabel = el.getAttribute('aria-label');
91
+ if (ariaLabel) return aetherNorm(ariaLabel);
92
+ return aetherLabelFor(el) || aetherNorm(el.getAttribute('title') || el.getAttribute('placeholder') || el.getAttribute('name') || '');
93
+ }
94
+
95
+ function aetherStableSelector(el) {
96
+ if (!el || el.nodeType !== Node.ELEMENT_NODE) return '';
97
+ if (el.id) return '#' + CSS.escape(el.id);
98
+ const path = [];
99
+ let node = el;
100
+ while (node && node.nodeType === Node.ELEMENT_NODE && node !== document.body && node !== document.documentElement) {
101
+ let seg = node.nodeName.toLowerCase();
102
+ if (node.classList && node.classList.length) {
103
+ seg += '.' + Array.from(node.classList).slice(0, 2).map(function(c) { return CSS.escape(c); }).join('.');
104
+ }
105
+ const parent = node.parentElement;
106
+ if (parent) {
107
+ const siblings = Array.from(parent.children).filter(function(c) { return c.nodeName === node.nodeName; });
108
+ if (siblings.length > 1) seg += ':nth-of-type(' + (siblings.indexOf(node) + 1) + ')';
109
+ }
110
+ path.unshift(seg);
111
+ node = parent;
112
+ }
113
+ return path.length ? path.join(' > ') : node ? node.nodeName.toLowerCase() : '';
114
+ }
115
+
116
+ function aetherXpath(el) {
117
+ const parts = [];
118
+ let node = el;
119
+ while (node && node.nodeType === Node.ELEMENT_NODE) {
120
+ let index = 1;
121
+ let sibling = node.previousElementSibling;
122
+ while (sibling) {
123
+ if (sibling.nodeName === node.nodeName) index++;
124
+ sibling = sibling.previousElementSibling;
125
+ }
126
+ parts.unshift(node.nodeName.toLowerCase() + '[' + index + ']');
127
+ node = node.parentElement;
128
+ }
129
+ return '/' + parts.join('/');
130
+ }
131
+ `;
132
+ // ─── Element Collection (for list_interactive_elements, snapshot_compact, get_state) ──
133
+ function makeElementCollectionScript(params) {
134
+ const target = JSON.stringify(params.target ?? "");
135
+ const role = JSON.stringify((params.role ?? "").toLowerCase());
136
+ const selector = JSON.stringify(params.selector ?? "");
137
+ const max = params.maxElements;
138
+ const includeText = params.includeText;
139
+ const withOverlay = params.withOverlay;
140
+ const mode = params.mode;
141
+ return `
142
+ (function() {
143
+ ${exports.SHARED_DOM_HELPERS}
144
+ const _target = ${target};
145
+ const _targetLower = _target.toLowerCase();
146
+ const _roleHint = ${role};
147
+ const _selectorHint = ${selector};
148
+ const _maxCandidates = ${max};
149
+ const _mode = ${JSON.stringify(mode)};
150
+
151
+ const INTERACTIVE = [
152
+ 'a[href]', 'button', 'input:not([type="hidden"])', 'select', 'textarea',
153
+ '[onclick]', '[role]', '[tabindex]:not([tabindex="-1"])', 'label', 'summary',
154
+ '[contenteditable="true"]', '[aria-label]', '[placeholder]'
155
+ ].join(', ');
156
+
157
+ function scoreField(field, value, exact, includes) {
158
+ const text = aetherNorm(value);
159
+ const lower = text.toLowerCase();
160
+ if (!_targetLower) return { score: 0, by: '' };
161
+ if (lower === _targetLower) return { score: exact, by: field + ':exact' };
162
+ if (lower.includes(_targetLower)) return { score: includes, by: field + ':contains' };
163
+ if (_targetLower.includes(lower) && lower.length >= 3) return { score: Math.max(1, includes - 1), by: field + ':contained_by_target' };
164
+ return { score: 0, by: '' };
165
+ }
166
+
167
+ function scoreCandidate(item) {
168
+ let score = 0;
169
+ let matchedBy = '';
170
+ const fields = [
171
+ ['selector', item.selector, 13, 10],
172
+ ['xpath', item.xpath, 11, 8],
173
+ ['name', item.name, 12, 10],
174
+ ['label', item.label, 12, 10],
175
+ ['placeholder', item.placeholder, 11, 9],
176
+ ['text', item.text, 10, 8],
177
+ ['title', item.title, 8, 6],
178
+ ['value', item.value, 7, 5]
179
+ ];
180
+ for (var i = 0; i < fields.length; i++) {
181
+ var f = fields[i];
182
+ var match = scoreField(f[0], f[1], f[2], f[3]);
183
+ if (match.score > score) {
184
+ score = match.score;
185
+ matchedBy = match.by;
186
+ }
187
+ }
188
+ if (_roleHint) {
189
+ if (item.role === _roleHint) score += 5;
190
+ else if (_roleHint === 'textbox' && ['input', 'textarea'].indexOf(item.tag) >= 0) score += 3;
191
+ else score -= 3;
192
+ }
193
+ if (!_targetLower && _roleHint && item.role === _roleHint) {
194
+ matchedBy = 'role';
195
+ score = Math.max(score, 5);
196
+ }
197
+ if (item.scope !== 'document') score += 1;
198
+ item.score = score;
199
+ item.matchedBy = matchedBy || (_selectorHint ? 'selector' : '');
200
+ return item;
201
+ }
202
+
203
+ // Remove old Set-of-Marks overlays
204
+ var oldContainer = document.getElementById('aether-som-container');
205
+ if (oldContainer) oldContainer.remove();
206
+ var oldMarkers = document.querySelectorAll('.aether-som-marker');
207
+ for (var om = 0; om < oldMarkers.length; om++) oldMarkers[om].remove();
208
+
209
+ var container = null;
210
+ if (${withOverlay}) {
211
+ container = document.createElement('div');
212
+ container.id = 'aether-som-container';
213
+ container.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483647;pointer-events:none;';
214
+ document.documentElement.appendChild(container);
215
+ }
216
+
217
+ function collectFromRoot(root, framePath, frameOffset, shadowDepth, scope, out) {
218
+ var nodes = [];
219
+ try {
220
+ nodes = _selectorHint
221
+ ? Array.from(root.querySelectorAll(_selectorHint))
222
+ : Array.from(root.querySelectorAll(INTERACTIVE));
223
+ } catch(e) { nodes = []; }
224
+
225
+ for (var i = 0; i < nodes.length; i++) {
226
+ var el = nodes[i];
227
+ if (!aetherVisible(el)) continue;
228
+ var rect = el.getBoundingClientRect();
229
+ var sel = aetherStableSelector(el);
230
+ var roleVal = aetherImplicitRole(el);
231
+ var label = aetherLabelFor(el);
232
+ var item = {
233
+ ref: '',
234
+ selector: sel,
235
+ xpath: aetherXpath(el),
236
+ scope: scope,
237
+ framePath: framePath.slice(),
238
+ shadowDepth: shadowDepth,
239
+ tag: el.tagName.toLowerCase(),
240
+ role: roleVal,
241
+ type: el.getAttribute('type') || '',
242
+ name: aetherNorm(el.getAttribute('aria-label') || label || aetherAccessibleName(el) || el.getAttribute('name')),
243
+ text: ${includeText} ? aetherNorm(el.innerText || el.textContent).substring(0, 180) : '',
244
+ label: label,
245
+ placeholder: aetherNorm(el.getAttribute('placeholder')),
246
+ title: aetherNorm(el.getAttribute('title')),
247
+ value: aetherNorm(el.getAttribute('value')),
248
+ score: 0,
249
+ matchedBy: '',
250
+ bounds: {
251
+ x: Math.round(frameOffset.x + rect.left),
252
+ y: Math.round(frameOffset.y + rect.top),
253
+ width: Math.round(rect.width),
254
+ height: Math.round(rect.height)
255
+ }
256
+ };
257
+ item.ref = (item.scope === 'document' && item.framePath.length === 0 && item.shadowDepth === 0)
258
+ ? 'css:' + item.selector
259
+ : 'point:' + Math.round(item.bounds.x + item.bounds.width / 2) + ',' + Math.round(item.bounds.y + item.bounds.height / 2);
260
+ out.push(scoreCandidate(item));
261
+
262
+ // SoM overlay marker
263
+ if (container) {
264
+ var id = String(out.length);
265
+ var w = Math.max(20, id.length * 8 + 14);
266
+ var marker = document.createElement('div');
267
+ marker.className = 'aether-som-marker';
268
+ marker.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="' + w + '" height="20" style="display:block">'
269
+ + '<rect width="' + w + '" height="20" rx="10" fill="#1e40af"/>'
270
+ + '<text x="' + (w / 2) + '" y="10" dominant-baseline="central" text-anchor="middle" font-family="ui-monospace,monospace" font-size="11" font-weight="700" fill="white">' + id + '</text>'
271
+ + '</svg>';
272
+ marker.style.cssText = 'position:absolute;left:' + rect.left + 'px;top:' + rect.top + 'px;pointer-events:none;filter:drop-shadow(0 1px 4px rgba(0,0,0,0.35));transform:translate(-4px,-4px);';
273
+ container.appendChild(marker);
274
+ }
275
+ }
276
+
277
+ // Shadow DOM and iframe traversal
278
+ var all = [];
279
+ try { all = Array.from(root.querySelectorAll('*')); } catch(e) {}
280
+ for (var si = 0; si < all.length; si++) {
281
+ var cel = all[si];
282
+ if (cel.shadowRoot) {
283
+ collectFromRoot(cel.shadowRoot, framePath, frameOffset, shadowDepth + 1, 'shadow', out);
284
+ }
285
+ if (cel.tagName && cel.tagName.toLowerCase() === 'iframe') {
286
+ try {
287
+ var doc = cel.contentDocument;
288
+ if (doc) {
289
+ var frect = cel.getBoundingClientRect();
290
+ collectFromRoot(doc, framePath.concat([Array.from(cel.ownerDocument.querySelectorAll('iframe')).indexOf(cel)]),
291
+ { x: frameOffset.x + frect.left, y: frameOffset.y + frect.top }, shadowDepth, 'frame', out);
292
+ }
293
+ } catch(e) {}
294
+ }
295
+ }
296
+ }
297
+
298
+ var candidates = [];
299
+ collectFromRoot(document, [], { x: 0, y: 0 }, 0, 'document', candidates);
300
+ candidates.sort(function(a, b) {
301
+ if (_mode === 'list') return (a.bounds.y - b.bounds.y) || (a.bounds.x - b.bounds.x);
302
+ return b.score - a.score || a.bounds.y - b.bounds.y || a.bounds.x - b.bounds.x;
303
+ });
304
+ return { candidates: candidates.slice(0, _maxCandidates), somInjected: !!container };
305
+ })()
306
+ `;
307
+ }
308
+ // ─── Actionability Gate ───────────────────────────────────────────────
309
+ function makeActionabilityCheckScript(selector) {
310
+ return `
311
+ (function() {
312
+ var el = document.querySelector(${JSON.stringify(selector)});
313
+ if (!el) return { ok: false, reason: 'not_found' };
314
+ el.scrollIntoView({ block: 'center', inline: 'center', behavior: 'instant' });
315
+ var rect = el.getBoundingClientRect();
316
+ var style = window.getComputedStyle(el);
317
+ if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0' || rect.width === 0 || rect.height === 0) {
318
+ return { ok: false, reason: 'not_visible' };
319
+ }
320
+ if (el.disabled === true || el.getAttribute('aria-disabled') === 'true' || style.pointerEvents === 'none') {
321
+ return { ok: false, reason: 'disabled' };
322
+ }
323
+ var cx = rect.left + rect.width / 2;
324
+ var cy = rect.top + rect.height / 2;
325
+ var vw = window.innerWidth || document.documentElement.clientWidth;
326
+ var vh = window.innerHeight || document.documentElement.clientHeight;
327
+ if (cx < 0 || cy < 0 || cx > vw || cy > vh) {
328
+ return { ok: false, reason: 'offscreen' };
329
+ }
330
+ var top = document.elementFromPoint(cx, cy);
331
+ var reachable = top === el || (top && el.contains(top)) || (top && top.contains(el));
332
+ return {
333
+ ok: !!reachable,
334
+ reason: reachable ? 'ok' : 'obscured',
335
+ obscuredBy: !reachable && top ? top.tagName.toLowerCase() + (top.id ? '#' + top.id : '') : '',
336
+ x: cx,
337
+ y: cy,
338
+ w: rect.width,
339
+ boxKey: Math.round(rect.left) + ',' + Math.round(rect.top) + ',' + Math.round(rect.width) + ',' + Math.round(rect.height)
340
+ };
341
+ })()
342
+ `;
343
+ }
344
+ // ─── Click Element by ID (Set-of-Marks) ──────────────────────────────
345
+ function makeClickByIdScript(elementId) {
346
+ return `
347
+ (function() {
348
+ ${exports.SHARED_DOM_HELPERS}
349
+ var targetId = Number(${JSON.stringify(String(elementId).replace(/@/g, ''))});
350
+ var elements = Array.from(document.querySelectorAll('a[href], button, input:not([type="hidden"]), select, textarea, [onclick], [role="button"], [role="link"], [role="checkbox"], [tabindex]:not([tabindex="-1"]), label, summary'))
351
+ .filter(function(el) { return aetherVisible(el); });
352
+ var el = elements[targetId - 1];
353
+ if (el) {
354
+ el.scrollIntoView({ block: 'center', inline: 'center', behavior: 'instant' });
355
+ var rect = el.getBoundingClientRect();
356
+ return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2, w: rect.width };
357
+ }
358
+ return null;
359
+ })()
360
+ `;
361
+ }
362
+ // ─── Action Facts (before/after snapshots) ────────────────────────────
363
+ function makeActionFactsScript(selector) {
364
+ return `
365
+ (function() {
366
+ var sel = ${JSON.stringify(selector || "")};
367
+ var active = document.activeElement;
368
+ var target = sel ? document.querySelector(sel) : active;
369
+ var visibleErrors = Array.from(document.querySelectorAll('[role="alert"], .error, .errors, [aria-invalid="true"]'))
370
+ .filter(function(el) {
371
+ var rect = el.getBoundingClientRect();
372
+ var style = window.getComputedStyle(el);
373
+ return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
374
+ })
375
+ .map(function(el) { return String(el.innerText || el.textContent || el.getAttribute('aria-label') || '').trim().replace(/\\s+/g, ' '); })
376
+ .filter(Boolean)
377
+ .slice(0, 5);
378
+
379
+ function describe(el) {
380
+ if (!el) return null;
381
+ return {
382
+ tag: el.tagName.toLowerCase(),
383
+ id: el.id || '',
384
+ name: el.getAttribute('name') || '',
385
+ role: el.getAttribute('role') || '',
386
+ type: el.getAttribute('type') || '',
387
+ value: 'value' in el ? String(el.value || '') : '',
388
+ checked: 'checked' in el ? !!el.checked : undefined,
389
+ selectedIndex: 'selectedIndex' in el ? el.selectedIndex : undefined,
390
+ text: String(el.innerText || el.textContent || '').trim().replace(/\\s+/g, ' ').substring(0, 160)
391
+ };
392
+ }
393
+
394
+ return {
395
+ url: window.location.href,
396
+ title: document.title,
397
+ readyState: document.readyState,
398
+ focused: describe(active),
399
+ target: describe(target),
400
+ visibleErrors: visibleErrors
401
+ };
402
+ })()
403
+ `;
404
+ }
405
+ // ─── Wait for Selector (compact boolean check) ────────────────────────
406
+ function makeWaitForSelectorScript(selector, requireVisible) {
407
+ return `
408
+ (function() {
409
+ ${exports.SHARED_DOM_HELPERS}
410
+ var el = document.querySelector(${JSON.stringify(selector)});
411
+ if (!el) return { found: false };
412
+ if (${requireVisible} && !aetherVisible(el)) return { found: true, visible: false };
413
+ var rect = el.getBoundingClientRect();
414
+ return {
415
+ found: true,
416
+ visible: aetherVisible(el),
417
+ box: { x: Math.round(rect.left), y: Math.round(rect.top), width: Math.round(rect.width), height: Math.round(rect.height) }
418
+ };
419
+ })()
420
+ `;
421
+ }
422
+ // ─── Focus and Clear Input ────────────────────────────────────────────
423
+ function makeFocusAndClearScript(selector) {
424
+ return `
425
+ (function() {
426
+ var el = document.querySelector(${JSON.stringify(selector)});
427
+ if (!el) return false;
428
+ el.focus();
429
+ if ('value' in el) {
430
+ el.value = '';
431
+ el.dispatchEvent(new Event('input', { bubbles: true }));
432
+ el.dispatchEvent(new Event('change', { bubbles: true }));
433
+ }
434
+ return true;
435
+ })()
436
+ `;
437
+ }
438
+ // ─── Get Element Center Coordinates ───────────────────────────────────
439
+ function makeGetElementCenterScript(selector) {
440
+ return `
441
+ (function() {
442
+ ${exports.SHARED_DOM_HELPERS}
443
+ var el = document.querySelector(${JSON.stringify(selector)});
444
+ if (!el || !aetherVisible(el)) return false;
445
+ var rect = el.getBoundingClientRect();
446
+ return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2, w: rect.width };
447
+ })()
448
+ `;
449
+ }
450
+ // ─── CAPTCHA Detection ────────────────────────────────────────────────
451
+ exports.CAPTCHA_DETECTION_SCRIPT = `
452
+ (function() {
453
+ ${exports.SHARED_DOM_HELPERS}
454
+ var selectors = [
455
+ 'iframe[src*="recaptcha"]',
456
+ 'iframe[src*="hcaptcha"]',
457
+ 'iframe[src*="challenges.cloudflare.com"]',
458
+ 'iframe[src*="arkoselabs"]',
459
+ 'iframe[src*="funcaptcha"]',
460
+ '[class*="g-recaptcha"]',
461
+ '[class*="h-captcha"]',
462
+ '[data-sitekey]',
463
+ '[id*="captcha" i]',
464
+ '[class*="captcha" i]',
465
+ '[aria-label*="captcha" i]'
466
+ ];
467
+ var textPatterns = [
468
+ /captcha/i,
469
+ /i am not a robot/i,
470
+ /verify you are human/i,
471
+ /verify that you are human/i,
472
+ /security check/i,
473
+ /human verification/i,
474
+ /cloudflare.*verify/i
475
+ ];
476
+
477
+ var selectorMatches = [];
478
+ for (var s = 0; s < selectors.length; s++) {
479
+ var els = document.querySelectorAll(selectors[s]);
480
+ for (var e = 0; e < els.length && selectorMatches.length < 5; e++) {
481
+ var el = els[e];
482
+ if (!aetherVisible(el)) continue;
483
+ var rect = el.getBoundingClientRect();
484
+ selectorMatches.push({
485
+ selector: selectors[s],
486
+ tag: el.tagName.toLowerCase(),
487
+ text: String(el.innerText || el.textContent || el.getAttribute('aria-label') || '').trim().replace(/\\s+/g, ' ').substring(0, 160),
488
+ src: el.getAttribute('src') || '',
489
+ bounds: { x: Math.round(rect.left), y: Math.round(rect.top), width: Math.round(rect.width), height: Math.round(rect.height) }
490
+ });
491
+ }
492
+ }
493
+
494
+ var bodyText = String(document.body && document.body.innerText || '').replace(/\\s+/g, ' ').substring(0, 5000);
495
+ var textMatches = [];
496
+ for (var t = 0; t < textPatterns.length; t++) {
497
+ if (textPatterns[t].test(bodyText)) {
498
+ textMatches.push(textPatterns[t].toString());
499
+ }
500
+ }
501
+
502
+ var detected = selectorMatches.length > 0 || textMatches.length > 0;
503
+ return {
504
+ detected: detected,
505
+ captchaRequired: detected,
506
+ message: detected ? 'CAPTCHA detected. Manual solve required before continuing.' : 'No CAPTCHA detected.',
507
+ matches: selectorMatches,
508
+ textMatches: textMatches,
509
+ url: window.location.href,
510
+ title: document.title
511
+ };
512
+ })()
513
+ `;
514
+ // ─── Wait for Text ────────────────────────────────────────────────────
515
+ function makeWaitForTextScript(text) {
516
+ return `
517
+ (document.body && document.body.innerText || '').includes(${JSON.stringify(text)})
518
+ `;
519
+ }
520
+ // ─── Dismiss Popups ───────────────────────────────────────────────────
521
+ exports.DISMISS_POPUPS_SCRIPT = `
522
+ (function() {
523
+ var selectors = [
524
+ '[aria-label*="close" i]', '[aria-label*="dismiss" i]',
525
+ '.close', '.dismiss', '.modal-close',
526
+ 'button[class*="close"]', '[data-dismiss="modal"]'
527
+ ];
528
+ for (var i = 0; i < selectors.length; i++) {
529
+ var el = document.querySelector(selectors[i]);
530
+ if (el && el.offsetParent !== null) {
531
+ el.click();
532
+ return true;
533
+ }
534
+ }
535
+ return false;
536
+ })()
537
+ `;
538
+ // ─── Collect Forms ────────────────────────────────────────────────────
539
+ exports.COLLECT_FORMS_SCRIPT = `
540
+ (function() {
541
+ var forms = Array.from(document.querySelectorAll('form'));
542
+ return forms.map(function(form, idx) {
543
+ var inputs = Array.from(form.querySelectorAll('input, select, textarea'));
544
+ return {
545
+ id: 'form-' + idx,
546
+ action: form.action,
547
+ method: form.method,
548
+ inputs: inputs.map(function(input) {
549
+ return {
550
+ type: input.type || 'text',
551
+ name: input.name || '',
552
+ id: input.id || '',
553
+ required: input.required,
554
+ placeholder: input.placeholder || ''
555
+ };
556
+ })
557
+ };
558
+ });
559
+ })()
560
+ `;
561
+ // ─── Self-Healing Fuzzy Match ─────────────────────────────────────────
562
+ function makeFuzzyMatchScript(text) {
563
+ return `
564
+ (function() {
565
+ ${exports.SHARED_DOM_HELPERS}
566
+ var searchText = ${JSON.stringify(text)};
567
+ var searchLower = String(searchText || '').trim().toLowerCase();
568
+
569
+ function textFor(el) {
570
+ return String(
571
+ el.innerText || el.textContent || el.getAttribute('aria-label') ||
572
+ el.getAttribute('placeholder') || el.getAttribute('name') || ''
573
+ ).trim();
574
+ }
575
+
576
+ var elements = Array.from(document.querySelectorAll('a[href], button, input:not([type="hidden"]), select, textarea, [role="button"], [role="link"], [onclick]'))
577
+ .filter(function(el) { return aetherVisible(el); });
578
+
579
+ // Exact match
580
+ var best = null;
581
+ for (var i = 0; i < elements.length; i++) {
582
+ if (textFor(elements[i]).toLowerCase() === searchLower) {
583
+ best = elements[i];
584
+ break;
585
+ }
586
+ }
587
+ if (best) return { selector: aetherStableSelector(best), confidence: 1.0 };
588
+
589
+ // Partial match
590
+ for (var i = 0; i < elements.length; i++) {
591
+ if (searchLower.length >= 3 && textFor(elements[i]).toLowerCase().indexOf(searchLower) >= 0) {
592
+ best = elements[i];
593
+ break;
594
+ }
595
+ }
596
+ if (best) return { selector: aetherStableSelector(best), confidence: 0.8 };
597
+
598
+ // Levenshtein fuzzy match
599
+ function levenshtein(a, b) {
600
+ if (a.length === 0) return b.length;
601
+ if (b.length === 0) return a.length;
602
+ var matrix = [];
603
+ for (var i = 0; i <= b.length; i++) matrix[i] = [i];
604
+ for (var j = 0; j <= a.length; j++) matrix[0][j] = j;
605
+ for (var i = 1; i <= b.length; i++) {
606
+ for (var j = 1; j <= a.length; j++) {
607
+ matrix[i][j] = b.charAt(i-1) === a.charAt(j-1) ? matrix[i-1][j-1] :
608
+ Math.min(matrix[i-1][j-1] + 1, matrix[i][j-1] + 1, matrix[i-1][j] + 1);
609
+ }
610
+ }
611
+ return matrix[b.length][a.length];
612
+ }
613
+
614
+ var minDist = Infinity;
615
+ var bestEl = null;
616
+ for (var i = 0; i < elements.length; i++) {
617
+ var elText = textFor(elements[i]);
618
+ if (!elText || Math.abs(elText.length - searchText.length) > 8) continue;
619
+ var dist = levenshtein(searchText, elText);
620
+ if (dist < minDist && dist <= 3) {
621
+ minDist = dist;
622
+ bestEl = elements[i];
623
+ }
624
+ }
625
+ if (bestEl) return { selector: aetherStableSelector(bestEl), confidence: 0.6 };
626
+ return null;
627
+ })()
628
+ `;
629
+ }
630
+ // ─── Element at Point ─────────────────────────────────────────────────
631
+ function makeElementAtPointScript(x, y) {
632
+ return `
633
+ (function() {
634
+ ${exports.SHARED_DOM_HELPERS}
635
+ var el = document.elementFromPoint(${x}, ${y});
636
+ if (!el) return { found: false };
637
+ var rect = el.getBoundingClientRect();
638
+ return {
639
+ found: true,
640
+ selector: aetherStableSelector(el),
641
+ tag: el.tagName.toLowerCase(),
642
+ text: String(el.innerText || el.textContent || el.getAttribute('aria-label') || '').trim().replace(/\\s+/g, ' ').substring(0, 160),
643
+ role: el.getAttribute('role') || '',
644
+ bounds: { x: Math.round(rect.left), y: Math.round(rect.top), width: Math.round(rect.width), height: Math.round(rect.height) }
645
+ };
646
+ })()
647
+ `;
648
+ }
649
+ // ─── Select Option Inspection ─────────────────────────────────────────
650
+ function makeSelectInspectScript(selector, value) {
651
+ return `
652
+ (function() {
653
+ var select = document.querySelector(${JSON.stringify(selector)});
654
+ if (!select) return { success: false, error: "Element not found" };
655
+ if (select.tagName.toLowerCase() !== 'select') return { success: false, error: "Element is not a select" };
656
+ if (select.disabled) return { success: false, error: "Element is disabled" };
657
+ var wanted = ${JSON.stringify(value)};
658
+ var options = Array.from(select.options || []);
659
+ var index = -1;
660
+ for (var i = 0; i < options.length; i++) {
661
+ if (options[i].value === wanted || options[i].text === wanted || options[i].label === wanted) {
662
+ index = i;
663
+ break;
664
+ }
665
+ }
666
+ return {
667
+ success: true,
668
+ selectedValue: select.value,
669
+ index: index,
670
+ optionCount: options.length,
671
+ wantedValue: index >= 0 ? options[index].value : wanted
672
+ };
673
+ })()
674
+ `;
675
+ }
676
+ // ─── Checkbox/Radio State Inspection ──────────────────────────────────
677
+ function makeCheckedStateScript(selector) {
678
+ return `
679
+ (function() {
680
+ var el = document.querySelector(${JSON.stringify(selector)});
681
+ if (!el) return { success: false, error: "Element not found" };
682
+ if (el.type !== 'checkbox' && el.type !== 'radio') return { success: false, error: "Element is not a checkbox or radio" };
683
+ if (el.disabled) return { success: false, error: "Element is disabled" };
684
+ return { success: true, checked: !!el.checked, type: el.type };
685
+ })()
686
+ `;
687
+ }
688
+ // ─── Set Value via JS (fallback) ──────────────────────────────────────
689
+ function makeSetValueScript(selector, value) {
690
+ return `
691
+ (function() {
692
+ var el = document.querySelector(${JSON.stringify(selector)});
693
+ if (!el) return { success: false, error: "Element not found" };
694
+ el.value = ${JSON.stringify(value)};
695
+ el.dispatchEvent(new Event('input', { bubbles: true }));
696
+ el.dispatchEvent(new Event('change', { bubbles: true }));
697
+ return { success: true, value: el.value };
698
+ })()
699
+ `;
700
+ }
701
+ // ─── Set Checked State via JS (fallback) ──────────────────────────────
702
+ function makeSetCheckedScript(selector, checked) {
703
+ return `
704
+ (function() {
705
+ var el = document.querySelector(${JSON.stringify(selector)});
706
+ if (!el) return { success: false, error: "Element not found" };
707
+ if (el.type !== 'checkbox' && el.type !== 'radio') return { success: false, error: "Element is not a checkbox or radio" };
708
+ el.checked = ${checked};
709
+ el.dispatchEvent(new Event('input', { bubbles: true }));
710
+ el.dispatchEvent(new Event('change', { bubbles: true }));
711
+ return { success: true, checked: el.checked };
712
+ })()
713
+ `;
714
+ }
715
+ // ─── Assert Conditions ────────────────────────────────────────────────
716
+ function makeAssertionScript(selector, assertionType, expectedText) {
717
+ return `
718
+ (function() {
719
+ var el = ${JSON.stringify(selector)} ? document.querySelector(${JSON.stringify(selector)}) : null;
720
+ switch(${JSON.stringify(assertionType)}) {
721
+ case 'element_exists':
722
+ return { success: !!el, message: el ? 'Element exists' : 'Element not found' };
723
+ case 'element_not_exists':
724
+ return { success: !el, message: !el ? 'Element does not exist' : 'Element found' };
725
+ case 'element_contains_text':
726
+ if (!el) return { success: false, message: 'Element not found' };
727
+ var text = (el.innerText || el.textContent || '').trim();
728
+ var matches = text.indexOf(${JSON.stringify(expectedText)}) >= 0;
729
+ return { success: matches, message: matches ? 'Text matches' : 'Text does not match', actualText: text };
730
+ case 'url_contains':
731
+ var urlMatches = window.location.href.indexOf(${JSON.stringify(expectedText)}) >= 0;
732
+ return { success: urlMatches, message: urlMatches ? 'URL contains text' : 'URL does not contain text' };
733
+ default:
734
+ return { success: false, message: 'Unknown assertion type' };
735
+ }
736
+ })()
737
+ `;
738
+ }
739
+ // ─── Verify UI State ──────────────────────────────────────────────────
740
+ function makeVerifyUIScript(selector) {
741
+ return `
742
+ (function() {
743
+ ${exports.SHARED_DOM_HELPERS}
744
+ var el = document.querySelector(${JSON.stringify(selector)});
745
+ if (!el) return { exists: false, visible: false };
746
+ var rect = el.getBoundingClientRect();
747
+ var vis = aetherVisible(el);
748
+ return {
749
+ exists: true,
750
+ visible: vis,
751
+ text: (el.innerText || el.textContent || '').trim().substring(0, 200),
752
+ bounds: { x: rect.left, y: rect.top, width: rect.width, height: rect.height }
753
+ };
754
+ })()
755
+ `;
756
+ }
757
+ // ─── Get Computed Style ───────────────────────────────────────────────
758
+ function makeComputedStyleScript(selector, property) {
759
+ return `
760
+ (function() {
761
+ var el = document.querySelector(${JSON.stringify(selector)});
762
+ if (!el) return null;
763
+ var style = window.getComputedStyle(el);
764
+ ${property ? `return style.getPropertyValue(${JSON.stringify(property)});` : `return JSON.parse(JSON.stringify(style));`}
765
+ })()
766
+ `;
767
+ }
768
+ // ─── Get Page Text (Markdown/Plain Text extraction) ───────────────────
769
+ function makeGetPageTextScript(format, selector, includeLinks) {
770
+ return `
771
+ (function() {
772
+ var FORMAT = ${JSON.stringify(format)};
773
+ var SELECTOR = ${JSON.stringify(selector)};
774
+ var INCLUDE_LINKS = ${JSON.stringify(includeLinks)};
775
+ var SKIP = new Set(['SCRIPT','STYLE','NOSCRIPT','SVG','CANVAS','TEMPLATE','IFRAME','OBJECT','EMBED','NAV','FOOTER','HEADER','ASIDE']);
776
+
777
+ function pickRoot() {
778
+ if (SELECTOR) {
779
+ var el = document.querySelector(SELECTOR);
780
+ if (el) return el;
781
+ }
782
+ return document.querySelector('main, article, [role="main"]') || document.body || document.documentElement;
783
+ }
784
+
785
+ function isHidden(el) {
786
+ if (el.getAttribute && el.getAttribute('aria-hidden') === 'true') return true;
787
+ var style = window.getComputedStyle(el);
788
+ if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return true;
789
+ var rect = el.getBoundingClientRect();
790
+ return rect.width === 0 && rect.height === 0 && el.tagName !== 'BR' && el.tagName !== 'HR';
791
+ }
792
+
793
+ function inline(node) {
794
+ var out = '';
795
+ node.childNodes.forEach(function(child) {
796
+ if (child.nodeType === Node.TEXT_NODE) {
797
+ out += child.textContent.replace(/\\s+/g, ' ');
798
+ } else if (child.nodeType === Node.ELEMENT_NODE) {
799
+ if (SKIP.has(child.tagName) || isHidden(child)) return;
800
+ var tag = child.tagName.toLowerCase();
801
+ var inner = inline(child);
802
+ if (FORMAT === 'markdown') {
803
+ if (tag === 'a' && INCLUDE_LINKS && child.getAttribute('href')) {
804
+ var href = child.getAttribute('href');
805
+ out += inner.trim() ? '[' + inner.trim() + '](' + href + ')' : '';
806
+ } else if (tag === 'strong' || tag === 'b') {
807
+ out += inner.trim() ? '**' + inner.trim() + '**' : '';
808
+ } else if (tag === 'em' || tag === 'i') {
809
+ out += inner.trim() ? '*' + inner.trim() + '*' : '';
810
+ } else if (tag === 'code') {
811
+ out += inner.trim() ? '\`' + inner.trim() + '\`' : '';
812
+ } else if (tag === 'br') {
813
+ out += '\\n';
814
+ } else {
815
+ out += inner;
816
+ }
817
+ } else {
818
+ out += (tag === 'br') ? '\\n' : inner;
819
+ }
820
+ }
821
+ });
822
+ return out;
823
+ }
824
+
825
+ var BLOCK = new Set(['P','DIV','SECTION','ARTICLE','UL','OL','LI','TABLE','TR','BLOCKQUOTE','PRE','H1','H2','H3','H4','H5','H6','HR','FIGURE','FIGCAPTION']);
826
+ var lines = [];
827
+
828
+ function walk(node, depth) {
829
+ if (node.nodeType !== Node.ELEMENT_NODE) return;
830
+ if (SKIP.has(node.tagName) || isHidden(node)) return;
831
+ var tag = node.tagName.toLowerCase();
832
+
833
+ if (/^h[1-6]$/.test(tag)) {
834
+ var t = inline(node).trim();
835
+ if (t) lines.push(FORMAT === 'markdown' ? '#'.repeat(Number(tag[1])) + ' ' + t : t);
836
+ return;
837
+ }
838
+ if (tag === 'hr') { lines.push(FORMAT === 'markdown' ? '---' : ''); return; }
839
+ if (tag === 'pre') {
840
+ var t = (node.innerText || node.textContent || '').replace(/\\s+$/,'');
841
+ if (t) lines.push(FORMAT === 'markdown' ? '\\n\`\`\`\\n' + t + '\\n\`\`\`\\n' : t);
842
+ return;
843
+ }
844
+ if (tag === 'li') {
845
+ var t = inline(node).trim();
846
+ if (t) {
847
+ var indent = ' '.repeat(Math.max(0, depth));
848
+ lines.push(FORMAT === 'markdown' ? indent + '- ' + t : indent + '\u2022 ' + t);
849
+ }
850
+ return;
851
+ }
852
+ if (tag === 'blockquote') {
853
+ var t = inline(node).trim();
854
+ if (t) lines.push(FORMAT === 'markdown' ? '> ' + t : t);
855
+ return;
856
+ }
857
+ if (tag === 'tr') {
858
+ var cells = Array.from(node.children).map(function(c) { return inline(c).trim(); });
859
+ if (cells.some(Boolean)) lines.push(FORMAT === 'markdown' ? '| ' + cells.join(' | ') + ' |' : cells.join('\\t'));
860
+ return;
861
+ }
862
+
863
+ var hasBlockChild = Array.from(node.children).some(function(c) { return BLOCK.has(c.tagName); });
864
+ if (!hasBlockChild && (tag === 'p' || tag === 'div' || tag === 'section' || tag === 'figcaption' || tag === 'td' || tag === 'th')) {
865
+ var t = inline(node).trim();
866
+ if (t) lines.push(t);
867
+ return;
868
+ }
869
+
870
+ var nextDepth = (tag === 'ul' || tag === 'ol') ? depth + 1 : depth;
871
+ node.childNodes.forEach(function(child) { walk(child, nextDepth); });
872
+ }
873
+
874
+ var root = pickRoot();
875
+ walk(root, 0);
876
+
877
+ var text = lines.join('\\n\\n').replace(/\\n{3,}/g, '\\n\\n').replace(/[ \\t]+\\n/g, '\\n').trim();
878
+ return { title: document.title || '', url: location.href, text: text };
879
+ })()
880
+ `;
881
+ }
882
+ // ─── Auth State - Export Storage ──────────────────────────────────────
883
+ exports.EXPORT_STORAGE_SCRIPT = `
884
+ (function() {
885
+ var ls = {}, ss = {};
886
+ try {
887
+ for (var i = 0; i < localStorage.length; i++) {
888
+ var k = localStorage.key(i);
889
+ ls[k] = localStorage.getItem(k);
890
+ }
891
+ } catch(e) {}
892
+ try {
893
+ for (var i = 0; i < sessionStorage.length; i++) {
894
+ var k = sessionStorage.key(i);
895
+ ss[k] = sessionStorage.getItem(k);
896
+ }
897
+ } catch(e) {}
898
+ return { origin: location.origin, localStorage: ls, sessionStorage: ss };
899
+ })()
900
+ `;
901
+ // ─── Locator Bootstrap (pre-injected once on connect, then called by name) ──
902
+ /**
903
+ * The full locator engine injected once via Page.addScriptToEvaluateOnNewDocument.
904
+ * Registers window.__aetherLocate(paramsJson) which accepts a JSON string of
905
+ * {target, role, selector, maxCandidates, includeText, mode} and returns
906
+ * JSON string {candidates: [...]}.
907
+ *
908
+ * This eliminates re-injecting the ~300-line script on every locator call.
909
+ */
910
+ exports.LOCATOR_BOOTSTRAP_SCRIPT = `
911
+ window.__aetherLocate = function(paramsJson) {
912
+ try {
913
+ var p = JSON.parse(paramsJson);
914
+ } catch(e) {
915
+ return JSON.stringify({ candidates: [], error: 'invalid params' });
916
+ }
917
+ var target = p.target || '';
918
+ var targetLower = target.toLowerCase();
919
+ var roleHint = p.role || '';
920
+ var selectorHint = p.selector || '';
921
+ var maxCandidates = p.maxCandidates || 20;
922
+ var includeText = p.includeText !== false;
923
+ var mode = p.mode || 'resolve';
924
+
925
+ ` + exports.SHARED_DOM_HELPERS + `
926
+
927
+ var INTERACTIVE = [
928
+ 'a[href]', 'button', 'input:not([type="hidden"])', 'select', 'textarea',
929
+ '[onclick]', '[role]', '[tabindex]:not([tabindex="-1"])', 'label', 'summary',
930
+ '[contenteditable="true"]', '[aria-label]', '[placeholder]'
931
+ ].join(', ');
932
+
933
+ function scoreField(field, value, exact, includes) {
934
+ var text = aetherNorm(value);
935
+ var lower = text.toLowerCase();
936
+ if (!targetLower) return { score: 0, by: '' };
937
+ if (lower === targetLower) return { score: exact, by: field + ':exact' };
938
+ if (lower.indexOf(targetLower) >= 0) return { score: includes, by: field + ':contains' };
939
+ if (targetLower.indexOf(lower) >= 0 && lower.length >= 3) return { score: Math.max(1, includes - 1), by: field + ':contained_by_target' };
940
+ return { score: 0, by: '' };
941
+ }
942
+
943
+ function scoreCandidate(item) {
944
+ var score = 0, matchedBy = '';
945
+ var fields = [
946
+ ['selector', item.selector, 13, 10], ['xpath', item.xpath, 11, 8],
947
+ ['name', item.name, 12, 10], ['label', item.label, 12, 10],
948
+ ['placeholder', item.placeholder, 11, 9], ['text', item.text, 10, 8],
949
+ ['title', item.title, 8, 6], ['value', item.value, 7, 5]
950
+ ];
951
+ for (var i = 0; i < fields.length; i++) {
952
+ var f = fields[i];
953
+ var match = scoreField(f[0], f[1], f[2], f[3]);
954
+ if (match.score > score) { score = match.score; matchedBy = match.by; }
955
+ }
956
+ if (roleHint) {
957
+ if (item.role === roleHint) score += 5;
958
+ else if (roleHint === 'textbox' && (item.tag === 'input' || item.tag === 'textarea')) score += 3;
959
+ else score -= 3;
960
+ }
961
+ if (!targetLower && roleHint && item.role === roleHint) { matchedBy = 'role'; score = Math.max(score, 5); }
962
+ if (item.scope !== 'document') score += 1;
963
+ item.score = score;
964
+ item.matchedBy = matchedBy || (selectorHint ? 'selector' : '');
965
+ return item;
966
+ }
967
+
968
+ function collectFromRoot(root, framePath, frameOffset, shadowDepth, scope, out) {
969
+ var nodes = [];
970
+ try { nodes = selectorHint ? Array.from(root.querySelectorAll(selectorHint)) : Array.from(root.querySelectorAll(INTERACTIVE)); } catch(e) { nodes = []; }
971
+
972
+ for (var i = 0; i < nodes.length; i++) {
973
+ var el = nodes[i];
974
+ if (!aetherVisible(el)) continue;
975
+ var rect = el.getBoundingClientRect();
976
+ var sel = aetherStableSelector(el), roleVal = aetherImplicitRole(el), label = aetherLabelFor(el);
977
+ var item = {
978
+ ref: '', selector: sel, xpath: aetherXpath(el), scope: scope,
979
+ framePath: framePath.slice(), shadowDepth: shadowDepth,
980
+ tag: el.tagName.toLowerCase(), role: roleVal,
981
+ type: el.getAttribute('type') || '',
982
+ name: aetherNorm(el.getAttribute('aria-label') || label || aetherAccessibleName(el) || el.getAttribute('name')),
983
+ text: includeText ? aetherNorm(el.innerText || el.textContent).substring(0, 180) : '',
984
+ label: label, placeholder: aetherNorm(el.getAttribute('placeholder')),
985
+ title: aetherNorm(el.getAttribute('title')), value: aetherNorm(el.getAttribute('value')),
986
+ score: 0, matchedBy: '',
987
+ bounds: { x: Math.round(frameOffset.x + rect.left), y: Math.round(frameOffset.y + rect.top),
988
+ width: Math.round(rect.width), height: Math.round(rect.height) }
989
+ };
990
+ item.ref = (item.scope === 'document' && item.framePath.length === 0 && item.shadowDepth === 0)
991
+ ? 'css:' + item.selector
992
+ : 'point:' + Math.round(item.bounds.x + item.bounds.width / 2) + ',' + Math.round(item.bounds.y + item.bounds.height / 2);
993
+ out.push(scoreCandidate(item));
994
+ }
995
+
996
+ var all = [];
997
+ try { all = Array.from(root.querySelectorAll('*')); } catch(e) {}
998
+ for (var si = 0; si < all.length; si++) {
999
+ var cel = all[si];
1000
+ if (cel.shadowRoot) { collectFromRoot(cel.shadowRoot, framePath, frameOffset, shadowDepth + 1, 'shadow', out); }
1001
+ if (cel.tagName && cel.tagName.toLowerCase() === 'iframe') {
1002
+ try {
1003
+ var doc = cel.contentDocument;
1004
+ if (doc) {
1005
+ var frect = cel.getBoundingClientRect();
1006
+ collectFromRoot(doc,
1007
+ framePath.concat([Array.from(cel.ownerDocument.querySelectorAll('iframe')).indexOf(cel)]),
1008
+ { x: frameOffset.x + frect.left, y: frameOffset.y + frect.top },
1009
+ shadowDepth, 'frame', out);
1010
+ }
1011
+ } catch(e) {}
1012
+ }
1013
+ }
1014
+ }
1015
+
1016
+ var candidates = [];
1017
+ collectFromRoot(document, [], { x: 0, y: 0 }, 0, 'document', candidates);
1018
+ candidates.sort(function(a, b) {
1019
+ if (mode === 'list') return (a.bounds.y - b.bounds.y) || (a.bounds.x - b.bounds.x);
1020
+ return b.score - a.score || a.bounds.y - b.bounds.y || a.bounds.x - b.bounds.x;
1021
+ });
1022
+ return JSON.stringify({ candidates: candidates.slice(0, maxCandidates) });
1023
+ };
1024
+ `;