@weavetab/mcp 2.5.0 → 2.5.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.
- package/AGENTS.md +49 -49
- package/ARCHITECTURE.md +590 -590
- package/LICENSE +216 -216
- package/README.md +269 -269
- package/SECURITY.md +207 -207
- package/TROUBLESHOOTING.md +373 -365
- package/dist/anticaptcha/detector.js +395 -395
- package/dist/anticaptcha/solver.js +130 -130
- package/dist/cdp/bridge.js +73 -73
- package/dist/cdp/connector.js +46 -22
- package/dist/cdp/extension.js +34 -34
- package/dist/cdp/helpers.js +174 -174
- package/dist/cdp/pruner.js +195 -195
- package/dist/cdp/ref_cache.js +66 -66
- package/dist/cdp/security.js +10 -10
- package/dist/cdp/verify.js +57 -57
- package/dist/cdp/walker.js +563 -525
- package/dist/cli/plugin.js +26 -3
- package/dist/config/loader.d.ts +0 -3
- package/dist/config/loader.js +0 -9
- package/dist/index.js +0 -4
- package/dist/intelligence/intent.js +81 -81
- package/dist/intelligence/sessionMemory.js +2 -2
- package/dist/overlay/ghost.js +22 -22
- package/dist/overlay/injector.js +89 -89
- package/dist/plugin/extension_bridge.js +54 -54
- package/dist/plugin/interface.d.ts +7 -0
- package/dist/plugin/loader.js +3 -4
- package/dist/plugin/schema.d.ts +11 -0
- package/dist/plugin/schema.js +59 -0
- package/dist/security/guard.js +18 -18
- package/dist/sensors/actionability.js +167 -167
- package/dist/sensors/guard.js +29 -29
- package/dist/sensors/network.js +15 -17
- package/dist/server.d.ts +1 -1
- package/dist/server.js +173 -144
- package/dist/state/delta.js +6 -7
- package/dist/tools/automation.js +85 -85
- package/dist/tools/canvas.js +48 -25
- package/dist/tools/checkpoint.js +94 -35
- package/dist/tools/click.js +52 -41
- package/dist/tools/clipboard.js +80 -74
- package/dist/tools/design_extract.js +441 -441
- package/dist/tools/dialog.js +13 -13
- package/dist/tools/emulation.d.ts +2 -0
- package/dist/tools/emulation.js +7 -0
- package/dist/tools/fill.js +344 -347
- package/dist/tools/find.js +78 -52
- package/dist/tools/highlight.js +67 -67
- package/dist/tools/inspect.js +54 -25
- package/dist/tools/key.js +42 -35
- package/dist/tools/navigate.d.ts +1 -1
- package/dist/tools/navigate.js +35 -35
- package/dist/tools/office/parse.js +62 -18
- package/dist/tools/pdf.d.ts +9 -0
- package/dist/tools/pdf.js +7 -4
- package/dist/tools/performance.d.ts +11 -4
- package/dist/tools/performance.js +94 -42
- package/dist/tools/performance_test.d.ts +2 -2
- package/dist/tools/performance_test.js +69 -69
- package/dist/tools/pointer.js +116 -116
- package/dist/tools/read.js +99 -67
- package/dist/tools/recording.js +9 -0
- package/dist/tools/scrape.js +146 -146
- package/dist/tools/screenshot.d.ts +2 -0
- package/dist/tools/screenshot.js +46 -4
- package/dist/tools/scroll.d.ts +3 -1
- package/dist/tools/scroll.js +13 -15
- package/dist/tools/select.d.ts +3 -0
- package/dist/tools/select.js +131 -124
- package/dist/tools/snapshot.js +8 -6
- package/dist/tools/storage.d.ts +5 -1
- package/dist/tools/storage.js +16 -0
- package/dist/tools/tabs.d.ts +7 -5
- package/dist/tools/tabs.js +79 -26
- package/dist/tools/thought.js +8 -8
- package/dist/tools/type.js +274 -274
- package/dist/tools/type_secret.js +23 -23
- package/dist/tools/upload.js +3 -3
- package/dist/tools/video_compiler.js +9 -0
- package/dist/tools/wait.js +35 -35
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/DOCUMENTATION.md +583 -585
- package/docs/PLUGINS.md +139 -121
- package/docs/TOOLS.md +3 -4
- package/docs/architecture/blind-injection.md +83 -83
- package/docs/architecture/overview.md +49 -49
- package/docs/architecture/security.md +36 -36
- package/docs/architecture/transport.md +37 -37
- package/docs/cli/cli-commands.md +50 -50
- package/docs/cli/cli-flags.md +28 -28
- package/docs/cli/cli-scripting.md +35 -35
- package/docs/configuration/config-locations.md +23 -23
- package/docs/configuration/configuration.md +39 -27
- package/docs/configuration/env-variables.md +19 -19
- package/docs/configuration/folders.md +62 -55
- package/docs/desktop-automation/electron-apps.md +225 -214
- package/docs/get-started/installation.md +1 -1
- package/docs/get-started/introduction.md +60 -60
- package/docs/get-started/quickstart.md +2 -2
- package/docs/get-started/setup-wizard.md +27 -27
- package/docs/intelligence/site-profiles.md +151 -151
- package/docs/tools/tools.md +582 -631
- package/docs/troubleshooting/trouble-browser.md +27 -27
- package/docs/troubleshooting/trouble-loop.md +31 -31
- package/ext-dist/background.js +415 -415
- package/ext-dist/content.js +2527 -2527
- package/ext-dist/dashboard.html +583 -583
- package/ext-dist/hud/hud.js +3 -3
- package/ext-dist/hud/index.html +20 -20
- package/ext-dist/manifest.json +73 -73
- package/ext-dist/styles.css +223 -223
- package/mcp.json +796 -805
- package/package.json +118 -118
- package/server.json +23 -23
- package/dist/cdp/walker.test.d.ts +0 -2
- package/dist/cdp/walker.test.js +0 -172
- package/dist/intelligence/sessionMemory.test.d.ts +0 -2
- package/dist/intelligence/sessionMemory.test.js +0 -62
- package/dist/intelligence/trail.test.d.ts +0 -2
- package/dist/intelligence/trail.test.js +0 -110
- package/dist/state/cache.test.d.ts +0 -2
- package/dist/state/cache.test.js +0 -76
- package/dist/state/delta.test.d.ts +0 -2
- package/dist/state/delta.test.js +0 -279
- package/dist/state/groups.d.ts +0 -15
- package/dist/state/groups.js +0 -80
- package/dist/tools/accessibility.d.ts +0 -14
- package/dist/tools/accessibility.js +0 -32
- package/dist/tools/ask.d.ts +0 -14
- package/dist/tools/ask.js +0 -49
- package/dist/tools/click_and_wait.d.ts +0 -33
- package/dist/tools/click_and_wait.js +0 -149
- package/dist/tools/drag.d.ts +0 -15
- package/dist/tools/drag.js +0 -29
- package/dist/tools/geo.d.ts +0 -19
- package/dist/tools/geo.js +0 -29
- package/dist/tools/github/analyze.d.ts +0 -4
- package/dist/tools/github/analyze.js +0 -113
- package/dist/tools/github/cache.d.ts +0 -21
- package/dist/tools/github/cache.js +0 -28
- package/dist/tools/github/index.d.ts +0 -19
- package/dist/tools/github/index.js +0 -40
- package/dist/tools/github/issues.d.ts +0 -9
- package/dist/tools/github/issues.js +0 -88
- package/dist/tools/github/pr.d.ts +0 -5
- package/dist/tools/github/pr.js +0 -88
- package/dist/tools/github/rateLimiter.d.ts +0 -4
- package/dist/tools/github/rateLimiter.js +0 -46
- package/dist/tools/github/read.d.ts +0 -34
- package/dist/tools/github/read.js +0 -74
- package/dist/tools/github/repoContext.d.ts +0 -20
- package/dist/tools/github/repoContext.js +0 -129
- package/dist/tools/hover.d.ts +0 -19
- package/dist/tools/hover.js +0 -57
- package/dist/tools/integration.test.d.ts +0 -2
- package/dist/tools/integration.test.js +0 -96
- package/dist/tools/macro_compiler.d.ts +0 -7
- package/dist/tools/macro_compiler.js +0 -82
- package/dist/tools/mouse.d.ts +0 -21
- package/dist/tools/mouse.js +0 -62
- package/dist/tools/response.d.ts +0 -129
- package/dist/tools/response.js +0 -5
- package/dist/tools/wait_for.d.ts +0 -23
- package/dist/tools/wait_for.js +0 -30
- package/dist/tools/windows.d.ts +0 -22
- package/dist/tools/windows.js +0 -57
package/dist/cdp/walker.js
CHANGED
|
@@ -3,9 +3,9 @@ import { SessionSecretTracker } from "../security/secrets.js";
|
|
|
3
3
|
import { getStoredRef, getStoredProps } from "../state/delta.js";
|
|
4
4
|
import { getRawCachedActionMap } from "../state/cache.js";
|
|
5
5
|
// ─── Public types ─────────────────────────────────────────────────────────────
|
|
6
|
-
export const WT_REGISTRY_INIT_EXPR = `
|
|
7
|
-
window.__weavetab = window.__weavetab || {};
|
|
8
|
-
window.__weavetab.registry = window.__weavetab.registry || new WeakMap();
|
|
6
|
+
export const WT_REGISTRY_INIT_EXPR = `
|
|
7
|
+
window.__weavetab = window.__weavetab || {};
|
|
8
|
+
window.__weavetab.registry = window.__weavetab.registry || new WeakMap();
|
|
9
9
|
`;
|
|
10
10
|
const viewportCache = new Map();
|
|
11
11
|
export function clearViewportCache(targetId) {
|
|
@@ -101,7 +101,23 @@ export async function buildActionMap(session, options = {}, targetId = "default"
|
|
|
101
101
|
// ── Begin a new walk pass — resets per-pass dedup registry ─────────────────
|
|
102
102
|
const passCounterBase = beginWalkPass();
|
|
103
103
|
const url = options.url ?? (await session.Page.getFrameTree()).frameTree.frame.url;
|
|
104
|
-
|
|
104
|
+
let title = options.title;
|
|
105
|
+
if (!title) {
|
|
106
|
+
try {
|
|
107
|
+
const evalRes = await session.Runtime.evaluate({
|
|
108
|
+
expression: "document.title",
|
|
109
|
+
returnByValue: true
|
|
110
|
+
});
|
|
111
|
+
if (evalRes?.result?.value) {
|
|
112
|
+
title = String(evalRes.result.value);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch { /* ignore */ }
|
|
116
|
+
}
|
|
117
|
+
if (!title) {
|
|
118
|
+
const { sessionRegistry } = await import("../state/session.js");
|
|
119
|
+
title = sessionRegistry.getAll().find(c => c.targetId === targetId)?.title || "";
|
|
120
|
+
}
|
|
105
121
|
// Inherit sticky pruning state from session if not explicitly set in options
|
|
106
122
|
if (options.prune === undefined) {
|
|
107
123
|
const { sessionRegistry } = await import("../state/session.js");
|
|
@@ -129,10 +145,10 @@ export async function buildActionMap(session, options = {}, targetId = "default"
|
|
|
129
145
|
if (sampleIds.length > 0) {
|
|
130
146
|
try {
|
|
131
147
|
const { result: domVerify } = await session.Runtime.evaluate({
|
|
132
|
-
expression: `(function(ids) {
|
|
133
|
-
return ids.filter(function(id) {
|
|
134
|
-
return !document.querySelector('[data-wt-id="' + id + '"]');
|
|
135
|
-
}).length;
|
|
148
|
+
expression: `(function(ids) {
|
|
149
|
+
return ids.filter(function(id) {
|
|
150
|
+
return !document.querySelector('[data-wt-id="' + id + '"]');
|
|
151
|
+
}).length;
|
|
136
152
|
})(${JSON.stringify(sampleIds)})`,
|
|
137
153
|
returnByValue: true
|
|
138
154
|
});
|
|
@@ -149,14 +165,14 @@ export async function buildActionMap(session, options = {}, targetId = "default"
|
|
|
149
165
|
// Architecture change: Try to read from extension's live index first
|
|
150
166
|
// The extension now maintains window.__wt_liveIndex via continuous MutationObserver
|
|
151
167
|
const { result: liveIndexResult } = await session.Runtime.evaluate({
|
|
152
|
-
expression: `(function() {
|
|
153
|
-
if (typeof window.__wt_liveIndex !== 'undefined' && Array.isArray(window.__wt_liveIndex)) {
|
|
154
|
-
return JSON.stringify({
|
|
155
|
-
elements: window.__wt_liveIndex,
|
|
156
|
-
source: 'live-index'
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
return JSON.stringify({ elements: null, source: 'unavailable' });
|
|
168
|
+
expression: `(function() {
|
|
169
|
+
if (typeof window.__wt_liveIndex !== 'undefined' && Array.isArray(window.__wt_liveIndex)) {
|
|
170
|
+
return JSON.stringify({
|
|
171
|
+
elements: window.__wt_liveIndex,
|
|
172
|
+
source: 'live-index'
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
return JSON.stringify({ elements: null, source: 'unavailable' });
|
|
160
176
|
})()`,
|
|
161
177
|
returnByValue: true
|
|
162
178
|
});
|
|
@@ -178,256 +194,257 @@ export async function buildActionMap(session, options = {}, targetId = "default"
|
|
|
178
194
|
// Hardened with per-element try-catch — one bad node no longer kills the entire map
|
|
179
195
|
// Deep shadow DOM traversal — falls back to recursive shadow-penetrating query when flat query is sparse
|
|
180
196
|
const { result } = await session.Runtime.evaluate({
|
|
181
|
-
expression: `(function() {
|
|
182
|
-
var INTERACTIVE_SEL = 'a, button, input, textarea, select, [role="button"], [role="link"], [role="textbox"], [role="checkbox"], [role="radio"], [role="combobox"], [role="searchbox"], [role="menuitem"], [role="tab"], [tabindex]:not([tabindex="-1"])';
|
|
183
|
-
|
|
184
|
-
// deepQuery has a hard depth limit and a visited-set guard so circular
|
|
185
|
-
// shadow root references can't cause a stack overflow.
|
|
186
|
-
function deepQuery(root, selector, visited, depth) {
|
|
187
|
-
if (!visited) visited = new Set();
|
|
188
|
-
if (depth === undefined) depth = 0;
|
|
189
|
-
if (depth > 10) return []; // hard depth limit
|
|
190
|
-
if (visited.has(root)) return []; // break cycles
|
|
191
|
-
visited.add(root);
|
|
192
|
-
var all = [];
|
|
193
|
-
try {
|
|
194
|
-
var direct = root.querySelectorAll(selector);
|
|
195
|
-
for (var i = 0; i < direct.length; i++) all.push(direct[i]);
|
|
196
|
-
} catch (e) {}
|
|
197
|
-
// TreeWalker — lazy DOM traversal, avoids building a static NodeList of every element
|
|
198
|
-
try {
|
|
199
|
-
var walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, null, false);
|
|
200
|
-
while (walker.nextNode()) {
|
|
201
|
-
try {
|
|
202
|
-
var el = walker.currentNode;
|
|
203
|
-
if (el.shadowRoot && !visited.has(el.shadowRoot)) {
|
|
204
|
-
var inner = deepQuery(el.shadowRoot, selector, visited, depth + 1);
|
|
205
|
-
for (var k = 0; k < inner.length; k++) all.push(inner[k]);
|
|
206
|
-
}
|
|
207
|
-
} catch (e) {}
|
|
208
|
-
}
|
|
209
|
-
} catch (e) {}
|
|
210
|
-
return all;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
var hasShadowCache = ${JSON.stringify(!!shadowDomCache.get(targetId))};
|
|
214
|
-
var useDeep = ${JSON.stringify(!!options.deep)} || hasShadowCache;
|
|
215
|
-
var els = [];
|
|
216
|
-
|
|
217
|
-
// Phase 1: flat query (skip when cache confirms shadow DOM exists — deepQuery covers root too)
|
|
218
|
-
if (!hasShadowCache) {
|
|
219
|
-
try {
|
|
220
|
-
var flatEls = document.querySelectorAll(INTERACTIVE_SEL);
|
|
221
|
-
for (var fi = 0; fi < flatEls.length; fi++) els.push(flatEls[fi]);
|
|
222
|
-
} catch (e) {}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Phase 2: deep query if cache hit OR flat results are sparse
|
|
226
|
-
if (useDeep || els.length < 3) {
|
|
227
|
-
try {
|
|
228
|
-
var deepEls = deepQuery(document, INTERACTIVE_SEL);
|
|
229
|
-
// Deduplicate by comparing tagName + label + position fingerprint
|
|
230
|
-
var seen = new Set();
|
|
231
|
-
for (var di = 0; di < els.length; di++) {
|
|
232
|
-
try {
|
|
233
|
-
var r = els[di].getBoundingClientRect();
|
|
234
|
-
seen.add(els[di].tagName + '|' + Math.round(r.left) + '|' + Math.round(r.top));
|
|
235
|
-
} catch (e) {}
|
|
236
|
-
}
|
|
237
|
-
for (var di2 = 0; di2 < deepEls.length; di2++) {
|
|
238
|
-
try {
|
|
239
|
-
var el2 = deepEls[di2];
|
|
240
|
-
var r2 = el2.getBoundingClientRect();
|
|
241
|
-
var key = el2.tagName + '|' + Math.round(r2.left) + '|' + Math.round(r2.top);
|
|
242
|
-
if (!seen.has(key)) {
|
|
243
|
-
els.push(el2);
|
|
244
|
-
seen.add(key);
|
|
245
|
-
}
|
|
246
|
-
} catch (e) {}
|
|
247
|
-
}
|
|
248
|
-
} catch (e) {}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// ── Content-addressable IDs — stable across re-renders ────────────────
|
|
252
|
-
// djb2 hash of (tagName + role + trimmed text/label + sibling-index + parent context).
|
|
253
|
-
// Same element always gets the same ID even if the walker runs multiple
|
|
254
|
-
// times. Sequential counter only used as collision fallback.
|
|
255
|
-
function wtHashId(el) {
|
|
256
|
-
var tag = el.tagName || '';
|
|
257
|
-
var role = el.getAttribute('role') || '';
|
|
258
|
-
var text = (el.getAttribute('aria-label') || el.textContent || el.value || el.placeholder || '').slice(0, 60).trim();
|
|
259
|
-
var rect = el.getBoundingClientRect();
|
|
260
|
-
var absX = Math.round(rect.left + (window.scrollX || 0));
|
|
261
|
-
var absY = Math.round(rect.top + (window.scrollY || 0));
|
|
262
|
-
var path = '';
|
|
263
|
-
try {
|
|
264
|
-
var curr = el;
|
|
265
|
-
for(var level = 0; level < 3 && curr; level++) {
|
|
266
|
-
var p = curr.parentElement;
|
|
267
|
-
if (!p) break;
|
|
268
|
-
var idx = 0;
|
|
269
|
-
var siblings = p.children;
|
|
270
|
-
for (var si = 0; si < siblings.length; si++) {
|
|
271
|
-
if (siblings[si] === curr) { idx = si; break; }
|
|
272
|
-
}
|
|
273
|
-
path += '|' + (curr.tagName||'') + '[' + idx + ']';
|
|
274
|
-
curr = p;
|
|
275
|
-
}
|
|
276
|
-
} catch(e) {}
|
|
277
|
-
var raw = tag + '|' + role + '|' + text + '|' + absX + '|' + absY + path;
|
|
278
|
-
var h = 5381;
|
|
279
|
-
for (var ci = 0; ci < raw.length; ci++) {
|
|
280
|
-
h = ((h << 5) + h) ^ raw.charCodeAt(ci);
|
|
281
|
-
h = h >>> 0; // keep unsigned 32-bit
|
|
282
|
-
}
|
|
283
|
-
return 'wt' + h.toString(36);
|
|
284
|
-
}
|
|
285
|
-
var usedHashes = {};
|
|
286
|
-
// Bug #5 fix: seed collision counter from the global walk-pass base so that
|
|
287
|
-
// elements in different iframes processed in the same pass never share a
|
|
288
|
-
// numeric suffix. passCounterBase is injected by the TypeScript host.
|
|
289
|
-
var collisionCounter = ${JSON.stringify(passCounterBase)};
|
|
290
|
-
var _coIdx = 0;
|
|
291
|
-
var results = [];
|
|
292
|
-
for (var i2 = 0; i2 < els.length; i2++) {
|
|
293
|
-
try {
|
|
294
|
-
var el = els[i2];
|
|
295
|
-
if (el.closest('[data-wt-ignore]') !== null) continue;
|
|
296
|
-
var rect = el.getBoundingClientRect();
|
|
297
|
-
if (rect.width === 0 || rect.height === 0 || rect.bottom < 0 || rect.top > window.innerHeight) continue;
|
|
298
|
-
var id = el.getAttribute('data-wt-id');
|
|
299
|
-
if (!id) {
|
|
300
|
-
var hid = wtHashId(el);
|
|
301
|
-
// Resolve collision: if same hash already used, append counter
|
|
302
|
-
if (usedHashes[hid]) {
|
|
303
|
-
hid = hid + '_' + (collisionCounter++);
|
|
304
|
-
}
|
|
305
|
-
usedHashes[hid] = true;
|
|
306
|
-
id = hid;
|
|
307
|
-
el.setAttribute('data-wt-id', id);
|
|
308
|
-
}
|
|
309
|
-
var role = el.tagName.toLowerCase();
|
|
310
|
-
if (role === 'a') role = 'link';
|
|
311
|
-
else if (role === 'input' && (el.type === 'text' || el.type === 'search')) role = 'textbox';
|
|
312
|
-
var frameUrl = '';
|
|
313
|
-
try { if (el.ownerDocument !== document) frameUrl = el.ownerDocument.defaultView.location.href; } catch(e) { frameUrl = 'cross-origin-' + (_coIdx++); }
|
|
314
|
-
var secretRegex = /password|passcode|pin|passphrase|secret|token|auth.?key|access.?code|mot.?de.?passe|contrase[nñ]a|kennwort|senha/i;
|
|
315
|
-
var isSecret = el.hasAttribute('data-wt-secret') ||
|
|
316
|
-
(el.tagName === 'INPUT' && (
|
|
317
|
-
el.type === 'password' ||
|
|
318
|
-
secretRegex.test(el.getAttribute('autocomplete') || '') ||
|
|
319
|
-
secretRegex.test(el.getAttribute('name') || '') ||
|
|
320
|
-
secretRegex.test(el.getAttribute('id') || '') ||
|
|
321
|
-
secretRegex.test(el.getAttribute('aria-label') || '')
|
|
322
|
-
));
|
|
323
|
-
var
|
|
324
|
-
|
|
325
|
-
//
|
|
326
|
-
|
|
327
|
-
var
|
|
328
|
-
var
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
var
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
var
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
var
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
//
|
|
361
|
-
//
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
var
|
|
366
|
-
var
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
var
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
var
|
|
388
|
-
var
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
var
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
197
|
+
expression: `(function() {
|
|
198
|
+
var INTERACTIVE_SEL = 'a, button, input, textarea, select, [role="button"], [role="link"], [role="textbox"], [role="checkbox"], [role="radio"], [role="combobox"], [role="searchbox"], [role="menuitem"], [role="tab"], [tabindex]:not([tabindex="-1"])';
|
|
199
|
+
|
|
200
|
+
// deepQuery has a hard depth limit and a visited-set guard so circular
|
|
201
|
+
// shadow root references can't cause a stack overflow.
|
|
202
|
+
function deepQuery(root, selector, visited, depth) {
|
|
203
|
+
if (!visited) visited = new Set();
|
|
204
|
+
if (depth === undefined) depth = 0;
|
|
205
|
+
if (depth > 10) return []; // hard depth limit
|
|
206
|
+
if (visited.has(root)) return []; // break cycles
|
|
207
|
+
visited.add(root);
|
|
208
|
+
var all = [];
|
|
209
|
+
try {
|
|
210
|
+
var direct = root.querySelectorAll(selector);
|
|
211
|
+
for (var i = 0; i < direct.length; i++) all.push(direct[i]);
|
|
212
|
+
} catch (e) {}
|
|
213
|
+
// TreeWalker — lazy DOM traversal, avoids building a static NodeList of every element
|
|
214
|
+
try {
|
|
215
|
+
var walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, null, false);
|
|
216
|
+
while (walker.nextNode()) {
|
|
217
|
+
try {
|
|
218
|
+
var el = walker.currentNode;
|
|
219
|
+
if (el.shadowRoot && !visited.has(el.shadowRoot)) {
|
|
220
|
+
var inner = deepQuery(el.shadowRoot, selector, visited, depth + 1);
|
|
221
|
+
for (var k = 0; k < inner.length; k++) all.push(inner[k]);
|
|
222
|
+
}
|
|
223
|
+
} catch (e) {}
|
|
224
|
+
}
|
|
225
|
+
} catch (e) {}
|
|
226
|
+
return all;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
var hasShadowCache = ${JSON.stringify(!!shadowDomCache.get(targetId))};
|
|
230
|
+
var useDeep = ${JSON.stringify(!!options.deep)} || hasShadowCache;
|
|
231
|
+
var els = [];
|
|
232
|
+
|
|
233
|
+
// Phase 1: flat query (skip when cache confirms shadow DOM exists — deepQuery covers root too)
|
|
234
|
+
if (!hasShadowCache) {
|
|
235
|
+
try {
|
|
236
|
+
var flatEls = document.querySelectorAll(INTERACTIVE_SEL);
|
|
237
|
+
for (var fi = 0; fi < flatEls.length; fi++) els.push(flatEls[fi]);
|
|
238
|
+
} catch (e) {}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Phase 2: deep query if cache hit OR flat results are sparse
|
|
242
|
+
if (useDeep || els.length < 3) {
|
|
243
|
+
try {
|
|
244
|
+
var deepEls = deepQuery(document, INTERACTIVE_SEL);
|
|
245
|
+
// Deduplicate by comparing tagName + label + position fingerprint
|
|
246
|
+
var seen = new Set();
|
|
247
|
+
for (var di = 0; di < els.length; di++) {
|
|
248
|
+
try {
|
|
249
|
+
var r = els[di].getBoundingClientRect();
|
|
250
|
+
seen.add(els[di].tagName + '|' + Math.round(r.left) + '|' + Math.round(r.top));
|
|
251
|
+
} catch (e) {}
|
|
252
|
+
}
|
|
253
|
+
for (var di2 = 0; di2 < deepEls.length; di2++) {
|
|
254
|
+
try {
|
|
255
|
+
var el2 = deepEls[di2];
|
|
256
|
+
var r2 = el2.getBoundingClientRect();
|
|
257
|
+
var key = el2.tagName + '|' + Math.round(r2.left) + '|' + Math.round(r2.top);
|
|
258
|
+
if (!seen.has(key)) {
|
|
259
|
+
els.push(el2);
|
|
260
|
+
seen.add(key);
|
|
261
|
+
}
|
|
262
|
+
} catch (e) {}
|
|
263
|
+
}
|
|
264
|
+
} catch (e) {}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ── Content-addressable IDs — stable across re-renders ────────────────
|
|
268
|
+
// djb2 hash of (tagName + role + trimmed text/label + sibling-index + parent context).
|
|
269
|
+
// Same element always gets the same ID even if the walker runs multiple
|
|
270
|
+
// times. Sequential counter only used as collision fallback.
|
|
271
|
+
function wtHashId(el) {
|
|
272
|
+
var tag = el.tagName || '';
|
|
273
|
+
var role = el.getAttribute('role') || '';
|
|
274
|
+
var text = (el.getAttribute('aria-label') || el.textContent || el.value || el.placeholder || '').slice(0, 60).trim();
|
|
275
|
+
var rect = el.getBoundingClientRect();
|
|
276
|
+
var absX = Math.round(rect.left + (window.scrollX || 0));
|
|
277
|
+
var absY = Math.round(rect.top + (window.scrollY || 0));
|
|
278
|
+
var path = '';
|
|
279
|
+
try {
|
|
280
|
+
var curr = el;
|
|
281
|
+
for(var level = 0; level < 3 && curr; level++) {
|
|
282
|
+
var p = curr.parentElement;
|
|
283
|
+
if (!p) break;
|
|
284
|
+
var idx = 0;
|
|
285
|
+
var siblings = p.children;
|
|
286
|
+
for (var si = 0; si < siblings.length; si++) {
|
|
287
|
+
if (siblings[si] === curr) { idx = si; break; }
|
|
288
|
+
}
|
|
289
|
+
path += '|' + (curr.tagName||'') + '[' + idx + ']';
|
|
290
|
+
curr = p;
|
|
291
|
+
}
|
|
292
|
+
} catch(e) {}
|
|
293
|
+
var raw = tag + '|' + role + '|' + text + '|' + absX + '|' + absY + path;
|
|
294
|
+
var h = 5381;
|
|
295
|
+
for (var ci = 0; ci < raw.length; ci++) {
|
|
296
|
+
h = ((h << 5) + h) ^ raw.charCodeAt(ci);
|
|
297
|
+
h = h >>> 0; // keep unsigned 32-bit
|
|
298
|
+
}
|
|
299
|
+
return 'wt' + h.toString(36);
|
|
300
|
+
}
|
|
301
|
+
var usedHashes = {};
|
|
302
|
+
// Bug #5 fix: seed collision counter from the global walk-pass base so that
|
|
303
|
+
// elements in different iframes processed in the same pass never share a
|
|
304
|
+
// numeric suffix. passCounterBase is injected by the TypeScript host.
|
|
305
|
+
var collisionCounter = ${JSON.stringify(passCounterBase)};
|
|
306
|
+
var _coIdx = 0;
|
|
307
|
+
var results = [];
|
|
308
|
+
for (var i2 = 0; i2 < els.length; i2++) {
|
|
309
|
+
try {
|
|
310
|
+
var el = els[i2];
|
|
311
|
+
if (el.closest('[data-wt-ignore]') !== null) continue;
|
|
312
|
+
var rect = el.getBoundingClientRect();
|
|
313
|
+
if (rect.width === 0 || rect.height === 0 || rect.bottom < 0 || rect.top > window.innerHeight) continue;
|
|
314
|
+
var id = el.getAttribute('data-wt-id');
|
|
315
|
+
if (!id) {
|
|
316
|
+
var hid = wtHashId(el);
|
|
317
|
+
// Resolve collision: if same hash already used, append counter
|
|
318
|
+
if (usedHashes[hid]) {
|
|
319
|
+
hid = hid + '_' + (collisionCounter++);
|
|
320
|
+
}
|
|
321
|
+
usedHashes[hid] = true;
|
|
322
|
+
id = hid;
|
|
323
|
+
el.setAttribute('data-wt-id', id);
|
|
324
|
+
}
|
|
325
|
+
var role = el.tagName.toLowerCase();
|
|
326
|
+
if (role === 'a') role = 'link';
|
|
327
|
+
else if (role === 'input' && (el.type === 'text' || el.type === 'search')) role = 'textbox';
|
|
328
|
+
var frameUrl = '';
|
|
329
|
+
try { if (el.ownerDocument !== document) frameUrl = el.ownerDocument.defaultView.location.href; } catch(e) { frameUrl = 'cross-origin-' + (_coIdx++); }
|
|
330
|
+
var secretRegex = /password|passcode|pin|passphrase|secret|token|auth.?key|access.?code|mot.?de.?passe|contrase[nñ]a|kennwort|senha/i;
|
|
331
|
+
var isSecret = el.hasAttribute('data-wt-secret') ||
|
|
332
|
+
(el.tagName === 'INPUT' && (
|
|
333
|
+
el.type === 'password' ||
|
|
334
|
+
secretRegex.test(el.getAttribute('autocomplete') || '') ||
|
|
335
|
+
secretRegex.test(el.getAttribute('name') || '') ||
|
|
336
|
+
secretRegex.test(el.getAttribute('id') || '') ||
|
|
337
|
+
secretRegex.test(el.getAttribute('aria-label') || '')
|
|
338
|
+
));
|
|
339
|
+
var richPh = el.getAttribute('placeholder') || el.getAttribute('data-placeholder') || el.getAttribute('aria-placeholder') || (el.querySelector ? (el.querySelector('[data-slate-placeholder]')?.textContent || '') : '') || '';
|
|
340
|
+
var labelStr = el.getAttribute('aria-label') || el.innerText || (isSecret ? '' : el.value) || richPh || '';
|
|
341
|
+
// For radio/checkbox, serialize checked state as 'true'/'false' so the agent
|
|
342
|
+
// can see which option is selected without having to scrape the DOM separately.
|
|
343
|
+
var elType = (el.type || '').toLowerCase();
|
|
344
|
+
var elRole = (el.getAttribute('role') || el.tagName.toLowerCase());
|
|
345
|
+
var isCheckable = elType === 'radio' || elType === 'checkbox' ||
|
|
346
|
+
elRole === 'radio' || elRole === 'checkbox';
|
|
347
|
+
var isOption = el.tagName === 'OPTION';
|
|
348
|
+
var valStr;
|
|
349
|
+
if (isCheckable) {
|
|
350
|
+
valStr = el.checked ? 'true' : 'false';
|
|
351
|
+
} else if (isOption) {
|
|
352
|
+
valStr = el.selected ? 'true' : 'false';
|
|
353
|
+
} else {
|
|
354
|
+
valStr = el.value !== undefined ? String(el.value) : undefined;
|
|
355
|
+
}
|
|
356
|
+
var txtContent = (el.textContent || '').slice(0, 200);
|
|
357
|
+
|
|
358
|
+
if (isSecret) {
|
|
359
|
+
var rawAria = el.getAttribute('aria-label') || '';
|
|
360
|
+
var rawPlaceholder = el.placeholder || '';
|
|
361
|
+
labelStr = rawAria || rawPlaceholder || '[REDACTED SECRET FIELD]';
|
|
362
|
+
if (valStr !== undefined) valStr = '[REDACTED]';
|
|
363
|
+
txtContent = '[REDACTED]';
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Capture structural idiom (e.g. aria-controls, aria-owns, etc.)
|
|
367
|
+
var idiom = '';
|
|
368
|
+
var CONTROLS_ATTRS = ['aria-controls', 'aria-owns', 'data-target', 'data-bs-target'];
|
|
369
|
+
for (var ca = 0; ca < CONTROLS_ATTRS.length; ca++) {
|
|
370
|
+
if (el.hasAttribute(CONTROLS_ATTRS[ca])) {
|
|
371
|
+
idiom = CONTROLS_ATTRS[ca];
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// ── Container context binding ─────────────────────────────────────
|
|
377
|
+
// Walk up the DOM to find a meaningful card/row/list-item heading that
|
|
378
|
+
// describes this element's entity context, e.g. "Pro Plan ($29/mo)".
|
|
379
|
+
var containerCtx = '';
|
|
380
|
+
try {
|
|
381
|
+
var CARD_SEL = '[class*="card"],[class*="row"],[class*="item"],[class*="product"],[class*="plan"],[class*="entry"],[class*="result"],[class*="tile"],[class*="option"],[data-entity]';
|
|
382
|
+
var ancestor = el.parentElement;
|
|
383
|
+
var depth3 = 0;
|
|
384
|
+
while (ancestor && depth3 < 8) {
|
|
385
|
+
if (ancestor.matches && ancestor.matches(CARD_SEL)) {
|
|
386
|
+
var heading = ancestor.querySelector('h1,h2,h3,h4,h5,h6,[class*="title"],[class*="name"],[class*="heading"],[class*="label"]');
|
|
387
|
+
if (heading) {
|
|
388
|
+
var hText = (heading.innerText || heading.textContent || '').trim().slice(0, 60);
|
|
389
|
+
if (hText && hText !== labelStr) { containerCtx = hText; break; }
|
|
390
|
+
}
|
|
391
|
+
var allText = (ancestor.innerText || ancestor.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 60);
|
|
392
|
+
if (allText && allText !== labelStr) { containerCtx = allText; break; }
|
|
393
|
+
}
|
|
394
|
+
ancestor = ancestor.parentElement;
|
|
395
|
+
depth3++;
|
|
396
|
+
}
|
|
397
|
+
} catch(e) {}
|
|
398
|
+
|
|
399
|
+
// ── Modal detection ───────────────────────────────────────────────
|
|
400
|
+
var inModal = false;
|
|
401
|
+
var modalTitleStr = '';
|
|
402
|
+
try {
|
|
403
|
+
var MODAL_SEL = 'dialog[open],[role="dialog"],[aria-modal="true"],[class*="modal"],[class*="overlay"],[class*="drawer"]';
|
|
404
|
+
var modalAnc = el.parentElement;
|
|
405
|
+
var md = 0;
|
|
406
|
+
while (modalAnc && md < 15) {
|
|
407
|
+
if (modalAnc.matches && modalAnc.matches(MODAL_SEL)) {
|
|
408
|
+
inModal = true;
|
|
409
|
+
var mh = modalAnc.querySelector('h1,h2,h3,h4,[class*="title"]');
|
|
410
|
+
if (mh) {
|
|
411
|
+
var lbId = modalAnc.getAttribute('aria-labelledby');
|
|
412
|
+
var lbEl = lbId ? document.getElementById(lbId) : null;
|
|
413
|
+
modalTitleStr = ((lbEl || mh).innerText || (lbEl || mh).textContent || '').trim().slice(0, 60);
|
|
414
|
+
}
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
modalAnc = modalAnc.parentElement;
|
|
418
|
+
md++;
|
|
419
|
+
}
|
|
420
|
+
} catch(e) {}
|
|
421
|
+
|
|
422
|
+
results.push({
|
|
423
|
+
id: String(id),
|
|
424
|
+
role: el.getAttribute('role') || role,
|
|
425
|
+
type: elType || undefined,
|
|
426
|
+
label: labelStr,
|
|
427
|
+
placeholder: el.placeholder || richPh || undefined,
|
|
428
|
+
value: valStr,
|
|
429
|
+
x: Math.round(rect.left + rect.width / 2),
|
|
430
|
+
y: Math.round(rect.top + rect.height / 2),
|
|
431
|
+
selector: '[data-wt-id="' + id + '"]',
|
|
432
|
+
href: el.href,
|
|
433
|
+
textContent: txtContent,
|
|
434
|
+
className: el.className || '',
|
|
435
|
+
frameUrl: frameUrl,
|
|
436
|
+
structuralIdiom: idiom || undefined,
|
|
437
|
+
containerContext: containerCtx || undefined,
|
|
438
|
+
isModal: inModal || undefined,
|
|
439
|
+
modalTitle: modalTitleStr || undefined,
|
|
440
|
+
disabled: (el.disabled || el.getAttribute('aria-disabled') === 'true') || undefined,
|
|
441
|
+
checked: isCheckable ? el.checked : undefined,
|
|
442
|
+
});
|
|
443
|
+
} catch (e) {
|
|
444
|
+
// Silently skip elements that cause errors (cross-origin SVG, detached nodes, etc.)
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return JSON.stringify({ elements: results, shadowFound: results.length > 0 && typeof flatEls !== 'undefined' && els.length > flatEls.length });
|
|
431
448
|
})()`,
|
|
432
449
|
returnByValue: true
|
|
433
450
|
});
|
|
@@ -459,31 +476,31 @@ export async function buildActionMap(session, options = {}, targetId = "default"
|
|
|
459
476
|
for (const [, childSession] of childSessions.entries()) {
|
|
460
477
|
try {
|
|
461
478
|
const { result: childResult } = await childSession.Runtime.evaluate({
|
|
462
|
-
expression: `(function() {
|
|
463
|
-
var INTERACTIVE_SEL = 'a, button, input, textarea, select, [role="button"], [role="link"], [role="textbox"], [role="checkbox"], [role="radio"], [role="combobox"], [role="searchbox"], [role="menuitem"], [role="tab"], [contenteditable="true"], [tabindex]:not([tabindex="-1"])';
|
|
464
|
-
var els = document.querySelectorAll(INTERACTIVE_SEL);
|
|
465
|
-
var results = [];
|
|
466
|
-
for (var i = 0; i < els.length; i++) {
|
|
467
|
-
var el = els[i];
|
|
468
|
-
var rect = el.getBoundingClientRect();
|
|
469
|
-
if (rect.width === 0 || rect.height === 0 || rect.bottom < 0 || rect.top > window.innerHeight) continue;
|
|
470
|
-
var id = el.getAttribute('data-wt-id');
|
|
471
|
-
if (!id) {
|
|
472
|
-
id = 'wv_' + Math.random().toString(36).slice(2, 9);
|
|
473
|
-
el.setAttribute('data-wt-id', id);
|
|
474
|
-
}
|
|
475
|
-
results.push({
|
|
476
|
-
id: id,
|
|
477
|
-
role: el.getAttribute('role') || el.tagName.toLowerCase(),
|
|
478
|
-
label: el.getAttribute('aria-label') || el.innerText || el.value || el.placeholder || '',
|
|
479
|
-
value: el.value,
|
|
480
|
-
x: Math.round(rect.left + rect.width / 2),
|
|
481
|
-
y: Math.round(rect.top + rect.height / 2),
|
|
482
|
-
selector: '[data-wt-id="' + id + '"]',
|
|
483
|
-
frameUrl: window.location.href,
|
|
484
|
-
});
|
|
485
|
-
}
|
|
486
|
-
return JSON.stringify(results);
|
|
479
|
+
expression: `(function() {
|
|
480
|
+
var INTERACTIVE_SEL = 'a, button, input, textarea, select, [role="button"], [role="link"], [role="textbox"], [role="checkbox"], [role="radio"], [role="combobox"], [role="searchbox"], [role="menuitem"], [role="tab"], [contenteditable="true"], [tabindex]:not([tabindex="-1"])';
|
|
481
|
+
var els = document.querySelectorAll(INTERACTIVE_SEL);
|
|
482
|
+
var results = [];
|
|
483
|
+
for (var i = 0; i < els.length; i++) {
|
|
484
|
+
var el = els[i];
|
|
485
|
+
var rect = el.getBoundingClientRect();
|
|
486
|
+
if (rect.width === 0 || rect.height === 0 || rect.bottom < 0 || rect.top > window.innerHeight) continue;
|
|
487
|
+
var id = el.getAttribute('data-wt-id');
|
|
488
|
+
if (!id) {
|
|
489
|
+
id = 'wv_' + Math.random().toString(36).slice(2, 9);
|
|
490
|
+
el.setAttribute('data-wt-id', id);
|
|
491
|
+
}
|
|
492
|
+
results.push({
|
|
493
|
+
id: id,
|
|
494
|
+
role: el.getAttribute('role') || el.tagName.toLowerCase(),
|
|
495
|
+
label: el.getAttribute('aria-label') || el.innerText || el.value || el.placeholder || '',
|
|
496
|
+
value: el.value,
|
|
497
|
+
x: Math.round(rect.left + rect.width / 2),
|
|
498
|
+
y: Math.round(rect.top + rect.height / 2),
|
|
499
|
+
selector: '[data-wt-id="' + id + '"]',
|
|
500
|
+
frameUrl: window.location.href,
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
return JSON.stringify(results);
|
|
487
504
|
})()`,
|
|
488
505
|
returnByValue: true,
|
|
489
506
|
});
|
|
@@ -624,12 +641,12 @@ export async function buildActionMap(session, options = {}, targetId = "default"
|
|
|
624
641
|
let scopedElements = prunedElements;
|
|
625
642
|
if (options.scope) {
|
|
626
643
|
const { result: scopeResult } = await session.Runtime.evaluate({
|
|
627
|
-
expression: `(function(sel) {
|
|
628
|
-
const el = document.querySelector(sel);
|
|
629
|
-
if (!el) return null;
|
|
630
|
-
const r = el.getBoundingClientRect();
|
|
631
|
-
const ids = [...el.querySelectorAll('[data-wt-id]')].map(e => e.getAttribute('data-wt-id')).filter(Boolean);
|
|
632
|
-
return { left: r.left, top: r.top, right: r.right, bottom: r.bottom, ids: ids };
|
|
644
|
+
expression: `(function(sel) {
|
|
645
|
+
const el = document.querySelector(sel);
|
|
646
|
+
if (!el) return null;
|
|
647
|
+
const r = el.getBoundingClientRect();
|
|
648
|
+
const ids = [...el.querySelectorAll('[data-wt-id]')].map(e => e.getAttribute('data-wt-id')).filter(Boolean);
|
|
649
|
+
return { left: r.left, top: r.top, right: r.right, bottom: r.bottom, ids: ids };
|
|
633
650
|
})(${JSON.stringify(options.scope)})`,
|
|
634
651
|
returnByValue: true,
|
|
635
652
|
}).catch(() => ({ result: { value: null } }));
|
|
@@ -697,15 +714,27 @@ export function findAllMatches(elements, query) {
|
|
|
697
714
|
// 1. Exact match (highest priority)
|
|
698
715
|
elements.forEach(el => { if (n(el.label) === q)
|
|
699
716
|
addMatch(el); });
|
|
717
|
+
// 1.5 Prefix stripped exact match (e.g. searching "general" for "#general" or vice versa)
|
|
718
|
+
const qStripped = q.replace(/^[#@]/, '');
|
|
719
|
+
if (qStripped !== q) {
|
|
720
|
+
elements.forEach(el => {
|
|
721
|
+
const elLabelClean = n(el.label).replace(/^[#@]/, '');
|
|
722
|
+
if (elLabelClean === qStripped)
|
|
723
|
+
addMatch(el);
|
|
724
|
+
});
|
|
725
|
+
}
|
|
700
726
|
// 2. placeholder text
|
|
701
|
-
elements.forEach(el => { if (n(el.placeholder) === q)
|
|
727
|
+
elements.forEach(el => { if (n(el.placeholder) === q || n(el.placeholder) === qStripped)
|
|
702
728
|
addMatch(el); });
|
|
703
729
|
// 3. data-testid
|
|
704
730
|
elements.forEach(el => { if (n(el.testId) === q)
|
|
705
731
|
addMatch(el); });
|
|
706
732
|
// 4. visible text content / partial label match
|
|
707
|
-
elements.forEach(el => {
|
|
708
|
-
|
|
733
|
+
elements.forEach(el => {
|
|
734
|
+
const lbl = n(el.label);
|
|
735
|
+
if (lbl.includes(q) || (qStripped.length >= 3 && lbl.includes(qStripped)))
|
|
736
|
+
addMatch(el);
|
|
737
|
+
});
|
|
709
738
|
// 4.5 fuzzy partial match (split words)
|
|
710
739
|
const words = q.split(/\s+/);
|
|
711
740
|
if (words.length > 1) {
|
|
@@ -766,99 +795,108 @@ export async function resolveByAria(session, query) {
|
|
|
766
795
|
if (!query || !query.trim())
|
|
767
796
|
return null;
|
|
768
797
|
const resp = await session.Runtime.evaluate({
|
|
769
|
-
expression: `(function(q) {
|
|
770
|
-
var INTERACTIVE = 'a[href], button, input:not([type="hidden"]), textarea, select,' +
|
|
771
|
-
'[role="button"],[role="link"],[role="textbox"],[role="checkbox"],' +
|
|
772
|
-
'[role="radio"],[role="combobox"],[role="searchbox"],[role="menuitem"],' +
|
|
773
|
-
'[role="tab"],[role="option"],[tabindex]:not([tabindex="-1"])';
|
|
774
|
-
|
|
775
|
-
function norm(s) { return (s || '').toLowerCase().replace(/[.\\u2026\\s]+/g, ' ').trim(); }
|
|
776
|
-
var qLow = norm(q);
|
|
777
|
-
|
|
778
|
-
// Score: prefer leaf elements (no interactive child = score 0, has child = score 10)
|
|
779
|
-
function score(el) {
|
|
780
|
-
try { return el.querySelector(INTERACTIVE) !== null ? 10 : 0; } catch(e) { return 0; }
|
|
781
|
-
}
|
|
782
|
-
function best(list) {
|
|
783
|
-
if (!list || !list.length) return null;
|
|
784
|
-
var arr = Array.prototype.slice.call(list);
|
|
785
|
-
arr.sort(function(a,b){ return score(a)-score(b); });
|
|
786
|
-
return arr[0];
|
|
787
|
-
}
|
|
788
|
-
function makeResult(el) {
|
|
789
|
-
if (!el) return null;
|
|
790
|
-
var r = el.getBoundingClientRect();
|
|
791
|
-
if (!r || r.width === 0 || r.height === 0) return null;
|
|
792
|
-
if (r.bottom < 0 || r.top > window.innerHeight) return null;
|
|
793
|
-
var sel = '';
|
|
794
|
-
var wtId = el.getAttribute('data-wt-id');
|
|
795
|
-
if (wtId) {
|
|
796
|
-
sel = '[data-wt-id="' + wtId.replace(/"/g, '\\\\"') + '"]';
|
|
797
|
-
} else if (el.id) {
|
|
798
|
-
try { sel = '#' + CSS.escape(el.id); } catch(e) { sel = '#' + el.id; }
|
|
799
|
-
} else {
|
|
800
|
-
var aria = el.getAttribute('aria-label');
|
|
801
|
-
if (aria) sel = el.tagName.toLowerCase() + '[aria-label="' + aria.replace(/"/g, '\\\\"') + '"]';
|
|
802
|
-
}
|
|
803
|
-
if (!sel) {
|
|
804
|
-
var tmp = 'wt-tmp-' + Math.random().toString(36).slice(2,8);
|
|
805
|
-
el.setAttribute('data-wt-id', tmp);
|
|
806
|
-
sel = '[data-wt-id="' + tmp + '"]';
|
|
807
|
-
}
|
|
808
|
-
return {
|
|
809
|
-
label: el.getAttribute('aria-label') || (el.textContent||'').trim().slice(0,80) || q,
|
|
810
|
-
x: Math.round(r.left + r.width/2),
|
|
811
|
-
y: Math.round(r.top + r.height/2),
|
|
812
|
-
selector: sel
|
|
813
|
-
};
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
var all, found, i, hit;
|
|
817
|
-
|
|
818
|
-
// 1. Exact aria-label
|
|
819
|
-
all = document.querySelectorAll('[aria-label]');
|
|
820
|
-
found = [];
|
|
821
|
-
for (i=0;i<all.length;i++) {
|
|
822
|
-
if (norm(all[i].getAttribute('aria-label'))===qLow) found.push(all[i]);
|
|
823
|
-
}
|
|
824
|
-
if (found.length) { hit=makeResult(best(found)); if(hit) return hit; }
|
|
825
|
-
|
|
826
|
-
//
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
if (
|
|
840
|
-
}
|
|
841
|
-
if (found.length) { hit=makeResult(best(found)); if(hit) return hit; }
|
|
842
|
-
|
|
843
|
-
//
|
|
844
|
-
all = document.querySelectorAll('[
|
|
845
|
-
found = [];
|
|
846
|
-
for (i=0;i<all.length;i++) {
|
|
847
|
-
var
|
|
848
|
-
if (
|
|
849
|
-
}
|
|
850
|
-
if (found.length) { hit=makeResult(best(found)); if(hit) return hit; }
|
|
851
|
-
|
|
852
|
-
//
|
|
853
|
-
all = document.querySelectorAll(
|
|
854
|
-
found = [];
|
|
855
|
-
for (i=0;i<all.length;i++) {
|
|
856
|
-
var
|
|
857
|
-
if (
|
|
858
|
-
}
|
|
859
|
-
if (found.length) { hit=makeResult(best(found)); if(hit) return hit; }
|
|
860
|
-
|
|
861
|
-
|
|
798
|
+
expression: `(function(q) {
|
|
799
|
+
var INTERACTIVE = 'a[href], button, input:not([type="hidden"]), textarea, select,' +
|
|
800
|
+
'[role="button"],[role="link"],[role="textbox"],[role="checkbox"],' +
|
|
801
|
+
'[role="radio"],[role="combobox"],[role="searchbox"],[role="menuitem"],' +
|
|
802
|
+
'[role="tab"],[role="option"],[tabindex]:not([tabindex="-1"])';
|
|
803
|
+
|
|
804
|
+
function norm(s) { return (s || '').toLowerCase().replace(/[.\\u2026\\s]+/g, ' ').trim(); }
|
|
805
|
+
var qLow = norm(q);
|
|
806
|
+
|
|
807
|
+
// Score: prefer leaf elements (no interactive child = score 0, has child = score 10)
|
|
808
|
+
function score(el) {
|
|
809
|
+
try { return el.querySelector(INTERACTIVE) !== null ? 10 : 0; } catch(e) { return 0; }
|
|
810
|
+
}
|
|
811
|
+
function best(list) {
|
|
812
|
+
if (!list || !list.length) return null;
|
|
813
|
+
var arr = Array.prototype.slice.call(list);
|
|
814
|
+
arr.sort(function(a,b){ return score(a)-score(b); });
|
|
815
|
+
return arr[0];
|
|
816
|
+
}
|
|
817
|
+
function makeResult(el) {
|
|
818
|
+
if (!el) return null;
|
|
819
|
+
var r = el.getBoundingClientRect();
|
|
820
|
+
if (!r || r.width === 0 || r.height === 0) return null;
|
|
821
|
+
if (r.bottom < 0 || r.top > window.innerHeight) return null;
|
|
822
|
+
var sel = '';
|
|
823
|
+
var wtId = el.getAttribute('data-wt-id');
|
|
824
|
+
if (wtId) {
|
|
825
|
+
sel = '[data-wt-id="' + wtId.replace(/"/g, '\\\\"') + '"]';
|
|
826
|
+
} else if (el.id) {
|
|
827
|
+
try { sel = '#' + CSS.escape(el.id); } catch(e) { sel = '#' + el.id; }
|
|
828
|
+
} else {
|
|
829
|
+
var aria = el.getAttribute('aria-label');
|
|
830
|
+
if (aria) sel = el.tagName.toLowerCase() + '[aria-label="' + aria.replace(/"/g, '\\\\"') + '"]';
|
|
831
|
+
}
|
|
832
|
+
if (!sel) {
|
|
833
|
+
var tmp = 'wt-tmp-' + Math.random().toString(36).slice(2,8);
|
|
834
|
+
el.setAttribute('data-wt-id', tmp);
|
|
835
|
+
sel = '[data-wt-id="' + tmp + '"]';
|
|
836
|
+
}
|
|
837
|
+
return {
|
|
838
|
+
label: el.getAttribute('aria-label') || (el.textContent||'').trim().slice(0,80) || q,
|
|
839
|
+
x: Math.round(r.left + r.width/2),
|
|
840
|
+
y: Math.round(r.top + r.height/2),
|
|
841
|
+
selector: sel
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
var all, found, i, hit;
|
|
846
|
+
|
|
847
|
+
// 1. Exact aria-label
|
|
848
|
+
all = document.querySelectorAll('[aria-label]');
|
|
849
|
+
found = [];
|
|
850
|
+
for (i=0;i<all.length;i++) {
|
|
851
|
+
if (norm(all[i].getAttribute('aria-label'))===qLow) found.push(all[i]);
|
|
852
|
+
}
|
|
853
|
+
if (found.length) { hit=makeResult(best(found)); if(hit) return hit; }
|
|
854
|
+
|
|
855
|
+
// 1.5 Partial aria-label match (e.g. "general" in "general (text channel)")
|
|
856
|
+
found = [];
|
|
857
|
+
for (i=0;i<all.length;i++) {
|
|
858
|
+
var aVal = norm(all[i].getAttribute('aria-label'));
|
|
859
|
+
if (aVal.indexOf(qLow)>=0 && aVal.length < qLow.length*4) found.push(all[i]);
|
|
860
|
+
}
|
|
861
|
+
if (found.length) { hit=makeResult(best(found)); if(hit) return hit; }
|
|
862
|
+
|
|
863
|
+
// 2. Exact innerText on interactive elements
|
|
864
|
+
all = document.querySelectorAll(INTERACTIVE);
|
|
865
|
+
found = [];
|
|
866
|
+
for (i=0;i<all.length;i++) {
|
|
867
|
+
var t = norm(all[i].innerText||all[i].textContent);
|
|
868
|
+
if (t===qLow) found.push(all[i]);
|
|
869
|
+
}
|
|
870
|
+
if (found.length) { hit=makeResult(best(found)); if(hit) return hit; }
|
|
871
|
+
|
|
872
|
+
// 3. Placeholder and rich placeholders (data-placeholder, aria-placeholder, data-slate-placeholder)
|
|
873
|
+
all = document.querySelectorAll('[placeholder],[data-placeholder],[aria-placeholder],[data-slate-placeholder]');
|
|
874
|
+
found = [];
|
|
875
|
+
for (i=0;i<all.length;i++) {
|
|
876
|
+
var phVal = norm(all[i].getAttribute('placeholder') || all[i].getAttribute('data-placeholder') || all[i].getAttribute('aria-placeholder') || all[i].textContent);
|
|
877
|
+
if (phVal===qLow || phVal.indexOf(qLow)>=0) found.push(all[i]);
|
|
878
|
+
}
|
|
879
|
+
if (found.length) { hit=makeResult(best(found)); if(hit) return hit; }
|
|
880
|
+
|
|
881
|
+
// 4. title / alt
|
|
882
|
+
all = document.querySelectorAll('[title],[alt]');
|
|
883
|
+
found = [];
|
|
884
|
+
for (i=0;i<all.length;i++) {
|
|
885
|
+
var v = norm(all[i].getAttribute('title')||all[i].getAttribute('alt'));
|
|
886
|
+
if (v===qLow) found.push(all[i]);
|
|
887
|
+
}
|
|
888
|
+
if (found.length) { hit=makeResult(best(found)); if(hit) return hit; }
|
|
889
|
+
|
|
890
|
+
// 5. Fuzzy partial innerText
|
|
891
|
+
all = document.querySelectorAll(INTERACTIVE);
|
|
892
|
+
found = [];
|
|
893
|
+
for (i=0;i<all.length;i++) {
|
|
894
|
+
var t5 = norm(all[i].innerText||all[i].textContent);
|
|
895
|
+
if (t5.indexOf(qLow)>=0 && t5.length < qLow.length*4) found.push(all[i]);
|
|
896
|
+
}
|
|
897
|
+
if (found.length) { hit=makeResult(best(found)); if(hit) return hit; }
|
|
898
|
+
|
|
899
|
+
return null;
|
|
862
900
|
})(${JSON.stringify(query)})`,
|
|
863
901
|
returnByValue: true,
|
|
864
902
|
}).catch(() => null);
|
|
@@ -1070,138 +1108,138 @@ export async function recoverDetachedRef(session, targetId, ref, preferredCandid
|
|
|
1070
1108
|
};
|
|
1071
1109
|
try {
|
|
1072
1110
|
const { result } = await session.Runtime.evaluate({
|
|
1073
|
-
expression: `(function(refId, meta) {
|
|
1074
|
-
// 1. Check if node with data-wt-id=refId is already in DOM, connected, and visible
|
|
1075
|
-
const existing = document.querySelector('[data-wt-id="' + refId + '"]');
|
|
1076
|
-
if (existing && existing.isConnected) {
|
|
1077
|
-
const r = existing.getBoundingClientRect();
|
|
1078
|
-
if (r.width > 0 && r.height > 0) {
|
|
1079
|
-
return {
|
|
1080
|
-
success: true,
|
|
1081
|
-
strategy: 'connected_existing',
|
|
1082
|
-
x: Math.round(r.left + r.width / 2),
|
|
1083
|
-
y: Math.round(r.top + r.height / 2),
|
|
1084
|
-
label: existing.getAttribute('aria-label') || (existing.innerText || existing.textContent || '').trim() || existing.placeholder || '',
|
|
1085
|
-
role: existing.getAttribute('role') || existing.tagName.toLowerCase()
|
|
1086
|
-
};
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
function isValidCandidate(el) {
|
|
1091
|
-
if (!el || !el.isConnected) return false;
|
|
1092
|
-
const r = el.getBoundingClientRect();
|
|
1093
|
-
if (r.width <= 0 || r.height <= 0) return false;
|
|
1094
|
-
try {
|
|
1095
|
-
const style = window.getComputedStyle(el);
|
|
1096
|
-
if (style.visibility === 'hidden' || style.display === 'none') return false;
|
|
1097
|
-
} catch(e) {}
|
|
1098
|
-
return true;
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
// 2. Strategy: Stored Selector (if stable CSS selector recorded)
|
|
1102
|
-
if (meta.selector && !meta.selector.startsWith('[data-wt-id')) {
|
|
1103
|
-
try {
|
|
1104
|
-
const el = document.querySelector(meta.selector);
|
|
1105
|
-
if (isValidCandidate(el)) {
|
|
1106
|
-
el.setAttribute('data-wt-id', refId);
|
|
1107
|
-
const r = el.getBoundingClientRect();
|
|
1108
|
-
return {
|
|
1109
|
-
success: true,
|
|
1110
|
-
strategy: 'stored_selector',
|
|
1111
|
-
x: Math.round(r.left + r.width / 2),
|
|
1112
|
-
y: Math.round(r.top + r.height / 2),
|
|
1113
|
-
label: el.getAttribute('aria-label') || (el.innerText || el.textContent || '').trim() || el.placeholder || '',
|
|
1114
|
-
role: el.getAttribute('role') || el.tagName.toLowerCase()
|
|
1115
|
-
};
|
|
1116
|
-
}
|
|
1117
|
-
} catch(e) {}
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
const normLabel = (meta.label || '').toLowerCase().trim();
|
|
1121
|
-
const normPlaceholder = (meta.placeholder || '').toLowerCase().trim();
|
|
1122
|
-
|
|
1123
|
-
const candidates = Array.from(document.querySelectorAll(
|
|
1124
|
-
'button, a, input, select, textarea, [role="button"], [role="link"], [role="textbox"], [role="checkbox"], [role="tab"], [tabindex="0"]'
|
|
1125
|
-
));
|
|
1126
|
-
|
|
1127
|
-
// 3. Strategy: Exact Label / Text / Aria match
|
|
1128
|
-
if (normLabel) {
|
|
1129
|
-
for (const c of candidates) {
|
|
1130
|
-
if (!isValidCandidate(c)) continue;
|
|
1131
|
-
const aria = (c.getAttribute('aria-label') || '').toLowerCase().trim();
|
|
1132
|
-
const text = ((c.innerText || c.textContent || '')).toLowerCase().trim();
|
|
1133
|
-
const val = (c.value || '').toLowerCase().trim();
|
|
1134
|
-
const placeholder = (c.placeholder || '').toLowerCase().trim();
|
|
1135
|
-
const name = (c.getAttribute('name') || '').toLowerCase().trim();
|
|
1136
|
-
|
|
1137
|
-
if (aria === normLabel || text === normLabel || placeholder === normLabel || name === normLabel || (val && val === normLabel)) {
|
|
1138
|
-
c.setAttribute('data-wt-id', refId);
|
|
1139
|
-
const r = c.getBoundingClientRect();
|
|
1140
|
-
return {
|
|
1141
|
-
success: true,
|
|
1142
|
-
strategy: 'exact_label',
|
|
1143
|
-
x: Math.round(r.left + r.width / 2),
|
|
1144
|
-
y: Math.round(r.top + r.height / 2),
|
|
1145
|
-
label: c.getAttribute('aria-label') || (c.innerText || c.textContent || '').trim() || c.placeholder || '',
|
|
1146
|
-
role: c.getAttribute('role') || c.tagName.toLowerCase()
|
|
1147
|
-
};
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
// 4. Strategy: Placeholder or Name match
|
|
1153
|
-
if (normPlaceholder) {
|
|
1154
|
-
for (const c of candidates) {
|
|
1155
|
-
if (!isValidCandidate(c)) continue;
|
|
1156
|
-
const placeholder = (c.placeholder || '').toLowerCase().trim();
|
|
1157
|
-
const name = (c.getAttribute('name') || '').toLowerCase().trim();
|
|
1158
|
-
if (placeholder === normPlaceholder || name === normPlaceholder) {
|
|
1159
|
-
c.setAttribute('data-wt-id', refId);
|
|
1160
|
-
const r = c.getBoundingClientRect();
|
|
1161
|
-
return {
|
|
1162
|
-
success: true,
|
|
1163
|
-
strategy: 'placeholder_or_name',
|
|
1164
|
-
x: Math.round(r.left + r.width / 2),
|
|
1165
|
-
y: Math.round(r.top + r.height / 2),
|
|
1166
|
-
label: c.placeholder || c.getAttribute('name') || '',
|
|
1167
|
-
role: c.getAttribute('role') || c.tagName.toLowerCase()
|
|
1168
|
-
};
|
|
1169
|
-
}
|
|
1170
|
-
}
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
// 5. Strategy: Fuzzy Substring Match (if label length >= 3)
|
|
1174
|
-
if (normLabel && normLabel.length >= 3) {
|
|
1175
|
-
let bestMatch = null;
|
|
1176
|
-
let minDiff = Infinity;
|
|
1177
|
-
for (const c of candidates) {
|
|
1178
|
-
if (!isValidCandidate(c)) continue;
|
|
1179
|
-
const text = ((c.innerText || c.textContent || '')).toLowerCase().trim();
|
|
1180
|
-
const aria = (c.getAttribute('aria-label') || '').toLowerCase().trim();
|
|
1181
|
-
const candStr = aria || text;
|
|
1182
|
-
if (candStr && candStr.includes(normLabel)) {
|
|
1183
|
-
const diff = candStr.length - normLabel.length;
|
|
1184
|
-
if (diff >= 0 && diff < minDiff) {
|
|
1185
|
-
minDiff = diff;
|
|
1186
|
-
bestMatch = c;
|
|
1187
|
-
}
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
1190
|
-
if (bestMatch) {
|
|
1191
|
-
bestMatch.setAttribute('data-wt-id', refId);
|
|
1192
|
-
const r = bestMatch.getBoundingClientRect();
|
|
1193
|
-
return {
|
|
1194
|
-
success: true,
|
|
1195
|
-
strategy: 'fuzzy_label',
|
|
1196
|
-
x: Math.round(r.left + r.width / 2),
|
|
1197
|
-
y: Math.round(r.top + r.height / 2),
|
|
1198
|
-
label: bestMatch.getAttribute('aria-label') || (bestMatch.innerText || bestMatch.textContent || '').trim() || bestMatch.placeholder || '',
|
|
1199
|
-
role: bestMatch.getAttribute('role') || bestMatch.tagName.toLowerCase()
|
|
1200
|
-
};
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
return { success: false };
|
|
1111
|
+
expression: `(function(refId, meta) {
|
|
1112
|
+
// 1. Check if node with data-wt-id=refId is already in DOM, connected, and visible
|
|
1113
|
+
const existing = document.querySelector('[data-wt-id="' + refId + '"]');
|
|
1114
|
+
if (existing && existing.isConnected) {
|
|
1115
|
+
const r = existing.getBoundingClientRect();
|
|
1116
|
+
if (r.width > 0 && r.height > 0) {
|
|
1117
|
+
return {
|
|
1118
|
+
success: true,
|
|
1119
|
+
strategy: 'connected_existing',
|
|
1120
|
+
x: Math.round(r.left + r.width / 2),
|
|
1121
|
+
y: Math.round(r.top + r.height / 2),
|
|
1122
|
+
label: existing.getAttribute('aria-label') || (existing.innerText || existing.textContent || '').trim() || existing.placeholder || '',
|
|
1123
|
+
role: existing.getAttribute('role') || existing.tagName.toLowerCase()
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
function isValidCandidate(el) {
|
|
1129
|
+
if (!el || !el.isConnected) return false;
|
|
1130
|
+
const r = el.getBoundingClientRect();
|
|
1131
|
+
if (r.width <= 0 || r.height <= 0) return false;
|
|
1132
|
+
try {
|
|
1133
|
+
const style = window.getComputedStyle(el);
|
|
1134
|
+
if (style.visibility === 'hidden' || style.display === 'none') return false;
|
|
1135
|
+
} catch(e) {}
|
|
1136
|
+
return true;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// 2. Strategy: Stored Selector (if stable CSS selector recorded)
|
|
1140
|
+
if (meta.selector && !meta.selector.startsWith('[data-wt-id')) {
|
|
1141
|
+
try {
|
|
1142
|
+
const el = document.querySelector(meta.selector);
|
|
1143
|
+
if (isValidCandidate(el)) {
|
|
1144
|
+
el.setAttribute('data-wt-id', refId);
|
|
1145
|
+
const r = el.getBoundingClientRect();
|
|
1146
|
+
return {
|
|
1147
|
+
success: true,
|
|
1148
|
+
strategy: 'stored_selector',
|
|
1149
|
+
x: Math.round(r.left + r.width / 2),
|
|
1150
|
+
y: Math.round(r.top + r.height / 2),
|
|
1151
|
+
label: el.getAttribute('aria-label') || (el.innerText || el.textContent || '').trim() || el.placeholder || '',
|
|
1152
|
+
role: el.getAttribute('role') || el.tagName.toLowerCase()
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
} catch(e) {}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
const normLabel = (meta.label || '').toLowerCase().trim();
|
|
1159
|
+
const normPlaceholder = (meta.placeholder || '').toLowerCase().trim();
|
|
1160
|
+
|
|
1161
|
+
const candidates = Array.from(document.querySelectorAll(
|
|
1162
|
+
'button, a, input, select, textarea, [role="button"], [role="link"], [role="textbox"], [role="checkbox"], [role="tab"], [tabindex="0"]'
|
|
1163
|
+
));
|
|
1164
|
+
|
|
1165
|
+
// 3. Strategy: Exact Label / Text / Aria match
|
|
1166
|
+
if (normLabel) {
|
|
1167
|
+
for (const c of candidates) {
|
|
1168
|
+
if (!isValidCandidate(c)) continue;
|
|
1169
|
+
const aria = (c.getAttribute('aria-label') || '').toLowerCase().trim();
|
|
1170
|
+
const text = ((c.innerText || c.textContent || '')).toLowerCase().trim();
|
|
1171
|
+
const val = (c.value || '').toLowerCase().trim();
|
|
1172
|
+
const placeholder = (c.placeholder || '').toLowerCase().trim();
|
|
1173
|
+
const name = (c.getAttribute('name') || '').toLowerCase().trim();
|
|
1174
|
+
|
|
1175
|
+
if (aria === normLabel || text === normLabel || placeholder === normLabel || name === normLabel || (val && val === normLabel)) {
|
|
1176
|
+
c.setAttribute('data-wt-id', refId);
|
|
1177
|
+
const r = c.getBoundingClientRect();
|
|
1178
|
+
return {
|
|
1179
|
+
success: true,
|
|
1180
|
+
strategy: 'exact_label',
|
|
1181
|
+
x: Math.round(r.left + r.width / 2),
|
|
1182
|
+
y: Math.round(r.top + r.height / 2),
|
|
1183
|
+
label: c.getAttribute('aria-label') || (c.innerText || c.textContent || '').trim() || c.placeholder || '',
|
|
1184
|
+
role: c.getAttribute('role') || c.tagName.toLowerCase()
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
// 4. Strategy: Placeholder or Name match
|
|
1191
|
+
if (normPlaceholder) {
|
|
1192
|
+
for (const c of candidates) {
|
|
1193
|
+
if (!isValidCandidate(c)) continue;
|
|
1194
|
+
const placeholder = (c.placeholder || '').toLowerCase().trim();
|
|
1195
|
+
const name = (c.getAttribute('name') || '').toLowerCase().trim();
|
|
1196
|
+
if (placeholder === normPlaceholder || name === normPlaceholder) {
|
|
1197
|
+
c.setAttribute('data-wt-id', refId);
|
|
1198
|
+
const r = c.getBoundingClientRect();
|
|
1199
|
+
return {
|
|
1200
|
+
success: true,
|
|
1201
|
+
strategy: 'placeholder_or_name',
|
|
1202
|
+
x: Math.round(r.left + r.width / 2),
|
|
1203
|
+
y: Math.round(r.top + r.height / 2),
|
|
1204
|
+
label: c.placeholder || c.getAttribute('name') || '',
|
|
1205
|
+
role: c.getAttribute('role') || c.tagName.toLowerCase()
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
// 5. Strategy: Fuzzy Substring Match (if label length >= 3)
|
|
1212
|
+
if (normLabel && normLabel.length >= 3) {
|
|
1213
|
+
let bestMatch = null;
|
|
1214
|
+
let minDiff = Infinity;
|
|
1215
|
+
for (const c of candidates) {
|
|
1216
|
+
if (!isValidCandidate(c)) continue;
|
|
1217
|
+
const text = ((c.innerText || c.textContent || '')).toLowerCase().trim();
|
|
1218
|
+
const aria = (c.getAttribute('aria-label') || '').toLowerCase().trim();
|
|
1219
|
+
const candStr = aria || text;
|
|
1220
|
+
if (candStr && candStr.includes(normLabel)) {
|
|
1221
|
+
const diff = candStr.length - normLabel.length;
|
|
1222
|
+
if (diff >= 0 && diff < minDiff) {
|
|
1223
|
+
minDiff = diff;
|
|
1224
|
+
bestMatch = c;
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
if (bestMatch) {
|
|
1229
|
+
bestMatch.setAttribute('data-wt-id', refId);
|
|
1230
|
+
const r = bestMatch.getBoundingClientRect();
|
|
1231
|
+
return {
|
|
1232
|
+
success: true,
|
|
1233
|
+
strategy: 'fuzzy_label',
|
|
1234
|
+
x: Math.round(r.left + r.width / 2),
|
|
1235
|
+
y: Math.round(r.top + r.height / 2),
|
|
1236
|
+
label: bestMatch.getAttribute('aria-label') || (bestMatch.innerText || bestMatch.textContent || '').trim() || bestMatch.placeholder || '',
|
|
1237
|
+
role: bestMatch.getAttribute('role') || bestMatch.tagName.toLowerCase()
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
return { success: false };
|
|
1205
1243
|
})(${JSON.stringify(refNumericId)}, ${JSON.stringify(meta)})`,
|
|
1206
1244
|
returnByValue: true,
|
|
1207
1245
|
});
|