browser-pilot 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2108 @@
1
+ import {
2
+ createCDPClient
3
+ } from "./chunk-BCOZUKWS.mjs";
4
+ import {
5
+ createProvider
6
+ } from "./chunk-R3PS4PCM.mjs";
7
+ import {
8
+ BatchExecutor
9
+ } from "./chunk-YEHK2XY3.mjs";
10
+
11
+ // src/network/interceptor.ts
12
+ var RequestInterceptor = class {
13
+ cdp;
14
+ enabled = false;
15
+ handlers = [];
16
+ pendingRequests = /* @__PURE__ */ new Map();
17
+ boundHandleRequestPaused;
18
+ boundHandleAuthRequired;
19
+ constructor(cdp) {
20
+ this.cdp = cdp;
21
+ this.boundHandleRequestPaused = this.handleRequestPaused.bind(this);
22
+ this.boundHandleAuthRequired = this.handleAuthRequired.bind(this);
23
+ }
24
+ /**
25
+ * Enable request interception with optional patterns
26
+ */
27
+ async enable(patterns) {
28
+ if (this.enabled) return;
29
+ this.cdp.on("Fetch.requestPaused", this.boundHandleRequestPaused);
30
+ this.cdp.on("Fetch.authRequired", this.boundHandleAuthRequired);
31
+ await this.cdp.send("Fetch.enable", {
32
+ patterns: patterns?.map((p) => ({
33
+ urlPattern: p.urlPattern ?? "*",
34
+ resourceType: p.resourceType,
35
+ requestStage: p.requestStage ?? "Request"
36
+ })) ?? [{ urlPattern: "*" }],
37
+ handleAuthRequests: true
38
+ });
39
+ this.enabled = true;
40
+ }
41
+ /**
42
+ * Disable request interception
43
+ */
44
+ async disable() {
45
+ if (!this.enabled) return;
46
+ await this.cdp.send("Fetch.disable");
47
+ this.cdp.off("Fetch.requestPaused", this.boundHandleRequestPaused);
48
+ this.cdp.off("Fetch.authRequired", this.boundHandleAuthRequired);
49
+ this.enabled = false;
50
+ this.handlers = [];
51
+ this.pendingRequests.clear();
52
+ }
53
+ /**
54
+ * Add a request handler
55
+ */
56
+ addHandler(pattern, handler) {
57
+ const entry = { pattern, handler };
58
+ this.handlers.push(entry);
59
+ return () => {
60
+ const idx = this.handlers.indexOf(entry);
61
+ if (idx !== -1) this.handlers.splice(idx, 1);
62
+ };
63
+ }
64
+ /**
65
+ * Handle paused request from CDP
66
+ */
67
+ async handleRequestPaused(params) {
68
+ const requestId = params["requestId"];
69
+ const request = params["request"];
70
+ const responseStatusCode = params["responseStatusCode"];
71
+ const responseHeaders = params["responseHeaders"];
72
+ const intercepted = {
73
+ requestId,
74
+ url: request["url"],
75
+ method: request["method"],
76
+ headers: request["headers"],
77
+ postData: request["postData"],
78
+ resourceType: params["resourceType"],
79
+ frameId: params["frameId"],
80
+ isNavigationRequest: params["isNavigationRequest"],
81
+ responseStatusCode,
82
+ responseHeaders: responseHeaders ? Object.fromEntries(responseHeaders.map((h) => [h.name, h.value])) : void 0
83
+ };
84
+ this.pendingRequests.set(requestId, { request: intercepted, handled: false });
85
+ const matchingHandler = this.handlers.find((h) => this.matchesPattern(intercepted, h.pattern));
86
+ if (matchingHandler) {
87
+ const actions = this.createActions(requestId);
88
+ try {
89
+ await matchingHandler.handler(intercepted, actions);
90
+ } catch (err) {
91
+ console.error("[RequestInterceptor] Handler error:", err);
92
+ if (!this.pendingRequests.get(requestId)?.handled) {
93
+ await actions.continue();
94
+ }
95
+ }
96
+ } else {
97
+ await this.continueRequest(requestId);
98
+ }
99
+ this.pendingRequests.delete(requestId);
100
+ }
101
+ /**
102
+ * Handle auth challenge
103
+ */
104
+ async handleAuthRequired(params) {
105
+ const requestId = params["requestId"];
106
+ await this.cdp.send("Fetch.continueWithAuth", {
107
+ requestId,
108
+ authChallengeResponse: { response: "CancelAuth" }
109
+ });
110
+ }
111
+ /**
112
+ * Check if request matches pattern
113
+ */
114
+ matchesPattern(request, pattern) {
115
+ if (pattern.resourceType && request.resourceType !== pattern.resourceType) {
116
+ return false;
117
+ }
118
+ if (pattern.urlPattern) {
119
+ const regex = this.globToRegex(pattern.urlPattern);
120
+ if (!regex.test(request.url)) {
121
+ return false;
122
+ }
123
+ }
124
+ return true;
125
+ }
126
+ /**
127
+ * Convert glob pattern to regex
128
+ */
129
+ globToRegex(pattern) {
130
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
131
+ return new RegExp(`^${escaped}$`);
132
+ }
133
+ /**
134
+ * Create actions object for handler
135
+ */
136
+ createActions(requestId) {
137
+ const pending = this.pendingRequests.get(requestId);
138
+ const markHandled = () => {
139
+ if (pending) pending.handled = true;
140
+ };
141
+ return {
142
+ continue: async (options) => {
143
+ markHandled();
144
+ await this.continueRequest(requestId, options);
145
+ },
146
+ fulfill: async (options) => {
147
+ markHandled();
148
+ await this.fulfillRequest(requestId, options);
149
+ },
150
+ fail: async (options) => {
151
+ markHandled();
152
+ await this.failRequest(requestId, options);
153
+ }
154
+ };
155
+ }
156
+ /**
157
+ * Continue a paused request
158
+ */
159
+ async continueRequest(requestId, options) {
160
+ await this.cdp.send("Fetch.continueRequest", {
161
+ requestId,
162
+ url: options?.url,
163
+ method: options?.method,
164
+ headers: options?.headers ? Object.entries(options.headers).map(([name, value]) => ({ name, value })) : void 0,
165
+ postData: options?.postData ? btoa(options.postData) : void 0
166
+ });
167
+ }
168
+ /**
169
+ * Fulfill a request with custom response
170
+ */
171
+ async fulfillRequest(requestId, options) {
172
+ const headers = Object.entries(options.headers ?? {}).map(([name, value]) => ({
173
+ name,
174
+ value
175
+ }));
176
+ await this.cdp.send("Fetch.fulfillRequest", {
177
+ requestId,
178
+ responseCode: options.status,
179
+ responseHeaders: headers,
180
+ body: options.isBase64Encoded ? options.body : options.body ? btoa(options.body) : void 0
181
+ });
182
+ }
183
+ /**
184
+ * Fail/abort a request
185
+ */
186
+ async failRequest(requestId, options) {
187
+ await this.cdp.send("Fetch.failRequest", {
188
+ requestId,
189
+ errorReason: options?.reason ?? "BlockedByClient"
190
+ });
191
+ }
192
+ };
193
+
194
+ // src/wait/strategies.ts
195
+ var DEEP_QUERY_SCRIPT = `
196
+ function deepQuery(selector, root = document) {
197
+ // Try direct query first (fastest path)
198
+ let el = root.querySelector(selector);
199
+ if (el) return el;
200
+
201
+ // Search in shadow roots recursively
202
+ const searchShadows = (node) => {
203
+ // Check if this node has a shadow root
204
+ if (node.shadowRoot) {
205
+ el = node.shadowRoot.querySelector(selector);
206
+ if (el) return el;
207
+ // Search children of shadow root
208
+ for (const child of node.shadowRoot.querySelectorAll('*')) {
209
+ el = searchShadows(child);
210
+ if (el) return el;
211
+ }
212
+ }
213
+ // Search children that might have shadow roots
214
+ for (const child of node.querySelectorAll('*')) {
215
+ if (child.shadowRoot) {
216
+ el = searchShadows(child);
217
+ if (el) return el;
218
+ }
219
+ }
220
+ return null;
221
+ };
222
+
223
+ return searchShadows(root);
224
+ }
225
+ `;
226
+ async function isElementVisible(cdp, selector, contextId) {
227
+ const params = {
228
+ expression: `(() => {
229
+ ${DEEP_QUERY_SCRIPT}
230
+ const el = deepQuery(${JSON.stringify(selector)});
231
+ if (!el) return false;
232
+ const style = getComputedStyle(el);
233
+ if (style.display === 'none') return false;
234
+ if (style.visibility === 'hidden') return false;
235
+ if (parseFloat(style.opacity) === 0) return false;
236
+ const rect = el.getBoundingClientRect();
237
+ return rect.width > 0 && rect.height > 0;
238
+ })()`,
239
+ returnByValue: true
240
+ };
241
+ if (contextId !== void 0) {
242
+ params["contextId"] = contextId;
243
+ }
244
+ const result = await cdp.send("Runtime.evaluate", params);
245
+ return result.result.value === true;
246
+ }
247
+ async function isElementAttached(cdp, selector, contextId) {
248
+ const params = {
249
+ expression: `(() => {
250
+ ${DEEP_QUERY_SCRIPT}
251
+ return deepQuery(${JSON.stringify(selector)}) !== null;
252
+ })()`,
253
+ returnByValue: true
254
+ };
255
+ if (contextId !== void 0) {
256
+ params["contextId"] = contextId;
257
+ }
258
+ const result = await cdp.send("Runtime.evaluate", params);
259
+ return result.result.value === true;
260
+ }
261
+ function sleep(ms) {
262
+ return new Promise((resolve) => setTimeout(resolve, ms));
263
+ }
264
+ async function waitForElement(cdp, selector, options = {}) {
265
+ const { state = "visible", timeout = 3e4, pollInterval = 100, contextId } = options;
266
+ const startTime = Date.now();
267
+ const deadline = startTime + timeout;
268
+ while (Date.now() < deadline) {
269
+ let conditionMet = false;
270
+ switch (state) {
271
+ case "visible":
272
+ conditionMet = await isElementVisible(cdp, selector, contextId);
273
+ break;
274
+ case "hidden":
275
+ conditionMet = !await isElementVisible(cdp, selector, contextId);
276
+ break;
277
+ case "attached":
278
+ conditionMet = await isElementAttached(cdp, selector, contextId);
279
+ break;
280
+ case "detached":
281
+ conditionMet = !await isElementAttached(cdp, selector, contextId);
282
+ break;
283
+ }
284
+ if (conditionMet) {
285
+ return { success: true, waitedMs: Date.now() - startTime };
286
+ }
287
+ await sleep(pollInterval);
288
+ }
289
+ return { success: false, waitedMs: Date.now() - startTime };
290
+ }
291
+ async function waitForAnyElement(cdp, selectors, options = {}) {
292
+ const { state = "visible", timeout = 3e4, pollInterval = 100, contextId } = options;
293
+ const startTime = Date.now();
294
+ const deadline = startTime + timeout;
295
+ while (Date.now() < deadline) {
296
+ for (const selector of selectors) {
297
+ let conditionMet = false;
298
+ switch (state) {
299
+ case "visible":
300
+ conditionMet = await isElementVisible(cdp, selector, contextId);
301
+ break;
302
+ case "hidden":
303
+ conditionMet = !await isElementVisible(cdp, selector, contextId);
304
+ break;
305
+ case "attached":
306
+ conditionMet = await isElementAttached(cdp, selector, contextId);
307
+ break;
308
+ case "detached":
309
+ conditionMet = !await isElementAttached(cdp, selector, contextId);
310
+ break;
311
+ }
312
+ if (conditionMet) {
313
+ return { success: true, selector, waitedMs: Date.now() - startTime };
314
+ }
315
+ }
316
+ await sleep(pollInterval);
317
+ }
318
+ return { success: false, waitedMs: Date.now() - startTime };
319
+ }
320
+ async function getCurrentUrl(cdp) {
321
+ const result = await cdp.send("Runtime.evaluate", {
322
+ expression: "location.href",
323
+ returnByValue: true
324
+ });
325
+ return result.result.value;
326
+ }
327
+ async function waitForNavigation(cdp, options = {}) {
328
+ const { timeout = 3e4, allowSameDocument = true } = options;
329
+ const startTime = Date.now();
330
+ let startUrl;
331
+ try {
332
+ startUrl = await getCurrentUrl(cdp);
333
+ } catch {
334
+ startUrl = "";
335
+ }
336
+ return new Promise((resolve) => {
337
+ let resolved = false;
338
+ const cleanup = [];
339
+ const done = (success) => {
340
+ if (resolved) return;
341
+ resolved = true;
342
+ for (const fn of cleanup) fn();
343
+ resolve({ success, waitedMs: Date.now() - startTime });
344
+ };
345
+ const timer = setTimeout(() => done(false), timeout);
346
+ cleanup.push(() => clearTimeout(timer));
347
+ const onLoad = () => done(true);
348
+ cdp.on("Page.loadEventFired", onLoad);
349
+ cleanup.push(() => cdp.off("Page.loadEventFired", onLoad));
350
+ const onFrameNavigated = (params) => {
351
+ const frame = params["frame"];
352
+ if (frame && !frame.parentId && frame.url !== startUrl) {
353
+ done(true);
354
+ }
355
+ };
356
+ cdp.on("Page.frameNavigated", onFrameNavigated);
357
+ cleanup.push(() => cdp.off("Page.frameNavigated", onFrameNavigated));
358
+ if (allowSameDocument) {
359
+ const onSameDoc = () => done(true);
360
+ cdp.on("Page.navigatedWithinDocument", onSameDoc);
361
+ cleanup.push(() => cdp.off("Page.navigatedWithinDocument", onSameDoc));
362
+ }
363
+ const pollUrl = async () => {
364
+ while (!resolved && Date.now() < startTime + timeout) {
365
+ await sleep(100);
366
+ if (resolved) return;
367
+ try {
368
+ const currentUrl = await getCurrentUrl(cdp);
369
+ if (startUrl && currentUrl !== startUrl) {
370
+ done(true);
371
+ return;
372
+ }
373
+ } catch {
374
+ }
375
+ }
376
+ };
377
+ pollUrl();
378
+ });
379
+ }
380
+ async function waitForNetworkIdle(cdp, options = {}) {
381
+ const { timeout = 3e4, idleTime = 500 } = options;
382
+ const startTime = Date.now();
383
+ await cdp.send("Network.enable");
384
+ return new Promise((resolve) => {
385
+ let inFlight = 0;
386
+ let idleTimer = null;
387
+ const timeoutTimer = setTimeout(() => {
388
+ cleanup();
389
+ resolve({ success: false, waitedMs: Date.now() - startTime });
390
+ }, timeout);
391
+ const checkIdle = () => {
392
+ if (inFlight === 0) {
393
+ if (idleTimer) clearTimeout(idleTimer);
394
+ idleTimer = setTimeout(() => {
395
+ cleanup();
396
+ resolve({ success: true, waitedMs: Date.now() - startTime });
397
+ }, idleTime);
398
+ }
399
+ };
400
+ const onRequestStart = () => {
401
+ inFlight++;
402
+ if (idleTimer) {
403
+ clearTimeout(idleTimer);
404
+ idleTimer = null;
405
+ }
406
+ };
407
+ const onRequestEnd = () => {
408
+ inFlight = Math.max(0, inFlight - 1);
409
+ checkIdle();
410
+ };
411
+ const cleanup = () => {
412
+ clearTimeout(timeoutTimer);
413
+ if (idleTimer) clearTimeout(idleTimer);
414
+ cdp.off("Network.requestWillBeSent", onRequestStart);
415
+ cdp.off("Network.loadingFinished", onRequestEnd);
416
+ cdp.off("Network.loadingFailed", onRequestEnd);
417
+ };
418
+ cdp.on("Network.requestWillBeSent", onRequestStart);
419
+ cdp.on("Network.loadingFinished", onRequestEnd);
420
+ cdp.on("Network.loadingFailed", onRequestEnd);
421
+ checkIdle();
422
+ });
423
+ }
424
+
425
+ // src/browser/types.ts
426
+ var ElementNotFoundError = class extends Error {
427
+ selectors;
428
+ constructor(selectors) {
429
+ const selectorList = Array.isArray(selectors) ? selectors : [selectors];
430
+ super(`Element not found: ${selectorList.join(", ")}`);
431
+ this.name = "ElementNotFoundError";
432
+ this.selectors = selectorList;
433
+ }
434
+ };
435
+ var TimeoutError = class extends Error {
436
+ constructor(message = "Operation timed out") {
437
+ super(message);
438
+ this.name = "TimeoutError";
439
+ }
440
+ };
441
+ var NavigationError = class extends Error {
442
+ constructor(message) {
443
+ super(message);
444
+ this.name = "NavigationError";
445
+ }
446
+ };
447
+
448
+ // src/browser/page.ts
449
+ var DEFAULT_TIMEOUT = 3e4;
450
+ var Page = class {
451
+ cdp;
452
+ rootNodeId = null;
453
+ batchExecutor;
454
+ emulationState = {};
455
+ interceptor = null;
456
+ consoleHandlers = /* @__PURE__ */ new Set();
457
+ errorHandlers = /* @__PURE__ */ new Set();
458
+ dialogHandler = null;
459
+ consoleEnabled = false;
460
+ /** Map of ref (e.g., "e4") to backendNodeId for ref-based selectors */
461
+ refMap = /* @__PURE__ */ new Map();
462
+ /** Current frame context (null = main frame) */
463
+ currentFrame = null;
464
+ /** Stored frame document node IDs for context switching */
465
+ frameContexts = /* @__PURE__ */ new Map();
466
+ /** Map of frameId → executionContextId for JS evaluation in frames */
467
+ frameExecutionContexts = /* @__PURE__ */ new Map();
468
+ /** Current frame's execution context ID (null = main frame default) */
469
+ currentFrameContextId = null;
470
+ constructor(cdp) {
471
+ this.cdp = cdp;
472
+ this.batchExecutor = new BatchExecutor(this);
473
+ }
474
+ /**
475
+ * Initialize the page (enable required CDP domains)
476
+ */
477
+ async init() {
478
+ this.cdp.on("Runtime.executionContextCreated", (params) => {
479
+ const context = params["context"];
480
+ if (context.auxData?.frameId && context.auxData?.isDefault) {
481
+ this.frameExecutionContexts.set(context.auxData.frameId, context.id);
482
+ }
483
+ });
484
+ this.cdp.on("Runtime.executionContextDestroyed", (params) => {
485
+ const contextId = params["executionContextId"];
486
+ for (const [frameId, ctxId] of this.frameExecutionContexts.entries()) {
487
+ if (ctxId === contextId) {
488
+ this.frameExecutionContexts.delete(frameId);
489
+ break;
490
+ }
491
+ }
492
+ });
493
+ this.cdp.on("Page.javascriptDialogOpening", this.handleDialogOpening.bind(this));
494
+ await Promise.all([
495
+ this.cdp.send("Page.enable"),
496
+ this.cdp.send("DOM.enable"),
497
+ this.cdp.send("Runtime.enable"),
498
+ this.cdp.send("Network.enable")
499
+ ]);
500
+ }
501
+ // ============ Navigation ============
502
+ /**
503
+ * Navigate to a URL
504
+ */
505
+ async goto(url, options = {}) {
506
+ const { timeout = DEFAULT_TIMEOUT } = options;
507
+ const navPromise = this.waitForNavigation({ timeout });
508
+ await this.cdp.send("Page.navigate", { url });
509
+ const result = await navPromise;
510
+ if (!result) {
511
+ throw new TimeoutError(`Navigation to ${url} timed out after ${timeout}ms`);
512
+ }
513
+ this.rootNodeId = null;
514
+ this.refMap.clear();
515
+ }
516
+ /**
517
+ * Get the current URL
518
+ */
519
+ async url() {
520
+ const result = await this.cdp.send("Runtime.evaluate", {
521
+ expression: "location.href",
522
+ returnByValue: true
523
+ });
524
+ return result.result.value;
525
+ }
526
+ /**
527
+ * Get the page title
528
+ */
529
+ async title() {
530
+ const result = await this.cdp.send("Runtime.evaluate", {
531
+ expression: "document.title",
532
+ returnByValue: true
533
+ });
534
+ return result.result.value;
535
+ }
536
+ /**
537
+ * Reload the page
538
+ */
539
+ async reload(options = {}) {
540
+ const { timeout = DEFAULT_TIMEOUT } = options;
541
+ const navPromise = this.waitForNavigation({ timeout });
542
+ await this.cdp.send("Page.reload");
543
+ await navPromise;
544
+ this.rootNodeId = null;
545
+ this.refMap.clear();
546
+ }
547
+ /**
548
+ * Go back in history
549
+ */
550
+ async goBack(options = {}) {
551
+ const { timeout = DEFAULT_TIMEOUT } = options;
552
+ const history = await this.cdp.send("Page.getNavigationHistory");
553
+ if (history.currentIndex <= 0) {
554
+ return;
555
+ }
556
+ const navPromise = this.waitForNavigation({ timeout });
557
+ await this.cdp.send("Page.navigateToHistoryEntry", {
558
+ entryId: history.entries[history.currentIndex - 1].id
559
+ });
560
+ await navPromise;
561
+ this.rootNodeId = null;
562
+ this.refMap.clear();
563
+ }
564
+ /**
565
+ * Go forward in history
566
+ */
567
+ async goForward(options = {}) {
568
+ const { timeout = DEFAULT_TIMEOUT } = options;
569
+ const history = await this.cdp.send("Page.getNavigationHistory");
570
+ if (history.currentIndex >= history.entries.length - 1) {
571
+ return;
572
+ }
573
+ const navPromise = this.waitForNavigation({ timeout });
574
+ await this.cdp.send("Page.navigateToHistoryEntry", {
575
+ entryId: history.entries[history.currentIndex + 1].id
576
+ });
577
+ await navPromise;
578
+ this.rootNodeId = null;
579
+ this.refMap.clear();
580
+ }
581
+ // ============ Core Actions ============
582
+ /**
583
+ * Click an element (supports multi-selector)
584
+ *
585
+ * Uses CDP mouse events for regular elements. For form submit buttons,
586
+ * uses dispatchEvent to reliably trigger form submission in headless Chrome.
587
+ */
588
+ async click(selector, options = {}) {
589
+ return this.withStaleNodeRetry(async () => {
590
+ const element = await this.findElement(selector, options);
591
+ if (!element) {
592
+ if (options.optional) return false;
593
+ throw new ElementNotFoundError(selector);
594
+ }
595
+ await this.scrollIntoView(element.nodeId);
596
+ const submitResult = await this.evaluateInFrame(
597
+ `(() => {
598
+ const el = document.querySelector(${JSON.stringify(element.selector)});
599
+ if (!el) return { isSubmit: false };
600
+
601
+ // Check if this is a form submit button
602
+ const isSubmitButton = (el instanceof HTMLButtonElement && (el.type === 'submit' || (el.form && el.type !== 'button'))) ||
603
+ (el instanceof HTMLInputElement && el.type === 'submit');
604
+
605
+ if (isSubmitButton && el.form) {
606
+ // Dispatch submit event directly - works reliably in headless Chrome
607
+ el.form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
608
+ return { isSubmit: true };
609
+ }
610
+ return { isSubmit: false };
611
+ })()`
612
+ );
613
+ const isSubmit = submitResult.result.value?.isSubmit;
614
+ if (!isSubmit) {
615
+ await this.clickElement(element.nodeId);
616
+ }
617
+ return true;
618
+ });
619
+ }
620
+ /**
621
+ * Fill an input field (clears first by default)
622
+ */
623
+ async fill(selector, value, options = {}) {
624
+ const { clear = true } = options;
625
+ return this.withStaleNodeRetry(async () => {
626
+ const element = await this.findElement(selector, options);
627
+ if (!element) {
628
+ if (options.optional) return false;
629
+ throw new ElementNotFoundError(selector);
630
+ }
631
+ await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
632
+ if (clear) {
633
+ await this.evaluateInFrame(
634
+ `(() => {
635
+ const el = document.querySelector(${JSON.stringify(element.selector)});
636
+ if (el) {
637
+ el.value = '';
638
+ el.dispatchEvent(new Event('input', { bubbles: true }));
639
+ }
640
+ })()`
641
+ );
642
+ }
643
+ await this.cdp.send("Input.insertText", { text: value });
644
+ await this.evaluateInFrame(
645
+ `(() => {
646
+ const el = document.querySelector(${JSON.stringify(element.selector)});
647
+ if (el) {
648
+ el.dispatchEvent(new Event('input', { bubbles: true }));
649
+ el.dispatchEvent(new Event('change', { bubbles: true }));
650
+ }
651
+ })()`
652
+ );
653
+ return true;
654
+ });
655
+ }
656
+ /**
657
+ * Type text character by character (for autocomplete fields, etc.)
658
+ */
659
+ async type(selector, text, options = {}) {
660
+ const { delay = 50 } = options;
661
+ const element = await this.findElement(selector, options);
662
+ if (!element) {
663
+ if (options.optional) return false;
664
+ throw new ElementNotFoundError(selector);
665
+ }
666
+ await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
667
+ for (const char of text) {
668
+ await this.cdp.send("Input.dispatchKeyEvent", {
669
+ type: "keyDown",
670
+ key: char,
671
+ text: char
672
+ });
673
+ await this.cdp.send("Input.dispatchKeyEvent", {
674
+ type: "keyUp",
675
+ key: char
676
+ });
677
+ if (delay > 0) {
678
+ await sleep2(delay);
679
+ }
680
+ }
681
+ return true;
682
+ }
683
+ async select(selectorOrConfig, valueOrOptions, maybeOptions) {
684
+ if (typeof selectorOrConfig === "object" && !Array.isArray(selectorOrConfig) && "trigger" in selectorOrConfig) {
685
+ return this.selectCustom(selectorOrConfig, valueOrOptions);
686
+ }
687
+ const selector = selectorOrConfig;
688
+ const value = valueOrOptions;
689
+ const options = maybeOptions ?? {};
690
+ const element = await this.findElement(selector, options);
691
+ if (!element) {
692
+ if (options.optional) return false;
693
+ throw new ElementNotFoundError(selector);
694
+ }
695
+ const values = Array.isArray(value) ? value : [value];
696
+ await this.cdp.send("Runtime.evaluate", {
697
+ expression: `(() => {
698
+ const el = document.querySelector(${JSON.stringify(element.selector)});
699
+ if (!el || el.tagName !== 'SELECT') return false;
700
+ const values = ${JSON.stringify(values)};
701
+ for (const opt of el.options) {
702
+ opt.selected = values.includes(opt.value) || values.includes(opt.text);
703
+ }
704
+ el.dispatchEvent(new Event('change', { bubbles: true }));
705
+ return true;
706
+ })()`,
707
+ returnByValue: true
708
+ });
709
+ return true;
710
+ }
711
+ /**
712
+ * Handle custom (non-native) select/dropdown components
713
+ */
714
+ async selectCustom(config, options = {}) {
715
+ const { trigger, option, value, match = "text" } = config;
716
+ await this.click(trigger, options);
717
+ await sleep2(100);
718
+ let optionSelector;
719
+ const optionSelectors = Array.isArray(option) ? option : [option];
720
+ if (match === "contains") {
721
+ optionSelector = optionSelectors.map((s) => `${s}:has-text("${value}")`).join(", ");
722
+ } else if (match === "value") {
723
+ optionSelector = optionSelectors.map((s) => `${s}[data-value="${value}"], ${s}[value="${value}"]`).join(", ");
724
+ } else {
725
+ optionSelector = optionSelectors.map((s) => `${s}`).join(", ");
726
+ }
727
+ const result = await this.cdp.send("Runtime.evaluate", {
728
+ expression: `(() => {
729
+ const options = document.querySelectorAll(${JSON.stringify(optionSelector)});
730
+ for (const opt of options) {
731
+ const text = opt.textContent?.trim();
732
+ if (${match === "text" ? `text === ${JSON.stringify(value)}` : match === "contains" ? `text?.includes(${JSON.stringify(value)})` : "true"}) {
733
+ opt.click();
734
+ return true;
735
+ }
736
+ }
737
+ return false;
738
+ })()`,
739
+ returnByValue: true
740
+ });
741
+ if (!result.result.value) {
742
+ if (options.optional) return false;
743
+ throw new ElementNotFoundError(`Option with ${match} "${value}"`);
744
+ }
745
+ return true;
746
+ }
747
+ /**
748
+ * Check a checkbox or radio button
749
+ */
750
+ async check(selector, options = {}) {
751
+ const element = await this.findElement(selector, options);
752
+ if (!element) {
753
+ if (options.optional) return false;
754
+ throw new ElementNotFoundError(selector);
755
+ }
756
+ const result = await this.cdp.send("Runtime.evaluate", {
757
+ expression: `(() => {
758
+ const el = document.querySelector(${JSON.stringify(element.selector)});
759
+ if (!el) return false;
760
+ if (!el.checked) el.click();
761
+ return true;
762
+ })()`,
763
+ returnByValue: true
764
+ });
765
+ return result.result.value;
766
+ }
767
+ /**
768
+ * Uncheck a checkbox
769
+ */
770
+ async uncheck(selector, options = {}) {
771
+ const element = await this.findElement(selector, options);
772
+ if (!element) {
773
+ if (options.optional) return false;
774
+ throw new ElementNotFoundError(selector);
775
+ }
776
+ const result = await this.cdp.send("Runtime.evaluate", {
777
+ expression: `(() => {
778
+ const el = document.querySelector(${JSON.stringify(element.selector)});
779
+ if (!el) return false;
780
+ if (el.checked) el.click();
781
+ return true;
782
+ })()`,
783
+ returnByValue: true
784
+ });
785
+ return result.result.value;
786
+ }
787
+ /**
788
+ * Submit a form (tries Enter key first, then click)
789
+ *
790
+ * Navigation waiting behavior:
791
+ * - 'auto' (default): Attempt to detect navigation for 1 second, then assume client-side handling
792
+ * - true: Wait for full navigation (traditional forms)
793
+ * - false: Return immediately (AJAX forms where you'll wait for something else)
794
+ */
795
+ async submit(selector, options = {}) {
796
+ const { method = "enter+click", waitForNavigation: shouldWait = "auto" } = options;
797
+ const element = await this.findElement(selector, options);
798
+ if (!element) {
799
+ if (options.optional) return false;
800
+ throw new ElementNotFoundError(selector);
801
+ }
802
+ await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
803
+ if (method.includes("enter")) {
804
+ await this.press("Enter");
805
+ if (shouldWait === true) {
806
+ try {
807
+ await this.waitForNavigation({ timeout: options.timeout ?? DEFAULT_TIMEOUT });
808
+ return true;
809
+ } catch {
810
+ }
811
+ } else if (shouldWait === "auto") {
812
+ const navigationDetected = await Promise.race([
813
+ this.waitForNavigation({ timeout: 1e3, optional: true }).then(
814
+ (success) => success ? "nav" : null
815
+ ),
816
+ sleep2(500).then(() => "timeout")
817
+ ]);
818
+ if (navigationDetected === "nav") {
819
+ return true;
820
+ }
821
+ } else {
822
+ if (method === "enter") return true;
823
+ }
824
+ }
825
+ if (method.includes("click")) {
826
+ await this.click(element.selector, { ...options, optional: false });
827
+ if (shouldWait === true) {
828
+ await this.waitForNavigation({ timeout: options.timeout ?? DEFAULT_TIMEOUT });
829
+ } else if (shouldWait === "auto") {
830
+ await sleep2(100);
831
+ }
832
+ }
833
+ return true;
834
+ }
835
+ /**
836
+ * Press a key
837
+ */
838
+ async press(key) {
839
+ const keyMap = {
840
+ Enter: { key: "Enter", code: "Enter", keyCode: 13 },
841
+ Tab: { key: "Tab", code: "Tab", keyCode: 9 },
842
+ Escape: { key: "Escape", code: "Escape", keyCode: 27 },
843
+ Backspace: { key: "Backspace", code: "Backspace", keyCode: 8 },
844
+ Delete: { key: "Delete", code: "Delete", keyCode: 46 },
845
+ ArrowUp: { key: "ArrowUp", code: "ArrowUp", keyCode: 38 },
846
+ ArrowDown: { key: "ArrowDown", code: "ArrowDown", keyCode: 40 },
847
+ ArrowLeft: { key: "ArrowLeft", code: "ArrowLeft", keyCode: 37 },
848
+ ArrowRight: { key: "ArrowRight", code: "ArrowRight", keyCode: 39 }
849
+ };
850
+ const keyInfo = keyMap[key] ?? { key, code: key, keyCode: 0 };
851
+ await this.cdp.send("Input.dispatchKeyEvent", {
852
+ type: "keyDown",
853
+ key: keyInfo.key,
854
+ code: keyInfo.code,
855
+ windowsVirtualKeyCode: keyInfo.keyCode
856
+ });
857
+ await this.cdp.send("Input.dispatchKeyEvent", {
858
+ type: "keyUp",
859
+ key: keyInfo.key,
860
+ code: keyInfo.code,
861
+ windowsVirtualKeyCode: keyInfo.keyCode
862
+ });
863
+ }
864
+ /**
865
+ * Focus an element
866
+ */
867
+ async focus(selector, options = {}) {
868
+ const element = await this.findElement(selector, options);
869
+ if (!element) {
870
+ if (options.optional) return false;
871
+ throw new ElementNotFoundError(selector);
872
+ }
873
+ await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
874
+ return true;
875
+ }
876
+ /**
877
+ * Hover over an element
878
+ */
879
+ async hover(selector, options = {}) {
880
+ return this.withStaleNodeRetry(async () => {
881
+ const element = await this.findElement(selector, options);
882
+ if (!element) {
883
+ if (options.optional) return false;
884
+ throw new ElementNotFoundError(selector);
885
+ }
886
+ await this.scrollIntoView(element.nodeId);
887
+ const box = await this.getBoxModel(element.nodeId);
888
+ if (!box) {
889
+ if (options.optional) return false;
890
+ throw new Error("Could not get element box model");
891
+ }
892
+ const x = box.content[0] + box.width / 2;
893
+ const y = box.content[1] + box.height / 2;
894
+ await this.cdp.send("Input.dispatchMouseEvent", {
895
+ type: "mouseMoved",
896
+ x,
897
+ y
898
+ });
899
+ return true;
900
+ });
901
+ }
902
+ /**
903
+ * Scroll an element into view (or scroll to coordinates)
904
+ */
905
+ async scroll(selector, options = {}) {
906
+ const { x, y } = options;
907
+ if (x !== void 0 || y !== void 0) {
908
+ await this.cdp.send("Runtime.evaluate", {
909
+ expression: `window.scrollTo(${x ?? 0}, ${y ?? 0})`
910
+ });
911
+ return true;
912
+ }
913
+ const element = await this.findElement(selector, options);
914
+ if (!element) {
915
+ if (options.optional) return false;
916
+ throw new ElementNotFoundError(selector);
917
+ }
918
+ await this.scrollIntoView(element.nodeId);
919
+ return true;
920
+ }
921
+ // ============ Frame Navigation ============
922
+ /**
923
+ * Switch context to an iframe for subsequent actions
924
+ * @param selector - Selector for the iframe element
925
+ * @param options - Optional timeout and optional flags
926
+ * @returns true if switch succeeded
927
+ */
928
+ async switchToFrame(selector, options = {}) {
929
+ const element = await this.findElement(selector, options);
930
+ if (!element) {
931
+ if (options.optional) return false;
932
+ throw new ElementNotFoundError(selector);
933
+ }
934
+ const descResult = await this.cdp.send("DOM.describeNode", {
935
+ nodeId: element.nodeId,
936
+ depth: 1
937
+ });
938
+ if (!descResult.node.contentDocument) {
939
+ if (options.optional) return false;
940
+ throw new Error(
941
+ "Cannot access iframe content. This may be a cross-origin iframe which requires different handling."
942
+ );
943
+ }
944
+ const frameKey = Array.isArray(selector) ? selector[0] : selector;
945
+ this.frameContexts.set(frameKey, descResult.node.contentDocument.nodeId);
946
+ this.currentFrame = frameKey;
947
+ this.rootNodeId = descResult.node.contentDocument.nodeId;
948
+ if (descResult.node.frameId) {
949
+ let contextId = this.frameExecutionContexts.get(descResult.node.frameId);
950
+ if (!contextId) {
951
+ await new Promise((resolve) => setTimeout(resolve, 50));
952
+ contextId = this.frameExecutionContexts.get(descResult.node.frameId);
953
+ }
954
+ if (contextId) {
955
+ this.currentFrameContextId = contextId;
956
+ }
957
+ }
958
+ this.refMap.clear();
959
+ return true;
960
+ }
961
+ /**
962
+ * Switch back to the main document from an iframe
963
+ */
964
+ async switchToMain() {
965
+ this.currentFrame = null;
966
+ this.rootNodeId = null;
967
+ this.currentFrameContextId = null;
968
+ this.refMap.clear();
969
+ }
970
+ /**
971
+ * Get the current frame context (null = main frame)
972
+ */
973
+ getCurrentFrame() {
974
+ return this.currentFrame;
975
+ }
976
+ // ============ Waiting ============
977
+ /**
978
+ * Wait for an element to reach a state
979
+ */
980
+ async waitFor(selector, options = {}) {
981
+ const { timeout = DEFAULT_TIMEOUT, state = "visible" } = options;
982
+ const selectors = Array.isArray(selector) ? selector : [selector];
983
+ const result = await waitForAnyElement(this.cdp, selectors, {
984
+ state,
985
+ timeout,
986
+ contextId: this.currentFrameContextId ?? void 0
987
+ });
988
+ if (!result.success && !options.optional) {
989
+ throw new TimeoutError(`Timeout waiting for ${selectors.join(" or ")} to be ${state}`);
990
+ }
991
+ return result.success;
992
+ }
993
+ /**
994
+ * Wait for navigation to complete
995
+ */
996
+ async waitForNavigation(options = {}) {
997
+ const { timeout = DEFAULT_TIMEOUT } = options;
998
+ const result = await waitForNavigation(this.cdp, { timeout });
999
+ if (!result.success && !options.optional) {
1000
+ throw new TimeoutError("Navigation timeout");
1001
+ }
1002
+ this.rootNodeId = null;
1003
+ this.refMap.clear();
1004
+ return result.success;
1005
+ }
1006
+ /**
1007
+ * Wait for network to be idle
1008
+ */
1009
+ async waitForNetworkIdle(options = {}) {
1010
+ const { timeout = DEFAULT_TIMEOUT, idleTime = 500 } = options;
1011
+ const result = await waitForNetworkIdle(this.cdp, { timeout, idleTime });
1012
+ if (!result.success && !options.optional) {
1013
+ throw new TimeoutError("Network idle timeout");
1014
+ }
1015
+ return result.success;
1016
+ }
1017
+ // ============ JavaScript Execution ============
1018
+ /**
1019
+ * Evaluate JavaScript in the page context (or current frame context if in iframe)
1020
+ */
1021
+ async evaluate(expression, ...args) {
1022
+ let script;
1023
+ if (typeof expression === "function") {
1024
+ const argString = args.map((a) => JSON.stringify(a)).join(", ");
1025
+ script = `(${expression.toString()})(${argString})`;
1026
+ } else {
1027
+ script = expression;
1028
+ }
1029
+ const params = {
1030
+ expression: script,
1031
+ returnByValue: true,
1032
+ awaitPromise: true
1033
+ };
1034
+ if (this.currentFrameContextId !== null) {
1035
+ params["contextId"] = this.currentFrameContextId;
1036
+ }
1037
+ const result = await this.cdp.send("Runtime.evaluate", params);
1038
+ if (result.exceptionDetails) {
1039
+ throw new Error(`Evaluation failed: ${result.exceptionDetails.text}`);
1040
+ }
1041
+ return result.result.value;
1042
+ }
1043
+ // ============ Screenshots ============
1044
+ /**
1045
+ * Take a screenshot
1046
+ */
1047
+ async screenshot(options = {}) {
1048
+ const { format = "png", quality, fullPage = false } = options;
1049
+ let clip;
1050
+ if (fullPage) {
1051
+ const metrics = await this.cdp.send("Page.getLayoutMetrics");
1052
+ clip = {
1053
+ x: 0,
1054
+ y: 0,
1055
+ width: metrics.contentSize.width,
1056
+ height: metrics.contentSize.height,
1057
+ scale: 1
1058
+ };
1059
+ }
1060
+ const result = await this.cdp.send("Page.captureScreenshot", {
1061
+ format,
1062
+ quality: format === "png" ? void 0 : quality,
1063
+ clip,
1064
+ captureBeyondViewport: fullPage
1065
+ });
1066
+ return result.data;
1067
+ }
1068
+ // ============ Text Extraction ============
1069
+ /**
1070
+ * Get text content from the page or a specific element
1071
+ */
1072
+ async text(selector) {
1073
+ const expression = selector ? `document.querySelector(${JSON.stringify(selector)})?.innerText ?? ''` : "document.body.innerText";
1074
+ const result = await this.evaluateInFrame(expression);
1075
+ return result.result.value ?? "";
1076
+ }
1077
+ // ============ File Handling ============
1078
+ /**
1079
+ * Set files on a file input
1080
+ */
1081
+ async setInputFiles(selector, files, options = {}) {
1082
+ const element = await this.findElement(selector, options);
1083
+ if (!element) {
1084
+ if (options.optional) return false;
1085
+ throw new ElementNotFoundError(selector);
1086
+ }
1087
+ const fileData = await Promise.all(
1088
+ files.map(async (f) => {
1089
+ let base64;
1090
+ if (typeof f.buffer === "string") {
1091
+ base64 = f.buffer;
1092
+ } else {
1093
+ const bytes = new Uint8Array(f.buffer);
1094
+ base64 = btoa(String.fromCharCode(...bytes));
1095
+ }
1096
+ return { name: f.name, mimeType: f.mimeType, data: base64 };
1097
+ })
1098
+ );
1099
+ await this.cdp.send("Runtime.evaluate", {
1100
+ expression: `(() => {
1101
+ const input = document.querySelector(${JSON.stringify(element.selector)});
1102
+ if (!input) return false;
1103
+
1104
+ const files = ${JSON.stringify(fileData)};
1105
+ const dt = new DataTransfer();
1106
+
1107
+ for (const f of files) {
1108
+ const bytes = Uint8Array.from(atob(f.data), c => c.charCodeAt(0));
1109
+ const file = new File([bytes], f.name, { type: f.mimeType });
1110
+ dt.items.add(file);
1111
+ }
1112
+
1113
+ input.files = dt.files;
1114
+ input.dispatchEvent(new Event('change', { bubbles: true }));
1115
+ return true;
1116
+ })()`,
1117
+ returnByValue: true
1118
+ });
1119
+ return true;
1120
+ }
1121
+ /**
1122
+ * Wait for a download to complete, triggered by an action
1123
+ */
1124
+ async waitForDownload(trigger, options = {}) {
1125
+ const { timeout = DEFAULT_TIMEOUT } = options;
1126
+ await this.cdp.send("Browser.setDownloadBehavior", {
1127
+ behavior: "allowAndName",
1128
+ eventsEnabled: true
1129
+ });
1130
+ return new Promise((resolve, reject) => {
1131
+ let downloadGuid;
1132
+ let suggestedFilename;
1133
+ let resolved = false;
1134
+ const timeoutTimer = setTimeout(() => {
1135
+ if (!resolved) {
1136
+ cleanup();
1137
+ reject(new TimeoutError(`Download timed out after ${timeout}ms`));
1138
+ }
1139
+ }, timeout);
1140
+ const onDownloadWillBegin = (params) => {
1141
+ downloadGuid = params["guid"];
1142
+ suggestedFilename = params["suggestedFilename"];
1143
+ };
1144
+ const onDownloadProgress = (params) => {
1145
+ if (params["guid"] === downloadGuid && params["state"] === "completed") {
1146
+ resolved = true;
1147
+ cleanup();
1148
+ const download = {
1149
+ filename: suggestedFilename ?? "unknown",
1150
+ content: async () => {
1151
+ return new ArrayBuffer(0);
1152
+ }
1153
+ };
1154
+ resolve(download);
1155
+ } else if (params["guid"] === downloadGuid && params["state"] === "canceled") {
1156
+ resolved = true;
1157
+ cleanup();
1158
+ reject(new Error("Download was canceled"));
1159
+ }
1160
+ };
1161
+ const cleanup = () => {
1162
+ clearTimeout(timeoutTimer);
1163
+ this.cdp.off("Browser.downloadWillBegin", onDownloadWillBegin);
1164
+ this.cdp.off("Browser.downloadProgress", onDownloadProgress);
1165
+ };
1166
+ this.cdp.on("Browser.downloadWillBegin", onDownloadWillBegin);
1167
+ this.cdp.on("Browser.downloadProgress", onDownloadProgress);
1168
+ trigger().catch((err) => {
1169
+ if (!resolved) {
1170
+ resolved = true;
1171
+ cleanup();
1172
+ reject(err);
1173
+ }
1174
+ });
1175
+ });
1176
+ }
1177
+ // ============ Snapshot ============
1178
+ /**
1179
+ * Get an accessibility tree snapshot of the page
1180
+ */
1181
+ async snapshot() {
1182
+ const [url, title, axTree] = await Promise.all([
1183
+ this.url(),
1184
+ this.title(),
1185
+ this.cdp.send("Accessibility.getFullAXTree")
1186
+ ]);
1187
+ const nodes = axTree.nodes.filter((n) => !n.ignored);
1188
+ const nodeMap = new Map(nodes.map((n) => [n.nodeId, n]));
1189
+ let refCounter = 0;
1190
+ const nodeRefs = /* @__PURE__ */ new Map();
1191
+ this.refMap.clear();
1192
+ for (const node of nodes) {
1193
+ const ref = `e${++refCounter}`;
1194
+ nodeRefs.set(node.nodeId, ref);
1195
+ if (node.backendDOMNodeId !== void 0) {
1196
+ this.refMap.set(ref, node.backendDOMNodeId);
1197
+ }
1198
+ }
1199
+ const buildNode = (nodeId) => {
1200
+ const node = nodeMap.get(nodeId);
1201
+ if (!node) return null;
1202
+ const role = node.role?.value ?? "generic";
1203
+ const name = node.name?.value;
1204
+ const value = node.value?.value;
1205
+ const ref = nodeRefs.get(nodeId);
1206
+ const children = [];
1207
+ if (node.childIds) {
1208
+ for (const childId of node.childIds) {
1209
+ const child = buildNode(childId);
1210
+ if (child) children.push(child);
1211
+ }
1212
+ }
1213
+ const disabled = node.properties?.find((p) => p.name === "disabled")?.value.value;
1214
+ const checked = node.properties?.find((p) => p.name === "checked")?.value.value;
1215
+ return {
1216
+ role,
1217
+ name,
1218
+ value,
1219
+ ref,
1220
+ children: children.length > 0 ? children : void 0,
1221
+ disabled,
1222
+ checked
1223
+ };
1224
+ };
1225
+ const rootNodes = nodes.filter((n) => !n.parentId || !nodeMap.has(n.parentId));
1226
+ const accessibilityTree = rootNodes.map((n) => buildNode(n.nodeId)).filter((n) => n !== null);
1227
+ const interactiveRoles = /* @__PURE__ */ new Set([
1228
+ "button",
1229
+ "link",
1230
+ "textbox",
1231
+ "checkbox",
1232
+ "radio",
1233
+ "combobox",
1234
+ "listbox",
1235
+ "menuitem",
1236
+ "menuitemcheckbox",
1237
+ "menuitemradio",
1238
+ "option",
1239
+ "searchbox",
1240
+ "slider",
1241
+ "spinbutton",
1242
+ "switch",
1243
+ "tab",
1244
+ "treeitem"
1245
+ ]);
1246
+ const interactiveElements = [];
1247
+ for (const node of nodes) {
1248
+ const role = node.role?.value;
1249
+ if (role && interactiveRoles.has(role)) {
1250
+ const ref = nodeRefs.get(node.nodeId);
1251
+ const name = node.name?.value ?? "";
1252
+ const disabled = node.properties?.find((p) => p.name === "disabled")?.value.value;
1253
+ const selector = node.backendDOMNodeId ? `[data-backend-node-id="${node.backendDOMNodeId}"]` : `[aria-label="${name}"]`;
1254
+ interactiveElements.push({
1255
+ ref,
1256
+ role,
1257
+ name,
1258
+ selector,
1259
+ disabled
1260
+ });
1261
+ }
1262
+ }
1263
+ const formatTree = (nodes2, depth = 0) => {
1264
+ const lines = [];
1265
+ for (const node of nodes2) {
1266
+ let line = `${" ".repeat(depth)}- ${node.role}`;
1267
+ if (node.name) line += ` "${node.name}"`;
1268
+ line += ` [ref=${node.ref}]`;
1269
+ if (node.disabled) line += " (disabled)";
1270
+ if (node.checked !== void 0) line += node.checked ? " (checked)" : " (unchecked)";
1271
+ lines.push(line);
1272
+ if (node.children) {
1273
+ lines.push(formatTree(node.children, depth + 1));
1274
+ }
1275
+ }
1276
+ return lines.join("\n");
1277
+ };
1278
+ const text = formatTree(accessibilityTree);
1279
+ return {
1280
+ url,
1281
+ title,
1282
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1283
+ accessibilityTree,
1284
+ interactiveElements,
1285
+ text
1286
+ };
1287
+ }
1288
+ // ============ Batch Execution ============
1289
+ /**
1290
+ * Execute a batch of steps
1291
+ */
1292
+ async batch(steps, options) {
1293
+ return this.batchExecutor.execute(steps, options);
1294
+ }
1295
+ // ============ Emulation ============
1296
+ /**
1297
+ * Set the viewport size and device metrics
1298
+ */
1299
+ async setViewport(options) {
1300
+ const {
1301
+ width,
1302
+ height,
1303
+ deviceScaleFactor = 1,
1304
+ isMobile = false,
1305
+ hasTouch = false,
1306
+ isLandscape = false
1307
+ } = options;
1308
+ await this.cdp.send("Emulation.setDeviceMetricsOverride", {
1309
+ width,
1310
+ height,
1311
+ deviceScaleFactor,
1312
+ mobile: isMobile,
1313
+ screenWidth: width,
1314
+ screenHeight: height,
1315
+ screenOrientation: {
1316
+ type: isLandscape ? "landscapePrimary" : "portraitPrimary",
1317
+ angle: isLandscape ? 90 : 0
1318
+ }
1319
+ });
1320
+ if (hasTouch) {
1321
+ await this.cdp.send("Emulation.setTouchEmulationEnabled", {
1322
+ enabled: true,
1323
+ maxTouchPoints: 5
1324
+ });
1325
+ }
1326
+ this.emulationState.viewport = options;
1327
+ }
1328
+ /**
1329
+ * Clear viewport override, return to default
1330
+ */
1331
+ async clearViewport() {
1332
+ await this.cdp.send("Emulation.clearDeviceMetricsOverride");
1333
+ await this.cdp.send("Emulation.setTouchEmulationEnabled", { enabled: false });
1334
+ this.emulationState.viewport = void 0;
1335
+ }
1336
+ /**
1337
+ * Set the user agent string and optional metadata
1338
+ */
1339
+ async setUserAgent(options) {
1340
+ const opts = typeof options === "string" ? { userAgent: options } : options;
1341
+ await this.cdp.send("Emulation.setUserAgentOverride", {
1342
+ userAgent: opts.userAgent,
1343
+ acceptLanguage: opts.acceptLanguage,
1344
+ platform: opts.platform,
1345
+ userAgentMetadata: opts.userAgentMetadata
1346
+ });
1347
+ this.emulationState.userAgent = opts;
1348
+ }
1349
+ /**
1350
+ * Set geolocation coordinates
1351
+ */
1352
+ async setGeolocation(options) {
1353
+ const { latitude, longitude, accuracy = 1 } = options;
1354
+ await this.cdp.send("Browser.grantPermissions", {
1355
+ permissions: ["geolocation"]
1356
+ });
1357
+ await this.cdp.send("Emulation.setGeolocationOverride", {
1358
+ latitude,
1359
+ longitude,
1360
+ accuracy
1361
+ });
1362
+ this.emulationState.geolocation = options;
1363
+ }
1364
+ /**
1365
+ * Clear geolocation override
1366
+ */
1367
+ async clearGeolocation() {
1368
+ await this.cdp.send("Emulation.clearGeolocationOverride");
1369
+ this.emulationState.geolocation = void 0;
1370
+ }
1371
+ /**
1372
+ * Set timezone override
1373
+ */
1374
+ async setTimezone(timezoneId) {
1375
+ await this.cdp.send("Emulation.setTimezoneOverride", { timezoneId });
1376
+ this.emulationState.timezone = timezoneId;
1377
+ }
1378
+ /**
1379
+ * Set locale override
1380
+ */
1381
+ async setLocale(locale) {
1382
+ await this.cdp.send("Emulation.setLocaleOverride", { locale });
1383
+ this.emulationState.locale = locale;
1384
+ }
1385
+ /**
1386
+ * Emulate a specific device
1387
+ */
1388
+ async emulate(device) {
1389
+ await this.setViewport(device.viewport);
1390
+ await this.setUserAgent(device.userAgent);
1391
+ }
1392
+ /**
1393
+ * Get current emulation state
1394
+ */
1395
+ getEmulationState() {
1396
+ return { ...this.emulationState };
1397
+ }
1398
+ // ============ Request Interception ============
1399
+ /**
1400
+ * Add request interception handler
1401
+ * @param pattern URL pattern or resource type to match
1402
+ * @param handler Handler function for matched requests
1403
+ * @returns Unsubscribe function
1404
+ */
1405
+ async intercept(pattern, handler) {
1406
+ if (!this.interceptor) {
1407
+ this.interceptor = new RequestInterceptor(this.cdp);
1408
+ await this.interceptor.enable();
1409
+ }
1410
+ const normalizedPattern = typeof pattern === "string" ? { urlPattern: pattern } : pattern;
1411
+ return this.interceptor.addHandler(normalizedPattern, handler);
1412
+ }
1413
+ /**
1414
+ * Route requests matching pattern to a mock response
1415
+ * Convenience wrapper around intercept()
1416
+ */
1417
+ async route(urlPattern, options) {
1418
+ return this.intercept({ urlPattern }, async (_request, actions) => {
1419
+ let body = options.body;
1420
+ const headers = { ...options.headers };
1421
+ if (typeof body === "object") {
1422
+ body = JSON.stringify(body);
1423
+ headers["content-type"] ??= "application/json";
1424
+ }
1425
+ if (options.contentType) {
1426
+ headers["content-type"] = options.contentType;
1427
+ }
1428
+ await actions.fulfill({
1429
+ status: options.status ?? 200,
1430
+ headers,
1431
+ body
1432
+ });
1433
+ });
1434
+ }
1435
+ /**
1436
+ * Block requests matching resource types
1437
+ */
1438
+ async blockResources(types) {
1439
+ return this.intercept({}, async (request, actions) => {
1440
+ if (types.includes(request.resourceType)) {
1441
+ await actions.fail({ reason: "BlockedByClient" });
1442
+ } else {
1443
+ await actions.continue();
1444
+ }
1445
+ });
1446
+ }
1447
+ /**
1448
+ * Disable all request interception
1449
+ */
1450
+ async disableInterception() {
1451
+ if (this.interceptor) {
1452
+ await this.interceptor.disable();
1453
+ this.interceptor = null;
1454
+ }
1455
+ }
1456
+ // ============ Cookies & Storage ============
1457
+ /**
1458
+ * Get all cookies for the current page
1459
+ */
1460
+ async cookies(urls) {
1461
+ const targetUrls = urls ?? [await this.url()];
1462
+ const result = await this.cdp.send("Network.getCookies", {
1463
+ urls: targetUrls
1464
+ });
1465
+ return result.cookies;
1466
+ }
1467
+ /**
1468
+ * Set a cookie
1469
+ */
1470
+ async setCookie(options) {
1471
+ const { name, value, domain, path = "/", expires, httpOnly, secure, sameSite, url } = options;
1472
+ let expireTime;
1473
+ if (expires instanceof Date) {
1474
+ expireTime = Math.floor(expires.getTime() / 1e3);
1475
+ } else if (typeof expires === "number") {
1476
+ expireTime = expires;
1477
+ }
1478
+ const result = await this.cdp.send("Network.setCookie", {
1479
+ name,
1480
+ value,
1481
+ domain,
1482
+ path,
1483
+ expires: expireTime,
1484
+ httpOnly,
1485
+ secure,
1486
+ sameSite,
1487
+ url: url ?? (domain ? void 0 : await this.url())
1488
+ });
1489
+ return result.success;
1490
+ }
1491
+ /**
1492
+ * Set multiple cookies
1493
+ */
1494
+ async setCookies(cookies) {
1495
+ for (const cookie of cookies) {
1496
+ await this.setCookie(cookie);
1497
+ }
1498
+ }
1499
+ /**
1500
+ * Delete a specific cookie
1501
+ */
1502
+ async deleteCookie(options) {
1503
+ const { name, domain, path, url } = options;
1504
+ await this.cdp.send("Network.deleteCookies", {
1505
+ name,
1506
+ domain,
1507
+ path,
1508
+ url: url ?? (domain ? void 0 : await this.url())
1509
+ });
1510
+ }
1511
+ /**
1512
+ * Delete multiple cookies
1513
+ */
1514
+ async deleteCookies(cookies) {
1515
+ for (const cookie of cookies) {
1516
+ await this.deleteCookie(cookie);
1517
+ }
1518
+ }
1519
+ /**
1520
+ * Clear all cookies
1521
+ */
1522
+ async clearCookies(options) {
1523
+ if (options?.domain) {
1524
+ const domainCookies = await this.cookies([`https://${options.domain}`]);
1525
+ for (const cookie of domainCookies) {
1526
+ await this.deleteCookie({
1527
+ name: cookie.name,
1528
+ domain: cookie.domain,
1529
+ path: cookie.path
1530
+ });
1531
+ }
1532
+ } else {
1533
+ await this.cdp.send("Storage.clearCookies", {});
1534
+ }
1535
+ }
1536
+ /**
1537
+ * Get localStorage value
1538
+ */
1539
+ async getLocalStorage(key) {
1540
+ const result = await this.cdp.send("Runtime.evaluate", {
1541
+ expression: `localStorage.getItem(${JSON.stringify(key)})`,
1542
+ returnByValue: true
1543
+ });
1544
+ return result.result.value;
1545
+ }
1546
+ /**
1547
+ * Set localStorage value
1548
+ */
1549
+ async setLocalStorage(key, value) {
1550
+ await this.cdp.send("Runtime.evaluate", {
1551
+ expression: `localStorage.setItem(${JSON.stringify(key)}, ${JSON.stringify(value)})`
1552
+ });
1553
+ }
1554
+ /**
1555
+ * Remove localStorage item
1556
+ */
1557
+ async removeLocalStorage(key) {
1558
+ await this.cdp.send("Runtime.evaluate", {
1559
+ expression: `localStorage.removeItem(${JSON.stringify(key)})`
1560
+ });
1561
+ }
1562
+ /**
1563
+ * Clear localStorage
1564
+ */
1565
+ async clearLocalStorage() {
1566
+ await this.cdp.send("Runtime.evaluate", {
1567
+ expression: "localStorage.clear()"
1568
+ });
1569
+ }
1570
+ /**
1571
+ * Get sessionStorage value
1572
+ */
1573
+ async getSessionStorage(key) {
1574
+ const result = await this.cdp.send("Runtime.evaluate", {
1575
+ expression: `sessionStorage.getItem(${JSON.stringify(key)})`,
1576
+ returnByValue: true
1577
+ });
1578
+ return result.result.value;
1579
+ }
1580
+ /**
1581
+ * Set sessionStorage value
1582
+ */
1583
+ async setSessionStorage(key, value) {
1584
+ await this.cdp.send("Runtime.evaluate", {
1585
+ expression: `sessionStorage.setItem(${JSON.stringify(key)}, ${JSON.stringify(value)})`
1586
+ });
1587
+ }
1588
+ /**
1589
+ * Remove sessionStorage item
1590
+ */
1591
+ async removeSessionStorage(key) {
1592
+ await this.cdp.send("Runtime.evaluate", {
1593
+ expression: `sessionStorage.removeItem(${JSON.stringify(key)})`
1594
+ });
1595
+ }
1596
+ /**
1597
+ * Clear sessionStorage
1598
+ */
1599
+ async clearSessionStorage() {
1600
+ await this.cdp.send("Runtime.evaluate", {
1601
+ expression: "sessionStorage.clear()"
1602
+ });
1603
+ }
1604
+ // ============ Console & Errors ============
1605
+ /**
1606
+ * Enable console message capture
1607
+ */
1608
+ async enableConsole() {
1609
+ if (this.consoleEnabled) return;
1610
+ this.cdp.on("Runtime.consoleAPICalled", this.handleConsoleMessage.bind(this));
1611
+ this.cdp.on("Runtime.exceptionThrown", this.handleException.bind(this));
1612
+ this.consoleEnabled = true;
1613
+ }
1614
+ /**
1615
+ * Handle console API calls
1616
+ */
1617
+ handleConsoleMessage(params) {
1618
+ const args = params["args"];
1619
+ const stackTrace = params["stackTrace"];
1620
+ const message = {
1621
+ type: params["type"],
1622
+ text: this.formatConsoleArgs(args ?? []),
1623
+ args: args?.map((a) => a.value) ?? [],
1624
+ timestamp: params["timestamp"],
1625
+ stackTrace: stackTrace?.callFrames?.map((f) => `${f.url}:${f.lineNumber}`)
1626
+ };
1627
+ for (const handler of this.consoleHandlers) {
1628
+ try {
1629
+ handler(message);
1630
+ } catch (e) {
1631
+ console.error("[Console handler error]", e);
1632
+ }
1633
+ }
1634
+ }
1635
+ /**
1636
+ * Handle JavaScript exceptions
1637
+ */
1638
+ handleException(params) {
1639
+ const details = params["exceptionDetails"];
1640
+ const exception = details["exception"];
1641
+ const stackTrace = details["stackTrace"];
1642
+ const error = {
1643
+ message: exception?.description ?? details["text"],
1644
+ url: details["url"],
1645
+ lineNumber: details["lineNumber"],
1646
+ columnNumber: details["columnNumber"],
1647
+ timestamp: params["timestamp"],
1648
+ stackTrace: stackTrace?.callFrames?.map((f) => `${f.url}:${f.lineNumber}`)
1649
+ };
1650
+ for (const handler of this.errorHandlers) {
1651
+ try {
1652
+ handler(error);
1653
+ } catch (e) {
1654
+ console.error("[Error handler error]", e);
1655
+ }
1656
+ }
1657
+ }
1658
+ /**
1659
+ * Handle dialog opening
1660
+ */
1661
+ async handleDialogOpening(params) {
1662
+ const dialog = {
1663
+ type: params["type"],
1664
+ message: params["message"],
1665
+ defaultValue: params["defaultPrompt"],
1666
+ accept: async (promptText) => {
1667
+ await this.cdp.send("Page.handleJavaScriptDialog", {
1668
+ accept: true,
1669
+ promptText
1670
+ });
1671
+ },
1672
+ dismiss: async () => {
1673
+ await this.cdp.send("Page.handleJavaScriptDialog", {
1674
+ accept: false
1675
+ });
1676
+ }
1677
+ };
1678
+ if (this.dialogHandler) {
1679
+ try {
1680
+ await this.dialogHandler(dialog);
1681
+ } catch (e) {
1682
+ console.error("[Dialog handler error]", e);
1683
+ await dialog.dismiss();
1684
+ }
1685
+ } else {
1686
+ await dialog.dismiss();
1687
+ }
1688
+ }
1689
+ /**
1690
+ * Format console arguments to string
1691
+ */
1692
+ formatConsoleArgs(args) {
1693
+ return args.map((arg) => {
1694
+ if (arg.value !== void 0) return String(arg.value);
1695
+ if (arg.description) return arg.description;
1696
+ return "[object]";
1697
+ }).join(" ");
1698
+ }
1699
+ /**
1700
+ * Subscribe to console messages
1701
+ */
1702
+ async onConsole(handler) {
1703
+ await this.enableConsole();
1704
+ this.consoleHandlers.add(handler);
1705
+ return () => this.consoleHandlers.delete(handler);
1706
+ }
1707
+ /**
1708
+ * Subscribe to page errors
1709
+ */
1710
+ async onError(handler) {
1711
+ await this.enableConsole();
1712
+ this.errorHandlers.add(handler);
1713
+ return () => this.errorHandlers.delete(handler);
1714
+ }
1715
+ /**
1716
+ * Set dialog handler (only one at a time)
1717
+ */
1718
+ async onDialog(handler) {
1719
+ await this.enableConsole();
1720
+ this.dialogHandler = handler;
1721
+ }
1722
+ /**
1723
+ * Collect console messages during an action
1724
+ */
1725
+ async collectConsole(fn) {
1726
+ const messages = [];
1727
+ const unsubscribe = await this.onConsole((msg) => messages.push(msg));
1728
+ try {
1729
+ const result = await fn();
1730
+ return { result, messages };
1731
+ } finally {
1732
+ unsubscribe();
1733
+ }
1734
+ }
1735
+ /**
1736
+ * Collect errors during an action
1737
+ */
1738
+ async collectErrors(fn) {
1739
+ const errors = [];
1740
+ const unsubscribe = await this.onError((err) => errors.push(err));
1741
+ try {
1742
+ const result = await fn();
1743
+ return { result, errors };
1744
+ } finally {
1745
+ unsubscribe();
1746
+ }
1747
+ }
1748
+ // ============ Lifecycle ============
1749
+ /**
1750
+ * Reset page state for clean test isolation
1751
+ * - Stops any pending operations
1752
+ * - Clears localStorage and sessionStorage
1753
+ * - Resets internal state
1754
+ */
1755
+ async reset() {
1756
+ this.rootNodeId = null;
1757
+ this.refMap.clear();
1758
+ this.currentFrame = null;
1759
+ this.currentFrameContextId = null;
1760
+ this.frameContexts.clear();
1761
+ this.dialogHandler = null;
1762
+ try {
1763
+ await this.cdp.send("Page.stopLoading");
1764
+ } catch {
1765
+ }
1766
+ try {
1767
+ await this.cdp.send("Runtime.evaluate", {
1768
+ expression: `(() => {
1769
+ try { localStorage.clear(); } catch {}
1770
+ try { sessionStorage.clear(); } catch {}
1771
+ })()`
1772
+ });
1773
+ } catch {
1774
+ }
1775
+ }
1776
+ /**
1777
+ * Close this page (no-op for now, managed by Browser)
1778
+ * This is a placeholder for API compatibility
1779
+ */
1780
+ async close() {
1781
+ }
1782
+ // ============ Private Helpers ============
1783
+ /**
1784
+ * Retry wrapper for operations that may encounter stale nodes
1785
+ * Catches "Could not find node with given id" errors and retries
1786
+ */
1787
+ async withStaleNodeRetry(fn, options = {}) {
1788
+ const { retries = 2, delay = 50 } = options;
1789
+ let lastError;
1790
+ for (let attempt = 0; attempt <= retries; attempt++) {
1791
+ try {
1792
+ return await fn();
1793
+ } catch (e) {
1794
+ 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"))) {
1795
+ lastError = e;
1796
+ if (attempt < retries) {
1797
+ this.rootNodeId = null;
1798
+ await sleep2(delay);
1799
+ continue;
1800
+ }
1801
+ }
1802
+ throw e;
1803
+ }
1804
+ }
1805
+ throw lastError ?? new Error("Stale node retry exhausted");
1806
+ }
1807
+ /**
1808
+ * Find an element using single or multiple selectors
1809
+ * Supports ref: prefix for ref-based selectors (e.g., "ref:e4")
1810
+ */
1811
+ async findElement(selectors, options = {}) {
1812
+ const { timeout = DEFAULT_TIMEOUT } = options;
1813
+ const selectorList = Array.isArray(selectors) ? selectors : [selectors];
1814
+ for (const selector of selectorList) {
1815
+ if (selector.startsWith("ref:")) {
1816
+ const ref = selector.slice(4);
1817
+ const backendNodeId = this.refMap.get(ref);
1818
+ if (!backendNodeId) {
1819
+ continue;
1820
+ }
1821
+ try {
1822
+ await this.ensureRootNode();
1823
+ const pushResult = await this.cdp.send(
1824
+ "DOM.pushNodesByBackendIdsToFrontend",
1825
+ {
1826
+ backendNodeIds: [backendNodeId]
1827
+ }
1828
+ );
1829
+ if (pushResult.nodeIds?.[0]) {
1830
+ return {
1831
+ nodeId: pushResult.nodeIds[0],
1832
+ backendNodeId,
1833
+ selector,
1834
+ waitedMs: 0
1835
+ };
1836
+ }
1837
+ } catch {
1838
+ }
1839
+ }
1840
+ }
1841
+ const cssSelectors = selectorList.filter((s) => !s.startsWith("ref:"));
1842
+ if (cssSelectors.length === 0) {
1843
+ return null;
1844
+ }
1845
+ const result = await waitForAnyElement(this.cdp, cssSelectors, {
1846
+ state: "visible",
1847
+ timeout,
1848
+ contextId: this.currentFrameContextId ?? void 0
1849
+ });
1850
+ if (!result.success || !result.selector) {
1851
+ return null;
1852
+ }
1853
+ await this.ensureRootNode();
1854
+ const queryResult = await this.cdp.send("DOM.querySelector", {
1855
+ nodeId: this.rootNodeId,
1856
+ selector: result.selector
1857
+ });
1858
+ if (queryResult.nodeId) {
1859
+ const describeResult2 = await this.cdp.send(
1860
+ "DOM.describeNode",
1861
+ { nodeId: queryResult.nodeId }
1862
+ );
1863
+ return {
1864
+ nodeId: queryResult.nodeId,
1865
+ backendNodeId: describeResult2.node.backendNodeId,
1866
+ selector: result.selector,
1867
+ waitedMs: result.waitedMs
1868
+ };
1869
+ }
1870
+ const deepQueryResult = await this.evaluateInFrame(
1871
+ `(() => {
1872
+ ${DEEP_QUERY_SCRIPT}
1873
+ return deepQuery(${JSON.stringify(result.selector)});
1874
+ })()`,
1875
+ { returnByValue: false }
1876
+ );
1877
+ if (!deepQueryResult.result.objectId) {
1878
+ return null;
1879
+ }
1880
+ const nodeResult = await this.cdp.send("DOM.requestNode", {
1881
+ objectId: deepQueryResult.result.objectId
1882
+ });
1883
+ if (!nodeResult.nodeId) {
1884
+ return null;
1885
+ }
1886
+ const describeResult = await this.cdp.send(
1887
+ "DOM.describeNode",
1888
+ { nodeId: nodeResult.nodeId }
1889
+ );
1890
+ return {
1891
+ nodeId: nodeResult.nodeId,
1892
+ backendNodeId: describeResult.node.backendNodeId,
1893
+ selector: result.selector,
1894
+ waitedMs: result.waitedMs
1895
+ };
1896
+ }
1897
+ /**
1898
+ * Ensure we have a valid root node ID
1899
+ */
1900
+ async ensureRootNode() {
1901
+ if (this.rootNodeId) return;
1902
+ const doc = await this.cdp.send("DOM.getDocument", {
1903
+ depth: 0
1904
+ });
1905
+ this.rootNodeId = doc.root.nodeId;
1906
+ }
1907
+ /**
1908
+ * Execute Runtime.evaluate in the current frame context
1909
+ * Automatically injects contextId when in an iframe
1910
+ */
1911
+ async evaluateInFrame(expression, options = {}) {
1912
+ const params = {
1913
+ expression,
1914
+ returnByValue: options.returnByValue ?? true,
1915
+ awaitPromise: options.awaitPromise ?? false
1916
+ };
1917
+ if (this.currentFrameContextId !== null) {
1918
+ params["contextId"] = this.currentFrameContextId;
1919
+ }
1920
+ return this.cdp.send("Runtime.evaluate", params);
1921
+ }
1922
+ /**
1923
+ * Scroll an element into view
1924
+ */
1925
+ async scrollIntoView(nodeId) {
1926
+ await this.cdp.send("DOM.scrollIntoViewIfNeeded", { nodeId });
1927
+ }
1928
+ /**
1929
+ * Get element box model (position and dimensions)
1930
+ */
1931
+ async getBoxModel(nodeId) {
1932
+ try {
1933
+ const result = await this.cdp.send("DOM.getBoxModel", {
1934
+ nodeId
1935
+ });
1936
+ return result.model;
1937
+ } catch {
1938
+ return null;
1939
+ }
1940
+ }
1941
+ /**
1942
+ * Click an element by node ID
1943
+ */
1944
+ async clickElement(nodeId) {
1945
+ const box = await this.getBoxModel(nodeId);
1946
+ if (!box) {
1947
+ throw new Error("Could not get element box model for click");
1948
+ }
1949
+ const x = box.content[0] + box.width / 2;
1950
+ const y = box.content[1] + box.height / 2;
1951
+ await this.cdp.send("Input.dispatchMouseEvent", {
1952
+ type: "mousePressed",
1953
+ x,
1954
+ y,
1955
+ button: "left",
1956
+ clickCount: 1
1957
+ });
1958
+ await this.cdp.send("Input.dispatchMouseEvent", {
1959
+ type: "mouseReleased",
1960
+ x,
1961
+ y,
1962
+ button: "left",
1963
+ clickCount: 1
1964
+ });
1965
+ }
1966
+ };
1967
+ function sleep2(ms) {
1968
+ return new Promise((resolve) => setTimeout(resolve, ms));
1969
+ }
1970
+
1971
+ // src/browser/browser.ts
1972
+ var Browser = class _Browser {
1973
+ cdp;
1974
+ providerSession;
1975
+ pages = /* @__PURE__ */ new Map();
1976
+ constructor(cdp, _provider, providerSession, _options) {
1977
+ this.cdp = cdp;
1978
+ this.providerSession = providerSession;
1979
+ }
1980
+ /**
1981
+ * Connect to a browser instance
1982
+ */
1983
+ static async connect(options) {
1984
+ const provider = createProvider(options);
1985
+ const session = await provider.createSession(options.session);
1986
+ const cdp = await createCDPClient(session.wsUrl, {
1987
+ debug: options.debug,
1988
+ timeout: options.timeout
1989
+ });
1990
+ return new _Browser(cdp, provider, session, options);
1991
+ }
1992
+ /**
1993
+ * Get or create a page by name
1994
+ * If no name is provided, returns the first available page or creates a new one
1995
+ */
1996
+ async page(name) {
1997
+ const pageName = name ?? "default";
1998
+ const cached = this.pages.get(pageName);
1999
+ if (cached) return cached;
2000
+ const targets = await this.cdp.send("Target.getTargets");
2001
+ const pageTargets = targets.targetInfos.filter((t) => t.type === "page");
2002
+ let targetId;
2003
+ if (pageTargets.length > 0) {
2004
+ targetId = pageTargets[0].targetId;
2005
+ } else {
2006
+ const result = await this.cdp.send("Target.createTarget", {
2007
+ url: "about:blank"
2008
+ });
2009
+ targetId = result.targetId;
2010
+ }
2011
+ await this.cdp.attachToTarget(targetId);
2012
+ const page = new Page(this.cdp);
2013
+ await page.init();
2014
+ this.pages.set(pageName, page);
2015
+ return page;
2016
+ }
2017
+ /**
2018
+ * Create a new page (tab)
2019
+ */
2020
+ async newPage(url = "about:blank") {
2021
+ const result = await this.cdp.send("Target.createTarget", {
2022
+ url
2023
+ });
2024
+ await this.cdp.attachToTarget(result.targetId);
2025
+ const page = new Page(this.cdp);
2026
+ await page.init();
2027
+ const name = `page-${this.pages.size + 1}`;
2028
+ this.pages.set(name, page);
2029
+ return page;
2030
+ }
2031
+ /**
2032
+ * Close a page by name
2033
+ */
2034
+ async closePage(name) {
2035
+ const page = this.pages.get(name);
2036
+ if (!page) return;
2037
+ const targets = await this.cdp.send("Target.getTargets");
2038
+ const pageTargets = targets.targetInfos.filter((t) => t.type === "page");
2039
+ if (pageTargets.length > 0) {
2040
+ await this.cdp.send("Target.closeTarget", {
2041
+ targetId: pageTargets[0].targetId
2042
+ });
2043
+ }
2044
+ this.pages.delete(name);
2045
+ }
2046
+ /**
2047
+ * Get the WebSocket URL for this browser connection
2048
+ */
2049
+ get wsUrl() {
2050
+ return this.providerSession.wsUrl;
2051
+ }
2052
+ /**
2053
+ * Get the provider session ID (for resumption)
2054
+ */
2055
+ get sessionId() {
2056
+ return this.providerSession.sessionId;
2057
+ }
2058
+ /**
2059
+ * Get provider metadata
2060
+ */
2061
+ get metadata() {
2062
+ return this.providerSession.metadata;
2063
+ }
2064
+ /**
2065
+ * Check if connected
2066
+ */
2067
+ get isConnected() {
2068
+ return this.cdp.isConnected;
2069
+ }
2070
+ /**
2071
+ * Disconnect from the browser (keeps provider session alive for reconnection)
2072
+ */
2073
+ async disconnect() {
2074
+ this.pages.clear();
2075
+ await this.cdp.close();
2076
+ }
2077
+ /**
2078
+ * Close the browser session completely
2079
+ */
2080
+ async close() {
2081
+ this.pages.clear();
2082
+ await this.cdp.close();
2083
+ await this.providerSession.close();
2084
+ }
2085
+ /**
2086
+ * Get the underlying CDP client (for advanced usage)
2087
+ */
2088
+ get cdpClient() {
2089
+ return this.cdp;
2090
+ }
2091
+ };
2092
+ function connect(options) {
2093
+ return Browser.connect(options);
2094
+ }
2095
+
2096
+ export {
2097
+ RequestInterceptor,
2098
+ waitForElement,
2099
+ waitForAnyElement,
2100
+ waitForNavigation,
2101
+ waitForNetworkIdle,
2102
+ ElementNotFoundError,
2103
+ TimeoutError,
2104
+ NavigationError,
2105
+ Page,
2106
+ Browser,
2107
+ connect
2108
+ };