@btraut/browser-bridge 0.13.2 → 0.14.0
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/CHANGELOG.md +21 -0
- package/README.md +28 -37
- package/dist/api.js +1013 -626
- package/dist/api.js.map +4 -4
- package/dist/index.js +323 -308
- package/dist/index.js.map +4 -4
- package/extension/dist/background.js +1016 -675
- package/extension/dist/background.js.map +4 -4
- package/extension/dist/content.js +470 -76
- package/extension/dist/content.js.map +4 -4
- package/extension/dist/options-ui.js +2 -113
- package/extension/dist/options-ui.js.map +2 -2
- package/extension/manifest.json +2 -2
- package/package.json +1 -1
- package/skills/browser-bridge/SKILL.md +9 -0
- package/skills/browser-bridge/skill.json +1 -1
|
@@ -1,5 +1,403 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
(() => {
|
|
3
|
+
// packages/extension/src/popup-trigger-state.ts
|
|
4
|
+
var normalizeAttr = (value) => {
|
|
5
|
+
if (typeof value !== "string") {
|
|
6
|
+
return void 0;
|
|
7
|
+
}
|
|
8
|
+
const trimmed = value.trim();
|
|
9
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
10
|
+
};
|
|
11
|
+
var readPopupTriggerState = (target) => {
|
|
12
|
+
if (!(target instanceof HTMLElement)) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const ariaHasPopup = normalizeAttr(target.getAttribute("aria-haspopup"));
|
|
16
|
+
const ariaExpanded = normalizeAttr(target.getAttribute("aria-expanded"));
|
|
17
|
+
const dataState = normalizeAttr(target.getAttribute("data-state"));
|
|
18
|
+
const open = "open" in target && typeof target.open === "boolean" ? target.open ?? false : void 0;
|
|
19
|
+
const qualifiesAsPopupTrigger = Boolean(ariaHasPopup) || ariaExpanded !== void 0 || open !== void 0;
|
|
20
|
+
if (!qualifiesAsPopupTrigger && dataState === void 0) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
if (!qualifiesAsPopupTrigger) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
kind: "popup_trigger",
|
|
28
|
+
ariaHasPopup,
|
|
29
|
+
ariaExpanded,
|
|
30
|
+
dataState,
|
|
31
|
+
open
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
var popupTriggerStateChanged = (before, after) => {
|
|
35
|
+
if (!before || !after) {
|
|
36
|
+
return before !== after;
|
|
37
|
+
}
|
|
38
|
+
return before.ariaExpanded !== after.ariaExpanded || before.dataState !== after.dataState || before.open !== after.open;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// packages/extension/src/click-strategies/index.ts
|
|
42
|
+
var selectClickStrategy = (target) => {
|
|
43
|
+
const popupTriggerState = readPopupTriggerState(target);
|
|
44
|
+
if (popupTriggerState) {
|
|
45
|
+
return {
|
|
46
|
+
kind: "popup_trigger",
|
|
47
|
+
state: popupTriggerState
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return { kind: "generic" };
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// packages/extension/src/click-strategies/generic-click.ts
|
|
54
|
+
var executeGenericClick = (options) => {
|
|
55
|
+
const { target, clickCount } = options;
|
|
56
|
+
window.setTimeout(() => {
|
|
57
|
+
try {
|
|
58
|
+
if (target instanceof HTMLElement) {
|
|
59
|
+
try {
|
|
60
|
+
target.focus({ preventScroll: true });
|
|
61
|
+
} catch {
|
|
62
|
+
target.focus();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
for (let i = 0; i < clickCount; i += 1) {
|
|
66
|
+
target.click();
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
}
|
|
70
|
+
}, 0);
|
|
71
|
+
return { ok: true };
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// packages/extension/src/click-strategies/popup-trigger-click.ts
|
|
75
|
+
var executePopupTriggerClick = async (options) => {
|
|
76
|
+
const popupTarget = options.target;
|
|
77
|
+
try {
|
|
78
|
+
popupTarget.focus({ preventScroll: true });
|
|
79
|
+
} catch {
|
|
80
|
+
popupTarget.focus();
|
|
81
|
+
}
|
|
82
|
+
for (let i = 0; i < options.clickCount; i += 1) {
|
|
83
|
+
popupTarget.click();
|
|
84
|
+
}
|
|
85
|
+
await options.sleep(options.settleMs ?? 50);
|
|
86
|
+
if (window.location.href !== options.locationBefore || !options.target.isConnected) {
|
|
87
|
+
return { ok: true };
|
|
88
|
+
}
|
|
89
|
+
const popupTriggerAfter = readPopupTriggerState(options.target);
|
|
90
|
+
if (!popupTriggerStateChanged(options.beforeState, popupTriggerAfter ?? null)) {
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
error: {
|
|
94
|
+
code: "FAILED_PRECONDITION",
|
|
95
|
+
message: "Click focused the popup trigger but did not change its open state.",
|
|
96
|
+
retryable: false,
|
|
97
|
+
details: {
|
|
98
|
+
reason: "click_state_unchanged",
|
|
99
|
+
control: options.beforeState.kind,
|
|
100
|
+
aria_haspopup: options.beforeState.ariaHasPopup,
|
|
101
|
+
aria_expanded_before: options.beforeState.ariaExpanded,
|
|
102
|
+
aria_expanded_after: popupTriggerAfter?.ariaExpanded,
|
|
103
|
+
data_state_before: options.beforeState.dataState,
|
|
104
|
+
data_state_after: popupTriggerAfter?.dataState
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return { ok: true };
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// packages/extension/src/locator-point.ts
|
|
113
|
+
var pointHitsTarget = (target, x, y, options) => {
|
|
114
|
+
if (typeof document.elementFromPoint !== "function") {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
const hit = document.elementFromPoint(x, y);
|
|
118
|
+
if (!hit) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
if (options?.directOnly) {
|
|
122
|
+
return target === hit;
|
|
123
|
+
}
|
|
124
|
+
return target === hit || target.contains(hit);
|
|
125
|
+
};
|
|
126
|
+
var getHittablePoint = (target, options) => {
|
|
127
|
+
const rect = target.getBoundingClientRect();
|
|
128
|
+
const insetX = Math.min(Math.max(rect.width * 0.25, 1), rect.width / 2);
|
|
129
|
+
const insetY = Math.min(Math.max(rect.height * 0.25, 1), rect.height / 2);
|
|
130
|
+
const candidatePoints = [
|
|
131
|
+
{ x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 },
|
|
132
|
+
{ x: rect.left + insetX, y: rect.top + insetY },
|
|
133
|
+
{ x: rect.right - insetX, y: rect.top + insetY },
|
|
134
|
+
{ x: rect.left + insetX, y: rect.bottom - insetY },
|
|
135
|
+
{ x: rect.right - insetX, y: rect.bottom - insetY },
|
|
136
|
+
{ x: rect.left + rect.width / 2, y: rect.top + insetY },
|
|
137
|
+
{ x: rect.left + rect.width / 2, y: rect.bottom - insetY },
|
|
138
|
+
{ x: rect.left + insetX, y: rect.top + rect.height / 2 },
|
|
139
|
+
{ x: rect.right - insetX, y: rect.top + rect.height / 2 }
|
|
140
|
+
];
|
|
141
|
+
if (options?.preferDirectHit) {
|
|
142
|
+
for (const point of candidatePoints) {
|
|
143
|
+
if (pointHitsTarget(target, point.x, point.y, { directOnly: true })) {
|
|
144
|
+
return point;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
for (const point of candidatePoints) {
|
|
149
|
+
if (pointHitsTarget(target, point.x, point.y)) {
|
|
150
|
+
return point;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return candidatePoints[0] ?? { x: rect.left, y: rect.top };
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// packages/extension/src/locator-ranking.ts
|
|
157
|
+
var collectVisibleText = (node, isVisible2) => {
|
|
158
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
159
|
+
return node.textContent || "";
|
|
160
|
+
}
|
|
161
|
+
if (!(node instanceof Element)) {
|
|
162
|
+
return "";
|
|
163
|
+
}
|
|
164
|
+
if (!isVisible2(node)) {
|
|
165
|
+
return "";
|
|
166
|
+
}
|
|
167
|
+
if (node instanceof HTMLElement) {
|
|
168
|
+
const { innerText } = node;
|
|
169
|
+
if (typeof innerText === "string" && innerText.length > 0) {
|
|
170
|
+
return innerText;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return Array.from(node.childNodes).map((child) => collectVisibleText(child, isVisible2)).join("");
|
|
174
|
+
};
|
|
175
|
+
var normalizeText = (value) => value.replace(/\s+/g, " ").trim();
|
|
176
|
+
var isClickable = (element) => element instanceof HTMLElement && element.matches(
|
|
177
|
+
'a,button,input,textarea,select,summary,label,[role="button"],[tabindex]'
|
|
178
|
+
);
|
|
179
|
+
var getNodeDepth = (element) => {
|
|
180
|
+
let depth = 0;
|
|
181
|
+
let current = element;
|
|
182
|
+
while (current) {
|
|
183
|
+
depth += 1;
|
|
184
|
+
current = current.parentElement;
|
|
185
|
+
}
|
|
186
|
+
return depth;
|
|
187
|
+
};
|
|
188
|
+
var isVisible = (element) => {
|
|
189
|
+
if (!(element instanceof HTMLElement)) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
const style = window.getComputedStyle(element);
|
|
193
|
+
if (style.visibility === "hidden" || style.display === "none") {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
const rect = element.getBoundingClientRect();
|
|
197
|
+
if (rect.width === 0 && rect.height === 0) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
if (element.offsetWidth === 0 && element.offsetHeight === 0 && element.getClientRects().length === 0) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
let current = element;
|
|
204
|
+
while (current) {
|
|
205
|
+
const currentStyle = window.getComputedStyle(current);
|
|
206
|
+
if (currentStyle.display === "none") {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
if (currentStyle.visibility === "hidden" || currentStyle.visibility === "collapse") {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
const opacity = Number.parseFloat(currentStyle.opacity ?? "1");
|
|
213
|
+
if (Number.isFinite(opacity) && opacity <= 0) {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
current = current.parentElement;
|
|
217
|
+
}
|
|
218
|
+
return true;
|
|
219
|
+
};
|
|
220
|
+
var isOnScreen = (element) => {
|
|
221
|
+
if (!(element instanceof HTMLElement) || !isVisible(element)) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
const rect = element.getBoundingClientRect();
|
|
225
|
+
return rect.right > 0 && rect.bottom > 0 && rect.left < window.innerWidth && rect.top < window.innerHeight;
|
|
226
|
+
};
|
|
227
|
+
var getRenderedText = (element) => normalizeText(collectVisibleText(element, isVisible));
|
|
228
|
+
var getRoleAccessibleName = (element) => normalizeText(
|
|
229
|
+
element.getAttribute("aria-label") ?? element.getAttribute("title") ?? getRenderedText(element)
|
|
230
|
+
);
|
|
231
|
+
var scoreCandidates = (candidates, options) => {
|
|
232
|
+
const queryText = options?.exactText ? normalizeText(options.exactText) : "";
|
|
233
|
+
const queryHref = options?.exactHref ?? "";
|
|
234
|
+
if (candidates.length === 0) {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
const scored = candidates.filter(isVisible).map((candidate) => {
|
|
238
|
+
const text = getRenderedText(candidate);
|
|
239
|
+
const href = candidate.getAttribute("href") ?? "";
|
|
240
|
+
return {
|
|
241
|
+
candidate,
|
|
242
|
+
exactText: queryText.length > 0 && text === queryText,
|
|
243
|
+
exactHref: queryHref.length > 0 && (href === queryHref || candidate instanceof HTMLAnchorElement && candidate.href === queryHref),
|
|
244
|
+
onScreen: isOnScreen(candidate),
|
|
245
|
+
clickable: isClickable(candidate),
|
|
246
|
+
textLength: text.length,
|
|
247
|
+
depth: getNodeDepth(candidate)
|
|
248
|
+
};
|
|
249
|
+
}).sort((a, b) => {
|
|
250
|
+
if (a.exactHref !== b.exactHref) {
|
|
251
|
+
return a.exactHref ? -1 : 1;
|
|
252
|
+
}
|
|
253
|
+
if (a.exactText !== b.exactText) {
|
|
254
|
+
return a.exactText ? -1 : 1;
|
|
255
|
+
}
|
|
256
|
+
if (a.onScreen !== b.onScreen) {
|
|
257
|
+
return a.onScreen ? -1 : 1;
|
|
258
|
+
}
|
|
259
|
+
if (a.clickable !== b.clickable) {
|
|
260
|
+
return a.clickable ? -1 : 1;
|
|
261
|
+
}
|
|
262
|
+
if (a.textLength !== b.textLength) {
|
|
263
|
+
return a.textLength - b.textLength;
|
|
264
|
+
}
|
|
265
|
+
return b.depth - a.depth;
|
|
266
|
+
});
|
|
267
|
+
return scored[0]?.candidate ?? candidates[0] ?? null;
|
|
268
|
+
};
|
|
269
|
+
var findByText = (text) => {
|
|
270
|
+
const query = normalizeText(text);
|
|
271
|
+
if (query.length === 0) {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
const candidateMap = /* @__PURE__ */ new Map();
|
|
275
|
+
const tree = document.createTreeWalker(
|
|
276
|
+
document.body,
|
|
277
|
+
NodeFilter.SHOW_ELEMENT
|
|
278
|
+
);
|
|
279
|
+
let node = tree.nextNode();
|
|
280
|
+
while (node) {
|
|
281
|
+
const element = node;
|
|
282
|
+
if (!isVisible(element)) {
|
|
283
|
+
node = tree.nextNode();
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const elementText = getRenderedText(element);
|
|
287
|
+
if (!elementText.includes(query)) {
|
|
288
|
+
node = tree.nextNode();
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
let preferredTarget = element;
|
|
292
|
+
let current = element;
|
|
293
|
+
while (current) {
|
|
294
|
+
if (current !== element && isVisible(current) && isClickable(current) && getRenderedText(current).includes(query)) {
|
|
295
|
+
preferredTarget = current;
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
current = current.parentElement;
|
|
299
|
+
}
|
|
300
|
+
const preferredText = getRenderedText(preferredTarget);
|
|
301
|
+
const currentBest = candidateMap.get(preferredTarget);
|
|
302
|
+
const nextScore = {
|
|
303
|
+
exact: preferredText === query || elementText === query,
|
|
304
|
+
onScreen: isOnScreen(preferredTarget),
|
|
305
|
+
clickable: isClickable(preferredTarget),
|
|
306
|
+
textLength: preferredText.length || elementText.length,
|
|
307
|
+
depth: getNodeDepth(preferredTarget)
|
|
308
|
+
};
|
|
309
|
+
if (!currentBest || Number(nextScore.exact) > Number(currentBest.exact) || nextScore.exact === currentBest.exact && Number(nextScore.onScreen) > Number(currentBest.onScreen) || nextScore.exact === currentBest.exact && nextScore.onScreen === currentBest.onScreen && Number(nextScore.clickable) > Number(currentBest.clickable) || nextScore.exact === currentBest.exact && nextScore.onScreen === currentBest.onScreen && nextScore.clickable === currentBest.clickable && nextScore.textLength < currentBest.textLength || nextScore.exact === currentBest.exact && nextScore.onScreen === currentBest.onScreen && nextScore.clickable === currentBest.clickable && nextScore.textLength === currentBest.textLength && nextScore.depth > currentBest.depth) {
|
|
310
|
+
candidateMap.set(preferredTarget, nextScore);
|
|
311
|
+
}
|
|
312
|
+
node = tree.nextNode();
|
|
313
|
+
}
|
|
314
|
+
const candidates = Array.from(candidateMap.entries()).sort((a, b) => {
|
|
315
|
+
const [, left] = a;
|
|
316
|
+
const [, right] = b;
|
|
317
|
+
if (left.exact !== right.exact) {
|
|
318
|
+
return left.exact ? -1 : 1;
|
|
319
|
+
}
|
|
320
|
+
if (left.onScreen !== right.onScreen) {
|
|
321
|
+
return left.onScreen ? -1 : 1;
|
|
322
|
+
}
|
|
323
|
+
if (left.clickable !== right.clickable) {
|
|
324
|
+
return left.clickable ? -1 : 1;
|
|
325
|
+
}
|
|
326
|
+
if (left.textLength !== right.textLength) {
|
|
327
|
+
return left.textLength - right.textLength;
|
|
328
|
+
}
|
|
329
|
+
return right.depth - left.depth;
|
|
330
|
+
});
|
|
331
|
+
return candidates[0]?.[0] ?? null;
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
// packages/extension/src/snapshot-ref-recovery.ts
|
|
335
|
+
var SNAPSHOT_REF_REGISTRY_ID = "__bb_snapshot_ref_registry__";
|
|
336
|
+
var readSnapshotRefRegistry = () => {
|
|
337
|
+
const registry = /* @__PURE__ */ new Map();
|
|
338
|
+
const el = document.getElementById(SNAPSHOT_REF_REGISTRY_ID);
|
|
339
|
+
const raw = el?.textContent;
|
|
340
|
+
if (!raw) {
|
|
341
|
+
return registry;
|
|
342
|
+
}
|
|
343
|
+
try {
|
|
344
|
+
const parsed = JSON.parse(raw);
|
|
345
|
+
if (!Array.isArray(parsed)) {
|
|
346
|
+
return registry;
|
|
347
|
+
}
|
|
348
|
+
for (const entry of parsed) {
|
|
349
|
+
if (!entry || typeof entry !== "object") {
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
const ref = entry.ref;
|
|
353
|
+
if (typeof ref !== "string" || ref.length === 0) {
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
registry.set(ref, {
|
|
357
|
+
role: typeof entry.role === "string" ? entry.role ?? void 0 : void 0,
|
|
358
|
+
name: typeof entry.name === "string" ? entry.name ?? void 0 : void 0,
|
|
359
|
+
url: typeof entry.url === "string" ? entry.url ?? void 0 : void 0
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
} catch {
|
|
363
|
+
return /* @__PURE__ */ new Map();
|
|
364
|
+
}
|
|
365
|
+
return registry;
|
|
366
|
+
};
|
|
367
|
+
var recoverElementBySnapshotRef = (ref, options) => {
|
|
368
|
+
const metadata = readSnapshotRefRegistry().get(ref);
|
|
369
|
+
if (!metadata) {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
if (typeof metadata.url === "string" && metadata.url.length > 0) {
|
|
373
|
+
const linkCandidates = Array.from(
|
|
374
|
+
document.querySelectorAll('a[href],[role="link"]')
|
|
375
|
+
);
|
|
376
|
+
const bestLink = scoreCandidates(linkCandidates, {
|
|
377
|
+
exactHref: metadata.url,
|
|
378
|
+
exactText: metadata.name
|
|
379
|
+
});
|
|
380
|
+
if (bestLink) {
|
|
381
|
+
return bestLink;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (typeof metadata.role === "string" && metadata.role.length > 0) {
|
|
385
|
+
const roleMatch = options.findByRole({
|
|
386
|
+
role: {
|
|
387
|
+
name: metadata.role,
|
|
388
|
+
...metadata.name ? { value: metadata.name } : {}
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
if (roleMatch) {
|
|
392
|
+
return roleMatch;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (typeof metadata.name === "string" && metadata.name.length > 0) {
|
|
396
|
+
return findByText(metadata.name);
|
|
397
|
+
}
|
|
398
|
+
return null;
|
|
399
|
+
};
|
|
400
|
+
|
|
3
401
|
// packages/extension/src/content.ts
|
|
4
402
|
var AGENT_TAB_BRANDING_ACTION = "drive.agent_tab_branding";
|
|
5
403
|
var AGENT_TAB_FAVICON_MARKER_ATTR = "data-bb-agent-favicon";
|
|
@@ -47,38 +445,6 @@
|
|
|
47
445
|
}
|
|
48
446
|
return value.replace(/[\\"']/g, "\\$&");
|
|
49
447
|
};
|
|
50
|
-
const isVisible = (element) => {
|
|
51
|
-
if (!(element instanceof HTMLElement)) {
|
|
52
|
-
return false;
|
|
53
|
-
}
|
|
54
|
-
const style = window.getComputedStyle(element);
|
|
55
|
-
if (style.visibility === "hidden" || style.display === "none") {
|
|
56
|
-
return false;
|
|
57
|
-
}
|
|
58
|
-
const rect = element.getBoundingClientRect();
|
|
59
|
-
if (rect.width === 0 && rect.height === 0) {
|
|
60
|
-
return false;
|
|
61
|
-
}
|
|
62
|
-
if (element.offsetWidth === 0 && element.offsetHeight === 0 && element.getClientRects().length === 0) {
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
let current = element;
|
|
66
|
-
while (current) {
|
|
67
|
-
const style2 = window.getComputedStyle(current);
|
|
68
|
-
if (style2.display === "none") {
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
71
|
-
if (style2.visibility === "hidden" || style2.visibility === "collapse") {
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
74
|
-
const opacity = Number.parseFloat(style2.opacity ?? "1");
|
|
75
|
-
if (Number.isFinite(opacity) && opacity <= 0) {
|
|
76
|
-
return false;
|
|
77
|
-
}
|
|
78
|
-
current = current.parentElement;
|
|
79
|
-
}
|
|
80
|
-
return true;
|
|
81
|
-
};
|
|
82
448
|
const buildUrlMatcher = (pattern) => {
|
|
83
449
|
const maxLength = 256;
|
|
84
450
|
if (pattern.length > maxLength) {
|
|
@@ -108,20 +474,17 @@
|
|
|
108
474
|
return { ok: true, matcher: (url) => url.includes(pattern) };
|
|
109
475
|
}
|
|
110
476
|
};
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
node = tree.nextNode();
|
|
123
|
-
}
|
|
124
|
-
return null;
|
|
477
|
+
const getRoleCandidates = (roleName) => {
|
|
478
|
+
const selectorMap = {
|
|
479
|
+
button: 'button,[role="button"],input[type="button"],input[type="submit"],input[type="reset"],summary',
|
|
480
|
+
link: 'a[href],[role="link"]',
|
|
481
|
+
checkbox: 'input[type="checkbox"],[role="checkbox"]',
|
|
482
|
+
radio: 'input[type="radio"],[role="radio"]',
|
|
483
|
+
textbox: 'textarea,input:not([type]),input[type="text"],input[type="search"],input[type="email"],input[type="url"],input[type="tel"],input[type="password"],[role="textbox"]',
|
|
484
|
+
combobox: 'select,input[list],[role="combobox"]'
|
|
485
|
+
};
|
|
486
|
+
const selector = selectorMap[roleName] ?? `[role="${escapeSelector(roleName)}"]`;
|
|
487
|
+
return Array.from(document.querySelectorAll(selector)).filter(isVisible);
|
|
125
488
|
};
|
|
126
489
|
const findByRole = (locator) => {
|
|
127
490
|
const role = locator.role;
|
|
@@ -133,17 +496,23 @@
|
|
|
133
496
|
if (typeof roleName !== "string" || roleName.length === 0) {
|
|
134
497
|
return null;
|
|
135
498
|
}
|
|
136
|
-
const candidates =
|
|
137
|
-
document.querySelectorAll(`[role="${escapeSelector(roleName)}"]`)
|
|
138
|
-
);
|
|
499
|
+
const candidates = getRoleCandidates(roleName);
|
|
139
500
|
if (typeof roleValue !== "string" || roleValue.length === 0) {
|
|
140
501
|
return candidates[0] ?? null;
|
|
141
502
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
503
|
+
const query = normalizeText(roleValue);
|
|
504
|
+
const matching = candidates.filter(
|
|
505
|
+
(candidate) => getRoleAccessibleName(candidate).includes(query)
|
|
506
|
+
);
|
|
507
|
+
if (matching.length === 0) {
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
const exactMatches = matching.filter(
|
|
511
|
+
(candidate) => getRoleAccessibleName(candidate) === query
|
|
512
|
+
);
|
|
513
|
+
return scoreCandidates(exactMatches.length > 0 ? exactMatches : matching, {
|
|
514
|
+
exactText: query
|
|
515
|
+
});
|
|
147
516
|
};
|
|
148
517
|
const resolveLocator = (locator) => {
|
|
149
518
|
if (!locator) {
|
|
@@ -157,18 +526,26 @@
|
|
|
157
526
|
if (found) {
|
|
158
527
|
return found;
|
|
159
528
|
}
|
|
529
|
+
const fallback = recoverElementBySnapshotRef(normalized, {
|
|
530
|
+
findByRole
|
|
531
|
+
});
|
|
532
|
+
if (fallback) {
|
|
533
|
+
return fallback;
|
|
534
|
+
}
|
|
160
535
|
}
|
|
161
536
|
const testid = locator.testid;
|
|
162
537
|
if (typeof testid === "string" && testid.length > 0) {
|
|
163
538
|
const selector = `[data-testid="${escapeSelector(testid)}"]`;
|
|
164
|
-
const found =
|
|
539
|
+
const found = scoreCandidates(
|
|
540
|
+
Array.from(document.querySelectorAll(selector))
|
|
541
|
+
);
|
|
165
542
|
if (found) {
|
|
166
543
|
return found;
|
|
167
544
|
}
|
|
168
545
|
}
|
|
169
546
|
const css = locator.css;
|
|
170
547
|
if (typeof css === "string" && css.length > 0) {
|
|
171
|
-
const found = document.
|
|
548
|
+
const found = scoreCandidates(Array.from(document.querySelectorAll(css)));
|
|
172
549
|
if (found) {
|
|
173
550
|
return found;
|
|
174
551
|
}
|
|
@@ -401,12 +778,29 @@
|
|
|
401
778
|
if (!target) {
|
|
402
779
|
return buildError("LOCATOR_NOT_FOUND", "Failed to resolve locator.");
|
|
403
780
|
}
|
|
404
|
-
const
|
|
781
|
+
const targetState = readPopupTriggerState(target);
|
|
782
|
+
const point = getHittablePoint(target, {
|
|
783
|
+
preferDirectHit: targetState !== null
|
|
784
|
+
});
|
|
405
785
|
return ok({
|
|
406
|
-
x:
|
|
407
|
-
y:
|
|
786
|
+
x: point.x,
|
|
787
|
+
y: point.y,
|
|
788
|
+
...targetState ? { target_state: targetState } : {}
|
|
408
789
|
});
|
|
409
790
|
}
|
|
791
|
+
case "drive.focus_locator": {
|
|
792
|
+
const { locator } = parseParams();
|
|
793
|
+
const target = resolveLocator(locator);
|
|
794
|
+
if (!target || !(target instanceof HTMLElement)) {
|
|
795
|
+
return buildError("LOCATOR_NOT_FOUND", "Failed to resolve locator.");
|
|
796
|
+
}
|
|
797
|
+
try {
|
|
798
|
+
target.focus({ preventScroll: true });
|
|
799
|
+
} catch {
|
|
800
|
+
target.focus();
|
|
801
|
+
}
|
|
802
|
+
return ok();
|
|
803
|
+
}
|
|
410
804
|
case "drive.snapshot_html": {
|
|
411
805
|
const html = document.documentElement?.outerHTML ?? "";
|
|
412
806
|
return ok({ format: "html", snapshot: html });
|
|
@@ -467,22 +861,20 @@
|
|
|
467
861
|
return buildError("LOCATOR_NOT_FOUND", "Failed to resolve locator.");
|
|
468
862
|
}
|
|
469
863
|
const count = typeof click_count === "number" && Number.isFinite(click_count) ? Math.max(1, Math.floor(click_count)) : 1;
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
}, 0);
|
|
485
|
-
return ok();
|
|
864
|
+
const strategy = selectClickStrategy(target);
|
|
865
|
+
if (strategy.kind === "popup_trigger") {
|
|
866
|
+
return await executePopupTriggerClick({
|
|
867
|
+
target,
|
|
868
|
+
beforeState: strategy.state,
|
|
869
|
+
clickCount: count,
|
|
870
|
+
locationBefore: window.location.href,
|
|
871
|
+
sleep
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
return executeGenericClick({
|
|
875
|
+
target,
|
|
876
|
+
clickCount: count
|
|
877
|
+
});
|
|
486
878
|
}
|
|
487
879
|
case "drive.hover": {
|
|
488
880
|
const { locator, delay_ms } = parseParams();
|
|
@@ -905,7 +1297,9 @@
|
|
|
905
1297
|
}
|
|
906
1298
|
const checkCondition = () => {
|
|
907
1299
|
if (kind === "text_present") {
|
|
908
|
-
return (document.body
|
|
1300
|
+
return getRenderedText(document.body).includes(
|
|
1301
|
+
normalizeText(value)
|
|
1302
|
+
);
|
|
909
1303
|
}
|
|
910
1304
|
if (kind === "url_matches") {
|
|
911
1305
|
return urlMatcher ? urlMatcher.matcher(window.location.href) : window.location.href.includes(value);
|