@btraut/browser-bridge 0.1.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.
@@ -0,0 +1,780 @@
1
+ "use strict";
2
+ (() => {
3
+ // packages/extension/src/content.ts
4
+ var runDriveAction = async (action, params) => {
5
+ const buildError = (code, message, details) => ({
6
+ ok: false,
7
+ error: {
8
+ code,
9
+ message,
10
+ retryable: false,
11
+ ...details ? { details } : {}
12
+ }
13
+ });
14
+ const ok = (result) => ({ ok: true, result });
15
+ const escapeSelector = (value) => {
16
+ if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
17
+ return CSS.escape(value);
18
+ }
19
+ return value.replace(/[\\"']/g, "\\$&");
20
+ };
21
+ const isVisible = (element) => {
22
+ if (!(element instanceof HTMLElement)) {
23
+ return false;
24
+ }
25
+ const style = window.getComputedStyle(element);
26
+ if (style.visibility === "hidden" || style.display === "none") {
27
+ return false;
28
+ }
29
+ const rect = element.getBoundingClientRect();
30
+ if (rect.width === 0 && rect.height === 0) {
31
+ return false;
32
+ }
33
+ if (element.offsetWidth === 0 && element.offsetHeight === 0 && element.getClientRects().length === 0) {
34
+ return false;
35
+ }
36
+ let current = element;
37
+ while (current) {
38
+ const style2 = window.getComputedStyle(current);
39
+ if (style2.display === "none") {
40
+ return false;
41
+ }
42
+ if (style2.visibility === "hidden" || style2.visibility === "collapse") {
43
+ return false;
44
+ }
45
+ const opacity = Number.parseFloat(style2.opacity ?? "1");
46
+ if (Number.isFinite(opacity) && opacity <= 0) {
47
+ return false;
48
+ }
49
+ current = current.parentElement;
50
+ }
51
+ return true;
52
+ };
53
+ const buildUrlMatcher = (pattern) => {
54
+ const maxLength = 256;
55
+ if (pattern.length > maxLength) {
56
+ return {
57
+ ok: false,
58
+ error: buildError(
59
+ "INVALID_ARGUMENT",
60
+ `url_matches pattern exceeds ${maxLength} characters.`
61
+ )
62
+ };
63
+ }
64
+ const nestedQuantifiers = /\((?:[^()\\]|\\.)*[+*{](?:[^()\\]|\\.)*\)[+*{]/.test(pattern);
65
+ const repeatedWildcards = /(\.\*.*\.\*)|(\.\+.*\.\+)/.test(pattern);
66
+ if (nestedQuantifiers || repeatedWildcards) {
67
+ return {
68
+ ok: false,
69
+ error: buildError(
70
+ "INVALID_ARGUMENT",
71
+ "url_matches pattern rejected due to unsafe regex complexity."
72
+ )
73
+ };
74
+ }
75
+ try {
76
+ const regex = new RegExp(pattern);
77
+ return { ok: true, matcher: (url) => regex.test(url) };
78
+ } catch {
79
+ return { ok: true, matcher: (url) => url.includes(pattern) };
80
+ }
81
+ };
82
+ const findByText = (text) => {
83
+ const tree = document.createTreeWalker(
84
+ document.body,
85
+ NodeFilter.SHOW_ELEMENT
86
+ );
87
+ let node = tree.nextNode();
88
+ while (node) {
89
+ const element = node;
90
+ if (element.textContent && element.textContent.includes(text)) {
91
+ return element;
92
+ }
93
+ node = tree.nextNode();
94
+ }
95
+ return null;
96
+ };
97
+ const findByRole = (locator) => {
98
+ const role = locator.role;
99
+ if (!role || typeof role !== "object") {
100
+ return null;
101
+ }
102
+ const roleName = role.name;
103
+ const roleValue = role.value;
104
+ if (typeof roleName !== "string" || roleName.length === 0) {
105
+ return null;
106
+ }
107
+ const candidates = Array.from(
108
+ document.querySelectorAll(`[role="${escapeSelector(roleName)}"]`)
109
+ );
110
+ if (typeof roleValue !== "string" || roleValue.length === 0) {
111
+ return candidates[0] ?? null;
112
+ }
113
+ return candidates.find((candidate) => {
114
+ const label = candidate.getAttribute("aria-label") ?? "";
115
+ const text = candidate.textContent ?? "";
116
+ return label.includes(roleValue) || text.includes(roleValue);
117
+ }) ?? null;
118
+ };
119
+ const resolveLocator = (locator) => {
120
+ if (!locator) {
121
+ return null;
122
+ }
123
+ const ref = locator.ref;
124
+ if (typeof ref === "string" && ref.length > 0) {
125
+ const normalized = ref.startsWith("@") ? ref : `@${ref}`;
126
+ const selector = `[data-bv-ref="${escapeSelector(normalized)}"]`;
127
+ const found = document.querySelector(selector);
128
+ if (found) {
129
+ return found;
130
+ }
131
+ }
132
+ const testid = locator.testid;
133
+ if (typeof testid === "string" && testid.length > 0) {
134
+ const selector = `[data-testid="${escapeSelector(testid)}"]`;
135
+ const found = document.querySelector(selector);
136
+ if (found) {
137
+ return found;
138
+ }
139
+ }
140
+ const css = locator.css;
141
+ if (typeof css === "string" && css.length > 0) {
142
+ const found = document.querySelector(css);
143
+ if (found) {
144
+ return found;
145
+ }
146
+ }
147
+ const byRole = findByRole(locator);
148
+ if (byRole) {
149
+ return byRole;
150
+ }
151
+ const text = locator.text;
152
+ if (typeof text === "string" && text.length > 0) {
153
+ return findByText(text);
154
+ }
155
+ return null;
156
+ };
157
+ const coerceBoolean = (value) => {
158
+ if (typeof value === "boolean") {
159
+ return value;
160
+ }
161
+ const normalized = value.trim().toLowerCase();
162
+ return ["true", "1", "yes", "y", "on", "checked"].includes(normalized);
163
+ };
164
+ const dispatchValueEvents = (element) => {
165
+ element.dispatchEvent(new Event("input", { bubbles: true }));
166
+ element.dispatchEvent(new Event("change", { bubbles: true }));
167
+ };
168
+ const selectOption = (select, value) => {
169
+ const option = Array.from(select.options).find(
170
+ (entry) => entry.value === value || entry.text === value
171
+ );
172
+ if (!option) {
173
+ return false;
174
+ }
175
+ select.value = option.value;
176
+ dispatchValueEvents(select);
177
+ return true;
178
+ };
179
+ const selectOptionByIndex = (select, index) => {
180
+ if (!Number.isInteger(index)) {
181
+ return false;
182
+ }
183
+ if (index < 0 || index >= select.options.length) {
184
+ return false;
185
+ }
186
+ select.selectedIndex = index;
187
+ dispatchValueEvents(select);
188
+ return true;
189
+ };
190
+ const setTextValue = (element, value, clear) => {
191
+ const tag = element.tagName.toLowerCase();
192
+ if (tag === "input" || tag === "textarea") {
193
+ const input = element;
194
+ if (clear) {
195
+ input.value = "";
196
+ }
197
+ input.value = `${input.value}${value}`;
198
+ dispatchValueEvents(input);
199
+ return true;
200
+ }
201
+ if (element.isContentEditable) {
202
+ if (clear) {
203
+ element.textContent = "";
204
+ }
205
+ element.textContent = `${element.textContent ?? ""}${value}`;
206
+ dispatchValueEvents(element);
207
+ return true;
208
+ }
209
+ return false;
210
+ };
211
+ const detectFieldType = (element) => {
212
+ if (element instanceof HTMLSelectElement) {
213
+ return "select";
214
+ }
215
+ if (element instanceof HTMLInputElement) {
216
+ const type = element.type.toLowerCase();
217
+ if (type === "checkbox") {
218
+ return "checkbox";
219
+ }
220
+ if (type === "radio") {
221
+ return "radio";
222
+ }
223
+ return "text";
224
+ }
225
+ if (element instanceof HTMLTextAreaElement) {
226
+ return "text";
227
+ }
228
+ if (element instanceof HTMLElement && element.isContentEditable) {
229
+ return "contentEditable";
230
+ }
231
+ return "text";
232
+ };
233
+ const submitIfRequested = (element) => {
234
+ const form = element.closest("form");
235
+ if (form && form instanceof HTMLFormElement) {
236
+ form.requestSubmit();
237
+ } else if (element instanceof HTMLElement) {
238
+ element.dispatchEvent(
239
+ new KeyboardEvent("keydown", { key: "Enter", bubbles: true })
240
+ );
241
+ }
242
+ };
243
+ const sleep = (ms) => new Promise((resolve) => window.setTimeout(resolve, ms));
244
+ const dispatchPointer = (element, type, x, y) => {
245
+ element.dispatchEvent(
246
+ new PointerEvent(type, {
247
+ bubbles: true,
248
+ cancelable: true,
249
+ clientX: x,
250
+ clientY: y,
251
+ button: 0
252
+ })
253
+ );
254
+ };
255
+ const dispatchDrag = (element, type, x, y, dataTransfer) => {
256
+ try {
257
+ element.dispatchEvent(
258
+ new DragEvent(type, {
259
+ bubbles: true,
260
+ cancelable: true,
261
+ clientX: x,
262
+ clientY: y,
263
+ dataTransfer
264
+ })
265
+ );
266
+ } catch {
267
+ element.dispatchEvent(
268
+ new Event(type, { bubbles: true, cancelable: true })
269
+ );
270
+ }
271
+ };
272
+ const keyToCode = (key) => {
273
+ const map = {
274
+ Enter: "Enter",
275
+ Tab: "Tab",
276
+ Escape: "Escape",
277
+ Esc: "Escape",
278
+ Backspace: "Backspace",
279
+ Delete: "Delete",
280
+ ArrowUp: "ArrowUp",
281
+ ArrowDown: "ArrowDown",
282
+ ArrowLeft: "ArrowLeft",
283
+ ArrowRight: "ArrowRight",
284
+ Home: "Home",
285
+ End: "End",
286
+ PageUp: "PageUp",
287
+ PageDown: "PageDown",
288
+ " ": "Space",
289
+ Space: "Space"
290
+ };
291
+ if (map[key]) {
292
+ return map[key];
293
+ }
294
+ if (key.length === 1) {
295
+ if (/[a-zA-Z]/.test(key)) {
296
+ return `Key${key.toUpperCase()}`;
297
+ }
298
+ if (/[0-9]/.test(key)) {
299
+ return `Digit${key}`;
300
+ }
301
+ }
302
+ return key;
303
+ };
304
+ const normalizeModifiers = (modifiers) => {
305
+ const state = { ctrl: false, alt: false, shift: false, meta: false };
306
+ if (Array.isArray(modifiers)) {
307
+ modifiers.forEach((modifier) => {
308
+ if (typeof modifier !== "string") {
309
+ return;
310
+ }
311
+ const normalized = modifier.toLowerCase();
312
+ if (normalized === "ctrl") {
313
+ state.ctrl = true;
314
+ } else if (normalized === "alt") {
315
+ state.alt = true;
316
+ } else if (normalized === "shift") {
317
+ state.shift = true;
318
+ } else if (normalized === "meta") {
319
+ state.meta = true;
320
+ }
321
+ });
322
+ return state;
323
+ }
324
+ if (modifiers && typeof modifiers === "object") {
325
+ const record = modifiers;
326
+ state.ctrl = Boolean(record.ctrl);
327
+ state.alt = Boolean(record.alt);
328
+ state.shift = Boolean(record.shift);
329
+ state.meta = Boolean(record.meta);
330
+ }
331
+ return state;
332
+ };
333
+ const activeEditableElement = () => {
334
+ const active = document.activeElement;
335
+ if (!active || !(active instanceof HTMLElement)) {
336
+ return null;
337
+ }
338
+ const tag = active.tagName.toLowerCase();
339
+ if (tag === "input" || tag === "textarea" || active.isContentEditable) {
340
+ return active;
341
+ }
342
+ return null;
343
+ };
344
+ const parseParams = () => params ?? {};
345
+ try {
346
+ switch (action) {
347
+ case "drive.navigate": {
348
+ const { url } = parseParams();
349
+ if (typeof url !== "string" || url.length === 0) {
350
+ return buildError(
351
+ "INVALID_ARGUMENT",
352
+ "url must be a non-empty string."
353
+ );
354
+ }
355
+ window.location.href = url;
356
+ return ok();
357
+ }
358
+ case "drive.click": {
359
+ const { locator, click_count } = parseParams();
360
+ const target = resolveLocator(locator);
361
+ if (!target) {
362
+ return buildError("LOCATOR_NOT_FOUND", "Failed to resolve locator.");
363
+ }
364
+ const count = typeof click_count === "number" && Number.isFinite(click_count) ? Math.max(1, Math.floor(click_count)) : 1;
365
+ window.setTimeout(() => {
366
+ try {
367
+ for (let i = 0; i < count; i += 1) {
368
+ target.click();
369
+ }
370
+ } catch {
371
+ }
372
+ }, 0);
373
+ return ok();
374
+ }
375
+ case "drive.hover": {
376
+ const { locator, delay_ms } = parseParams();
377
+ const target = resolveLocator(locator);
378
+ if (!target) {
379
+ return buildError("LOCATOR_NOT_FOUND", "Failed to resolve locator.");
380
+ }
381
+ const rect = target.getBoundingClientRect();
382
+ const centerX = rect.left + rect.width / 2;
383
+ const centerY = rect.top + rect.height / 2;
384
+ const eventInit = {
385
+ bubbles: true,
386
+ cancelable: true,
387
+ clientX: centerX,
388
+ clientY: centerY
389
+ };
390
+ target.dispatchEvent(new MouseEvent("mouseover", eventInit));
391
+ target.dispatchEvent(
392
+ new MouseEvent("mouseenter", { ...eventInit, bubbles: false })
393
+ );
394
+ target.dispatchEvent(new MouseEvent("mousemove", eventInit));
395
+ if (typeof delay_ms === "number" && Number.isFinite(delay_ms)) {
396
+ const waitMs = Math.min(Math.max(delay_ms, 0), 1e4);
397
+ if (waitMs > 0) {
398
+ await sleep(waitMs);
399
+ }
400
+ }
401
+ const html = document.documentElement?.outerHTML ?? "";
402
+ return ok({ format: "html", snapshot: html });
403
+ }
404
+ case "drive.select": {
405
+ const { locator, value, text, index } = parseParams();
406
+ const target = resolveLocator(locator);
407
+ if (!target) {
408
+ return buildError("LOCATOR_NOT_FOUND", "Failed to resolve locator.");
409
+ }
410
+ if (!(target instanceof HTMLSelectElement)) {
411
+ return buildError(
412
+ "INVALID_ARGUMENT",
413
+ "Target is not a select element."
414
+ );
415
+ }
416
+ let applied = false;
417
+ if (typeof index === "number" && Number.isFinite(index)) {
418
+ applied = selectOptionByIndex(target, Math.trunc(index));
419
+ }
420
+ if (!applied && typeof value === "string") {
421
+ applied = selectOption(target, value);
422
+ }
423
+ if (!applied && typeof text === "string") {
424
+ applied = selectOption(target, text);
425
+ }
426
+ if (!applied) {
427
+ return buildError(
428
+ "INVALID_ARGUMENT",
429
+ "No matching option found for select."
430
+ );
431
+ }
432
+ return ok();
433
+ }
434
+ case "drive.type": {
435
+ const { locator, text, clear, submit } = parseParams();
436
+ if (typeof text !== "string") {
437
+ return buildError("INVALID_ARGUMENT", "text must be a string.");
438
+ }
439
+ let target = resolveLocator(
440
+ locator
441
+ );
442
+ if (!target) {
443
+ const active = activeEditableElement();
444
+ if (active) {
445
+ target = active;
446
+ }
447
+ }
448
+ if (!target || !(target instanceof HTMLElement)) {
449
+ return buildError("LOCATOR_NOT_FOUND", "Failed to resolve locator.");
450
+ }
451
+ target.focus();
452
+ const tag = target.tagName.toLowerCase();
453
+ const shouldClear = Boolean(clear);
454
+ if (tag === "input" || tag === "textarea") {
455
+ const input = target;
456
+ if (shouldClear) {
457
+ input.value = "";
458
+ }
459
+ input.value = `${input.value}${text}`;
460
+ input.dispatchEvent(new Event("input", { bubbles: true }));
461
+ input.dispatchEvent(new Event("change", { bubbles: true }));
462
+ } else if (target.isContentEditable) {
463
+ if (shouldClear) {
464
+ target.textContent = "";
465
+ }
466
+ target.textContent = `${target.textContent ?? ""}${text}`;
467
+ target.dispatchEvent(new Event("input", { bubbles: true }));
468
+ } else {
469
+ return buildError(
470
+ "INVALID_ARGUMENT",
471
+ "Target is not editable (input, textarea, or contenteditable)."
472
+ );
473
+ }
474
+ if (submit) {
475
+ const form = target.closest("form");
476
+ if (form && form instanceof HTMLFormElement) {
477
+ form.requestSubmit();
478
+ } else {
479
+ target.dispatchEvent(
480
+ new KeyboardEvent("keydown", {
481
+ key: "Enter",
482
+ bubbles: true
483
+ })
484
+ );
485
+ }
486
+ }
487
+ return ok();
488
+ }
489
+ case "drive.fill_form": {
490
+ const { fields } = parseParams();
491
+ if (!Array.isArray(fields) || fields.length === 0) {
492
+ return buildError(
493
+ "INVALID_ARGUMENT",
494
+ "fields must be a non-empty array."
495
+ );
496
+ }
497
+ let filled = 0;
498
+ const errors = [];
499
+ fields.forEach((field, index) => {
500
+ if (!field || typeof field !== "object") {
501
+ errors.push(`Field ${index} is not an object.`);
502
+ return;
503
+ }
504
+ const record = field;
505
+ const selector = record.selector;
506
+ const locator = record.locator && typeof record.locator === "object" ? record.locator : void 0;
507
+ let element = null;
508
+ if (locator) {
509
+ element = resolveLocator(locator);
510
+ }
511
+ if (!element && typeof selector === "string" && selector.length > 0) {
512
+ element = document.querySelector(selector);
513
+ }
514
+ if (!element) {
515
+ errors.push(`Field ${index} could not be resolved.`);
516
+ return;
517
+ }
518
+ const value = record.value;
519
+ if (typeof value !== "string" && typeof value !== "boolean") {
520
+ errors.push(`Field ${index} has invalid value.`);
521
+ return;
522
+ }
523
+ const type = typeof record.type === "string" && record.type.length > 0 ? record.type : "auto";
524
+ const resolvedType = type === "auto" ? detectFieldType(element) : type;
525
+ const submit = Boolean(record.submit);
526
+ let applied = false;
527
+ if (resolvedType === "select") {
528
+ if (element instanceof HTMLSelectElement) {
529
+ applied = selectOption(element, String(value));
530
+ }
531
+ } else if (resolvedType === "checkbox" || resolvedType === "radio") {
532
+ if (element instanceof HTMLInputElement) {
533
+ const shouldCheck = typeof value === "boolean" ? value : coerceBoolean(value);
534
+ element.checked = shouldCheck;
535
+ dispatchValueEvents(element);
536
+ applied = true;
537
+ }
538
+ } else {
539
+ if (element instanceof HTMLElement) {
540
+ applied = setTextValue(element, String(value), true);
541
+ }
542
+ }
543
+ if (!applied) {
544
+ errors.push(`Field ${index} could not be filled.`);
545
+ return;
546
+ }
547
+ if (submit) {
548
+ submitIfRequested(element);
549
+ }
550
+ filled += 1;
551
+ });
552
+ return ok({
553
+ filled,
554
+ attempted: fields.length,
555
+ errors: errors.length > 0 ? errors : []
556
+ });
557
+ }
558
+ case "drive.drag": {
559
+ const { from, to, steps } = parseParams();
560
+ const fromEl = resolveLocator(from);
561
+ if (!fromEl) {
562
+ return buildError(
563
+ "LOCATOR_NOT_FOUND",
564
+ "Failed to resolve drag source."
565
+ );
566
+ }
567
+ const toEl = resolveLocator(to);
568
+ if (!toEl) {
569
+ return buildError(
570
+ "LOCATOR_NOT_FOUND",
571
+ "Failed to resolve drag target."
572
+ );
573
+ }
574
+ const fromRect = fromEl.getBoundingClientRect();
575
+ const toRect = toEl.getBoundingClientRect();
576
+ const startX = fromRect.left + fromRect.width / 2;
577
+ const startY = fromRect.top + fromRect.height / 2;
578
+ const endX = toRect.left + toRect.width / 2;
579
+ const endY = toRect.top + toRect.height / 2;
580
+ const totalSteps = typeof steps === "number" && Number.isFinite(steps) ? Math.max(1, Math.min(50, Math.floor(steps))) : 12;
581
+ let dataTransfer;
582
+ try {
583
+ dataTransfer = new DataTransfer();
584
+ } catch {
585
+ dataTransfer = void 0;
586
+ }
587
+ dispatchPointer(fromEl, "pointerdown", startX, startY);
588
+ dispatchDrag(fromEl, "dragstart", startX, startY, dataTransfer);
589
+ for (let i = 1; i <= totalSteps; i += 1) {
590
+ const progress = i / totalSteps;
591
+ const x = startX + (endX - startX) * progress;
592
+ const y = startY + (endY - startY) * progress;
593
+ const target = document.elementFromPoint(x, y) ?? toEl;
594
+ dispatchPointer(target, "pointermove", x, y);
595
+ dispatchDrag(target, "dragover", x, y, dataTransfer);
596
+ await sleep(10);
597
+ }
598
+ const dropTarget = document.elementFromPoint(endX, endY) ?? toEl;
599
+ dispatchDrag(dropTarget, "drop", endX, endY, dataTransfer);
600
+ dispatchPointer(dropTarget, "pointerup", endX, endY);
601
+ dispatchDrag(fromEl, "dragend", endX, endY, dataTransfer);
602
+ return ok();
603
+ }
604
+ case "drive.key_press": {
605
+ const { key, modifiers } = parseParams();
606
+ if (typeof key !== "string" || key.length === 0) {
607
+ return buildError(
608
+ "INVALID_ARGUMENT",
609
+ "key must be a non-empty string."
610
+ );
611
+ }
612
+ const target = document.activeElement instanceof HTMLElement ? document.activeElement : document.body;
613
+ if (!target) {
614
+ return buildError("INVALID_ARGUMENT", "No target for key press.");
615
+ }
616
+ const mods = normalizeModifiers(modifiers);
617
+ const eventInit = {
618
+ key,
619
+ code: keyToCode(key),
620
+ bubbles: true,
621
+ cancelable: true,
622
+ ctrlKey: mods.ctrl,
623
+ altKey: mods.alt,
624
+ shiftKey: mods.shift,
625
+ metaKey: mods.meta
626
+ };
627
+ target.dispatchEvent(new KeyboardEvent("keydown", eventInit));
628
+ target.dispatchEvent(new KeyboardEvent("keyup", eventInit));
629
+ return ok();
630
+ }
631
+ case "drive.key": {
632
+ const { key, modifiers, repeat } = parseParams();
633
+ if (typeof key !== "string" || key.length === 0) {
634
+ return buildError(
635
+ "INVALID_ARGUMENT",
636
+ "key must be a non-empty string."
637
+ );
638
+ }
639
+ const target = document.activeElement instanceof HTMLElement ? document.activeElement : document.body;
640
+ if (!target) {
641
+ return buildError("INVALID_ARGUMENT", "No target for key press.");
642
+ }
643
+ const mods = normalizeModifiers(modifiers);
644
+ const eventInit = {
645
+ key,
646
+ code: keyToCode(key),
647
+ bubbles: true,
648
+ cancelable: true,
649
+ ctrlKey: mods.ctrl,
650
+ altKey: mods.alt,
651
+ shiftKey: mods.shift,
652
+ metaKey: mods.meta
653
+ };
654
+ const count = typeof repeat === "number" && Number.isFinite(repeat) ? Math.max(1, Math.min(50, Math.floor(repeat))) : 1;
655
+ for (let i = 0; i < count; i += 1) {
656
+ target.dispatchEvent(new KeyboardEvent("keydown", eventInit));
657
+ target.dispatchEvent(new KeyboardEvent("keyup", eventInit));
658
+ }
659
+ return ok();
660
+ }
661
+ case "drive.scroll": {
662
+ const { delta_x, delta_y, top, left, behavior } = parseParams();
663
+ const scrollBehavior = behavior === "smooth" || behavior === "auto" ? behavior : void 0;
664
+ if (typeof top === "number" || typeof left === "number") {
665
+ window.scrollTo({
666
+ top: typeof top === "number" ? top : void 0,
667
+ left: typeof left === "number" ? left : void 0,
668
+ behavior: scrollBehavior
669
+ });
670
+ return ok();
671
+ }
672
+ if (typeof delta_x === "number" || typeof delta_y === "number") {
673
+ window.scrollBy({
674
+ left: typeof delta_x === "number" ? delta_x : 0,
675
+ top: typeof delta_y === "number" ? delta_y : 0,
676
+ behavior: scrollBehavior
677
+ });
678
+ return ok();
679
+ }
680
+ return buildError(
681
+ "INVALID_ARGUMENT",
682
+ "scroll requires delta_x/delta_y or top/left."
683
+ );
684
+ }
685
+ case "drive.wait_for": {
686
+ const { condition, timeout_ms } = parseParams();
687
+ if (!condition || typeof condition !== "object") {
688
+ return buildError("INVALID_ARGUMENT", "condition must be an object.");
689
+ }
690
+ const kind = condition.kind;
691
+ const value = condition.value;
692
+ if (typeof kind !== "string" || !["locator_visible", "text_present", "url_matches"].includes(kind)) {
693
+ return buildError(
694
+ "INVALID_ARGUMENT",
695
+ "condition.kind must be locator_visible, text_present, or url_matches."
696
+ );
697
+ }
698
+ if (typeof value !== "string" || value.length === 0) {
699
+ return buildError(
700
+ "INVALID_ARGUMENT",
701
+ "condition.value must be a non-empty string."
702
+ );
703
+ }
704
+ const timeout = typeof timeout_ms === "number" && Number.isFinite(timeout_ms) ? Math.max(0, timeout_ms) : 3e4;
705
+ const start = Date.now();
706
+ const urlMatcher = kind === "url_matches" ? buildUrlMatcher(value) : null;
707
+ if (urlMatcher && !urlMatcher.ok) {
708
+ return urlMatcher.error;
709
+ }
710
+ const checkCondition = () => {
711
+ if (kind === "text_present") {
712
+ return (document.body?.innerText ?? "").includes(value);
713
+ }
714
+ if (kind === "url_matches") {
715
+ return urlMatcher ? urlMatcher.matcher(window.location.href) : window.location.href.includes(value);
716
+ }
717
+ const selector = value;
718
+ const element = document.querySelector(selector);
719
+ return Boolean(element && isVisible(element));
720
+ };
721
+ return await new Promise((resolve) => {
722
+ const tick = () => {
723
+ if (checkCondition()) {
724
+ resolve(ok());
725
+ return;
726
+ }
727
+ if (Date.now() - start >= timeout) {
728
+ resolve(
729
+ buildError("TIMEOUT", `wait_for timed out after ${timeout}ms.`)
730
+ );
731
+ return;
732
+ }
733
+ window.setTimeout(tick, 100);
734
+ };
735
+ tick();
736
+ });
737
+ }
738
+ default:
739
+ return buildError("INVALID_ARGUMENT", `Unsupported action ${action}.`);
740
+ }
741
+ } catch (error) {
742
+ const message = error instanceof Error ? error.message : "Unknown error";
743
+ return buildError("EVALUATION_FAILED", message);
744
+ }
745
+ };
746
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
747
+ if (typeof chrome !== "undefined" && chrome.runtime?.onMessage) {
748
+ chrome.runtime.onMessage.addListener(
749
+ (message, _sender, sendResponse) => {
750
+ if (!isRecord(message) || typeof message.action !== "string") {
751
+ sendResponse({
752
+ ok: false,
753
+ error: {
754
+ code: "INVALID_ARGUMENT",
755
+ message: "Invalid content script request.",
756
+ retryable: false
757
+ }
758
+ });
759
+ return;
760
+ }
761
+ void runDriveAction(
762
+ message.action,
763
+ message.params
764
+ ).then(sendResponse).catch((error) => {
765
+ const messageText = error instanceof Error ? error.message : "Unknown error";
766
+ sendResponse({
767
+ ok: false,
768
+ error: {
769
+ code: "EVALUATION_FAILED",
770
+ message: messageText,
771
+ retryable: false
772
+ }
773
+ });
774
+ });
775
+ return true;
776
+ }
777
+ );
778
+ }
779
+ })();
780
+ //# sourceMappingURL=content.js.map