@btraut/browser-bridge 0.13.2 → 0.15.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 +51 -0
- package/README.md +53 -37
- package/dist/api.js +1792 -678
- package/dist/api.js.map +4 -4
- package/dist/index.js +666 -417
- package/dist/index.js.map +4 -4
- package/extension/dist/background.js +1484 -693
- package/extension/dist/background.js.map +4 -4
- package/extension/dist/content.js +534 -77
- 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/dist/permissions-request-ui.js +111 -0
- package/extension/dist/permissions-request-ui.js.map +7 -0
- package/extension/manifest.json +3 -3
- package/package.json +1 -1
- package/skills/browser-bridge/SKILL.md +18 -0
- package/skills/browser-bridge/skill.json +1 -1
|
@@ -1,8 +1,436 @@
|
|
|
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 getActionabilityScore = (element) => {
|
|
232
|
+
if (!(element instanceof HTMLElement) || !isVisible(element)) {
|
|
233
|
+
return { directHit: false, hittable: false };
|
|
234
|
+
}
|
|
235
|
+
const point = getHittablePoint(element, { preferDirectHit: true });
|
|
236
|
+
return {
|
|
237
|
+
directHit: pointHitsTarget(element, point.x, point.y, { directOnly: true }),
|
|
238
|
+
hittable: pointHitsTarget(element, point.x, point.y)
|
|
239
|
+
};
|
|
240
|
+
};
|
|
241
|
+
var scoreCandidates = (candidates, options) => {
|
|
242
|
+
const queryText = options?.exactText ? normalizeText(options.exactText) : "";
|
|
243
|
+
const queryHref = options?.exactHref ?? "";
|
|
244
|
+
if (candidates.length === 0) {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
const scored = candidates.filter(isVisible).map((candidate) => {
|
|
248
|
+
const text = getRenderedText(candidate);
|
|
249
|
+
const accessibleName = getRoleAccessibleName(candidate);
|
|
250
|
+
const href = candidate.getAttribute("href") ?? "";
|
|
251
|
+
const actionability = getActionabilityScore(candidate);
|
|
252
|
+
return {
|
|
253
|
+
candidate,
|
|
254
|
+
exactText: queryText.length > 0 && (text === queryText || accessibleName === queryText),
|
|
255
|
+
exactHref: queryHref.length > 0 && (href === queryHref || candidate instanceof HTMLAnchorElement && candidate.href === queryHref),
|
|
256
|
+
directHit: actionability.directHit,
|
|
257
|
+
hittable: actionability.hittable,
|
|
258
|
+
onScreen: isOnScreen(candidate),
|
|
259
|
+
clickable: isClickable(candidate),
|
|
260
|
+
textLength: text.length,
|
|
261
|
+
depth: getNodeDepth(candidate)
|
|
262
|
+
};
|
|
263
|
+
}).sort((a, b) => {
|
|
264
|
+
if (a.exactHref !== b.exactHref) {
|
|
265
|
+
return a.exactHref ? -1 : 1;
|
|
266
|
+
}
|
|
267
|
+
if (a.exactText !== b.exactText) {
|
|
268
|
+
return a.exactText ? -1 : 1;
|
|
269
|
+
}
|
|
270
|
+
if (a.directHit !== b.directHit) {
|
|
271
|
+
return a.directHit ? -1 : 1;
|
|
272
|
+
}
|
|
273
|
+
if (a.hittable !== b.hittable) {
|
|
274
|
+
return a.hittable ? -1 : 1;
|
|
275
|
+
}
|
|
276
|
+
if (a.onScreen !== b.onScreen) {
|
|
277
|
+
return a.onScreen ? -1 : 1;
|
|
278
|
+
}
|
|
279
|
+
if (a.clickable !== b.clickable) {
|
|
280
|
+
return a.clickable ? -1 : 1;
|
|
281
|
+
}
|
|
282
|
+
if (a.textLength !== b.textLength) {
|
|
283
|
+
return a.textLength - b.textLength;
|
|
284
|
+
}
|
|
285
|
+
return b.depth - a.depth;
|
|
286
|
+
});
|
|
287
|
+
return scored[0]?.candidate ?? candidates[0] ?? null;
|
|
288
|
+
};
|
|
289
|
+
var findByText = (text) => {
|
|
290
|
+
const query = normalizeText(text);
|
|
291
|
+
if (query.length === 0) {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
const candidateMap = /* @__PURE__ */ new Map();
|
|
295
|
+
const tree = document.createTreeWalker(
|
|
296
|
+
document.body,
|
|
297
|
+
NodeFilter.SHOW_ELEMENT
|
|
298
|
+
);
|
|
299
|
+
let node = tree.nextNode();
|
|
300
|
+
while (node) {
|
|
301
|
+
const element = node;
|
|
302
|
+
if (!isVisible(element)) {
|
|
303
|
+
node = tree.nextNode();
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
const elementText = getRenderedText(element);
|
|
307
|
+
if (!elementText.includes(query)) {
|
|
308
|
+
node = tree.nextNode();
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
let preferredTarget = element;
|
|
312
|
+
let current = element;
|
|
313
|
+
while (current) {
|
|
314
|
+
if (current !== element && isVisible(current) && isClickable(current) && getRenderedText(current).includes(query)) {
|
|
315
|
+
preferredTarget = current;
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
current = current.parentElement;
|
|
319
|
+
}
|
|
320
|
+
const preferredText = getRenderedText(preferredTarget);
|
|
321
|
+
const currentBest = candidateMap.get(preferredTarget);
|
|
322
|
+
const actionability = getActionabilityScore(preferredTarget);
|
|
323
|
+
const nextScore = {
|
|
324
|
+
exact: preferredText === query || elementText === query,
|
|
325
|
+
directHit: actionability.directHit,
|
|
326
|
+
hittable: actionability.hittable,
|
|
327
|
+
onScreen: isOnScreen(preferredTarget),
|
|
328
|
+
clickable: isClickable(preferredTarget),
|
|
329
|
+
textLength: preferredText.length || elementText.length,
|
|
330
|
+
depth: getNodeDepth(preferredTarget)
|
|
331
|
+
};
|
|
332
|
+
if (!currentBest || Number(nextScore.exact) > Number(currentBest.exact) || nextScore.exact === currentBest.exact && Number(nextScore.directHit) > Number(currentBest.directHit) || nextScore.exact === currentBest.exact && nextScore.directHit === currentBest.directHit && Number(nextScore.hittable) > Number(currentBest.hittable) || nextScore.exact === currentBest.exact && nextScore.directHit === currentBest.directHit && nextScore.hittable === currentBest.hittable && Number(nextScore.onScreen) > Number(currentBest.onScreen) || nextScore.exact === currentBest.exact && nextScore.directHit === currentBest.directHit && nextScore.hittable === currentBest.hittable && nextScore.onScreen === currentBest.onScreen && Number(nextScore.clickable) > Number(currentBest.clickable) || nextScore.exact === currentBest.exact && nextScore.directHit === currentBest.directHit && nextScore.hittable === currentBest.hittable && nextScore.onScreen === currentBest.onScreen && nextScore.clickable === currentBest.clickable && nextScore.textLength < currentBest.textLength || nextScore.exact === currentBest.exact && nextScore.directHit === currentBest.directHit && nextScore.hittable === currentBest.hittable && nextScore.onScreen === currentBest.onScreen && nextScore.clickable === currentBest.clickable && nextScore.textLength === currentBest.textLength && nextScore.depth > currentBest.depth) {
|
|
333
|
+
candidateMap.set(preferredTarget, nextScore);
|
|
334
|
+
}
|
|
335
|
+
node = tree.nextNode();
|
|
336
|
+
}
|
|
337
|
+
const candidates = Array.from(candidateMap.entries()).sort((a, b) => {
|
|
338
|
+
const [, left] = a;
|
|
339
|
+
const [, right] = b;
|
|
340
|
+
if (left.exact !== right.exact) {
|
|
341
|
+
return left.exact ? -1 : 1;
|
|
342
|
+
}
|
|
343
|
+
if (left.directHit !== right.directHit) {
|
|
344
|
+
return left.directHit ? -1 : 1;
|
|
345
|
+
}
|
|
346
|
+
if (left.hittable !== right.hittable) {
|
|
347
|
+
return left.hittable ? -1 : 1;
|
|
348
|
+
}
|
|
349
|
+
if (left.onScreen !== right.onScreen) {
|
|
350
|
+
return left.onScreen ? -1 : 1;
|
|
351
|
+
}
|
|
352
|
+
if (left.clickable !== right.clickable) {
|
|
353
|
+
return left.clickable ? -1 : 1;
|
|
354
|
+
}
|
|
355
|
+
if (left.textLength !== right.textLength) {
|
|
356
|
+
return left.textLength - right.textLength;
|
|
357
|
+
}
|
|
358
|
+
return right.depth - left.depth;
|
|
359
|
+
});
|
|
360
|
+
return candidates[0]?.[0] ?? null;
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
// packages/extension/src/snapshot-ref-recovery.ts
|
|
364
|
+
var SNAPSHOT_REF_REGISTRY_ID = "__bb_snapshot_ref_registry__";
|
|
365
|
+
var readSnapshotRefRegistry = () => {
|
|
366
|
+
const registry = /* @__PURE__ */ new Map();
|
|
367
|
+
const el = document.getElementById(SNAPSHOT_REF_REGISTRY_ID);
|
|
368
|
+
const raw = el?.textContent;
|
|
369
|
+
if (!raw) {
|
|
370
|
+
return registry;
|
|
371
|
+
}
|
|
372
|
+
try {
|
|
373
|
+
const parsed = JSON.parse(raw);
|
|
374
|
+
if (!Array.isArray(parsed)) {
|
|
375
|
+
return registry;
|
|
376
|
+
}
|
|
377
|
+
for (const entry of parsed) {
|
|
378
|
+
if (!entry || typeof entry !== "object") {
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
const ref = entry.ref;
|
|
382
|
+
if (typeof ref !== "string" || ref.length === 0) {
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
registry.set(ref, {
|
|
386
|
+
role: typeof entry.role === "string" ? entry.role ?? void 0 : void 0,
|
|
387
|
+
name: typeof entry.name === "string" ? entry.name ?? void 0 : void 0,
|
|
388
|
+
url: typeof entry.url === "string" ? entry.url ?? void 0 : void 0
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
} catch {
|
|
392
|
+
return /* @__PURE__ */ new Map();
|
|
393
|
+
}
|
|
394
|
+
return registry;
|
|
395
|
+
};
|
|
396
|
+
var recoverElementBySnapshotRef = (ref, options) => {
|
|
397
|
+
const metadata = readSnapshotRefRegistry().get(ref);
|
|
398
|
+
if (!metadata) {
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
if (typeof metadata.url === "string" && metadata.url.length > 0) {
|
|
402
|
+
const linkCandidates = Array.from(
|
|
403
|
+
document.querySelectorAll('a[href],[role="link"]')
|
|
404
|
+
);
|
|
405
|
+
const bestLink = scoreCandidates(linkCandidates, {
|
|
406
|
+
exactHref: metadata.url,
|
|
407
|
+
exactText: metadata.name
|
|
408
|
+
});
|
|
409
|
+
if (bestLink) {
|
|
410
|
+
return bestLink;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (typeof metadata.role === "string" && metadata.role.length > 0) {
|
|
414
|
+
const roleMatch = options.findByRole({
|
|
415
|
+
role: {
|
|
416
|
+
name: metadata.role,
|
|
417
|
+
...metadata.name ? { value: metadata.name } : {}
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
if (roleMatch) {
|
|
421
|
+
return roleMatch;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (typeof metadata.name === "string" && metadata.name.length > 0) {
|
|
425
|
+
return findByText(metadata.name);
|
|
426
|
+
}
|
|
427
|
+
return null;
|
|
428
|
+
};
|
|
429
|
+
|
|
3
430
|
// packages/extension/src/content.ts
|
|
4
431
|
var AGENT_TAB_BRANDING_ACTION = "drive.agent_tab_branding";
|
|
5
432
|
var AGENT_TAB_FAVICON_MARKER_ATTR = "data-bb-agent-favicon";
|
|
433
|
+
var CONTENT_LISTENER_MARKER = "__browserBridgeContentListenerInstalled__";
|
|
6
434
|
var applyAgentTabFavicon = (faviconUrl) => {
|
|
7
435
|
if (faviconUrl.length === 0) {
|
|
8
436
|
return;
|
|
@@ -47,38 +475,6 @@
|
|
|
47
475
|
}
|
|
48
476
|
return value.replace(/[\\"']/g, "\\$&");
|
|
49
477
|
};
|
|
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
478
|
const buildUrlMatcher = (pattern) => {
|
|
83
479
|
const maxLength = 256;
|
|
84
480
|
if (pattern.length > maxLength) {
|
|
@@ -108,20 +504,17 @@
|
|
|
108
504
|
return { ok: true, matcher: (url) => url.includes(pattern) };
|
|
109
505
|
}
|
|
110
506
|
};
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
node = tree.nextNode();
|
|
123
|
-
}
|
|
124
|
-
return null;
|
|
507
|
+
const getRoleCandidates = (roleName) => {
|
|
508
|
+
const selectorMap = {
|
|
509
|
+
button: 'button,[role="button"],input[type="button"],input[type="submit"],input[type="reset"],summary',
|
|
510
|
+
link: 'a[href],[role="link"]',
|
|
511
|
+
checkbox: 'input[type="checkbox"],[role="checkbox"]',
|
|
512
|
+
radio: 'input[type="radio"],[role="radio"]',
|
|
513
|
+
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"]',
|
|
514
|
+
combobox: 'select,input[list],[role="combobox"]'
|
|
515
|
+
};
|
|
516
|
+
const selector = selectorMap[roleName] ?? `[role="${escapeSelector(roleName)}"]`;
|
|
517
|
+
return Array.from(document.querySelectorAll(selector)).filter(isVisible);
|
|
125
518
|
};
|
|
126
519
|
const findByRole = (locator) => {
|
|
127
520
|
const role = locator.role;
|
|
@@ -133,17 +526,48 @@
|
|
|
133
526
|
if (typeof roleName !== "string" || roleName.length === 0) {
|
|
134
527
|
return null;
|
|
135
528
|
}
|
|
136
|
-
const candidates =
|
|
137
|
-
document.querySelectorAll(`[role="${escapeSelector(roleName)}"]`)
|
|
138
|
-
);
|
|
529
|
+
const candidates = getRoleCandidates(roleName);
|
|
139
530
|
if (typeof roleValue !== "string" || roleValue.length === 0) {
|
|
140
531
|
return candidates[0] ?? null;
|
|
141
532
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
533
|
+
const query = normalizeText(roleValue);
|
|
534
|
+
const matching = candidates.filter(
|
|
535
|
+
(candidate) => getRoleAccessibleName(candidate).includes(query)
|
|
536
|
+
);
|
|
537
|
+
if (matching.length > 0) {
|
|
538
|
+
const exactMatches = matching.filter(
|
|
539
|
+
(candidate) => getRoleAccessibleName(candidate) === query
|
|
540
|
+
);
|
|
541
|
+
return scoreCandidates(
|
|
542
|
+
exactMatches.length > 0 ? exactMatches : matching,
|
|
543
|
+
{
|
|
544
|
+
exactText: query
|
|
545
|
+
}
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
const fallbackCandidates = Array.from(
|
|
549
|
+
document.querySelectorAll("*")
|
|
550
|
+
).filter((candidate) => {
|
|
551
|
+
if (!isVisible(candidate)) {
|
|
552
|
+
return false;
|
|
553
|
+
}
|
|
554
|
+
if (roleName === "button" && !isClickable(candidate)) {
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
return getRoleAccessibleName(candidate).includes(query);
|
|
558
|
+
});
|
|
559
|
+
if (fallbackCandidates.length === 0) {
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
const exactFallbackMatches = fallbackCandidates.filter(
|
|
563
|
+
(candidate) => getRoleAccessibleName(candidate) === query
|
|
564
|
+
);
|
|
565
|
+
return scoreCandidates(
|
|
566
|
+
exactFallbackMatches.length > 0 ? exactFallbackMatches : fallbackCandidates,
|
|
567
|
+
{
|
|
568
|
+
exactText: query
|
|
569
|
+
}
|
|
570
|
+
);
|
|
147
571
|
};
|
|
148
572
|
const resolveLocator = (locator) => {
|
|
149
573
|
if (!locator) {
|
|
@@ -157,18 +581,26 @@
|
|
|
157
581
|
if (found) {
|
|
158
582
|
return found;
|
|
159
583
|
}
|
|
584
|
+
const fallback = recoverElementBySnapshotRef(normalized, {
|
|
585
|
+
findByRole
|
|
586
|
+
});
|
|
587
|
+
if (fallback) {
|
|
588
|
+
return fallback;
|
|
589
|
+
}
|
|
160
590
|
}
|
|
161
591
|
const testid = locator.testid;
|
|
162
592
|
if (typeof testid === "string" && testid.length > 0) {
|
|
163
593
|
const selector = `[data-testid="${escapeSelector(testid)}"]`;
|
|
164
|
-
const found =
|
|
594
|
+
const found = scoreCandidates(
|
|
595
|
+
Array.from(document.querySelectorAll(selector))
|
|
596
|
+
);
|
|
165
597
|
if (found) {
|
|
166
598
|
return found;
|
|
167
599
|
}
|
|
168
600
|
}
|
|
169
601
|
const css = locator.css;
|
|
170
602
|
if (typeof css === "string" && css.length > 0) {
|
|
171
|
-
const found = document.
|
|
603
|
+
const found = scoreCandidates(Array.from(document.querySelectorAll(css)));
|
|
172
604
|
if (found) {
|
|
173
605
|
return found;
|
|
174
606
|
}
|
|
@@ -401,12 +833,29 @@
|
|
|
401
833
|
if (!target) {
|
|
402
834
|
return buildError("LOCATOR_NOT_FOUND", "Failed to resolve locator.");
|
|
403
835
|
}
|
|
404
|
-
const
|
|
836
|
+
const targetState = readPopupTriggerState(target);
|
|
837
|
+
const point = getHittablePoint(target, {
|
|
838
|
+
preferDirectHit: targetState !== null
|
|
839
|
+
});
|
|
405
840
|
return ok({
|
|
406
|
-
x:
|
|
407
|
-
y:
|
|
841
|
+
x: point.x,
|
|
842
|
+
y: point.y,
|
|
843
|
+
...targetState ? { target_state: targetState } : {}
|
|
408
844
|
});
|
|
409
845
|
}
|
|
846
|
+
case "drive.focus_locator": {
|
|
847
|
+
const { locator } = parseParams();
|
|
848
|
+
const target = resolveLocator(locator);
|
|
849
|
+
if (!target || !(target instanceof HTMLElement)) {
|
|
850
|
+
return buildError("LOCATOR_NOT_FOUND", "Failed to resolve locator.");
|
|
851
|
+
}
|
|
852
|
+
try {
|
|
853
|
+
target.focus({ preventScroll: true });
|
|
854
|
+
} catch {
|
|
855
|
+
target.focus();
|
|
856
|
+
}
|
|
857
|
+
return ok();
|
|
858
|
+
}
|
|
410
859
|
case "drive.snapshot_html": {
|
|
411
860
|
const html = document.documentElement?.outerHTML ?? "";
|
|
412
861
|
return ok({ format: "html", snapshot: html });
|
|
@@ -467,22 +916,20 @@
|
|
|
467
916
|
return buildError("LOCATOR_NOT_FOUND", "Failed to resolve locator.");
|
|
468
917
|
}
|
|
469
918
|
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();
|
|
919
|
+
const strategy = selectClickStrategy(target);
|
|
920
|
+
if (strategy.kind === "popup_trigger") {
|
|
921
|
+
return await executePopupTriggerClick({
|
|
922
|
+
target,
|
|
923
|
+
beforeState: strategy.state,
|
|
924
|
+
clickCount: count,
|
|
925
|
+
locationBefore: window.location.href,
|
|
926
|
+
sleep
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
return executeGenericClick({
|
|
930
|
+
target,
|
|
931
|
+
clickCount: count
|
|
932
|
+
});
|
|
486
933
|
}
|
|
487
934
|
case "drive.hover": {
|
|
488
935
|
const { locator, delay_ms } = parseParams();
|
|
@@ -905,7 +1352,9 @@
|
|
|
905
1352
|
}
|
|
906
1353
|
const checkCondition = () => {
|
|
907
1354
|
if (kind === "text_present") {
|
|
908
|
-
return (document.body
|
|
1355
|
+
return getRenderedText(document.body).includes(
|
|
1356
|
+
normalizeText(value)
|
|
1357
|
+
);
|
|
909
1358
|
}
|
|
910
1359
|
if (kind === "url_matches") {
|
|
911
1360
|
return urlMatcher ? urlMatcher.matcher(window.location.href) : window.location.href.includes(value);
|
|
@@ -952,7 +1401,15 @@
|
|
|
952
1401
|
}
|
|
953
1402
|
};
|
|
954
1403
|
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
955
|
-
|
|
1404
|
+
var contentListenerInstalled = (() => {
|
|
1405
|
+
const target = globalThis;
|
|
1406
|
+
if (target[CONTENT_LISTENER_MARKER]) {
|
|
1407
|
+
return true;
|
|
1408
|
+
}
|
|
1409
|
+
target[CONTENT_LISTENER_MARKER] = true;
|
|
1410
|
+
return false;
|
|
1411
|
+
})();
|
|
1412
|
+
if (typeof chrome !== "undefined" && chrome.runtime?.onMessage && !contentListenerInstalled) {
|
|
956
1413
|
chrome.runtime.onMessage.addListener(
|
|
957
1414
|
(message, _sender, sendResponse) => {
|
|
958
1415
|
if (!isRecord(message) || typeof message.action !== "string") {
|