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