browser-pilot 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2765 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/browser/index.ts
21
+ var browser_exports = {};
22
+ __export(browser_exports, {
23
+ Browser: () => Browser,
24
+ ElementNotFoundError: () => ElementNotFoundError,
25
+ NavigationError: () => NavigationError,
26
+ Page: () => Page,
27
+ TimeoutError: () => TimeoutError,
28
+ connect: () => connect
29
+ });
30
+ module.exports = __toCommonJS(browser_exports);
31
+
32
+ // src/cdp/protocol.ts
33
+ var CDPError = class extends Error {
34
+ code;
35
+ data;
36
+ constructor(error) {
37
+ super(error.message);
38
+ this.name = "CDPError";
39
+ this.code = error.code;
40
+ this.data = error.data;
41
+ }
42
+ };
43
+
44
+ // src/cdp/transport.ts
45
+ function createTransport(wsUrl, options = {}) {
46
+ const { timeout = 3e4 } = options;
47
+ return new Promise((resolve, reject) => {
48
+ const timeoutId = setTimeout(() => {
49
+ reject(new Error(`WebSocket connection timeout after ${timeout}ms`));
50
+ }, timeout);
51
+ const ws = new WebSocket(wsUrl);
52
+ const messageHandlers = [];
53
+ const closeHandlers = [];
54
+ const errorHandlers = [];
55
+ ws.addEventListener("open", () => {
56
+ clearTimeout(timeoutId);
57
+ const transport = {
58
+ send(message) {
59
+ if (ws.readyState === WebSocket.OPEN) {
60
+ ws.send(message);
61
+ } else {
62
+ throw new Error(
63
+ `Cannot send message, WebSocket is ${getReadyStateString(ws.readyState)}`
64
+ );
65
+ }
66
+ },
67
+ async close() {
68
+ return new Promise((resolveClose) => {
69
+ if (ws.readyState === WebSocket.CLOSED) {
70
+ resolveClose();
71
+ return;
72
+ }
73
+ const onClose = () => {
74
+ ws.removeEventListener("close", onClose);
75
+ resolveClose();
76
+ };
77
+ ws.addEventListener("close", onClose);
78
+ ws.close();
79
+ setTimeout(resolveClose, 5e3);
80
+ });
81
+ },
82
+ onMessage(handler) {
83
+ messageHandlers.push(handler);
84
+ },
85
+ onClose(handler) {
86
+ closeHandlers.push(handler);
87
+ },
88
+ onError(handler) {
89
+ errorHandlers.push(handler);
90
+ }
91
+ };
92
+ resolve(transport);
93
+ });
94
+ ws.addEventListener("message", (event) => {
95
+ const data = typeof event.data === "string" ? event.data : String(event.data);
96
+ for (const handler of messageHandlers) {
97
+ handler(data);
98
+ }
99
+ });
100
+ ws.addEventListener("close", () => {
101
+ for (const handler of closeHandlers) {
102
+ handler();
103
+ }
104
+ });
105
+ ws.addEventListener("error", (_event) => {
106
+ clearTimeout(timeoutId);
107
+ const error = new Error("WebSocket connection error");
108
+ for (const handler of errorHandlers) {
109
+ handler(error);
110
+ }
111
+ reject(error);
112
+ });
113
+ });
114
+ }
115
+ function getReadyStateString(state) {
116
+ switch (state) {
117
+ case WebSocket.CONNECTING:
118
+ return "CONNECTING";
119
+ case WebSocket.OPEN:
120
+ return "OPEN";
121
+ case WebSocket.CLOSING:
122
+ return "CLOSING";
123
+ case WebSocket.CLOSED:
124
+ return "CLOSED";
125
+ default:
126
+ return "UNKNOWN";
127
+ }
128
+ }
129
+
130
+ // src/cdp/client.ts
131
+ async function createCDPClient(wsUrl, options = {}) {
132
+ const { debug = false, timeout = 3e4 } = options;
133
+ const transport = await createTransport(wsUrl, { timeout });
134
+ let messageId = 0;
135
+ let currentSessionId;
136
+ let connected = true;
137
+ const pending = /* @__PURE__ */ new Map();
138
+ const eventHandlers = /* @__PURE__ */ new Map();
139
+ const anyEventHandlers = /* @__PURE__ */ new Set();
140
+ transport.onMessage((raw) => {
141
+ let msg;
142
+ try {
143
+ msg = JSON.parse(raw);
144
+ } catch {
145
+ if (debug) console.error("[CDP] Failed to parse message:", raw);
146
+ return;
147
+ }
148
+ if (debug) {
149
+ console.log("[CDP] <--", JSON.stringify(msg, null, 2).slice(0, 500));
150
+ }
151
+ if ("id" in msg && typeof msg.id === "number") {
152
+ const response = msg;
153
+ const request = pending.get(response.id);
154
+ if (request) {
155
+ pending.delete(response.id);
156
+ clearTimeout(request.timer);
157
+ if (response.error) {
158
+ request.reject(new CDPError(response.error));
159
+ } else {
160
+ request.resolve(response.result);
161
+ }
162
+ }
163
+ return;
164
+ }
165
+ if ("method" in msg) {
166
+ const event = msg;
167
+ const params = event.params ?? {};
168
+ for (const handler of anyEventHandlers) {
169
+ try {
170
+ handler(event.method, params);
171
+ } catch (e) {
172
+ if (debug) console.error("[CDP] Error in any-event handler:", e);
173
+ }
174
+ }
175
+ const handlers = eventHandlers.get(event.method);
176
+ if (handlers) {
177
+ for (const handler of handlers) {
178
+ try {
179
+ handler(params);
180
+ } catch (e) {
181
+ if (debug) console.error(`[CDP] Error in handler for ${event.method}:`, e);
182
+ }
183
+ }
184
+ }
185
+ }
186
+ });
187
+ transport.onClose(() => {
188
+ connected = false;
189
+ for (const [id, request] of pending) {
190
+ clearTimeout(request.timer);
191
+ request.reject(new Error("WebSocket connection closed"));
192
+ pending.delete(id);
193
+ }
194
+ });
195
+ transport.onError((error) => {
196
+ if (debug) console.error("[CDP] Transport error:", error);
197
+ });
198
+ const client = {
199
+ async send(method, params, sessionId) {
200
+ if (!connected) {
201
+ throw new Error("CDP client is not connected");
202
+ }
203
+ const id = ++messageId;
204
+ const effectiveSessionId = sessionId ?? currentSessionId;
205
+ const request = { id, method };
206
+ if (params !== void 0) {
207
+ request.params = params;
208
+ }
209
+ if (effectiveSessionId !== void 0) {
210
+ request.sessionId = effectiveSessionId;
211
+ }
212
+ const message = JSON.stringify(request);
213
+ if (debug) {
214
+ console.log("[CDP] -->", message.slice(0, 500));
215
+ }
216
+ return new Promise((resolve, reject) => {
217
+ const timer = setTimeout(() => {
218
+ pending.delete(id);
219
+ reject(new Error(`CDP command ${method} timed out after ${timeout}ms`));
220
+ }, timeout);
221
+ pending.set(id, {
222
+ resolve,
223
+ reject,
224
+ method,
225
+ timer
226
+ });
227
+ try {
228
+ transport.send(message);
229
+ } catch (e) {
230
+ pending.delete(id);
231
+ clearTimeout(timer);
232
+ reject(e);
233
+ }
234
+ });
235
+ },
236
+ on(event, handler) {
237
+ let handlers = eventHandlers.get(event);
238
+ if (!handlers) {
239
+ handlers = /* @__PURE__ */ new Set();
240
+ eventHandlers.set(event, handlers);
241
+ }
242
+ handlers.add(handler);
243
+ },
244
+ off(event, handler) {
245
+ const handlers = eventHandlers.get(event);
246
+ if (handlers) {
247
+ handlers.delete(handler);
248
+ if (handlers.size === 0) {
249
+ eventHandlers.delete(event);
250
+ }
251
+ }
252
+ },
253
+ onAny(handler) {
254
+ anyEventHandlers.add(handler);
255
+ },
256
+ async close() {
257
+ connected = false;
258
+ await transport.close();
259
+ },
260
+ async attachToTarget(targetId) {
261
+ const result = await this.send("Target.attachToTarget", {
262
+ targetId,
263
+ flatten: true
264
+ });
265
+ currentSessionId = result.sessionId;
266
+ return result.sessionId;
267
+ },
268
+ get sessionId() {
269
+ return currentSessionId;
270
+ },
271
+ get isConnected() {
272
+ return connected;
273
+ }
274
+ };
275
+ return client;
276
+ }
277
+
278
+ // src/providers/browserbase.ts
279
+ var BrowserBaseProvider = class {
280
+ name = "browserbase";
281
+ apiKey;
282
+ projectId;
283
+ baseUrl;
284
+ constructor(options) {
285
+ this.apiKey = options.apiKey;
286
+ this.projectId = options.projectId;
287
+ this.baseUrl = options.baseUrl ?? "https://api.browserbase.com";
288
+ }
289
+ async createSession(options = {}) {
290
+ const response = await fetch(`${this.baseUrl}/v1/sessions`, {
291
+ method: "POST",
292
+ headers: {
293
+ "X-BB-API-Key": this.apiKey,
294
+ "Content-Type": "application/json"
295
+ },
296
+ body: JSON.stringify({
297
+ projectId: this.projectId,
298
+ browserSettings: {
299
+ viewport: options.width && options.height ? {
300
+ width: options.width,
301
+ height: options.height
302
+ } : void 0
303
+ },
304
+ ...options
305
+ })
306
+ });
307
+ if (!response.ok) {
308
+ const text = await response.text();
309
+ throw new Error(`BrowserBase createSession failed: ${response.status} ${text}`);
310
+ }
311
+ const session = await response.json();
312
+ const connectResponse = await fetch(`${this.baseUrl}/v1/sessions/${session.id}`, {
313
+ headers: {
314
+ "X-BB-API-Key": this.apiKey
315
+ }
316
+ });
317
+ if (!connectResponse.ok) {
318
+ throw new Error(`BrowserBase getSession failed: ${connectResponse.status}`);
319
+ }
320
+ const sessionDetails = await connectResponse.json();
321
+ if (!sessionDetails.connectUrl) {
322
+ throw new Error("BrowserBase session does not have a connectUrl");
323
+ }
324
+ return {
325
+ wsUrl: sessionDetails.connectUrl,
326
+ sessionId: session.id,
327
+ metadata: {
328
+ debugUrl: sessionDetails.debugUrl,
329
+ projectId: this.projectId,
330
+ status: sessionDetails.status
331
+ },
332
+ close: async () => {
333
+ await fetch(`${this.baseUrl}/v1/sessions/${session.id}`, {
334
+ method: "DELETE",
335
+ headers: {
336
+ "X-BB-API-Key": this.apiKey
337
+ }
338
+ });
339
+ }
340
+ };
341
+ }
342
+ async resumeSession(sessionId) {
343
+ const response = await fetch(`${this.baseUrl}/v1/sessions/${sessionId}`, {
344
+ headers: {
345
+ "X-BB-API-Key": this.apiKey
346
+ }
347
+ });
348
+ if (!response.ok) {
349
+ throw new Error(`BrowserBase resumeSession failed: ${response.status}`);
350
+ }
351
+ const session = await response.json();
352
+ if (!session.connectUrl) {
353
+ throw new Error("BrowserBase session does not have a connectUrl (may be closed)");
354
+ }
355
+ return {
356
+ wsUrl: session.connectUrl,
357
+ sessionId: session.id,
358
+ metadata: {
359
+ debugUrl: session.debugUrl,
360
+ projectId: this.projectId,
361
+ status: session.status
362
+ },
363
+ close: async () => {
364
+ await fetch(`${this.baseUrl}/v1/sessions/${sessionId}`, {
365
+ method: "DELETE",
366
+ headers: {
367
+ "X-BB-API-Key": this.apiKey
368
+ }
369
+ });
370
+ }
371
+ };
372
+ }
373
+ };
374
+
375
+ // src/providers/browserless.ts
376
+ var BrowserlessProvider = class {
377
+ name = "browserless";
378
+ token;
379
+ baseUrl;
380
+ constructor(options) {
381
+ this.token = options.token;
382
+ this.baseUrl = options.baseUrl ?? "wss://chrome.browserless.io";
383
+ }
384
+ async createSession(options = {}) {
385
+ const params = new URLSearchParams({
386
+ token: this.token
387
+ });
388
+ if (options.width && options.height) {
389
+ params.set("--window-size", `${options.width},${options.height}`);
390
+ }
391
+ if (options.proxy?.server) {
392
+ params.set("--proxy-server", options.proxy.server);
393
+ }
394
+ const wsUrl = `${this.baseUrl}?${params.toString()}`;
395
+ return {
396
+ wsUrl,
397
+ metadata: {
398
+ provider: "browserless"
399
+ },
400
+ close: async () => {
401
+ }
402
+ };
403
+ }
404
+ // Browserless doesn't support session resumption in the same way
405
+ // Each connection is a fresh browser instance
406
+ };
407
+
408
+ // src/providers/generic.ts
409
+ var GenericProvider = class {
410
+ name = "generic";
411
+ wsUrl;
412
+ constructor(options) {
413
+ this.wsUrl = options.wsUrl;
414
+ }
415
+ async createSession(_options = {}) {
416
+ return {
417
+ wsUrl: this.wsUrl,
418
+ metadata: {
419
+ provider: "generic"
420
+ },
421
+ close: async () => {
422
+ }
423
+ };
424
+ }
425
+ };
426
+
427
+ // src/providers/index.ts
428
+ function createProvider(options) {
429
+ switch (options.provider) {
430
+ case "browserbase":
431
+ if (!options.apiKey) {
432
+ throw new Error("BrowserBase provider requires apiKey");
433
+ }
434
+ if (!options.projectId) {
435
+ throw new Error("BrowserBase provider requires projectId");
436
+ }
437
+ return new BrowserBaseProvider({
438
+ apiKey: options.apiKey,
439
+ projectId: options.projectId
440
+ });
441
+ case "browserless":
442
+ if (!options.apiKey) {
443
+ throw new Error("Browserless provider requires apiKey (token)");
444
+ }
445
+ return new BrowserlessProvider({
446
+ token: options.apiKey
447
+ });
448
+ case "generic":
449
+ if (!options.wsUrl) {
450
+ throw new Error("Generic provider requires wsUrl");
451
+ }
452
+ return new GenericProvider({
453
+ wsUrl: options.wsUrl
454
+ });
455
+ default:
456
+ throw new Error(`Unknown provider: ${options.provider}`);
457
+ }
458
+ }
459
+
460
+ // src/actions/executor.ts
461
+ var DEFAULT_TIMEOUT = 3e4;
462
+ var BatchExecutor = class {
463
+ page;
464
+ constructor(page) {
465
+ this.page = page;
466
+ }
467
+ /**
468
+ * Execute a batch of steps
469
+ */
470
+ async execute(steps, options = {}) {
471
+ const { timeout = DEFAULT_TIMEOUT, onFail = "stop" } = options;
472
+ const results = [];
473
+ const startTime = Date.now();
474
+ for (let i = 0; i < steps.length; i++) {
475
+ const step = steps[i];
476
+ const stepStart = Date.now();
477
+ try {
478
+ const result = await this.executeStep(step, timeout);
479
+ results.push({
480
+ index: i,
481
+ action: step.action,
482
+ selector: step.selector,
483
+ selectorUsed: result.selectorUsed,
484
+ success: true,
485
+ durationMs: Date.now() - stepStart,
486
+ result: result.value,
487
+ text: result.text
488
+ });
489
+ } catch (error) {
490
+ const errorMessage = error instanceof Error ? error.message : String(error);
491
+ results.push({
492
+ index: i,
493
+ action: step.action,
494
+ selector: step.selector,
495
+ success: false,
496
+ durationMs: Date.now() - stepStart,
497
+ error: errorMessage
498
+ });
499
+ if (onFail === "stop" && !step.optional) {
500
+ return {
501
+ success: false,
502
+ stoppedAtIndex: i,
503
+ steps: results,
504
+ totalDurationMs: Date.now() - startTime
505
+ };
506
+ }
507
+ }
508
+ }
509
+ const allSuccess = results.every((r) => r.success || steps[r.index]?.optional);
510
+ return {
511
+ success: allSuccess,
512
+ steps: results,
513
+ totalDurationMs: Date.now() - startTime
514
+ };
515
+ }
516
+ /**
517
+ * Execute a single step
518
+ */
519
+ async executeStep(step, defaultTimeout) {
520
+ const timeout = step.timeout ?? defaultTimeout;
521
+ const optional = step.optional ?? false;
522
+ switch (step.action) {
523
+ case "goto": {
524
+ if (!step.url) throw new Error("goto requires url");
525
+ await this.page.goto(step.url, { timeout, optional });
526
+ return {};
527
+ }
528
+ case "click": {
529
+ if (!step.selector) throw new Error("click requires selector");
530
+ if (step.waitForNavigation) {
531
+ const navPromise = this.page.waitForNavigation({ timeout, optional });
532
+ await this.page.click(step.selector, { timeout, optional });
533
+ await navPromise;
534
+ } else {
535
+ await this.page.click(step.selector, { timeout, optional });
536
+ }
537
+ return { selectorUsed: this.getUsedSelector(step.selector) };
538
+ }
539
+ case "fill": {
540
+ if (!step.selector) throw new Error("fill requires selector");
541
+ if (typeof step.value !== "string") throw new Error("fill requires string value");
542
+ await this.page.fill(step.selector, step.value, {
543
+ timeout,
544
+ optional,
545
+ clear: step.clear ?? true
546
+ });
547
+ return { selectorUsed: this.getUsedSelector(step.selector) };
548
+ }
549
+ case "type": {
550
+ if (!step.selector) throw new Error("type requires selector");
551
+ if (typeof step.value !== "string") throw new Error("type requires string value");
552
+ await this.page.type(step.selector, step.value, {
553
+ timeout,
554
+ optional,
555
+ delay: step.delay ?? 50
556
+ });
557
+ return { selectorUsed: this.getUsedSelector(step.selector) };
558
+ }
559
+ case "select": {
560
+ if (step.trigger && step.option && typeof step.value === "string") {
561
+ await this.page.select(
562
+ {
563
+ trigger: step.trigger,
564
+ option: step.option,
565
+ value: step.value,
566
+ match: step.match
567
+ },
568
+ { timeout, optional }
569
+ );
570
+ return { selectorUsed: this.getUsedSelector(step.trigger) };
571
+ }
572
+ if (!step.selector) throw new Error("select requires selector");
573
+ if (!step.value) throw new Error("select requires value");
574
+ await this.page.select(step.selector, step.value, { timeout, optional });
575
+ return { selectorUsed: this.getUsedSelector(step.selector) };
576
+ }
577
+ case "check": {
578
+ if (!step.selector) throw new Error("check requires selector");
579
+ await this.page.check(step.selector, { timeout, optional });
580
+ return { selectorUsed: this.getUsedSelector(step.selector) };
581
+ }
582
+ case "uncheck": {
583
+ if (!step.selector) throw new Error("uncheck requires selector");
584
+ await this.page.uncheck(step.selector, { timeout, optional });
585
+ return { selectorUsed: this.getUsedSelector(step.selector) };
586
+ }
587
+ case "submit": {
588
+ if (!step.selector) throw new Error("submit requires selector");
589
+ await this.page.submit(step.selector, {
590
+ timeout,
591
+ optional,
592
+ method: step.method ?? "enter+click"
593
+ });
594
+ return { selectorUsed: this.getUsedSelector(step.selector) };
595
+ }
596
+ case "press": {
597
+ if (!step.key) throw new Error("press requires key");
598
+ await this.page.press(step.key);
599
+ return {};
600
+ }
601
+ case "focus": {
602
+ if (!step.selector) throw new Error("focus requires selector");
603
+ await this.page.focus(step.selector, { timeout, optional });
604
+ return { selectorUsed: this.getUsedSelector(step.selector) };
605
+ }
606
+ case "hover": {
607
+ if (!step.selector) throw new Error("hover requires selector");
608
+ await this.page.hover(step.selector, { timeout, optional });
609
+ return { selectorUsed: this.getUsedSelector(step.selector) };
610
+ }
611
+ case "scroll": {
612
+ if (step.x !== void 0 || step.y !== void 0) {
613
+ await this.page.scroll("body", { x: step.x, y: step.y, timeout, optional });
614
+ return {};
615
+ }
616
+ if (!step.selector && (step.direction || step.amount !== void 0)) {
617
+ const amount = step.amount ?? 500;
618
+ const direction = step.direction ?? "down";
619
+ const deltaY = direction === "down" ? amount : direction === "up" ? -amount : 0;
620
+ const deltaX = direction === "right" ? amount : direction === "left" ? -amount : 0;
621
+ await this.page.evaluate(`window.scrollBy(${deltaX}, ${deltaY})`);
622
+ return {};
623
+ }
624
+ if (!step.selector) throw new Error("scroll requires selector, coordinates, or direction");
625
+ await this.page.scroll(step.selector, { timeout, optional });
626
+ return { selectorUsed: this.getUsedSelector(step.selector) };
627
+ }
628
+ case "wait": {
629
+ if (!step.selector && !step.waitFor) {
630
+ const delay = step.timeout ?? 1e3;
631
+ await new Promise((resolve) => setTimeout(resolve, delay));
632
+ return {};
633
+ }
634
+ if (step.waitFor === "navigation") {
635
+ await this.page.waitForNavigation({ timeout, optional });
636
+ return {};
637
+ }
638
+ if (step.waitFor === "networkIdle") {
639
+ await this.page.waitForNetworkIdle({ timeout, optional });
640
+ return {};
641
+ }
642
+ if (!step.selector)
643
+ throw new Error(
644
+ "wait requires selector (or waitFor: navigation/networkIdle, or timeout for simple delay)"
645
+ );
646
+ await this.page.waitFor(step.selector, {
647
+ timeout,
648
+ optional,
649
+ state: step.waitFor ?? "visible"
650
+ });
651
+ return { selectorUsed: this.getUsedSelector(step.selector) };
652
+ }
653
+ case "snapshot": {
654
+ const snapshot = await this.page.snapshot();
655
+ return { value: snapshot };
656
+ }
657
+ case "screenshot": {
658
+ const data = await this.page.screenshot({
659
+ format: step.format,
660
+ quality: step.quality,
661
+ fullPage: step.fullPage
662
+ });
663
+ return { value: data };
664
+ }
665
+ case "evaluate": {
666
+ if (typeof step.value !== "string")
667
+ throw new Error("evaluate requires string value (expression)");
668
+ const result = await this.page.evaluate(step.value);
669
+ return { value: result };
670
+ }
671
+ case "text": {
672
+ const selector = Array.isArray(step.selector) ? step.selector[0] : step.selector;
673
+ const text = await this.page.text(selector);
674
+ return { text, selectorUsed: selector };
675
+ }
676
+ case "switchFrame": {
677
+ if (!step.selector) throw new Error("switchFrame requires selector");
678
+ await this.page.switchToFrame(step.selector, { timeout, optional });
679
+ return { selectorUsed: this.getUsedSelector(step.selector) };
680
+ }
681
+ case "switchToMain": {
682
+ await this.page.switchToMain();
683
+ return {};
684
+ }
685
+ default:
686
+ throw new Error(
687
+ `Unknown action: ${step.action}. Run 'bp actions' for available actions.`
688
+ );
689
+ }
690
+ }
691
+ /**
692
+ * Get the first selector if multiple were provided
693
+ * (actual used selector tracking would need to be implemented in Page)
694
+ */
695
+ getUsedSelector(selector) {
696
+ return Array.isArray(selector) ? selector[0] : selector;
697
+ }
698
+ };
699
+
700
+ // src/network/interceptor.ts
701
+ var RequestInterceptor = class {
702
+ cdp;
703
+ enabled = false;
704
+ handlers = [];
705
+ pendingRequests = /* @__PURE__ */ new Map();
706
+ boundHandleRequestPaused;
707
+ boundHandleAuthRequired;
708
+ constructor(cdp) {
709
+ this.cdp = cdp;
710
+ this.boundHandleRequestPaused = this.handleRequestPaused.bind(this);
711
+ this.boundHandleAuthRequired = this.handleAuthRequired.bind(this);
712
+ }
713
+ /**
714
+ * Enable request interception with optional patterns
715
+ */
716
+ async enable(patterns) {
717
+ if (this.enabled) return;
718
+ this.cdp.on("Fetch.requestPaused", this.boundHandleRequestPaused);
719
+ this.cdp.on("Fetch.authRequired", this.boundHandleAuthRequired);
720
+ await this.cdp.send("Fetch.enable", {
721
+ patterns: patterns?.map((p) => ({
722
+ urlPattern: p.urlPattern ?? "*",
723
+ resourceType: p.resourceType,
724
+ requestStage: p.requestStage ?? "Request"
725
+ })) ?? [{ urlPattern: "*" }],
726
+ handleAuthRequests: true
727
+ });
728
+ this.enabled = true;
729
+ }
730
+ /**
731
+ * Disable request interception
732
+ */
733
+ async disable() {
734
+ if (!this.enabled) return;
735
+ await this.cdp.send("Fetch.disable");
736
+ this.cdp.off("Fetch.requestPaused", this.boundHandleRequestPaused);
737
+ this.cdp.off("Fetch.authRequired", this.boundHandleAuthRequired);
738
+ this.enabled = false;
739
+ this.handlers = [];
740
+ this.pendingRequests.clear();
741
+ }
742
+ /**
743
+ * Add a request handler
744
+ */
745
+ addHandler(pattern, handler) {
746
+ const entry = { pattern, handler };
747
+ this.handlers.push(entry);
748
+ return () => {
749
+ const idx = this.handlers.indexOf(entry);
750
+ if (idx !== -1) this.handlers.splice(idx, 1);
751
+ };
752
+ }
753
+ /**
754
+ * Handle paused request from CDP
755
+ */
756
+ async handleRequestPaused(params) {
757
+ const requestId = params["requestId"];
758
+ const request = params["request"];
759
+ const responseStatusCode = params["responseStatusCode"];
760
+ const responseHeaders = params["responseHeaders"];
761
+ const intercepted = {
762
+ requestId,
763
+ url: request["url"],
764
+ method: request["method"],
765
+ headers: request["headers"],
766
+ postData: request["postData"],
767
+ resourceType: params["resourceType"],
768
+ frameId: params["frameId"],
769
+ isNavigationRequest: params["isNavigationRequest"],
770
+ responseStatusCode,
771
+ responseHeaders: responseHeaders ? Object.fromEntries(responseHeaders.map((h) => [h.name, h.value])) : void 0
772
+ };
773
+ this.pendingRequests.set(requestId, { request: intercepted, handled: false });
774
+ const matchingHandler = this.handlers.find((h) => this.matchesPattern(intercepted, h.pattern));
775
+ if (matchingHandler) {
776
+ const actions = this.createActions(requestId);
777
+ try {
778
+ await matchingHandler.handler(intercepted, actions);
779
+ } catch (err) {
780
+ console.error("[RequestInterceptor] Handler error:", err);
781
+ if (!this.pendingRequests.get(requestId)?.handled) {
782
+ await actions.continue();
783
+ }
784
+ }
785
+ } else {
786
+ await this.continueRequest(requestId);
787
+ }
788
+ this.pendingRequests.delete(requestId);
789
+ }
790
+ /**
791
+ * Handle auth challenge
792
+ */
793
+ async handleAuthRequired(params) {
794
+ const requestId = params["requestId"];
795
+ await this.cdp.send("Fetch.continueWithAuth", {
796
+ requestId,
797
+ authChallengeResponse: { response: "CancelAuth" }
798
+ });
799
+ }
800
+ /**
801
+ * Check if request matches pattern
802
+ */
803
+ matchesPattern(request, pattern) {
804
+ if (pattern.resourceType && request.resourceType !== pattern.resourceType) {
805
+ return false;
806
+ }
807
+ if (pattern.urlPattern) {
808
+ const regex = this.globToRegex(pattern.urlPattern);
809
+ if (!regex.test(request.url)) {
810
+ return false;
811
+ }
812
+ }
813
+ return true;
814
+ }
815
+ /**
816
+ * Convert glob pattern to regex
817
+ */
818
+ globToRegex(pattern) {
819
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
820
+ return new RegExp(`^${escaped}$`);
821
+ }
822
+ /**
823
+ * Create actions object for handler
824
+ */
825
+ createActions(requestId) {
826
+ const pending = this.pendingRequests.get(requestId);
827
+ const markHandled = () => {
828
+ if (pending) pending.handled = true;
829
+ };
830
+ return {
831
+ continue: async (options) => {
832
+ markHandled();
833
+ await this.continueRequest(requestId, options);
834
+ },
835
+ fulfill: async (options) => {
836
+ markHandled();
837
+ await this.fulfillRequest(requestId, options);
838
+ },
839
+ fail: async (options) => {
840
+ markHandled();
841
+ await this.failRequest(requestId, options);
842
+ }
843
+ };
844
+ }
845
+ /**
846
+ * Continue a paused request
847
+ */
848
+ async continueRequest(requestId, options) {
849
+ await this.cdp.send("Fetch.continueRequest", {
850
+ requestId,
851
+ url: options?.url,
852
+ method: options?.method,
853
+ headers: options?.headers ? Object.entries(options.headers).map(([name, value]) => ({ name, value })) : void 0,
854
+ postData: options?.postData ? btoa(options.postData) : void 0
855
+ });
856
+ }
857
+ /**
858
+ * Fulfill a request with custom response
859
+ */
860
+ async fulfillRequest(requestId, options) {
861
+ const headers = Object.entries(options.headers ?? {}).map(([name, value]) => ({
862
+ name,
863
+ value
864
+ }));
865
+ await this.cdp.send("Fetch.fulfillRequest", {
866
+ requestId,
867
+ responseCode: options.status,
868
+ responseHeaders: headers,
869
+ body: options.isBase64Encoded ? options.body : options.body ? btoa(options.body) : void 0
870
+ });
871
+ }
872
+ /**
873
+ * Fail/abort a request
874
+ */
875
+ async failRequest(requestId, options) {
876
+ await this.cdp.send("Fetch.failRequest", {
877
+ requestId,
878
+ errorReason: options?.reason ?? "BlockedByClient"
879
+ });
880
+ }
881
+ };
882
+
883
+ // src/wait/strategies.ts
884
+ var DEEP_QUERY_SCRIPT = `
885
+ function deepQuery(selector, root = document) {
886
+ // Try direct query first (fastest path)
887
+ let el = root.querySelector(selector);
888
+ if (el) return el;
889
+
890
+ // Search in shadow roots recursively
891
+ const searchShadows = (node) => {
892
+ // Check if this node has a shadow root
893
+ if (node.shadowRoot) {
894
+ el = node.shadowRoot.querySelector(selector);
895
+ if (el) return el;
896
+ // Search children of shadow root
897
+ for (const child of node.shadowRoot.querySelectorAll('*')) {
898
+ el = searchShadows(child);
899
+ if (el) return el;
900
+ }
901
+ }
902
+ // Search children that might have shadow roots
903
+ for (const child of node.querySelectorAll('*')) {
904
+ if (child.shadowRoot) {
905
+ el = searchShadows(child);
906
+ if (el) return el;
907
+ }
908
+ }
909
+ return null;
910
+ };
911
+
912
+ return searchShadows(root);
913
+ }
914
+ `;
915
+ async function isElementVisible(cdp, selector, contextId) {
916
+ const params = {
917
+ expression: `(() => {
918
+ ${DEEP_QUERY_SCRIPT}
919
+ const el = deepQuery(${JSON.stringify(selector)});
920
+ if (!el) return false;
921
+ const style = getComputedStyle(el);
922
+ if (style.display === 'none') return false;
923
+ if (style.visibility === 'hidden') return false;
924
+ if (parseFloat(style.opacity) === 0) return false;
925
+ const rect = el.getBoundingClientRect();
926
+ return rect.width > 0 && rect.height > 0;
927
+ })()`,
928
+ returnByValue: true
929
+ };
930
+ if (contextId !== void 0) {
931
+ params["contextId"] = contextId;
932
+ }
933
+ const result = await cdp.send("Runtime.evaluate", params);
934
+ return result.result.value === true;
935
+ }
936
+ async function isElementAttached(cdp, selector, contextId) {
937
+ const params = {
938
+ expression: `(() => {
939
+ ${DEEP_QUERY_SCRIPT}
940
+ return deepQuery(${JSON.stringify(selector)}) !== null;
941
+ })()`,
942
+ returnByValue: true
943
+ };
944
+ if (contextId !== void 0) {
945
+ params["contextId"] = contextId;
946
+ }
947
+ const result = await cdp.send("Runtime.evaluate", params);
948
+ return result.result.value === true;
949
+ }
950
+ function sleep(ms) {
951
+ return new Promise((resolve) => setTimeout(resolve, ms));
952
+ }
953
+ async function waitForAnyElement(cdp, selectors, options = {}) {
954
+ const { state = "visible", timeout = 3e4, pollInterval = 100, contextId } = options;
955
+ const startTime = Date.now();
956
+ const deadline = startTime + timeout;
957
+ while (Date.now() < deadline) {
958
+ for (const selector of selectors) {
959
+ let conditionMet = false;
960
+ switch (state) {
961
+ case "visible":
962
+ conditionMet = await isElementVisible(cdp, selector, contextId);
963
+ break;
964
+ case "hidden":
965
+ conditionMet = !await isElementVisible(cdp, selector, contextId);
966
+ break;
967
+ case "attached":
968
+ conditionMet = await isElementAttached(cdp, selector, contextId);
969
+ break;
970
+ case "detached":
971
+ conditionMet = !await isElementAttached(cdp, selector, contextId);
972
+ break;
973
+ }
974
+ if (conditionMet) {
975
+ return { success: true, selector, waitedMs: Date.now() - startTime };
976
+ }
977
+ }
978
+ await sleep(pollInterval);
979
+ }
980
+ return { success: false, waitedMs: Date.now() - startTime };
981
+ }
982
+ async function getCurrentUrl(cdp) {
983
+ const result = await cdp.send("Runtime.evaluate", {
984
+ expression: "location.href",
985
+ returnByValue: true
986
+ });
987
+ return result.result.value;
988
+ }
989
+ async function waitForNavigation(cdp, options = {}) {
990
+ const { timeout = 3e4, allowSameDocument = true } = options;
991
+ const startTime = Date.now();
992
+ let startUrl;
993
+ try {
994
+ startUrl = await getCurrentUrl(cdp);
995
+ } catch {
996
+ startUrl = "";
997
+ }
998
+ return new Promise((resolve) => {
999
+ let resolved = false;
1000
+ const cleanup = [];
1001
+ const done = (success) => {
1002
+ if (resolved) return;
1003
+ resolved = true;
1004
+ for (const fn of cleanup) fn();
1005
+ resolve({ success, waitedMs: Date.now() - startTime });
1006
+ };
1007
+ const timer = setTimeout(() => done(false), timeout);
1008
+ cleanup.push(() => clearTimeout(timer));
1009
+ const onLoad = () => done(true);
1010
+ cdp.on("Page.loadEventFired", onLoad);
1011
+ cleanup.push(() => cdp.off("Page.loadEventFired", onLoad));
1012
+ const onFrameNavigated = (params) => {
1013
+ const frame = params["frame"];
1014
+ if (frame && !frame.parentId && frame.url !== startUrl) {
1015
+ done(true);
1016
+ }
1017
+ };
1018
+ cdp.on("Page.frameNavigated", onFrameNavigated);
1019
+ cleanup.push(() => cdp.off("Page.frameNavigated", onFrameNavigated));
1020
+ if (allowSameDocument) {
1021
+ const onSameDoc = () => done(true);
1022
+ cdp.on("Page.navigatedWithinDocument", onSameDoc);
1023
+ cleanup.push(() => cdp.off("Page.navigatedWithinDocument", onSameDoc));
1024
+ }
1025
+ const pollUrl = async () => {
1026
+ while (!resolved && Date.now() < startTime + timeout) {
1027
+ await sleep(100);
1028
+ if (resolved) return;
1029
+ try {
1030
+ const currentUrl = await getCurrentUrl(cdp);
1031
+ if (startUrl && currentUrl !== startUrl) {
1032
+ done(true);
1033
+ return;
1034
+ }
1035
+ } catch {
1036
+ }
1037
+ }
1038
+ };
1039
+ pollUrl();
1040
+ });
1041
+ }
1042
+ async function waitForNetworkIdle(cdp, options = {}) {
1043
+ const { timeout = 3e4, idleTime = 500 } = options;
1044
+ const startTime = Date.now();
1045
+ await cdp.send("Network.enable");
1046
+ return new Promise((resolve) => {
1047
+ let inFlight = 0;
1048
+ let idleTimer = null;
1049
+ const timeoutTimer = setTimeout(() => {
1050
+ cleanup();
1051
+ resolve({ success: false, waitedMs: Date.now() - startTime });
1052
+ }, timeout);
1053
+ const checkIdle = () => {
1054
+ if (inFlight === 0) {
1055
+ if (idleTimer) clearTimeout(idleTimer);
1056
+ idleTimer = setTimeout(() => {
1057
+ cleanup();
1058
+ resolve({ success: true, waitedMs: Date.now() - startTime });
1059
+ }, idleTime);
1060
+ }
1061
+ };
1062
+ const onRequestStart = () => {
1063
+ inFlight++;
1064
+ if (idleTimer) {
1065
+ clearTimeout(idleTimer);
1066
+ idleTimer = null;
1067
+ }
1068
+ };
1069
+ const onRequestEnd = () => {
1070
+ inFlight = Math.max(0, inFlight - 1);
1071
+ checkIdle();
1072
+ };
1073
+ const cleanup = () => {
1074
+ clearTimeout(timeoutTimer);
1075
+ if (idleTimer) clearTimeout(idleTimer);
1076
+ cdp.off("Network.requestWillBeSent", onRequestStart);
1077
+ cdp.off("Network.loadingFinished", onRequestEnd);
1078
+ cdp.off("Network.loadingFailed", onRequestEnd);
1079
+ };
1080
+ cdp.on("Network.requestWillBeSent", onRequestStart);
1081
+ cdp.on("Network.loadingFinished", onRequestEnd);
1082
+ cdp.on("Network.loadingFailed", onRequestEnd);
1083
+ checkIdle();
1084
+ });
1085
+ }
1086
+
1087
+ // src/browser/types.ts
1088
+ var ElementNotFoundError = class extends Error {
1089
+ selectors;
1090
+ constructor(selectors) {
1091
+ const selectorList = Array.isArray(selectors) ? selectors : [selectors];
1092
+ super(`Element not found: ${selectorList.join(", ")}`);
1093
+ this.name = "ElementNotFoundError";
1094
+ this.selectors = selectorList;
1095
+ }
1096
+ };
1097
+ var TimeoutError = class extends Error {
1098
+ constructor(message = "Operation timed out") {
1099
+ super(message);
1100
+ this.name = "TimeoutError";
1101
+ }
1102
+ };
1103
+ var NavigationError = class extends Error {
1104
+ constructor(message) {
1105
+ super(message);
1106
+ this.name = "NavigationError";
1107
+ }
1108
+ };
1109
+
1110
+ // src/browser/page.ts
1111
+ var DEFAULT_TIMEOUT2 = 3e4;
1112
+ var Page = class {
1113
+ cdp;
1114
+ rootNodeId = null;
1115
+ batchExecutor;
1116
+ emulationState = {};
1117
+ interceptor = null;
1118
+ consoleHandlers = /* @__PURE__ */ new Set();
1119
+ errorHandlers = /* @__PURE__ */ new Set();
1120
+ dialogHandler = null;
1121
+ consoleEnabled = false;
1122
+ /** Map of ref (e.g., "e4") to backendNodeId for ref-based selectors */
1123
+ refMap = /* @__PURE__ */ new Map();
1124
+ /** Current frame context (null = main frame) */
1125
+ currentFrame = null;
1126
+ /** Stored frame document node IDs for context switching */
1127
+ frameContexts = /* @__PURE__ */ new Map();
1128
+ /** Map of frameId → executionContextId for JS evaluation in frames */
1129
+ frameExecutionContexts = /* @__PURE__ */ new Map();
1130
+ /** Current frame's execution context ID (null = main frame default) */
1131
+ currentFrameContextId = null;
1132
+ constructor(cdp) {
1133
+ this.cdp = cdp;
1134
+ this.batchExecutor = new BatchExecutor(this);
1135
+ }
1136
+ /**
1137
+ * Initialize the page (enable required CDP domains)
1138
+ */
1139
+ async init() {
1140
+ this.cdp.on("Runtime.executionContextCreated", (params) => {
1141
+ const context = params["context"];
1142
+ if (context.auxData?.frameId && context.auxData?.isDefault) {
1143
+ this.frameExecutionContexts.set(context.auxData.frameId, context.id);
1144
+ }
1145
+ });
1146
+ this.cdp.on("Runtime.executionContextDestroyed", (params) => {
1147
+ const contextId = params["executionContextId"];
1148
+ for (const [frameId, ctxId] of this.frameExecutionContexts.entries()) {
1149
+ if (ctxId === contextId) {
1150
+ this.frameExecutionContexts.delete(frameId);
1151
+ break;
1152
+ }
1153
+ }
1154
+ });
1155
+ this.cdp.on("Page.javascriptDialogOpening", this.handleDialogOpening.bind(this));
1156
+ await Promise.all([
1157
+ this.cdp.send("Page.enable"),
1158
+ this.cdp.send("DOM.enable"),
1159
+ this.cdp.send("Runtime.enable"),
1160
+ this.cdp.send("Network.enable")
1161
+ ]);
1162
+ }
1163
+ // ============ Navigation ============
1164
+ /**
1165
+ * Navigate to a URL
1166
+ */
1167
+ async goto(url, options = {}) {
1168
+ const { timeout = DEFAULT_TIMEOUT2 } = options;
1169
+ const navPromise = this.waitForNavigation({ timeout });
1170
+ await this.cdp.send("Page.navigate", { url });
1171
+ const result = await navPromise;
1172
+ if (!result) {
1173
+ throw new TimeoutError(`Navigation to ${url} timed out after ${timeout}ms`);
1174
+ }
1175
+ this.rootNodeId = null;
1176
+ this.refMap.clear();
1177
+ }
1178
+ /**
1179
+ * Get the current URL
1180
+ */
1181
+ async url() {
1182
+ const result = await this.cdp.send("Runtime.evaluate", {
1183
+ expression: "location.href",
1184
+ returnByValue: true
1185
+ });
1186
+ return result.result.value;
1187
+ }
1188
+ /**
1189
+ * Get the page title
1190
+ */
1191
+ async title() {
1192
+ const result = await this.cdp.send("Runtime.evaluate", {
1193
+ expression: "document.title",
1194
+ returnByValue: true
1195
+ });
1196
+ return result.result.value;
1197
+ }
1198
+ /**
1199
+ * Reload the page
1200
+ */
1201
+ async reload(options = {}) {
1202
+ const { timeout = DEFAULT_TIMEOUT2 } = options;
1203
+ const navPromise = this.waitForNavigation({ timeout });
1204
+ await this.cdp.send("Page.reload");
1205
+ await navPromise;
1206
+ this.rootNodeId = null;
1207
+ this.refMap.clear();
1208
+ }
1209
+ /**
1210
+ * Go back in history
1211
+ */
1212
+ async goBack(options = {}) {
1213
+ const { timeout = DEFAULT_TIMEOUT2 } = options;
1214
+ const history = await this.cdp.send("Page.getNavigationHistory");
1215
+ if (history.currentIndex <= 0) {
1216
+ return;
1217
+ }
1218
+ const navPromise = this.waitForNavigation({ timeout });
1219
+ await this.cdp.send("Page.navigateToHistoryEntry", {
1220
+ entryId: history.entries[history.currentIndex - 1].id
1221
+ });
1222
+ await navPromise;
1223
+ this.rootNodeId = null;
1224
+ this.refMap.clear();
1225
+ }
1226
+ /**
1227
+ * Go forward in history
1228
+ */
1229
+ async goForward(options = {}) {
1230
+ const { timeout = DEFAULT_TIMEOUT2 } = options;
1231
+ const history = await this.cdp.send("Page.getNavigationHistory");
1232
+ if (history.currentIndex >= history.entries.length - 1) {
1233
+ return;
1234
+ }
1235
+ const navPromise = this.waitForNavigation({ timeout });
1236
+ await this.cdp.send("Page.navigateToHistoryEntry", {
1237
+ entryId: history.entries[history.currentIndex + 1].id
1238
+ });
1239
+ await navPromise;
1240
+ this.rootNodeId = null;
1241
+ this.refMap.clear();
1242
+ }
1243
+ // ============ Core Actions ============
1244
+ /**
1245
+ * Click an element (supports multi-selector)
1246
+ *
1247
+ * Uses CDP mouse events for regular elements. For form submit buttons,
1248
+ * uses dispatchEvent to reliably trigger form submission in headless Chrome.
1249
+ */
1250
+ async click(selector, options = {}) {
1251
+ return this.withStaleNodeRetry(async () => {
1252
+ const element = await this.findElement(selector, options);
1253
+ if (!element) {
1254
+ if (options.optional) return false;
1255
+ throw new ElementNotFoundError(selector);
1256
+ }
1257
+ await this.scrollIntoView(element.nodeId);
1258
+ const submitResult = await this.evaluateInFrame(
1259
+ `(() => {
1260
+ const el = document.querySelector(${JSON.stringify(element.selector)});
1261
+ if (!el) return { isSubmit: false };
1262
+
1263
+ // Check if this is a form submit button
1264
+ const isSubmitButton = (el instanceof HTMLButtonElement && (el.type === 'submit' || (el.form && el.type !== 'button'))) ||
1265
+ (el instanceof HTMLInputElement && el.type === 'submit');
1266
+
1267
+ if (isSubmitButton && el.form) {
1268
+ // Dispatch submit event directly - works reliably in headless Chrome
1269
+ el.form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
1270
+ return { isSubmit: true };
1271
+ }
1272
+ return { isSubmit: false };
1273
+ })()`
1274
+ );
1275
+ const isSubmit = submitResult.result.value?.isSubmit;
1276
+ if (!isSubmit) {
1277
+ await this.clickElement(element.nodeId);
1278
+ }
1279
+ return true;
1280
+ });
1281
+ }
1282
+ /**
1283
+ * Fill an input field (clears first by default)
1284
+ */
1285
+ async fill(selector, value, options = {}) {
1286
+ const { clear = true } = options;
1287
+ return this.withStaleNodeRetry(async () => {
1288
+ const element = await this.findElement(selector, options);
1289
+ if (!element) {
1290
+ if (options.optional) return false;
1291
+ throw new ElementNotFoundError(selector);
1292
+ }
1293
+ await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
1294
+ if (clear) {
1295
+ await this.evaluateInFrame(
1296
+ `(() => {
1297
+ const el = document.querySelector(${JSON.stringify(element.selector)});
1298
+ if (el) {
1299
+ el.value = '';
1300
+ el.dispatchEvent(new Event('input', { bubbles: true }));
1301
+ }
1302
+ })()`
1303
+ );
1304
+ }
1305
+ await this.cdp.send("Input.insertText", { text: value });
1306
+ await this.evaluateInFrame(
1307
+ `(() => {
1308
+ const el = document.querySelector(${JSON.stringify(element.selector)});
1309
+ if (el) {
1310
+ el.dispatchEvent(new Event('input', { bubbles: true }));
1311
+ el.dispatchEvent(new Event('change', { bubbles: true }));
1312
+ }
1313
+ })()`
1314
+ );
1315
+ return true;
1316
+ });
1317
+ }
1318
+ /**
1319
+ * Type text character by character (for autocomplete fields, etc.)
1320
+ */
1321
+ async type(selector, text, options = {}) {
1322
+ const { delay = 50 } = options;
1323
+ const element = await this.findElement(selector, options);
1324
+ if (!element) {
1325
+ if (options.optional) return false;
1326
+ throw new ElementNotFoundError(selector);
1327
+ }
1328
+ await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
1329
+ for (const char of text) {
1330
+ await this.cdp.send("Input.dispatchKeyEvent", {
1331
+ type: "keyDown",
1332
+ key: char,
1333
+ text: char
1334
+ });
1335
+ await this.cdp.send("Input.dispatchKeyEvent", {
1336
+ type: "keyUp",
1337
+ key: char
1338
+ });
1339
+ if (delay > 0) {
1340
+ await sleep2(delay);
1341
+ }
1342
+ }
1343
+ return true;
1344
+ }
1345
+ async select(selectorOrConfig, valueOrOptions, maybeOptions) {
1346
+ if (typeof selectorOrConfig === "object" && !Array.isArray(selectorOrConfig) && "trigger" in selectorOrConfig) {
1347
+ return this.selectCustom(selectorOrConfig, valueOrOptions);
1348
+ }
1349
+ const selector = selectorOrConfig;
1350
+ const value = valueOrOptions;
1351
+ const options = maybeOptions ?? {};
1352
+ const element = await this.findElement(selector, options);
1353
+ if (!element) {
1354
+ if (options.optional) return false;
1355
+ throw new ElementNotFoundError(selector);
1356
+ }
1357
+ const values = Array.isArray(value) ? value : [value];
1358
+ await this.cdp.send("Runtime.evaluate", {
1359
+ expression: `(() => {
1360
+ const el = document.querySelector(${JSON.stringify(element.selector)});
1361
+ if (!el || el.tagName !== 'SELECT') return false;
1362
+ const values = ${JSON.stringify(values)};
1363
+ for (const opt of el.options) {
1364
+ opt.selected = values.includes(opt.value) || values.includes(opt.text);
1365
+ }
1366
+ el.dispatchEvent(new Event('change', { bubbles: true }));
1367
+ return true;
1368
+ })()`,
1369
+ returnByValue: true
1370
+ });
1371
+ return true;
1372
+ }
1373
+ /**
1374
+ * Handle custom (non-native) select/dropdown components
1375
+ */
1376
+ async selectCustom(config, options = {}) {
1377
+ const { trigger, option, value, match = "text" } = config;
1378
+ await this.click(trigger, options);
1379
+ await sleep2(100);
1380
+ let optionSelector;
1381
+ const optionSelectors = Array.isArray(option) ? option : [option];
1382
+ if (match === "contains") {
1383
+ optionSelector = optionSelectors.map((s) => `${s}:has-text("${value}")`).join(", ");
1384
+ } else if (match === "value") {
1385
+ optionSelector = optionSelectors.map((s) => `${s}[data-value="${value}"], ${s}[value="${value}"]`).join(", ");
1386
+ } else {
1387
+ optionSelector = optionSelectors.map((s) => `${s}`).join(", ");
1388
+ }
1389
+ const result = await this.cdp.send("Runtime.evaluate", {
1390
+ expression: `(() => {
1391
+ const options = document.querySelectorAll(${JSON.stringify(optionSelector)});
1392
+ for (const opt of options) {
1393
+ const text = opt.textContent?.trim();
1394
+ if (${match === "text" ? `text === ${JSON.stringify(value)}` : match === "contains" ? `text?.includes(${JSON.stringify(value)})` : "true"}) {
1395
+ opt.click();
1396
+ return true;
1397
+ }
1398
+ }
1399
+ return false;
1400
+ })()`,
1401
+ returnByValue: true
1402
+ });
1403
+ if (!result.result.value) {
1404
+ if (options.optional) return false;
1405
+ throw new ElementNotFoundError(`Option with ${match} "${value}"`);
1406
+ }
1407
+ return true;
1408
+ }
1409
+ /**
1410
+ * Check a checkbox or radio button
1411
+ */
1412
+ async check(selector, options = {}) {
1413
+ const element = await this.findElement(selector, options);
1414
+ if (!element) {
1415
+ if (options.optional) return false;
1416
+ throw new ElementNotFoundError(selector);
1417
+ }
1418
+ const result = await this.cdp.send("Runtime.evaluate", {
1419
+ expression: `(() => {
1420
+ const el = document.querySelector(${JSON.stringify(element.selector)});
1421
+ if (!el) return false;
1422
+ if (!el.checked) el.click();
1423
+ return true;
1424
+ })()`,
1425
+ returnByValue: true
1426
+ });
1427
+ return result.result.value;
1428
+ }
1429
+ /**
1430
+ * Uncheck a checkbox
1431
+ */
1432
+ async uncheck(selector, options = {}) {
1433
+ const element = await this.findElement(selector, options);
1434
+ if (!element) {
1435
+ if (options.optional) return false;
1436
+ throw new ElementNotFoundError(selector);
1437
+ }
1438
+ const result = await this.cdp.send("Runtime.evaluate", {
1439
+ expression: `(() => {
1440
+ const el = document.querySelector(${JSON.stringify(element.selector)});
1441
+ if (!el) return false;
1442
+ if (el.checked) el.click();
1443
+ return true;
1444
+ })()`,
1445
+ returnByValue: true
1446
+ });
1447
+ return result.result.value;
1448
+ }
1449
+ /**
1450
+ * Submit a form (tries Enter key first, then click)
1451
+ *
1452
+ * Navigation waiting behavior:
1453
+ * - 'auto' (default): Attempt to detect navigation for 1 second, then assume client-side handling
1454
+ * - true: Wait for full navigation (traditional forms)
1455
+ * - false: Return immediately (AJAX forms where you'll wait for something else)
1456
+ */
1457
+ async submit(selector, options = {}) {
1458
+ const { method = "enter+click", waitForNavigation: shouldWait = "auto" } = options;
1459
+ const element = await this.findElement(selector, options);
1460
+ if (!element) {
1461
+ if (options.optional) return false;
1462
+ throw new ElementNotFoundError(selector);
1463
+ }
1464
+ await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
1465
+ if (method.includes("enter")) {
1466
+ await this.press("Enter");
1467
+ if (shouldWait === true) {
1468
+ try {
1469
+ await this.waitForNavigation({ timeout: options.timeout ?? DEFAULT_TIMEOUT2 });
1470
+ return true;
1471
+ } catch {
1472
+ }
1473
+ } else if (shouldWait === "auto") {
1474
+ const navigationDetected = await Promise.race([
1475
+ this.waitForNavigation({ timeout: 1e3, optional: true }).then(
1476
+ (success) => success ? "nav" : null
1477
+ ),
1478
+ sleep2(500).then(() => "timeout")
1479
+ ]);
1480
+ if (navigationDetected === "nav") {
1481
+ return true;
1482
+ }
1483
+ } else {
1484
+ if (method === "enter") return true;
1485
+ }
1486
+ }
1487
+ if (method.includes("click")) {
1488
+ await this.click(element.selector, { ...options, optional: false });
1489
+ if (shouldWait === true) {
1490
+ await this.waitForNavigation({ timeout: options.timeout ?? DEFAULT_TIMEOUT2 });
1491
+ } else if (shouldWait === "auto") {
1492
+ await sleep2(100);
1493
+ }
1494
+ }
1495
+ return true;
1496
+ }
1497
+ /**
1498
+ * Press a key
1499
+ */
1500
+ async press(key) {
1501
+ const keyMap = {
1502
+ Enter: { key: "Enter", code: "Enter", keyCode: 13 },
1503
+ Tab: { key: "Tab", code: "Tab", keyCode: 9 },
1504
+ Escape: { key: "Escape", code: "Escape", keyCode: 27 },
1505
+ Backspace: { key: "Backspace", code: "Backspace", keyCode: 8 },
1506
+ Delete: { key: "Delete", code: "Delete", keyCode: 46 },
1507
+ ArrowUp: { key: "ArrowUp", code: "ArrowUp", keyCode: 38 },
1508
+ ArrowDown: { key: "ArrowDown", code: "ArrowDown", keyCode: 40 },
1509
+ ArrowLeft: { key: "ArrowLeft", code: "ArrowLeft", keyCode: 37 },
1510
+ ArrowRight: { key: "ArrowRight", code: "ArrowRight", keyCode: 39 }
1511
+ };
1512
+ const keyInfo = keyMap[key] ?? { key, code: key, keyCode: 0 };
1513
+ await this.cdp.send("Input.dispatchKeyEvent", {
1514
+ type: "keyDown",
1515
+ key: keyInfo.key,
1516
+ code: keyInfo.code,
1517
+ windowsVirtualKeyCode: keyInfo.keyCode
1518
+ });
1519
+ await this.cdp.send("Input.dispatchKeyEvent", {
1520
+ type: "keyUp",
1521
+ key: keyInfo.key,
1522
+ code: keyInfo.code,
1523
+ windowsVirtualKeyCode: keyInfo.keyCode
1524
+ });
1525
+ }
1526
+ /**
1527
+ * Focus an element
1528
+ */
1529
+ async focus(selector, options = {}) {
1530
+ const element = await this.findElement(selector, options);
1531
+ if (!element) {
1532
+ if (options.optional) return false;
1533
+ throw new ElementNotFoundError(selector);
1534
+ }
1535
+ await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
1536
+ return true;
1537
+ }
1538
+ /**
1539
+ * Hover over an element
1540
+ */
1541
+ async hover(selector, options = {}) {
1542
+ return this.withStaleNodeRetry(async () => {
1543
+ const element = await this.findElement(selector, options);
1544
+ if (!element) {
1545
+ if (options.optional) return false;
1546
+ throw new ElementNotFoundError(selector);
1547
+ }
1548
+ await this.scrollIntoView(element.nodeId);
1549
+ const box = await this.getBoxModel(element.nodeId);
1550
+ if (!box) {
1551
+ if (options.optional) return false;
1552
+ throw new Error("Could not get element box model");
1553
+ }
1554
+ const x = box.content[0] + box.width / 2;
1555
+ const y = box.content[1] + box.height / 2;
1556
+ await this.cdp.send("Input.dispatchMouseEvent", {
1557
+ type: "mouseMoved",
1558
+ x,
1559
+ y
1560
+ });
1561
+ return true;
1562
+ });
1563
+ }
1564
+ /**
1565
+ * Scroll an element into view (or scroll to coordinates)
1566
+ */
1567
+ async scroll(selector, options = {}) {
1568
+ const { x, y } = options;
1569
+ if (x !== void 0 || y !== void 0) {
1570
+ await this.cdp.send("Runtime.evaluate", {
1571
+ expression: `window.scrollTo(${x ?? 0}, ${y ?? 0})`
1572
+ });
1573
+ return true;
1574
+ }
1575
+ const element = await this.findElement(selector, options);
1576
+ if (!element) {
1577
+ if (options.optional) return false;
1578
+ throw new ElementNotFoundError(selector);
1579
+ }
1580
+ await this.scrollIntoView(element.nodeId);
1581
+ return true;
1582
+ }
1583
+ // ============ Frame Navigation ============
1584
+ /**
1585
+ * Switch context to an iframe for subsequent actions
1586
+ * @param selector - Selector for the iframe element
1587
+ * @param options - Optional timeout and optional flags
1588
+ * @returns true if switch succeeded
1589
+ */
1590
+ async switchToFrame(selector, options = {}) {
1591
+ const element = await this.findElement(selector, options);
1592
+ if (!element) {
1593
+ if (options.optional) return false;
1594
+ throw new ElementNotFoundError(selector);
1595
+ }
1596
+ const descResult = await this.cdp.send("DOM.describeNode", {
1597
+ nodeId: element.nodeId,
1598
+ depth: 1
1599
+ });
1600
+ if (!descResult.node.contentDocument) {
1601
+ if (options.optional) return false;
1602
+ throw new Error(
1603
+ "Cannot access iframe content. This may be a cross-origin iframe which requires different handling."
1604
+ );
1605
+ }
1606
+ const frameKey = Array.isArray(selector) ? selector[0] : selector;
1607
+ this.frameContexts.set(frameKey, descResult.node.contentDocument.nodeId);
1608
+ this.currentFrame = frameKey;
1609
+ this.rootNodeId = descResult.node.contentDocument.nodeId;
1610
+ if (descResult.node.frameId) {
1611
+ let contextId = this.frameExecutionContexts.get(descResult.node.frameId);
1612
+ if (!contextId) {
1613
+ await new Promise((resolve) => setTimeout(resolve, 50));
1614
+ contextId = this.frameExecutionContexts.get(descResult.node.frameId);
1615
+ }
1616
+ if (contextId) {
1617
+ this.currentFrameContextId = contextId;
1618
+ }
1619
+ }
1620
+ this.refMap.clear();
1621
+ return true;
1622
+ }
1623
+ /**
1624
+ * Switch back to the main document from an iframe
1625
+ */
1626
+ async switchToMain() {
1627
+ this.currentFrame = null;
1628
+ this.rootNodeId = null;
1629
+ this.currentFrameContextId = null;
1630
+ this.refMap.clear();
1631
+ }
1632
+ /**
1633
+ * Get the current frame context (null = main frame)
1634
+ */
1635
+ getCurrentFrame() {
1636
+ return this.currentFrame;
1637
+ }
1638
+ // ============ Waiting ============
1639
+ /**
1640
+ * Wait for an element to reach a state
1641
+ */
1642
+ async waitFor(selector, options = {}) {
1643
+ const { timeout = DEFAULT_TIMEOUT2, state = "visible" } = options;
1644
+ const selectors = Array.isArray(selector) ? selector : [selector];
1645
+ const result = await waitForAnyElement(this.cdp, selectors, {
1646
+ state,
1647
+ timeout,
1648
+ contextId: this.currentFrameContextId ?? void 0
1649
+ });
1650
+ if (!result.success && !options.optional) {
1651
+ throw new TimeoutError(`Timeout waiting for ${selectors.join(" or ")} to be ${state}`);
1652
+ }
1653
+ return result.success;
1654
+ }
1655
+ /**
1656
+ * Wait for navigation to complete
1657
+ */
1658
+ async waitForNavigation(options = {}) {
1659
+ const { timeout = DEFAULT_TIMEOUT2 } = options;
1660
+ const result = await waitForNavigation(this.cdp, { timeout });
1661
+ if (!result.success && !options.optional) {
1662
+ throw new TimeoutError("Navigation timeout");
1663
+ }
1664
+ this.rootNodeId = null;
1665
+ this.refMap.clear();
1666
+ return result.success;
1667
+ }
1668
+ /**
1669
+ * Wait for network to be idle
1670
+ */
1671
+ async waitForNetworkIdle(options = {}) {
1672
+ const { timeout = DEFAULT_TIMEOUT2, idleTime = 500 } = options;
1673
+ const result = await waitForNetworkIdle(this.cdp, { timeout, idleTime });
1674
+ if (!result.success && !options.optional) {
1675
+ throw new TimeoutError("Network idle timeout");
1676
+ }
1677
+ return result.success;
1678
+ }
1679
+ // ============ JavaScript Execution ============
1680
+ /**
1681
+ * Evaluate JavaScript in the page context (or current frame context if in iframe)
1682
+ */
1683
+ async evaluate(expression, ...args) {
1684
+ let script;
1685
+ if (typeof expression === "function") {
1686
+ const argString = args.map((a) => JSON.stringify(a)).join(", ");
1687
+ script = `(${expression.toString()})(${argString})`;
1688
+ } else {
1689
+ script = expression;
1690
+ }
1691
+ const params = {
1692
+ expression: script,
1693
+ returnByValue: true,
1694
+ awaitPromise: true
1695
+ };
1696
+ if (this.currentFrameContextId !== null) {
1697
+ params["contextId"] = this.currentFrameContextId;
1698
+ }
1699
+ const result = await this.cdp.send("Runtime.evaluate", params);
1700
+ if (result.exceptionDetails) {
1701
+ throw new Error(`Evaluation failed: ${result.exceptionDetails.text}`);
1702
+ }
1703
+ return result.result.value;
1704
+ }
1705
+ // ============ Screenshots ============
1706
+ /**
1707
+ * Take a screenshot
1708
+ */
1709
+ async screenshot(options = {}) {
1710
+ const { format = "png", quality, fullPage = false } = options;
1711
+ let clip;
1712
+ if (fullPage) {
1713
+ const metrics = await this.cdp.send("Page.getLayoutMetrics");
1714
+ clip = {
1715
+ x: 0,
1716
+ y: 0,
1717
+ width: metrics.contentSize.width,
1718
+ height: metrics.contentSize.height,
1719
+ scale: 1
1720
+ };
1721
+ }
1722
+ const result = await this.cdp.send("Page.captureScreenshot", {
1723
+ format,
1724
+ quality: format === "png" ? void 0 : quality,
1725
+ clip,
1726
+ captureBeyondViewport: fullPage
1727
+ });
1728
+ return result.data;
1729
+ }
1730
+ // ============ Text Extraction ============
1731
+ /**
1732
+ * Get text content from the page or a specific element
1733
+ */
1734
+ async text(selector) {
1735
+ const expression = selector ? `document.querySelector(${JSON.stringify(selector)})?.innerText ?? ''` : "document.body.innerText";
1736
+ const result = await this.evaluateInFrame(expression);
1737
+ return result.result.value ?? "";
1738
+ }
1739
+ // ============ File Handling ============
1740
+ /**
1741
+ * Set files on a file input
1742
+ */
1743
+ async setInputFiles(selector, files, options = {}) {
1744
+ const element = await this.findElement(selector, options);
1745
+ if (!element) {
1746
+ if (options.optional) return false;
1747
+ throw new ElementNotFoundError(selector);
1748
+ }
1749
+ const fileData = await Promise.all(
1750
+ files.map(async (f) => {
1751
+ let base64;
1752
+ if (typeof f.buffer === "string") {
1753
+ base64 = f.buffer;
1754
+ } else {
1755
+ const bytes = new Uint8Array(f.buffer);
1756
+ base64 = btoa(String.fromCharCode(...bytes));
1757
+ }
1758
+ return { name: f.name, mimeType: f.mimeType, data: base64 };
1759
+ })
1760
+ );
1761
+ await this.cdp.send("Runtime.evaluate", {
1762
+ expression: `(() => {
1763
+ const input = document.querySelector(${JSON.stringify(element.selector)});
1764
+ if (!input) return false;
1765
+
1766
+ const files = ${JSON.stringify(fileData)};
1767
+ const dt = new DataTransfer();
1768
+
1769
+ for (const f of files) {
1770
+ const bytes = Uint8Array.from(atob(f.data), c => c.charCodeAt(0));
1771
+ const file = new File([bytes], f.name, { type: f.mimeType });
1772
+ dt.items.add(file);
1773
+ }
1774
+
1775
+ input.files = dt.files;
1776
+ input.dispatchEvent(new Event('change', { bubbles: true }));
1777
+ return true;
1778
+ })()`,
1779
+ returnByValue: true
1780
+ });
1781
+ return true;
1782
+ }
1783
+ /**
1784
+ * Wait for a download to complete, triggered by an action
1785
+ */
1786
+ async waitForDownload(trigger, options = {}) {
1787
+ const { timeout = DEFAULT_TIMEOUT2 } = options;
1788
+ await this.cdp.send("Browser.setDownloadBehavior", {
1789
+ behavior: "allowAndName",
1790
+ eventsEnabled: true
1791
+ });
1792
+ return new Promise((resolve, reject) => {
1793
+ let downloadGuid;
1794
+ let suggestedFilename;
1795
+ let resolved = false;
1796
+ const timeoutTimer = setTimeout(() => {
1797
+ if (!resolved) {
1798
+ cleanup();
1799
+ reject(new TimeoutError(`Download timed out after ${timeout}ms`));
1800
+ }
1801
+ }, timeout);
1802
+ const onDownloadWillBegin = (params) => {
1803
+ downloadGuid = params["guid"];
1804
+ suggestedFilename = params["suggestedFilename"];
1805
+ };
1806
+ const onDownloadProgress = (params) => {
1807
+ if (params["guid"] === downloadGuid && params["state"] === "completed") {
1808
+ resolved = true;
1809
+ cleanup();
1810
+ const download = {
1811
+ filename: suggestedFilename ?? "unknown",
1812
+ content: async () => {
1813
+ return new ArrayBuffer(0);
1814
+ }
1815
+ };
1816
+ resolve(download);
1817
+ } else if (params["guid"] === downloadGuid && params["state"] === "canceled") {
1818
+ resolved = true;
1819
+ cleanup();
1820
+ reject(new Error("Download was canceled"));
1821
+ }
1822
+ };
1823
+ const cleanup = () => {
1824
+ clearTimeout(timeoutTimer);
1825
+ this.cdp.off("Browser.downloadWillBegin", onDownloadWillBegin);
1826
+ this.cdp.off("Browser.downloadProgress", onDownloadProgress);
1827
+ };
1828
+ this.cdp.on("Browser.downloadWillBegin", onDownloadWillBegin);
1829
+ this.cdp.on("Browser.downloadProgress", onDownloadProgress);
1830
+ trigger().catch((err) => {
1831
+ if (!resolved) {
1832
+ resolved = true;
1833
+ cleanup();
1834
+ reject(err);
1835
+ }
1836
+ });
1837
+ });
1838
+ }
1839
+ // ============ Snapshot ============
1840
+ /**
1841
+ * Get an accessibility tree snapshot of the page
1842
+ */
1843
+ async snapshot() {
1844
+ const [url, title, axTree] = await Promise.all([
1845
+ this.url(),
1846
+ this.title(),
1847
+ this.cdp.send("Accessibility.getFullAXTree")
1848
+ ]);
1849
+ const nodes = axTree.nodes.filter((n) => !n.ignored);
1850
+ const nodeMap = new Map(nodes.map((n) => [n.nodeId, n]));
1851
+ let refCounter = 0;
1852
+ const nodeRefs = /* @__PURE__ */ new Map();
1853
+ this.refMap.clear();
1854
+ for (const node of nodes) {
1855
+ const ref = `e${++refCounter}`;
1856
+ nodeRefs.set(node.nodeId, ref);
1857
+ if (node.backendDOMNodeId !== void 0) {
1858
+ this.refMap.set(ref, node.backendDOMNodeId);
1859
+ }
1860
+ }
1861
+ const buildNode = (nodeId) => {
1862
+ const node = nodeMap.get(nodeId);
1863
+ if (!node) return null;
1864
+ const role = node.role?.value ?? "generic";
1865
+ const name = node.name?.value;
1866
+ const value = node.value?.value;
1867
+ const ref = nodeRefs.get(nodeId);
1868
+ const children = [];
1869
+ if (node.childIds) {
1870
+ for (const childId of node.childIds) {
1871
+ const child = buildNode(childId);
1872
+ if (child) children.push(child);
1873
+ }
1874
+ }
1875
+ const disabled = node.properties?.find((p) => p.name === "disabled")?.value.value;
1876
+ const checked = node.properties?.find((p) => p.name === "checked")?.value.value;
1877
+ return {
1878
+ role,
1879
+ name,
1880
+ value,
1881
+ ref,
1882
+ children: children.length > 0 ? children : void 0,
1883
+ disabled,
1884
+ checked
1885
+ };
1886
+ };
1887
+ const rootNodes = nodes.filter((n) => !n.parentId || !nodeMap.has(n.parentId));
1888
+ const accessibilityTree = rootNodes.map((n) => buildNode(n.nodeId)).filter((n) => n !== null);
1889
+ const interactiveRoles = /* @__PURE__ */ new Set([
1890
+ "button",
1891
+ "link",
1892
+ "textbox",
1893
+ "checkbox",
1894
+ "radio",
1895
+ "combobox",
1896
+ "listbox",
1897
+ "menuitem",
1898
+ "menuitemcheckbox",
1899
+ "menuitemradio",
1900
+ "option",
1901
+ "searchbox",
1902
+ "slider",
1903
+ "spinbutton",
1904
+ "switch",
1905
+ "tab",
1906
+ "treeitem"
1907
+ ]);
1908
+ const interactiveElements = [];
1909
+ for (const node of nodes) {
1910
+ const role = node.role?.value;
1911
+ if (role && interactiveRoles.has(role)) {
1912
+ const ref = nodeRefs.get(node.nodeId);
1913
+ const name = node.name?.value ?? "";
1914
+ const disabled = node.properties?.find((p) => p.name === "disabled")?.value.value;
1915
+ const selector = node.backendDOMNodeId ? `[data-backend-node-id="${node.backendDOMNodeId}"]` : `[aria-label="${name}"]`;
1916
+ interactiveElements.push({
1917
+ ref,
1918
+ role,
1919
+ name,
1920
+ selector,
1921
+ disabled
1922
+ });
1923
+ }
1924
+ }
1925
+ const formatTree = (nodes2, depth = 0) => {
1926
+ const lines = [];
1927
+ for (const node of nodes2) {
1928
+ let line = `${" ".repeat(depth)}- ${node.role}`;
1929
+ if (node.name) line += ` "${node.name}"`;
1930
+ line += ` [ref=${node.ref}]`;
1931
+ if (node.disabled) line += " (disabled)";
1932
+ if (node.checked !== void 0) line += node.checked ? " (checked)" : " (unchecked)";
1933
+ lines.push(line);
1934
+ if (node.children) {
1935
+ lines.push(formatTree(node.children, depth + 1));
1936
+ }
1937
+ }
1938
+ return lines.join("\n");
1939
+ };
1940
+ const text = formatTree(accessibilityTree);
1941
+ return {
1942
+ url,
1943
+ title,
1944
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1945
+ accessibilityTree,
1946
+ interactiveElements,
1947
+ text
1948
+ };
1949
+ }
1950
+ // ============ Batch Execution ============
1951
+ /**
1952
+ * Execute a batch of steps
1953
+ */
1954
+ async batch(steps, options) {
1955
+ return this.batchExecutor.execute(steps, options);
1956
+ }
1957
+ // ============ Emulation ============
1958
+ /**
1959
+ * Set the viewport size and device metrics
1960
+ */
1961
+ async setViewport(options) {
1962
+ const {
1963
+ width,
1964
+ height,
1965
+ deviceScaleFactor = 1,
1966
+ isMobile = false,
1967
+ hasTouch = false,
1968
+ isLandscape = false
1969
+ } = options;
1970
+ await this.cdp.send("Emulation.setDeviceMetricsOverride", {
1971
+ width,
1972
+ height,
1973
+ deviceScaleFactor,
1974
+ mobile: isMobile,
1975
+ screenWidth: width,
1976
+ screenHeight: height,
1977
+ screenOrientation: {
1978
+ type: isLandscape ? "landscapePrimary" : "portraitPrimary",
1979
+ angle: isLandscape ? 90 : 0
1980
+ }
1981
+ });
1982
+ if (hasTouch) {
1983
+ await this.cdp.send("Emulation.setTouchEmulationEnabled", {
1984
+ enabled: true,
1985
+ maxTouchPoints: 5
1986
+ });
1987
+ }
1988
+ this.emulationState.viewport = options;
1989
+ }
1990
+ /**
1991
+ * Clear viewport override, return to default
1992
+ */
1993
+ async clearViewport() {
1994
+ await this.cdp.send("Emulation.clearDeviceMetricsOverride");
1995
+ await this.cdp.send("Emulation.setTouchEmulationEnabled", { enabled: false });
1996
+ this.emulationState.viewport = void 0;
1997
+ }
1998
+ /**
1999
+ * Set the user agent string and optional metadata
2000
+ */
2001
+ async setUserAgent(options) {
2002
+ const opts = typeof options === "string" ? { userAgent: options } : options;
2003
+ await this.cdp.send("Emulation.setUserAgentOverride", {
2004
+ userAgent: opts.userAgent,
2005
+ acceptLanguage: opts.acceptLanguage,
2006
+ platform: opts.platform,
2007
+ userAgentMetadata: opts.userAgentMetadata
2008
+ });
2009
+ this.emulationState.userAgent = opts;
2010
+ }
2011
+ /**
2012
+ * Set geolocation coordinates
2013
+ */
2014
+ async setGeolocation(options) {
2015
+ const { latitude, longitude, accuracy = 1 } = options;
2016
+ await this.cdp.send("Browser.grantPermissions", {
2017
+ permissions: ["geolocation"]
2018
+ });
2019
+ await this.cdp.send("Emulation.setGeolocationOverride", {
2020
+ latitude,
2021
+ longitude,
2022
+ accuracy
2023
+ });
2024
+ this.emulationState.geolocation = options;
2025
+ }
2026
+ /**
2027
+ * Clear geolocation override
2028
+ */
2029
+ async clearGeolocation() {
2030
+ await this.cdp.send("Emulation.clearGeolocationOverride");
2031
+ this.emulationState.geolocation = void 0;
2032
+ }
2033
+ /**
2034
+ * Set timezone override
2035
+ */
2036
+ async setTimezone(timezoneId) {
2037
+ await this.cdp.send("Emulation.setTimezoneOverride", { timezoneId });
2038
+ this.emulationState.timezone = timezoneId;
2039
+ }
2040
+ /**
2041
+ * Set locale override
2042
+ */
2043
+ async setLocale(locale) {
2044
+ await this.cdp.send("Emulation.setLocaleOverride", { locale });
2045
+ this.emulationState.locale = locale;
2046
+ }
2047
+ /**
2048
+ * Emulate a specific device
2049
+ */
2050
+ async emulate(device) {
2051
+ await this.setViewport(device.viewport);
2052
+ await this.setUserAgent(device.userAgent);
2053
+ }
2054
+ /**
2055
+ * Get current emulation state
2056
+ */
2057
+ getEmulationState() {
2058
+ return { ...this.emulationState };
2059
+ }
2060
+ // ============ Request Interception ============
2061
+ /**
2062
+ * Add request interception handler
2063
+ * @param pattern URL pattern or resource type to match
2064
+ * @param handler Handler function for matched requests
2065
+ * @returns Unsubscribe function
2066
+ */
2067
+ async intercept(pattern, handler) {
2068
+ if (!this.interceptor) {
2069
+ this.interceptor = new RequestInterceptor(this.cdp);
2070
+ await this.interceptor.enable();
2071
+ }
2072
+ const normalizedPattern = typeof pattern === "string" ? { urlPattern: pattern } : pattern;
2073
+ return this.interceptor.addHandler(normalizedPattern, handler);
2074
+ }
2075
+ /**
2076
+ * Route requests matching pattern to a mock response
2077
+ * Convenience wrapper around intercept()
2078
+ */
2079
+ async route(urlPattern, options) {
2080
+ return this.intercept({ urlPattern }, async (_request, actions) => {
2081
+ let body = options.body;
2082
+ const headers = { ...options.headers };
2083
+ if (typeof body === "object") {
2084
+ body = JSON.stringify(body);
2085
+ headers["content-type"] ??= "application/json";
2086
+ }
2087
+ if (options.contentType) {
2088
+ headers["content-type"] = options.contentType;
2089
+ }
2090
+ await actions.fulfill({
2091
+ status: options.status ?? 200,
2092
+ headers,
2093
+ body
2094
+ });
2095
+ });
2096
+ }
2097
+ /**
2098
+ * Block requests matching resource types
2099
+ */
2100
+ async blockResources(types) {
2101
+ return this.intercept({}, async (request, actions) => {
2102
+ if (types.includes(request.resourceType)) {
2103
+ await actions.fail({ reason: "BlockedByClient" });
2104
+ } else {
2105
+ await actions.continue();
2106
+ }
2107
+ });
2108
+ }
2109
+ /**
2110
+ * Disable all request interception
2111
+ */
2112
+ async disableInterception() {
2113
+ if (this.interceptor) {
2114
+ await this.interceptor.disable();
2115
+ this.interceptor = null;
2116
+ }
2117
+ }
2118
+ // ============ Cookies & Storage ============
2119
+ /**
2120
+ * Get all cookies for the current page
2121
+ */
2122
+ async cookies(urls) {
2123
+ const targetUrls = urls ?? [await this.url()];
2124
+ const result = await this.cdp.send("Network.getCookies", {
2125
+ urls: targetUrls
2126
+ });
2127
+ return result.cookies;
2128
+ }
2129
+ /**
2130
+ * Set a cookie
2131
+ */
2132
+ async setCookie(options) {
2133
+ const { name, value, domain, path = "/", expires, httpOnly, secure, sameSite, url } = options;
2134
+ let expireTime;
2135
+ if (expires instanceof Date) {
2136
+ expireTime = Math.floor(expires.getTime() / 1e3);
2137
+ } else if (typeof expires === "number") {
2138
+ expireTime = expires;
2139
+ }
2140
+ const result = await this.cdp.send("Network.setCookie", {
2141
+ name,
2142
+ value,
2143
+ domain,
2144
+ path,
2145
+ expires: expireTime,
2146
+ httpOnly,
2147
+ secure,
2148
+ sameSite,
2149
+ url: url ?? (domain ? void 0 : await this.url())
2150
+ });
2151
+ return result.success;
2152
+ }
2153
+ /**
2154
+ * Set multiple cookies
2155
+ */
2156
+ async setCookies(cookies) {
2157
+ for (const cookie of cookies) {
2158
+ await this.setCookie(cookie);
2159
+ }
2160
+ }
2161
+ /**
2162
+ * Delete a specific cookie
2163
+ */
2164
+ async deleteCookie(options) {
2165
+ const { name, domain, path, url } = options;
2166
+ await this.cdp.send("Network.deleteCookies", {
2167
+ name,
2168
+ domain,
2169
+ path,
2170
+ url: url ?? (domain ? void 0 : await this.url())
2171
+ });
2172
+ }
2173
+ /**
2174
+ * Delete multiple cookies
2175
+ */
2176
+ async deleteCookies(cookies) {
2177
+ for (const cookie of cookies) {
2178
+ await this.deleteCookie(cookie);
2179
+ }
2180
+ }
2181
+ /**
2182
+ * Clear all cookies
2183
+ */
2184
+ async clearCookies(options) {
2185
+ if (options?.domain) {
2186
+ const domainCookies = await this.cookies([`https://${options.domain}`]);
2187
+ for (const cookie of domainCookies) {
2188
+ await this.deleteCookie({
2189
+ name: cookie.name,
2190
+ domain: cookie.domain,
2191
+ path: cookie.path
2192
+ });
2193
+ }
2194
+ } else {
2195
+ await this.cdp.send("Storage.clearCookies", {});
2196
+ }
2197
+ }
2198
+ /**
2199
+ * Get localStorage value
2200
+ */
2201
+ async getLocalStorage(key) {
2202
+ const result = await this.cdp.send("Runtime.evaluate", {
2203
+ expression: `localStorage.getItem(${JSON.stringify(key)})`,
2204
+ returnByValue: true
2205
+ });
2206
+ return result.result.value;
2207
+ }
2208
+ /**
2209
+ * Set localStorage value
2210
+ */
2211
+ async setLocalStorage(key, value) {
2212
+ await this.cdp.send("Runtime.evaluate", {
2213
+ expression: `localStorage.setItem(${JSON.stringify(key)}, ${JSON.stringify(value)})`
2214
+ });
2215
+ }
2216
+ /**
2217
+ * Remove localStorage item
2218
+ */
2219
+ async removeLocalStorage(key) {
2220
+ await this.cdp.send("Runtime.evaluate", {
2221
+ expression: `localStorage.removeItem(${JSON.stringify(key)})`
2222
+ });
2223
+ }
2224
+ /**
2225
+ * Clear localStorage
2226
+ */
2227
+ async clearLocalStorage() {
2228
+ await this.cdp.send("Runtime.evaluate", {
2229
+ expression: "localStorage.clear()"
2230
+ });
2231
+ }
2232
+ /**
2233
+ * Get sessionStorage value
2234
+ */
2235
+ async getSessionStorage(key) {
2236
+ const result = await this.cdp.send("Runtime.evaluate", {
2237
+ expression: `sessionStorage.getItem(${JSON.stringify(key)})`,
2238
+ returnByValue: true
2239
+ });
2240
+ return result.result.value;
2241
+ }
2242
+ /**
2243
+ * Set sessionStorage value
2244
+ */
2245
+ async setSessionStorage(key, value) {
2246
+ await this.cdp.send("Runtime.evaluate", {
2247
+ expression: `sessionStorage.setItem(${JSON.stringify(key)}, ${JSON.stringify(value)})`
2248
+ });
2249
+ }
2250
+ /**
2251
+ * Remove sessionStorage item
2252
+ */
2253
+ async removeSessionStorage(key) {
2254
+ await this.cdp.send("Runtime.evaluate", {
2255
+ expression: `sessionStorage.removeItem(${JSON.stringify(key)})`
2256
+ });
2257
+ }
2258
+ /**
2259
+ * Clear sessionStorage
2260
+ */
2261
+ async clearSessionStorage() {
2262
+ await this.cdp.send("Runtime.evaluate", {
2263
+ expression: "sessionStorage.clear()"
2264
+ });
2265
+ }
2266
+ // ============ Console & Errors ============
2267
+ /**
2268
+ * Enable console message capture
2269
+ */
2270
+ async enableConsole() {
2271
+ if (this.consoleEnabled) return;
2272
+ this.cdp.on("Runtime.consoleAPICalled", this.handleConsoleMessage.bind(this));
2273
+ this.cdp.on("Runtime.exceptionThrown", this.handleException.bind(this));
2274
+ this.consoleEnabled = true;
2275
+ }
2276
+ /**
2277
+ * Handle console API calls
2278
+ */
2279
+ handleConsoleMessage(params) {
2280
+ const args = params["args"];
2281
+ const stackTrace = params["stackTrace"];
2282
+ const message = {
2283
+ type: params["type"],
2284
+ text: this.formatConsoleArgs(args ?? []),
2285
+ args: args?.map((a) => a.value) ?? [],
2286
+ timestamp: params["timestamp"],
2287
+ stackTrace: stackTrace?.callFrames?.map((f) => `${f.url}:${f.lineNumber}`)
2288
+ };
2289
+ for (const handler of this.consoleHandlers) {
2290
+ try {
2291
+ handler(message);
2292
+ } catch (e) {
2293
+ console.error("[Console handler error]", e);
2294
+ }
2295
+ }
2296
+ }
2297
+ /**
2298
+ * Handle JavaScript exceptions
2299
+ */
2300
+ handleException(params) {
2301
+ const details = params["exceptionDetails"];
2302
+ const exception = details["exception"];
2303
+ const stackTrace = details["stackTrace"];
2304
+ const error = {
2305
+ message: exception?.description ?? details["text"],
2306
+ url: details["url"],
2307
+ lineNumber: details["lineNumber"],
2308
+ columnNumber: details["columnNumber"],
2309
+ timestamp: params["timestamp"],
2310
+ stackTrace: stackTrace?.callFrames?.map((f) => `${f.url}:${f.lineNumber}`)
2311
+ };
2312
+ for (const handler of this.errorHandlers) {
2313
+ try {
2314
+ handler(error);
2315
+ } catch (e) {
2316
+ console.error("[Error handler error]", e);
2317
+ }
2318
+ }
2319
+ }
2320
+ /**
2321
+ * Handle dialog opening
2322
+ */
2323
+ async handleDialogOpening(params) {
2324
+ const dialog = {
2325
+ type: params["type"],
2326
+ message: params["message"],
2327
+ defaultValue: params["defaultPrompt"],
2328
+ accept: async (promptText) => {
2329
+ await this.cdp.send("Page.handleJavaScriptDialog", {
2330
+ accept: true,
2331
+ promptText
2332
+ });
2333
+ },
2334
+ dismiss: async () => {
2335
+ await this.cdp.send("Page.handleJavaScriptDialog", {
2336
+ accept: false
2337
+ });
2338
+ }
2339
+ };
2340
+ if (this.dialogHandler) {
2341
+ try {
2342
+ await this.dialogHandler(dialog);
2343
+ } catch (e) {
2344
+ console.error("[Dialog handler error]", e);
2345
+ await dialog.dismiss();
2346
+ }
2347
+ } else {
2348
+ await dialog.dismiss();
2349
+ }
2350
+ }
2351
+ /**
2352
+ * Format console arguments to string
2353
+ */
2354
+ formatConsoleArgs(args) {
2355
+ return args.map((arg) => {
2356
+ if (arg.value !== void 0) return String(arg.value);
2357
+ if (arg.description) return arg.description;
2358
+ return "[object]";
2359
+ }).join(" ");
2360
+ }
2361
+ /**
2362
+ * Subscribe to console messages
2363
+ */
2364
+ async onConsole(handler) {
2365
+ await this.enableConsole();
2366
+ this.consoleHandlers.add(handler);
2367
+ return () => this.consoleHandlers.delete(handler);
2368
+ }
2369
+ /**
2370
+ * Subscribe to page errors
2371
+ */
2372
+ async onError(handler) {
2373
+ await this.enableConsole();
2374
+ this.errorHandlers.add(handler);
2375
+ return () => this.errorHandlers.delete(handler);
2376
+ }
2377
+ /**
2378
+ * Set dialog handler (only one at a time)
2379
+ */
2380
+ async onDialog(handler) {
2381
+ await this.enableConsole();
2382
+ this.dialogHandler = handler;
2383
+ }
2384
+ /**
2385
+ * Collect console messages during an action
2386
+ */
2387
+ async collectConsole(fn) {
2388
+ const messages = [];
2389
+ const unsubscribe = await this.onConsole((msg) => messages.push(msg));
2390
+ try {
2391
+ const result = await fn();
2392
+ return { result, messages };
2393
+ } finally {
2394
+ unsubscribe();
2395
+ }
2396
+ }
2397
+ /**
2398
+ * Collect errors during an action
2399
+ */
2400
+ async collectErrors(fn) {
2401
+ const errors = [];
2402
+ const unsubscribe = await this.onError((err) => errors.push(err));
2403
+ try {
2404
+ const result = await fn();
2405
+ return { result, errors };
2406
+ } finally {
2407
+ unsubscribe();
2408
+ }
2409
+ }
2410
+ // ============ Lifecycle ============
2411
+ /**
2412
+ * Reset page state for clean test isolation
2413
+ * - Stops any pending operations
2414
+ * - Clears localStorage and sessionStorage
2415
+ * - Resets internal state
2416
+ */
2417
+ async reset() {
2418
+ this.rootNodeId = null;
2419
+ this.refMap.clear();
2420
+ this.currentFrame = null;
2421
+ this.currentFrameContextId = null;
2422
+ this.frameContexts.clear();
2423
+ this.dialogHandler = null;
2424
+ try {
2425
+ await this.cdp.send("Page.stopLoading");
2426
+ } catch {
2427
+ }
2428
+ try {
2429
+ await this.cdp.send("Runtime.evaluate", {
2430
+ expression: `(() => {
2431
+ try { localStorage.clear(); } catch {}
2432
+ try { sessionStorage.clear(); } catch {}
2433
+ })()`
2434
+ });
2435
+ } catch {
2436
+ }
2437
+ }
2438
+ /**
2439
+ * Close this page (no-op for now, managed by Browser)
2440
+ * This is a placeholder for API compatibility
2441
+ */
2442
+ async close() {
2443
+ }
2444
+ // ============ Private Helpers ============
2445
+ /**
2446
+ * Retry wrapper for operations that may encounter stale nodes
2447
+ * Catches "Could not find node with given id" errors and retries
2448
+ */
2449
+ async withStaleNodeRetry(fn, options = {}) {
2450
+ const { retries = 2, delay = 50 } = options;
2451
+ let lastError;
2452
+ for (let attempt = 0; attempt <= retries; attempt++) {
2453
+ try {
2454
+ return await fn();
2455
+ } catch (e) {
2456
+ if (e instanceof Error && (e.message.includes("Could not find node with given id") || e.message.includes("Node with given id does not belong to the document") || e.message.includes("No node with given id found"))) {
2457
+ lastError = e;
2458
+ if (attempt < retries) {
2459
+ this.rootNodeId = null;
2460
+ await sleep2(delay);
2461
+ continue;
2462
+ }
2463
+ }
2464
+ throw e;
2465
+ }
2466
+ }
2467
+ throw lastError ?? new Error("Stale node retry exhausted");
2468
+ }
2469
+ /**
2470
+ * Find an element using single or multiple selectors
2471
+ * Supports ref: prefix for ref-based selectors (e.g., "ref:e4")
2472
+ */
2473
+ async findElement(selectors, options = {}) {
2474
+ const { timeout = DEFAULT_TIMEOUT2 } = options;
2475
+ const selectorList = Array.isArray(selectors) ? selectors : [selectors];
2476
+ for (const selector of selectorList) {
2477
+ if (selector.startsWith("ref:")) {
2478
+ const ref = selector.slice(4);
2479
+ const backendNodeId = this.refMap.get(ref);
2480
+ if (!backendNodeId) {
2481
+ continue;
2482
+ }
2483
+ try {
2484
+ await this.ensureRootNode();
2485
+ const pushResult = await this.cdp.send(
2486
+ "DOM.pushNodesByBackendIdsToFrontend",
2487
+ {
2488
+ backendNodeIds: [backendNodeId]
2489
+ }
2490
+ );
2491
+ if (pushResult.nodeIds?.[0]) {
2492
+ return {
2493
+ nodeId: pushResult.nodeIds[0],
2494
+ backendNodeId,
2495
+ selector,
2496
+ waitedMs: 0
2497
+ };
2498
+ }
2499
+ } catch {
2500
+ }
2501
+ }
2502
+ }
2503
+ const cssSelectors = selectorList.filter((s) => !s.startsWith("ref:"));
2504
+ if (cssSelectors.length === 0) {
2505
+ return null;
2506
+ }
2507
+ const result = await waitForAnyElement(this.cdp, cssSelectors, {
2508
+ state: "visible",
2509
+ timeout,
2510
+ contextId: this.currentFrameContextId ?? void 0
2511
+ });
2512
+ if (!result.success || !result.selector) {
2513
+ return null;
2514
+ }
2515
+ await this.ensureRootNode();
2516
+ const queryResult = await this.cdp.send("DOM.querySelector", {
2517
+ nodeId: this.rootNodeId,
2518
+ selector: result.selector
2519
+ });
2520
+ if (queryResult.nodeId) {
2521
+ const describeResult2 = await this.cdp.send(
2522
+ "DOM.describeNode",
2523
+ { nodeId: queryResult.nodeId }
2524
+ );
2525
+ return {
2526
+ nodeId: queryResult.nodeId,
2527
+ backendNodeId: describeResult2.node.backendNodeId,
2528
+ selector: result.selector,
2529
+ waitedMs: result.waitedMs
2530
+ };
2531
+ }
2532
+ const deepQueryResult = await this.evaluateInFrame(
2533
+ `(() => {
2534
+ ${DEEP_QUERY_SCRIPT}
2535
+ return deepQuery(${JSON.stringify(result.selector)});
2536
+ })()`,
2537
+ { returnByValue: false }
2538
+ );
2539
+ if (!deepQueryResult.result.objectId) {
2540
+ return null;
2541
+ }
2542
+ const nodeResult = await this.cdp.send("DOM.requestNode", {
2543
+ objectId: deepQueryResult.result.objectId
2544
+ });
2545
+ if (!nodeResult.nodeId) {
2546
+ return null;
2547
+ }
2548
+ const describeResult = await this.cdp.send(
2549
+ "DOM.describeNode",
2550
+ { nodeId: nodeResult.nodeId }
2551
+ );
2552
+ return {
2553
+ nodeId: nodeResult.nodeId,
2554
+ backendNodeId: describeResult.node.backendNodeId,
2555
+ selector: result.selector,
2556
+ waitedMs: result.waitedMs
2557
+ };
2558
+ }
2559
+ /**
2560
+ * Ensure we have a valid root node ID
2561
+ */
2562
+ async ensureRootNode() {
2563
+ if (this.rootNodeId) return;
2564
+ const doc = await this.cdp.send("DOM.getDocument", {
2565
+ depth: 0
2566
+ });
2567
+ this.rootNodeId = doc.root.nodeId;
2568
+ }
2569
+ /**
2570
+ * Execute Runtime.evaluate in the current frame context
2571
+ * Automatically injects contextId when in an iframe
2572
+ */
2573
+ async evaluateInFrame(expression, options = {}) {
2574
+ const params = {
2575
+ expression,
2576
+ returnByValue: options.returnByValue ?? true,
2577
+ awaitPromise: options.awaitPromise ?? false
2578
+ };
2579
+ if (this.currentFrameContextId !== null) {
2580
+ params["contextId"] = this.currentFrameContextId;
2581
+ }
2582
+ return this.cdp.send("Runtime.evaluate", params);
2583
+ }
2584
+ /**
2585
+ * Scroll an element into view
2586
+ */
2587
+ async scrollIntoView(nodeId) {
2588
+ await this.cdp.send("DOM.scrollIntoViewIfNeeded", { nodeId });
2589
+ }
2590
+ /**
2591
+ * Get element box model (position and dimensions)
2592
+ */
2593
+ async getBoxModel(nodeId) {
2594
+ try {
2595
+ const result = await this.cdp.send("DOM.getBoxModel", {
2596
+ nodeId
2597
+ });
2598
+ return result.model;
2599
+ } catch {
2600
+ return null;
2601
+ }
2602
+ }
2603
+ /**
2604
+ * Click an element by node ID
2605
+ */
2606
+ async clickElement(nodeId) {
2607
+ const box = await this.getBoxModel(nodeId);
2608
+ if (!box) {
2609
+ throw new Error("Could not get element box model for click");
2610
+ }
2611
+ const x = box.content[0] + box.width / 2;
2612
+ const y = box.content[1] + box.height / 2;
2613
+ await this.cdp.send("Input.dispatchMouseEvent", {
2614
+ type: "mousePressed",
2615
+ x,
2616
+ y,
2617
+ button: "left",
2618
+ clickCount: 1
2619
+ });
2620
+ await this.cdp.send("Input.dispatchMouseEvent", {
2621
+ type: "mouseReleased",
2622
+ x,
2623
+ y,
2624
+ button: "left",
2625
+ clickCount: 1
2626
+ });
2627
+ }
2628
+ };
2629
+ function sleep2(ms) {
2630
+ return new Promise((resolve) => setTimeout(resolve, ms));
2631
+ }
2632
+
2633
+ // src/browser/browser.ts
2634
+ var Browser = class _Browser {
2635
+ cdp;
2636
+ providerSession;
2637
+ pages = /* @__PURE__ */ new Map();
2638
+ constructor(cdp, _provider, providerSession, _options) {
2639
+ this.cdp = cdp;
2640
+ this.providerSession = providerSession;
2641
+ }
2642
+ /**
2643
+ * Connect to a browser instance
2644
+ */
2645
+ static async connect(options) {
2646
+ const provider = createProvider(options);
2647
+ const session = await provider.createSession(options.session);
2648
+ const cdp = await createCDPClient(session.wsUrl, {
2649
+ debug: options.debug,
2650
+ timeout: options.timeout
2651
+ });
2652
+ return new _Browser(cdp, provider, session, options);
2653
+ }
2654
+ /**
2655
+ * Get or create a page by name
2656
+ * If no name is provided, returns the first available page or creates a new one
2657
+ */
2658
+ async page(name) {
2659
+ const pageName = name ?? "default";
2660
+ const cached = this.pages.get(pageName);
2661
+ if (cached) return cached;
2662
+ const targets = await this.cdp.send("Target.getTargets");
2663
+ const pageTargets = targets.targetInfos.filter((t) => t.type === "page");
2664
+ let targetId;
2665
+ if (pageTargets.length > 0) {
2666
+ targetId = pageTargets[0].targetId;
2667
+ } else {
2668
+ const result = await this.cdp.send("Target.createTarget", {
2669
+ url: "about:blank"
2670
+ });
2671
+ targetId = result.targetId;
2672
+ }
2673
+ await this.cdp.attachToTarget(targetId);
2674
+ const page = new Page(this.cdp);
2675
+ await page.init();
2676
+ this.pages.set(pageName, page);
2677
+ return page;
2678
+ }
2679
+ /**
2680
+ * Create a new page (tab)
2681
+ */
2682
+ async newPage(url = "about:blank") {
2683
+ const result = await this.cdp.send("Target.createTarget", {
2684
+ url
2685
+ });
2686
+ await this.cdp.attachToTarget(result.targetId);
2687
+ const page = new Page(this.cdp);
2688
+ await page.init();
2689
+ const name = `page-${this.pages.size + 1}`;
2690
+ this.pages.set(name, page);
2691
+ return page;
2692
+ }
2693
+ /**
2694
+ * Close a page by name
2695
+ */
2696
+ async closePage(name) {
2697
+ const page = this.pages.get(name);
2698
+ if (!page) return;
2699
+ const targets = await this.cdp.send("Target.getTargets");
2700
+ const pageTargets = targets.targetInfos.filter((t) => t.type === "page");
2701
+ if (pageTargets.length > 0) {
2702
+ await this.cdp.send("Target.closeTarget", {
2703
+ targetId: pageTargets[0].targetId
2704
+ });
2705
+ }
2706
+ this.pages.delete(name);
2707
+ }
2708
+ /**
2709
+ * Get the WebSocket URL for this browser connection
2710
+ */
2711
+ get wsUrl() {
2712
+ return this.providerSession.wsUrl;
2713
+ }
2714
+ /**
2715
+ * Get the provider session ID (for resumption)
2716
+ */
2717
+ get sessionId() {
2718
+ return this.providerSession.sessionId;
2719
+ }
2720
+ /**
2721
+ * Get provider metadata
2722
+ */
2723
+ get metadata() {
2724
+ return this.providerSession.metadata;
2725
+ }
2726
+ /**
2727
+ * Check if connected
2728
+ */
2729
+ get isConnected() {
2730
+ return this.cdp.isConnected;
2731
+ }
2732
+ /**
2733
+ * Disconnect from the browser (keeps provider session alive for reconnection)
2734
+ */
2735
+ async disconnect() {
2736
+ this.pages.clear();
2737
+ await this.cdp.close();
2738
+ }
2739
+ /**
2740
+ * Close the browser session completely
2741
+ */
2742
+ async close() {
2743
+ this.pages.clear();
2744
+ await this.cdp.close();
2745
+ await this.providerSession.close();
2746
+ }
2747
+ /**
2748
+ * Get the underlying CDP client (for advanced usage)
2749
+ */
2750
+ get cdpClient() {
2751
+ return this.cdp;
2752
+ }
2753
+ };
2754
+ function connect(options) {
2755
+ return Browser.connect(options);
2756
+ }
2757
+ // Annotate the CommonJS export names for ESM import in node:
2758
+ 0 && (module.exports = {
2759
+ Browser,
2760
+ ElementNotFoundError,
2761
+ NavigationError,
2762
+ Page,
2763
+ TimeoutError,
2764
+ connect
2765
+ });