Skip to main content

playwright_rs/protocol/
browser_context.rs

1// BrowserContext protocol object
2//
3// Represents an isolated browser context (session) within a browser instance.
4// Multiple contexts can exist in a single browser, each with its own cookies,
5// cache, and local storage.
6
7use crate::api::launch_options::IgnoreDefaultArgs;
8use crate::error::{Error, Result};
9use crate::protocol::api_request_context::APIRequestContext;
10use crate::protocol::cdp_session::CDPSession;
11use crate::protocol::event_waiter::EventWaiter;
12use crate::protocol::route::UnrouteBehavior;
13use crate::protocol::tracing::Tracing;
14use crate::protocol::{
15    Browser, Download, Frame, Page, ProxySettings, Request, ResponseObject, Route,
16};
17use crate::server::channel::Channel;
18use crate::server::channel_owner::{ChannelOwner, ChannelOwnerImpl, ParentOrConnection};
19use crate::server::connection::ConnectionExt;
20use serde::{Deserialize, Serialize};
21use serde_json::Value;
22use std::any::Any;
23use std::collections::HashMap;
24use std::future::Future;
25use std::pin::Pin;
26use std::sync::atomic::{AtomicBool, Ordering};
27use std::sync::{Arc, Mutex};
28
29use crate::protocol::event_registry::{EventRegistry, Handler};
30use tokio::sync::oneshot;
31
32/// BrowserContext represents an isolated browser session.
33///
34/// Contexts are isolated environments within a browser instance. Each context
35/// has its own cookies, cache, and local storage, enabling independent sessions
36/// without interference.
37///
38/// # Example
39///
40/// ```no_run
41/// use playwright_rs::protocol::Playwright;
42///
43/// #[tokio::main]
44/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
45///     let playwright = Playwright::launch().await?;
46///     let browser = playwright.chromium().launch().await?;
47///
48///     // Create isolated contexts
49///     let context1 = browser.new_context().await?;
50///     let context2 = browser.new_context().await?;
51///
52///     // Create pages in each context
53///     let page1 = context1.new_page().await?;
54///     let page2 = context2.new_page().await?;
55///
56///     // Access all pages in a context
57///     let pages = context1.pages();
58///     assert_eq!(pages.len(), 1);
59///
60///     // Access the browser from a context
61///     let ctx_browser = context1.browser().unwrap();
62///     assert_eq!(ctx_browser.name(), browser.name());
63///
64///     // App mode: access initial page created automatically
65///     let chromium = playwright.chromium();
66///     let app_context = chromium
67///         .launch_persistent_context_with_options(
68///             "/tmp/app-data",
69///             playwright_rs::protocol::BrowserContextOptions::builder()
70///                 .args(vec!["--app=https://example.com".to_string()])
71///                 .headless(true)
72///                 .build()
73///         )
74///         .await?;
75///
76///     // Get the initial page (don't create a new one!)
77///     let app_pages = app_context.pages();
78///     if !app_pages.is_empty() {
79///         let initial_page = &app_pages[0];
80///         // Use the initial page...
81///     }
82///
83///     // Cleanup
84///     context1.close().await?;
85///     context2.close().await?;
86///     app_context.close().await?;
87///     browser.close().await?;
88///     Ok(())
89/// }
90/// ```
91///
92/// See: <https://playwright.dev/docs/api/class-browsercontext>
93/// Type alias for boxed route handler future
94type RouteHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
95
96/// Type alias for boxed binding callback future
97type BindingCallbackFuture = Pin<Box<dyn Future<Output = serde_json::Value> + Send>>;
98
99/// Type alias for boxed service worker handler future
100type ServiceWorkerHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
101
102/// Context-level service worker event handler
103type ServiceWorkerHandler =
104    Arc<dyn Fn(crate::protocol::Worker) -> ServiceWorkerHandlerFuture + Send + Sync>;
105
106/// Context-level event handlers for the 1.60 lifecycle events. These are not
107/// wire events on the context channel; they are forwarded from each page's
108/// own events (see `wire_*` helpers), matching how the upstream clients
109/// synthesize them.
110type CtxHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
111/// Context `download` handler (receives the page's `Download`).
112type DownloadHandler = Arc<dyn Fn(Download) -> CtxHandlerFuture + Send + Sync>;
113/// Context frame handler (`frameAttached`/`frameDetached`/`frameNavigated`).
114type CtxFrameHandler = Arc<dyn Fn(Frame) -> CtxHandlerFuture + Send + Sync>;
115/// Context page-lifecycle handler (`pageLoad`/`pageClose`), receives the `Page`.
116type PageEventHandler = Arc<dyn Fn(Page) -> CtxHandlerFuture + Send + Sync>;
117
118/// Binding callback: receives deserialized JS args, returns a JSON value
119type BindingCallback = Arc<dyn Fn(Vec<serde_json::Value>) -> BindingCallbackFuture + Send + Sync>;
120
121/// Type alias for boxed WebSocketRoute handler future
122type WsRouteHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
123
124/// Storage for a single route handler
125#[derive(Clone)]
126struct RouteHandlerEntry {
127    pattern: String,
128    handler: Arc<dyn Fn(Route) -> RouteHandlerFuture + Send + Sync>,
129}
130
131/// Storage for a single WebSocket route handler entry
132#[derive(Clone)]
133struct ContextWsRouteHandlerEntry {
134    pattern: String,
135    handler: Arc<dyn Fn(crate::protocol::WebSocketRoute) -> WsRouteHandlerFuture + Send + Sync>,
136}
137
138#[derive(Clone)]
139pub struct BrowserContext {
140    base: ChannelOwnerImpl,
141    /// Browser instance that owns this context (None for persistent contexts)
142    browser: Option<Browser>,
143    /// All open pages in this context
144    pages: Arc<Mutex<Vec<Page>>>,
145    /// Route handlers for context-level network interception
146    route_handlers: Arc<Mutex<Vec<RouteHandlerEntry>>>,
147    /// APIRequestContext GUID from initializer (resolved lazily)
148    request_context_guid: Option<String>,
149    /// Tracing GUID from initializer (resolved lazily)
150    tracing_guid: Option<String>,
151    /// Debugger GUID from initializer (resolved lazily)
152    debugger_guid: Option<String>,
153    /// Default action timeout for all pages in this context (milliseconds), stored as f64 bits.
154    default_timeout_ms: Arc<std::sync::atomic::AtomicU64>,
155    /// Default navigation timeout for all pages in this context (milliseconds), stored as f64 bits.
156    default_navigation_timeout_ms: Arc<std::sync::atomic::AtomicU64>,
157    /// `page` event: handlers and one-shot `expect_page` waiters.
158    page_events: Arc<EventRegistry<Page>>,
159    /// `close` event: one-time transition; `dispatch_all` wakes every waiter.
160    close_events: Arc<EventRegistry<()>>,
161    /// `request` event: handlers and one-shot waiters.
162    request: Arc<EventRegistry<Request>>,
163    /// `requestFinished` event handlers (no `expect_*`; waiter queue stays empty).
164    request_finished: Arc<EventRegistry<Request>>,
165    /// `requestFailed` event handlers (no `expect_*`; waiter queue stays empty).
166    request_failed: Arc<EventRegistry<Request>>,
167    /// `response` event: handlers and one-shot waiters.
168    response: Arc<EventRegistry<ResponseObject>>,
169    /// `dialog` event handlers (no `expect_*`; waiter queue stays empty).
170    dialog: Arc<EventRegistry<crate::protocol::Dialog>>,
171    /// Registered binding callbacks keyed by name (for expose_function / expose_binding)
172    binding_callbacks: Arc<Mutex<HashMap<String, BindingCallback>>>,
173    /// `console` event: handlers and one-shot `expect_console_message` waiters.
174    console: Arc<EventRegistry<crate::protocol::ConsoleMessage>>,
175    /// `pageError`-derived weberror event: handlers and one-shot waiters.
176    weberror: Arc<EventRegistry<crate::protocol::WebError>>,
177    /// Context-level service worker event handlers (fired when a service worker is registered)
178    serviceworker_handlers: Arc<Mutex<Vec<ServiceWorkerHandler>>>,
179    /// Context-level lifecycle handlers, forwarded from each page's events.
180    download_handlers: Arc<Mutex<Vec<DownloadHandler>>>,
181    frame_attached_handlers: Arc<Mutex<Vec<CtxFrameHandler>>>,
182    frame_detached_handlers: Arc<Mutex<Vec<CtxFrameHandler>>>,
183    frame_navigated_handlers: Arc<Mutex<Vec<CtxFrameHandler>>>,
184    page_load_handlers: Arc<Mutex<Vec<PageEventHandler>>>,
185    page_close_handlers: Arc<Mutex<Vec<PageEventHandler>>>,
186    /// One-shot senders waiting for the next "serviceworker" event (expect_event("serviceworker"))
187    serviceworker_waiters: Arc<Mutex<Vec<oneshot::Sender<crate::protocol::Worker>>>>,
188    /// Active service workers tracked via "serviceWorker" events
189    service_workers_list: Arc<Mutex<Vec<crate::protocol::Worker>>>,
190    /// WebSocketRoute handlers for route_web_socket()
191    ws_route_handlers: Arc<Mutex<Vec<ContextWsRouteHandlerEntry>>>,
192    /// Whether this context has been closed.
193    /// Set to true when close() is called or a "close" event is received.
194    is_closed: Arc<AtomicBool>,
195}
196
197impl BrowserContext {
198    /// Creates a new BrowserContext from protocol initialization
199    ///
200    /// This is called by the object factory when the server sends a `__create__` message
201    /// for a BrowserContext object.
202    ///
203    /// # Arguments
204    ///
205    /// * `parent` - The parent Browser object
206    /// * `type_name` - The protocol type name ("BrowserContext")
207    /// * `guid` - The unique identifier for this context
208    /// * `initializer` - The initialization data from the server
209    ///
210    /// # Errors
211    ///
212    /// Returns error if initializer is malformed
213    pub fn new(
214        parent: Arc<dyn ChannelOwner>,
215        type_name: String,
216        guid: Arc<str>,
217        initializer: Value,
218    ) -> Result<Self> {
219        // Extract APIRequestContext GUID from initializer before moving it
220        let request_context_guid = initializer
221            .get("requestContext")
222            .and_then(|v| v.get("guid"))
223            .and_then(|v| v.as_str())
224            .map(|s| s.to_string());
225
226        // Extract Tracing GUID from initializer before moving it
227        let tracing_guid = initializer
228            .get("tracing")
229            .and_then(|v| v.get("guid"))
230            .and_then(|v| v.as_str())
231            .map(|s| s.to_string());
232
233        // Extract Debugger GUID from initializer before moving it
234        let debugger_guid = initializer
235            .get("debugger")
236            .and_then(|v| v.get("guid"))
237            .and_then(|v| v.as_str())
238            .map(|s| s.to_string());
239
240        let base = ChannelOwnerImpl::new(
241            ParentOrConnection::Parent(parent.clone()),
242            type_name,
243            guid,
244            initializer,
245        );
246
247        // Store browser reference if parent is a Browser
248        // Returns None only for special contexts (Android, Electron) where parent is not a Browser
249        // For both regular contexts and persistent contexts, parent is a Browser instance
250        let browser = parent.as_any().downcast_ref::<Browser>().cloned();
251
252        let context = Self {
253            base,
254            browser,
255            pages: Arc::new(Mutex::new(Vec::new())),
256            route_handlers: Arc::new(Mutex::new(Vec::new())),
257            request_context_guid,
258            tracing_guid,
259            debugger_guid,
260            default_timeout_ms: Arc::new(std::sync::atomic::AtomicU64::new(
261                crate::DEFAULT_TIMEOUT_MS.to_bits(),
262            )),
263            default_navigation_timeout_ms: Arc::new(std::sync::atomic::AtomicU64::new(
264                crate::DEFAULT_TIMEOUT_MS.to_bits(),
265            )),
266            page_events: EventRegistry::new("page"),
267            close_events: EventRegistry::new("close"),
268            request: EventRegistry::new("request"),
269            request_finished: EventRegistry::new("requestFinished"),
270            request_failed: EventRegistry::new("requestFailed"),
271            response: EventRegistry::new("response"),
272            dialog: EventRegistry::new("dialog"),
273            binding_callbacks: Arc::new(Mutex::new(HashMap::new())),
274            console: EventRegistry::new("console"),
275            weberror: EventRegistry::new("weberror"),
276            serviceworker_handlers: Arc::new(Mutex::new(Vec::new())),
277            download_handlers: Arc::new(Mutex::new(Vec::new())),
278            frame_attached_handlers: Arc::new(Mutex::new(Vec::new())),
279            frame_detached_handlers: Arc::new(Mutex::new(Vec::new())),
280            frame_navigated_handlers: Arc::new(Mutex::new(Vec::new())),
281            page_load_handlers: Arc::new(Mutex::new(Vec::new())),
282            page_close_handlers: Arc::new(Mutex::new(Vec::new())),
283            serviceworker_waiters: Arc::new(Mutex::new(Vec::new())),
284            service_workers_list: Arc::new(Mutex::new(Vec::new())),
285            ws_route_handlers: Arc::new(Mutex::new(Vec::new())),
286            is_closed: Arc::new(AtomicBool::new(false)),
287        };
288
289        // Enable dialog and console event subscriptions eagerly.
290        // Console events must be subscribed to receive them without a registered handler,
291        // enabling the console_messages() and page_errors() passive accumulators on Page.
292        let channel = context.channel().clone();
293        tokio::spawn(async move {
294            _ = channel.update_subscription("dialog", true).await;
295            _ = channel.update_subscription("console", true).await;
296        });
297
298        // Note: Selectors registration is done by the caller (e.g. Browser::new_context())
299        // after this object is returned, so that add_context() can be awaited properly.
300
301        Ok(context)
302    }
303
304    /// Returns the channel for sending protocol messages
305    ///
306    /// Used internally for sending RPC calls to the context.
307    fn channel(&self) -> &Channel {
308        self.base.channel()
309    }
310
311    /// Adds a script which would be evaluated in one of the following scenarios:
312    ///
313    /// - Whenever a page is created in the browser context or is navigated.
314    /// - Whenever a child frame is attached or navigated in any page in the browser context.
315    ///
316    /// The script is evaluated after the document was created but before any of its scripts
317    /// were run. This is useful to amend the JavaScript environment, e.g. to seed Math.random.
318    ///
319    /// # Arguments
320    ///
321    /// * `script` - Script to be evaluated in all pages in the browser context.
322    ///
323    /// # Errors
324    ///
325    /// Returns error if:
326    /// - Context has been closed
327    /// - Communication with browser process fails
328    ///
329    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-add-init-script>
330    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
331    pub async fn add_init_script(&self, script: &str) -> Result<()> {
332        self.channel()
333            .send_no_result("addInitScript", serde_json::json!({ "source": script }))
334            .await
335    }
336
337    /// Creates a new page in this browser context.
338    ///
339    /// Pages are isolated tabs/windows within a context. Each page starts
340    /// at "about:blank" and can be navigated independently.
341    ///
342    /// # Errors
343    ///
344    /// Returns error if:
345    /// - Context has been closed
346    /// - Communication with browser process fails
347    ///
348    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-new-page>
349    #[tracing::instrument(level = "info", skip_all, fields(guid = %self.guid()))]
350    pub async fn new_page(&self) -> Result<Page> {
351        // Response contains the GUID of the created Page
352        #[derive(Deserialize)]
353        struct NewPageResponse {
354            page: GuidRef,
355        }
356
357        #[derive(Deserialize)]
358        struct GuidRef {
359            #[serde(deserialize_with = "crate::server::connection::deserialize_arc_str")]
360            guid: Arc<str>,
361        }
362
363        // Send newPage RPC to server
364        let response: NewPageResponse = self
365            .channel()
366            .send("newPage", serde_json::json!({}))
367            .await?;
368
369        // Retrieve and downcast the Page object from the connection registry
370        let page: Page = self
371            .connection()
372            .get_typed::<Page>(&response.page.guid)
373            .await?;
374
375        // Note: Don't track the page here - it will be tracked via the "page" event
376        // that Playwright server sends automatically when a page is created.
377        // Tracking it here would create duplicates.
378
379        // Propagate context-level timeout defaults to the new page
380        let ctx_timeout = self.default_timeout_ms();
381        let ctx_nav_timeout = self.default_navigation_timeout_ms();
382        if ctx_timeout.to_bits() != crate::DEFAULT_TIMEOUT_MS.to_bits() {
383            page.set_default_timeout(ctx_timeout).await;
384        }
385        if ctx_nav_timeout.to_bits() != crate::DEFAULT_TIMEOUT_MS.to_bits() {
386            page.set_default_navigation_timeout(ctx_nav_timeout).await;
387        }
388
389        Ok(page)
390    }
391
392    /// Returns all open pages in the context.
393    ///
394    /// This method provides a snapshot of all currently active pages that belong
395    /// to this browser context instance. Pages created via `new_page()` and popup
396    /// pages opened through user interactions are included.
397    ///
398    /// In persistent contexts launched with `--app=url`, this will include the
399    /// initial page created automatically by Playwright.
400    ///
401    /// # Errors
402    ///
403    /// This method does not return errors. It provides a snapshot of pages at
404    /// the time of invocation.
405    ///
406    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-pages>
407    pub fn pages(&self) -> Vec<Page> {
408        self.pages.lock().unwrap().clone()
409    }
410
411    /// Returns all active service workers registered in this browser context.
412    ///
413    /// Service workers are accumulated as they are registered (`serviceWorker` event).
414    /// Each call returns a snapshot of the current list.
415    ///
416    /// Note: Testing service workers typically requires HTTPS. In plain HTTP or
417    /// `about:blank` contexts this list is empty.
418    ///
419    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-service-workers>
420    pub fn service_workers(&self) -> Vec<crate::protocol::Worker> {
421        self.service_workers_list.lock().unwrap().clone()
422    }
423
424    /// Returns the browser instance that owns this context.
425    ///
426    /// Returns `None` only for contexts created outside of normal browser
427    /// (e.g., Android or Electron contexts). For both regular contexts and
428    /// persistent contexts, this returns the owning Browser instance.
429    ///
430    /// # Errors
431    ///
432    /// This method does not return errors.
433    ///
434    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-browser>
435    pub fn browser(&self) -> Option<Browser> {
436        self.browser.clone()
437    }
438
439    /// Returns the APIRequestContext associated with this context.
440    ///
441    /// The APIRequestContext is created automatically by the server for each
442    /// BrowserContext. It enables performing HTTP requests and is used internally
443    /// by `Route::fetch()`.
444    ///
445    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-request>
446    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
447    pub async fn request(&self) -> Result<APIRequestContext> {
448        let guid = self.request_context_guid.as_ref().ok_or_else(|| {
449            crate::error::Error::ProtocolError(
450                "No APIRequestContext available for this context".to_string(),
451            )
452        })?;
453
454        self.connection().get_typed::<APIRequestContext>(guid).await
455    }
456
457    /// Creates a new Chrome DevTools Protocol session for the given page.
458    ///
459    /// CDPSession provides low-level access to the Chrome DevTools Protocol.
460    /// This method is only available in Chromium-based browsers.
461    ///
462    /// # Arguments
463    ///
464    /// * `page` - The page to create a CDP session for
465    ///
466    /// # Errors
467    ///
468    /// Returns error if:
469    /// - The browser is not Chromium-based
470    /// - Context has been closed
471    /// - Communication with browser process fails
472    ///
473    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-new-cdp-session>
474    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), page_guid = %page.guid()))]
475    pub async fn new_cdp_session(&self, page: &Page) -> Result<CDPSession> {
476        #[derive(serde::Deserialize)]
477        struct NewCDPSessionResponse {
478            session: GuidRef,
479        }
480
481        #[derive(serde::Deserialize)]
482        struct GuidRef {
483            #[serde(deserialize_with = "crate::server::connection::deserialize_arc_str")]
484            guid: Arc<str>,
485        }
486
487        let response: NewCDPSessionResponse = self
488            .channel()
489            .send(
490                "newCDPSession",
491                serde_json::json!({ "page": { "guid": page.guid() } }),
492            )
493            .await?;
494
495        self.connection()
496            .get_typed::<CDPSession>(&response.session.guid)
497            .await
498    }
499
500    /// Returns the Tracing object for this browser context.
501    ///
502    /// The Tracing object is created automatically by the Playwright server for each
503    /// BrowserContext. Use it to start and stop trace recording.
504    ///
505    /// # Errors
506    ///
507    /// Returns error if no Tracing object is available for this context (rare,
508    /// should not happen in normal usage).
509    ///
510    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-tracing>
511    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
512    pub async fn tracing(&self) -> Result<Tracing> {
513        let guid = self.tracing_guid.as_ref().ok_or_else(|| {
514            crate::error::Error::ProtocolError(
515                "No Tracing object available for this context".to_string(),
516            )
517        })?;
518
519        self.connection().get_typed::<Tracing>(guid).await
520    }
521
522    /// Returns the [`Debugger`](crate::protocol::Debugger) for this context.
523    ///
524    /// The Debugger surfaces programmatic control of Playwright Inspector's
525    /// "PAUSED" overlay — `request_pause`, `resume`, `next`, `run_to`, and a
526    /// `pausedStateChanged` event. Used by IDE integrations and
527    /// inspector-style tools.
528    ///
529    /// See: <https://playwright.dev/docs/api/class-debugger>
530    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
531    pub async fn debugger(&self) -> Result<crate::protocol::Debugger> {
532        let guid = self.debugger_guid.as_ref().ok_or_else(|| {
533            crate::error::Error::ProtocolError(
534                "No Debugger object available for this context".to_string(),
535            )
536        })?;
537        self.connection()
538            .get_typed::<crate::protocol::Debugger>(guid)
539            .await
540    }
541
542    /// Returns the Clock object for this browser context.
543    ///
544    /// The Clock object enables fake timer control — install fake timers,
545    /// fast-forward time, pause/resume, and set fixed or system time.
546    ///
547    /// `page.clock()` delegates to this method via the page's parent context.
548    ///
549    /// See: <https://playwright.dev/docs/api/class-clock>
550    pub fn clock(&self) -> crate::protocol::clock::Clock {
551        crate::protocol::clock::Clock::new(self.channel().clone())
552    }
553
554    /// Manage the context's virtual WebAuthn authenticator:
555    /// install it, then register / list / delete passkeys for
556    /// `navigator.credentials` testing.
557    ///
558    /// See: <https://playwright.dev/docs/api/class-credentials>
559    pub fn credentials(&self) -> crate::protocol::Credentials {
560        crate::protocol::Credentials::new(self.channel().clone())
561    }
562
563    /// Closes the browser context and all its pages.
564    ///
565    /// This is a graceful operation that sends a close command to the context
566    /// and waits for it to shut down properly.
567    ///
568    /// # Errors
569    ///
570    /// Returns error if:
571    /// - Context has already been closed
572    /// - Communication with browser process fails
573    ///
574    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-close>
575    #[tracing::instrument(level = "info", skip_all, fields(guid = %self.guid()))]
576    pub async fn close(&self) -> Result<()> {
577        // Unregister from Selectors coordinator so closed channels are not sent future messages.
578        let selectors = self.connection().selectors();
579        selectors.remove_context(self.channel());
580
581        // Send close RPC to server
582        let result = self
583            .channel()
584            .send_no_result("close", serde_json::json!({}))
585            .await;
586        // Mark as closed regardless of error (best-effort)
587        self.is_closed.store(true, Ordering::Relaxed);
588        result
589    }
590
591    /// Sets the default timeout for all operations in this browser context.
592    ///
593    /// This applies to all pages already open in this context as well as pages
594    /// created subsequently. Pass `0` to disable timeouts.
595    ///
596    /// # Arguments
597    ///
598    /// * `timeout` - Timeout in milliseconds
599    ///
600    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-timeout>
601    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
602    pub async fn set_default_timeout(&self, timeout: f64) {
603        self.default_timeout_ms
604            .store(timeout.to_bits(), std::sync::atomic::Ordering::Relaxed);
605        let pages: Vec<Page> = self.pages.lock().unwrap().clone();
606        for page in pages {
607            page.set_default_timeout(timeout).await;
608        }
609        crate::protocol::page::set_timeout_and_notify(
610            self.channel(),
611            "setDefaultTimeoutNoReply",
612            timeout,
613        )
614        .await;
615    }
616
617    /// Sets the default timeout for navigation operations in this browser context.
618    ///
619    /// This applies to all pages already open in this context as well as pages
620    /// created subsequently. Pass `0` to disable timeouts.
621    ///
622    /// # Arguments
623    ///
624    /// * `timeout` - Timeout in milliseconds
625    ///
626    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-navigation-timeout>
627    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
628    pub async fn set_default_navigation_timeout(&self, timeout: f64) {
629        self.default_navigation_timeout_ms
630            .store(timeout.to_bits(), std::sync::atomic::Ordering::Relaxed);
631        let pages: Vec<Page> = self.pages.lock().unwrap().clone();
632        for page in pages {
633            page.set_default_navigation_timeout(timeout).await;
634        }
635        crate::protocol::page::set_timeout_and_notify(
636            self.channel(),
637            "setDefaultNavigationTimeoutNoReply",
638            timeout,
639        )
640        .await;
641    }
642
643    /// Returns the context's current default action timeout in milliseconds.
644    fn default_timeout_ms(&self) -> f64 {
645        f64::from_bits(
646            self.default_timeout_ms
647                .load(std::sync::atomic::Ordering::Relaxed),
648        )
649    }
650
651    /// Returns the context's current default navigation timeout in milliseconds.
652    fn default_navigation_timeout_ms(&self) -> f64 {
653        f64::from_bits(
654            self.default_navigation_timeout_ms
655                .load(std::sync::atomic::Ordering::Relaxed),
656        )
657    }
658
659    /// Pauses the browser context.
660    ///
661    /// This pauses the execution of all pages in the context.
662    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
663    pub async fn pause(&self) -> Result<()> {
664        self.channel()
665            .send_no_result("pause", serde_json::Value::Null)
666            .await
667    }
668
669    /// Returns storage state for this browser context.
670    ///
671    /// Contains current cookies and local storage snapshots.
672    ///
673    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-storage-state>
674    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
675    pub async fn storage_state(
676        &self,
677        options: impl Into<Option<StorageStateOptions>>,
678    ) -> Result<StorageState> {
679        let params = serde_json::to_value(options.into().unwrap_or_default())
680            .map_err(|e| Error::ProtocolError(format!("Failed to serialize options: {e}")))?;
681        let response: StorageState = self.channel().send("storageState", params).await?;
682        Ok(response)
683    }
684
685    /// Replaces this context's storage state in-place via the driver's
686    /// `setStorageState`, matching `browserContext.setStorageState()` in the
687    /// JS/Python APIs. Useful for restoring authentication state without
688    /// recreating the context.
689    ///
690    /// This is a **replace**, not a merge, and the driver is thorough about
691    /// it. Beyond installing the cookies, origins and passkeys carried by
692    /// `state`, it also:
693    ///
694    /// - clears the HTTP cache;
695    /// - clears storage (localStorage, sessionStorage, IndexedDB, service
696    ///   workers) for **every origin the context has visited**, not only the
697    ///   origins listed in `state`;
698    /// - when `state` carries no `credentials`, disposes an installed
699    ///   virtual authenticator along with its passkeys. Capture the state
700    ///   with [`StorageStateOptions::credentials`] if the context being
701    ///   restored into should keep WebAuthn working.
702    ///
703    /// No client-visible page is opened: the driver uses an internal page,
704    /// navigated to each origin, to apply origin-scoped state.
705    ///
706    /// # Errors
707    ///
708    /// Returns an error if the state fails to serialize or the driver
709    /// rejects it, or if the context has closed.
710    ///
711    /// # Example
712    ///
713    /// ```no_run
714    /// # use playwright_rs::Playwright;
715    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
716    /// # let pw = Playwright::launch().await?;
717    /// # let browser = pw.chromium().launch().await?;
718    /// # let context = browser.new_context().await?;
719    /// use playwright_rs::protocol::{Cookie, StorageState};
720    ///
721    /// // Restore session cookie
722    /// let state = StorageState::default().cookies(vec![
723    ///     Cookie::new("session", "token123")
724    ///         .domain("example.com")
725    ///         .path("/")
726    ///         .http_only(true)
727    ///         .secure(true)
728    ///         .same_site("Lax"),
729    /// ]);
730    /// context.set_storage_state(state).await?;
731    /// # Ok(())
732    /// # }
733    /// ```
734    ///
735    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-set-storage-state>
736    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
737    pub async fn set_storage_state(&self, state: StorageState) -> Result<()> {
738        // Delegates to the driver rather than reconstructing the state
739        // client-side. The previous implementation cleared cookies, re-added
740        // them, then opened a throwaway page per origin to replay
741        // localStorage through `evaluate`. That could only ever restore what
742        // it knew how to replay, so WebAuthn passkeys and
743        // IndexedDB were silently dropped, and every origin cost a page
744        // navigation.
745        let storage_state = serde_json::to_value(&state)
746            .map_err(|e| Error::ProtocolError(format!("Failed to serialize storage state: {e}")))?;
747
748        self.channel()
749            .send_no_result(
750                "setStorageState",
751                serde_json::json!({ "storageState": storage_state }),
752            )
753            .await
754    }
755
756    /// Returns whether this browser context has been closed.
757    ///
758    /// Returns `true` after [`close()`](Self::close) has been called on this context, or after the
759    /// context receives a close event from the server (e.g. when the browser is closed).
760    ///
761    /// Note: this reflects eventual state. If the context was closed by a server-initiated
762    /// event, `is_closed()` becomes `true` only after the "close" event has been received
763    /// and processed.
764    ///
765    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-is-closed>
766    pub fn is_closed(&self) -> bool {
767        self.is_closed.load(Ordering::Relaxed)
768    }
769
770    /// Adds cookies into this browser context.
771    ///
772    /// All pages within this context will have these cookies installed. Cookies can be granularly specified
773    /// with `name`, `value`, `url`, `domain`, `path`, `expires`, `httpOnly`, `secure`, `sameSite`.
774    ///
775    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-add-cookies>
776    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), count = cookies.len()))]
777    pub async fn add_cookies(&self, cookies: &[Cookie]) -> Result<()> {
778        self.channel()
779            .send_no_result(
780                "addCookies",
781                serde_json::json!({
782                    "cookies": cookies
783                }),
784            )
785            .await
786    }
787
788    /// Returns cookies for this browser context, optionally filtered by URLs.
789    ///
790    /// If `urls` is `None` or empty, all cookies are returned.
791    ///
792    /// # Arguments
793    ///
794    /// * `urls` - Optional list of URLs to filter cookies by
795    ///
796    /// # Errors
797    ///
798    /// Returns error if:
799    /// - Context has been closed
800    /// - Communication with browser process fails
801    ///
802    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-cookies>
803    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), count = tracing::field::Empty))]
804    pub async fn cookies(&self, urls: Option<&[&str]>) -> Result<Vec<Cookie>> {
805        let url_list: Vec<&str> = urls.unwrap_or(&[]).to_vec();
806        #[derive(serde::Deserialize)]
807        struct CookiesResponse {
808            cookies: Vec<Cookie>,
809        }
810        let response: CookiesResponse = self
811            .channel()
812            .send("cookies", serde_json::json!({ "urls": url_list }))
813            .await?;
814        Ok(response.cookies)
815    }
816
817    /// Clears cookies from this browser context, with optional filters.
818    ///
819    /// When called with no options, all cookies are removed. Use `ClearCookiesOptions`
820    /// to filter which cookies to clear by name, domain, or path.
821    ///
822    /// # Arguments
823    ///
824    /// * `options` - Optional filters for which cookies to clear
825    ///
826    /// # Errors
827    ///
828    /// Returns error if:
829    /// - Context has been closed
830    /// - Communication with browser process fails
831    ///
832    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-clear-cookies>
833    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
834    pub async fn clear_cookies(
835        &self,
836        options: impl Into<Option<ClearCookiesOptions>>,
837    ) -> Result<()> {
838        let options = options.into();
839        let params = match options {
840            None => serde_json::json!({}),
841            Some(opts) => serde_json::to_value(opts).unwrap_or(serde_json::json!({})),
842        };
843        self.channel().send_no_result("clearCookies", params).await
844    }
845
846    /// Sets extra HTTP headers that will be sent with every request from this context.
847    ///
848    /// These headers are merged with per-page extra headers set with `page.set_extra_http_headers()`.
849    /// If the page has specific headers that conflict, page-level headers take precedence.
850    ///
851    /// # Arguments
852    ///
853    /// * `headers` - Map of header names to values. All header names are lowercased.
854    ///
855    /// # Errors
856    ///
857    /// Returns error if:
858    /// - Context has been closed
859    /// - Communication with browser process fails
860    ///
861    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-set-extra-http-headers>
862    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), count = headers.len()))]
863    pub async fn set_extra_http_headers(&self, headers: HashMap<String, String>) -> Result<()> {
864        // Playwright protocol expects an array of {name, value} objects
865        let headers_array: Vec<serde_json::Value> = headers
866            .into_iter()
867            .map(|(name, value)| serde_json::json!({ "name": name, "value": value }))
868            .collect();
869        self.channel()
870            .send_no_result(
871                "setExtraHTTPHeaders",
872                serde_json::json!({ "headers": headers_array }),
873            )
874            .await
875    }
876
877    /// Grants browser permissions to the context.
878    ///
879    /// Permissions are granted for all pages in the context. The optional `origin`
880    /// in `GrantPermissionsOptions` restricts the grant to a specific URL origin.
881    ///
882    /// Common permissions: `"geolocation"`, `"notifications"`, `"camera"`,
883    /// `"microphone"`, `"clipboard-read"`, `"clipboard-write"`.
884    ///
885    /// # Arguments
886    ///
887    /// * `permissions` - List of permission strings to grant
888    /// * `options` - Optional options, including `origin` to restrict the grant
889    ///
890    /// # Errors
891    ///
892    /// Returns error if:
893    /// - Permission name is not recognised
894    /// - Context has been closed
895    /// - Communication with browser process fails
896    ///
897    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-grant-permissions>
898    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
899    pub async fn grant_permissions(
900        &self,
901        permissions: &[&str],
902        options: impl Into<Option<GrantPermissionsOptions>>,
903    ) -> Result<()> {
904        let options = options.into();
905        let mut params = serde_json::json!({ "permissions": permissions });
906        if let Some(opts) = options
907            && let Some(origin) = opts.origin
908        {
909            params["origin"] = serde_json::Value::String(origin);
910        }
911        self.channel()
912            .send_no_result("grantPermissions", params)
913            .await
914    }
915
916    /// Clears all permission overrides for this browser context.
917    ///
918    /// Reverts all permissions previously set with `grant_permissions()` back to
919    /// the browser default state.
920    ///
921    /// # Errors
922    ///
923    /// Returns error if:
924    /// - Context has been closed
925    /// - Communication with browser process fails
926    ///
927    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-clear-permissions>
928    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
929    pub async fn clear_permissions(&self) -> Result<()> {
930        self.channel()
931            .send_no_result("clearPermissions", serde_json::json!({}))
932            .await
933    }
934
935    /// Sets or clears the geolocation for all pages in this context.
936    ///
937    /// Pass `Some(Geolocation { ... })` to set a specific location, or `None` to
938    /// clear the override and let the browser handle location requests naturally.
939    ///
940    /// Note: Geolocation access requires the `"geolocation"` permission to be granted
941    /// via `grant_permissions()` for navigator.geolocation to succeed.
942    ///
943    /// # Arguments
944    ///
945    /// * `geolocation` - Location to set, or `None` to clear
946    ///
947    /// # Errors
948    ///
949    /// Returns error if:
950    /// - Latitude or longitude is out of range
951    /// - Context has been closed
952    /// - Communication with browser process fails
953    ///
954    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-set-geolocation>
955    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
956    pub async fn set_geolocation(&self, geolocation: Option<Geolocation>) -> Result<()> {
957        // Playwright protocol: omit the "geolocation" key entirely to clear;
958        // passing null causes a validation error on the server side.
959        let params = match geolocation {
960            Some(geo) => serde_json::json!({ "geolocation": geo }),
961            None => serde_json::json!({}),
962        };
963        self.channel()
964            .send_no_result("setGeolocation", params)
965            .await
966    }
967
968    /// Toggles the offline mode for this browser context.
969    ///
970    /// When `true`, all network requests from pages in this context will fail with
971    /// a network error. Set to `false` to restore network connectivity.
972    ///
973    /// # Arguments
974    ///
975    /// * `offline` - `true` to go offline, `false` to go back online
976    ///
977    /// # Errors
978    ///
979    /// Returns error if:
980    /// - Context has been closed
981    /// - Communication with browser process fails
982    ///
983    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-set-offline>
984    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), offline))]
985    pub async fn set_offline(&self, offline: bool) -> Result<()> {
986        self.channel()
987            .send_no_result("setOffline", serde_json::json!({ "offline": offline }))
988            .await
989    }
990
991    /// Registers a route handler for context-level network interception.
992    ///
993    /// Routes registered on a context apply to all pages within the context.
994    /// Page-level routes take precedence over context-level routes.
995    ///
996    /// # Arguments
997    ///
998    /// * `pattern` - URL pattern to match (supports glob patterns like "**/*.png")
999    /// * `handler` - Async closure that handles the route
1000    ///
1001    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-route>
1002    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), url = %pattern))]
1003    pub async fn route<F, Fut>(&self, pattern: &str, handler: F) -> Result<()>
1004    where
1005        F: Fn(Route) -> Fut + Send + Sync + 'static,
1006        Fut: Future<Output = Result<()>> + Send + 'static,
1007    {
1008        let handler =
1009            Arc::new(move |route: Route| -> RouteHandlerFuture { Box::pin(handler(route)) });
1010
1011        self.route_handlers.lock().unwrap().push(RouteHandlerEntry {
1012            pattern: pattern.to_string(),
1013            handler,
1014        });
1015
1016        self.enable_network_interception().await
1017    }
1018
1019    /// Removes route handler(s) matching the given URL pattern.
1020    ///
1021    /// # Arguments
1022    ///
1023    /// * `pattern` - URL pattern to remove handlers for
1024    ///
1025    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-unroute>
1026    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), url = %pattern))]
1027    pub async fn unroute(&self, pattern: &str) -> Result<()> {
1028        self.route_handlers
1029            .lock()
1030            .unwrap()
1031            .retain(|entry| entry.pattern != pattern);
1032        self.enable_network_interception().await
1033    }
1034
1035    /// Removes all registered route handlers.
1036    ///
1037    /// # Arguments
1038    ///
1039    /// * `behavior` - Optional behavior for in-flight handlers
1040    ///
1041    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-unroute-all>
1042    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1043    pub async fn unroute_all(&self, _behavior: Option<UnrouteBehavior>) -> Result<()> {
1044        self.route_handlers.lock().unwrap().clear();
1045        self.enable_network_interception().await
1046    }
1047
1048    /// Replays network requests from a HAR file recorded previously.
1049    ///
1050    /// Requests matching `options.url` (or all requests if omitted) will be
1051    /// served from the archive for every page in this context.  Unmatched
1052    /// requests are either aborted or passed through depending on
1053    /// `options.not_found` (`"abort"` is the default).
1054    ///
1055    /// # Arguments
1056    ///
1057    /// * `har_path` - Path to the `.har` file on disk
1058    /// * `options` - Optional settings (url filter, not_found policy, update mode)
1059    ///
1060    /// # Errors
1061    ///
1062    /// Returns error if:
1063    /// - `har_path` does not exist or cannot be read by the Playwright server
1064    /// - The Playwright server fails to open the archive
1065    ///
1066    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-route-from-har>
1067    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1068    pub async fn route_from_har(
1069        &self,
1070        har_path: &str,
1071        options: impl Into<Option<crate::protocol::RouteFromHarOptions>>,
1072    ) -> Result<()> {
1073        let options = options.into();
1074        let opts = options.unwrap_or_default();
1075        let not_found = opts.not_found.unwrap_or_else(|| "abort".to_string());
1076        let url_filter = opts.url.clone();
1077
1078        let abs_path = std::path::Path::new(har_path).canonicalize().map_err(|e| {
1079            Error::InvalidPath(format!(
1080                "route_from_har: cannot resolve '{}': {}",
1081                har_path, e
1082            ))
1083        })?;
1084        let abs_str = abs_path.to_string_lossy().into_owned();
1085
1086        let connection = self.connection();
1087        let local_utils = {
1088            let all = connection.all_objects_sync();
1089            all.into_iter()
1090                .find(|o| o.type_name() == "LocalUtils")
1091                .and_then(|o| {
1092                    o.as_any()
1093                        .downcast_ref::<crate::protocol::LocalUtils>()
1094                        .cloned()
1095                })
1096                .ok_or_else(|| {
1097                    Error::ProtocolError(
1098                        "route_from_har: LocalUtils not found in connection registry".to_string(),
1099                    )
1100                })?
1101        };
1102
1103        let har_id = local_utils.har_open(&abs_str).await?;
1104
1105        let pattern = url_filter.unwrap_or_else(|| "**/*".to_string());
1106
1107        let har_id_clone = har_id.clone();
1108        let local_utils_clone = local_utils.clone();
1109        let not_found_clone = not_found.clone();
1110
1111        self.route(&pattern, move |route| {
1112            let har_id = har_id_clone.clone();
1113            let local_utils = local_utils_clone.clone();
1114            let not_found = not_found_clone.clone();
1115            async move {
1116                let request = route.request();
1117                let req_url = request.url().to_string();
1118                let req_method = request.method().to_string();
1119
1120                let headers: Vec<serde_json::Value> = request
1121                    .headers()
1122                    .iter()
1123                    .map(|(k, v)| serde_json::json!({"name": k, "value": v}))
1124                    .collect();
1125
1126                let lookup = local_utils
1127                    .har_lookup(
1128                        &har_id,
1129                        &req_url,
1130                        &req_method,
1131                        headers,
1132                        None,
1133                        request.is_navigation_request(),
1134                    )
1135                    .await;
1136
1137                match lookup {
1138                    Err(e) => {
1139                        tracing::warn!("har_lookup error for {}: {}", req_url, e);
1140                        route.continue_(None).await
1141                    }
1142                    Ok(result) => match result.action.as_str() {
1143                        "redirect" => {
1144                            let redirect_url = result.redirect_url.unwrap_or_default();
1145                            let opts = crate::protocol::ContinueOptions::builder()
1146                                .url(redirect_url)
1147                                .build();
1148                            route.continue_(Some(opts)).await
1149                        }
1150                        "fulfill" => {
1151                            let status = result.status.unwrap_or(200);
1152
1153                            let body_bytes = result.body.as_deref().map(|b64| {
1154                                use base64::Engine;
1155                                base64::engine::general_purpose::STANDARD
1156                                    .decode(b64)
1157                                    .unwrap_or_default()
1158                            });
1159
1160                            let mut headers_map = std::collections::HashMap::new();
1161                            if let Some(raw_headers) = result.headers {
1162                                for h in raw_headers {
1163                                    if let (Some(name), Some(value)) = (
1164                                        h.get("name").and_then(|v| v.as_str()),
1165                                        h.get("value").and_then(|v| v.as_str()),
1166                                    ) {
1167                                        headers_map.insert(name.to_string(), value.to_string());
1168                                    }
1169                                }
1170                            }
1171
1172                            let mut builder =
1173                                crate::protocol::FulfillOptions::builder().status(status);
1174
1175                            if !headers_map.is_empty() {
1176                                builder = builder.headers(headers_map);
1177                            }
1178
1179                            if let Some(body) = body_bytes {
1180                                builder = builder.body(body);
1181                            }
1182
1183                            route.fulfill(Some(builder.build())).await
1184                        }
1185                        _ => {
1186                            if not_found == "fallback" {
1187                                route.fallback(None).await
1188                            } else {
1189                                route.abort(None).await
1190                            }
1191                        }
1192                    },
1193                }
1194            }
1195        })
1196        .await
1197    }
1198
1199    /// Adds a listener for the `page` event.
1200    ///
1201    /// The handler is called whenever a new page is created in this context,
1202    /// including popup pages opened through user interactions.
1203    ///
1204    /// # Arguments
1205    ///
1206    /// * `handler` - Async function that receives the new `Page`
1207    ///
1208    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-page>
1209    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1210    /// Subscribe to `reg`'s event if nothing is listening yet.
1211    ///
1212    /// Same contract as `Page::subscribe_if_idle`: the server only pushes an
1213    /// event once asked, so the first handler or `expect_*` turns it on, and
1214    /// the name comes from the registry rather than being restated per site.
1215    async fn subscribe_if_idle<T>(&self, reg: &EventRegistry<T>) {
1216        if reg.is_idle() {
1217            _ = self.channel().update_subscription(reg.name(), true).await;
1218        }
1219    }
1220
1221    pub async fn on_page<F, Fut>(&self, handler: F) -> Result<()>
1222    where
1223        F: Fn(Page) -> Fut + Send + Sync + 'static,
1224        Fut: Future<Output = Result<()>> + Send + 'static,
1225    {
1226        let handler: Handler<Page> = Arc::new(move |page| Box::pin(handler(page)));
1227        self.page_events.add_handler(handler);
1228        Ok(())
1229    }
1230
1231    /// Adds a listener for the `download` event: fired when any page in the
1232    /// context starts a download. Forwarded from each page's own `download`
1233    /// event.
1234    ///
1235    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-download>
1236    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1237    pub async fn on_download<F, Fut>(&self, handler: F) -> Result<()>
1238    where
1239        F: Fn(Download) -> Fut + Send + Sync + 'static,
1240        Fut: Future<Output = Result<()>> + Send + 'static,
1241    {
1242        let handler = Arc::new(move |d: Download| -> CtxHandlerFuture { Box::pin(handler(d)) });
1243        let was_empty = self.download_handlers.lock().unwrap().is_empty();
1244        self.download_handlers.lock().unwrap().push(handler);
1245        if was_empty {
1246            for page in self.pages() {
1247                Self::wire_download(&page, self.download_handlers.clone()).await;
1248            }
1249        }
1250        Ok(())
1251    }
1252
1253    /// Adds a listener for the `frameAttached` event: fired when a frame is
1254    /// attached in any page of the context. Forwarded from each page.
1255    ///
1256    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-frame-attached>
1257    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1258    pub async fn on_frame_attached<F, Fut>(&self, handler: F) -> Result<()>
1259    where
1260        F: Fn(Frame) -> Fut + Send + Sync + 'static,
1261        Fut: Future<Output = Result<()>> + Send + 'static,
1262    {
1263        let handler = Arc::new(move |f: Frame| -> CtxHandlerFuture { Box::pin(handler(f)) });
1264        let was_empty = self.frame_attached_handlers.lock().unwrap().is_empty();
1265        self.frame_attached_handlers.lock().unwrap().push(handler);
1266        if was_empty {
1267            for page in self.pages() {
1268                Self::wire_frame_attached(&page, self.frame_attached_handlers.clone()).await;
1269            }
1270        }
1271        Ok(())
1272    }
1273
1274    /// Adds a listener for the `frameDetached` event: fired when a frame is
1275    /// detached in any page of the context. Forwarded from each page.
1276    ///
1277    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-frame-detached>
1278    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1279    pub async fn on_frame_detached<F, Fut>(&self, handler: F) -> Result<()>
1280    where
1281        F: Fn(Frame) -> Fut + Send + Sync + 'static,
1282        Fut: Future<Output = Result<()>> + Send + 'static,
1283    {
1284        let handler = Arc::new(move |f: Frame| -> CtxHandlerFuture { Box::pin(handler(f)) });
1285        let was_empty = self.frame_detached_handlers.lock().unwrap().is_empty();
1286        self.frame_detached_handlers.lock().unwrap().push(handler);
1287        if was_empty {
1288            for page in self.pages() {
1289                Self::wire_frame_detached(&page, self.frame_detached_handlers.clone()).await;
1290            }
1291        }
1292        Ok(())
1293    }
1294
1295    /// Adds a listener for the `frameNavigated` event: fired when a frame
1296    /// navigates in any page of the context. Forwarded from each page.
1297    ///
1298    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-frame-navigated>
1299    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1300    pub async fn on_frame_navigated<F, Fut>(&self, handler: F) -> Result<()>
1301    where
1302        F: Fn(Frame) -> Fut + Send + Sync + 'static,
1303        Fut: Future<Output = Result<()>> + Send + 'static,
1304    {
1305        let handler = Arc::new(move |f: Frame| -> CtxHandlerFuture { Box::pin(handler(f)) });
1306        let was_empty = self.frame_navigated_handlers.lock().unwrap().is_empty();
1307        self.frame_navigated_handlers.lock().unwrap().push(handler);
1308        if was_empty {
1309            for page in self.pages() {
1310                Self::wire_frame_navigated(&page, self.frame_navigated_handlers.clone()).await;
1311            }
1312        }
1313        Ok(())
1314    }
1315
1316    /// Adds a listener for the `pageLoad` event: fired when any page in the
1317    /// context fires its `load` event. The handler receives that `Page`.
1318    ///
1319    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-page-load>
1320    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1321    pub async fn on_page_load<F, Fut>(&self, handler: F) -> Result<()>
1322    where
1323        F: Fn(Page) -> Fut + Send + Sync + 'static,
1324        Fut: Future<Output = Result<()>> + Send + 'static,
1325    {
1326        let handler = Arc::new(move |p: Page| -> CtxHandlerFuture { Box::pin(handler(p)) });
1327        let was_empty = self.page_load_handlers.lock().unwrap().is_empty();
1328        self.page_load_handlers.lock().unwrap().push(handler);
1329        if was_empty {
1330            for page in self.pages() {
1331                Self::wire_page_load(&page, self.page_load_handlers.clone()).await;
1332            }
1333        }
1334        Ok(())
1335    }
1336
1337    /// Adds a listener for the `pageClose` event: fired when any page in the
1338    /// context closes. The handler receives that `Page`.
1339    ///
1340    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-page-close>
1341    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1342    pub async fn on_page_close<F, Fut>(&self, handler: F) -> Result<()>
1343    where
1344        F: Fn(Page) -> Fut + Send + Sync + 'static,
1345        Fut: Future<Output = Result<()>> + Send + 'static,
1346    {
1347        let handler = Arc::new(move |p: Page| -> CtxHandlerFuture { Box::pin(handler(p)) });
1348        let was_empty = self.page_close_handlers.lock().unwrap().is_empty();
1349        self.page_close_handlers.lock().unwrap().push(handler);
1350        if was_empty {
1351            for page in self.pages() {
1352                Self::wire_page_close(&page, self.page_close_handlers.clone()).await;
1353            }
1354        }
1355        Ok(())
1356    }
1357
1358    // --- Forwarders: wire a single page's events to the context handler vecs. ---
1359    // Each (page, event) is wired exactly once: on the first context handler
1360    // (current pages) or at page creation (future pages, see the "page" event
1361    // dispatch). The vec is cloned out under the lock before awaiting handlers.
1362
1363    async fn wire_download(page: &Page, handlers: Arc<Mutex<Vec<DownloadHandler>>>) {
1364        let _ = page
1365            .on_download(move |d: Download| {
1366                let handlers = handlers.clone();
1367                async move {
1368                    let hs = handlers.lock().unwrap().clone();
1369                    for h in hs {
1370                        let _ = h(d.clone()).await;
1371                    }
1372                    Ok(())
1373                }
1374            })
1375            .await;
1376    }
1377
1378    async fn wire_frame_attached(page: &Page, handlers: Arc<Mutex<Vec<CtxFrameHandler>>>) {
1379        let _ = page
1380            .on_frameattached(move |f: Frame| {
1381                let handlers = handlers.clone();
1382                async move {
1383                    let hs = handlers.lock().unwrap().clone();
1384                    for h in hs {
1385                        let _ = h(f.clone()).await;
1386                    }
1387                    Ok(())
1388                }
1389            })
1390            .await;
1391    }
1392
1393    async fn wire_frame_detached(page: &Page, handlers: Arc<Mutex<Vec<CtxFrameHandler>>>) {
1394        let _ = page
1395            .on_framedetached(move |f: Frame| {
1396                let handlers = handlers.clone();
1397                async move {
1398                    let hs = handlers.lock().unwrap().clone();
1399                    for h in hs {
1400                        let _ = h(f.clone()).await;
1401                    }
1402                    Ok(())
1403                }
1404            })
1405            .await;
1406    }
1407
1408    async fn wire_frame_navigated(page: &Page, handlers: Arc<Mutex<Vec<CtxFrameHandler>>>) {
1409        let _ = page
1410            .on_framenavigated(move |f: Frame| {
1411                let handlers = handlers.clone();
1412                async move {
1413                    let hs = handlers.lock().unwrap().clone();
1414                    for h in hs {
1415                        let _ = h(f.clone()).await;
1416                    }
1417                    Ok(())
1418                }
1419            })
1420            .await;
1421    }
1422
1423    async fn wire_page_load(page: &Page, handlers: Arc<Mutex<Vec<PageEventHandler>>>) {
1424        let p = page.clone();
1425        let _ = page
1426            .on_load(move || {
1427                let handlers = handlers.clone();
1428                let p = p.clone();
1429                async move {
1430                    let hs = handlers.lock().unwrap().clone();
1431                    for h in hs {
1432                        let _ = h(p.clone()).await;
1433                    }
1434                    Ok(())
1435                }
1436            })
1437            .await;
1438    }
1439
1440    async fn wire_page_close(page: &Page, handlers: Arc<Mutex<Vec<PageEventHandler>>>) {
1441        let p = page.clone();
1442        let _ = page
1443            .on_close(move || {
1444                let handlers = handlers.clone();
1445                let p = p.clone();
1446                async move {
1447                    let hs = handlers.lock().unwrap().clone();
1448                    for h in hs {
1449                        let _ = h(p.clone()).await;
1450                    }
1451                    Ok(())
1452                }
1453            })
1454            .await;
1455    }
1456
1457    /// Adds a listener for the `close` event.
1458    ///
1459    /// The handler is called when the browser context is closed.
1460    ///
1461    /// # Arguments
1462    ///
1463    /// * `handler` - Async function called with no arguments when the context closes
1464    ///
1465    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-close>
1466    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1467    pub async fn on_close<F, Fut>(&self, handler: F) -> Result<()>
1468    where
1469        F: Fn() -> Fut + Send + Sync + 'static,
1470        Fut: Future<Output = Result<()>> + Send + 'static,
1471    {
1472        let handler: Handler<()> = Arc::new(move |()| Box::pin(handler()));
1473        self.close_events.add_handler(handler);
1474        Ok(())
1475    }
1476
1477    /// Adds a listener for the `request` event.
1478    ///
1479    /// The handler fires whenever a request is issued from any page in the context.
1480    /// This is equivalent to subscribing to `on_request` on each individual page,
1481    /// but covers all current and future pages of the context.
1482    ///
1483    /// Context-level handlers fire before page-level handlers.
1484    ///
1485    /// # Arguments
1486    ///
1487    /// * `handler` - Async function that receives the `Request`
1488    ///
1489    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-request>
1490    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1491    pub async fn on_request<F, Fut>(&self, handler: F) -> Result<()>
1492    where
1493        F: Fn(Request) -> Fut + Send + Sync + 'static,
1494        Fut: Future<Output = Result<()>> + Send + 'static,
1495    {
1496        let handler: Handler<Request> = Arc::new(move |request| Box::pin(handler(request)));
1497        self.subscribe_if_idle(&self.request).await;
1498        self.request.add_handler(handler);
1499        Ok(())
1500    }
1501
1502    /// Adds a listener for the `requestFinished` event.
1503    ///
1504    /// The handler fires after the request has been successfully received by the server
1505    /// and a response has been fully downloaded for any page in the context.
1506    ///
1507    /// Context-level handlers fire before page-level handlers.
1508    ///
1509    /// # Arguments
1510    ///
1511    /// * `handler` - Async function that receives the completed `Request`
1512    ///
1513    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-request-finished>
1514    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1515    pub async fn on_request_finished<F, Fut>(&self, handler: F) -> Result<()>
1516    where
1517        F: Fn(Request) -> Fut + Send + Sync + 'static,
1518        Fut: Future<Output = Result<()>> + Send + 'static,
1519    {
1520        let handler: Handler<Request> = Arc::new(move |request| Box::pin(handler(request)));
1521        self.subscribe_if_idle(&self.request_finished).await;
1522        self.request_finished.add_handler(handler);
1523        Ok(())
1524    }
1525
1526    /// Adds a listener for the `requestFailed` event.
1527    ///
1528    /// The handler fires when a request from any page in the context fails,
1529    /// for example due to a network error or if the server returned an error response.
1530    ///
1531    /// Context-level handlers fire before page-level handlers.
1532    ///
1533    /// # Arguments
1534    ///
1535    /// * `handler` - Async function that receives the failed `Request`
1536    ///
1537    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-request-failed>
1538    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1539    pub async fn on_request_failed<F, Fut>(&self, handler: F) -> Result<()>
1540    where
1541        F: Fn(Request) -> Fut + Send + Sync + 'static,
1542        Fut: Future<Output = Result<()>> + Send + 'static,
1543    {
1544        let handler: Handler<Request> = Arc::new(move |request| Box::pin(handler(request)));
1545        self.subscribe_if_idle(&self.request_failed).await;
1546        self.request_failed.add_handler(handler);
1547        Ok(())
1548    }
1549
1550    /// Adds a listener for the `response` event.
1551    ///
1552    /// The handler fires whenever a response is received from any page in the context.
1553    ///
1554    /// Context-level handlers fire before page-level handlers.
1555    ///
1556    /// # Arguments
1557    ///
1558    /// * `handler` - Async function that receives the `ResponseObject`
1559    ///
1560    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-response>
1561    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1562    pub async fn on_response<F, Fut>(&self, handler: F) -> Result<()>
1563    where
1564        F: Fn(ResponseObject) -> Fut + Send + Sync + 'static,
1565        Fut: Future<Output = Result<()>> + Send + 'static,
1566    {
1567        let handler: Handler<ResponseObject> =
1568            Arc::new(move |response| Box::pin(handler(response)));
1569        self.subscribe_if_idle(&self.response).await;
1570        self.response.add_handler(handler);
1571        Ok(())
1572    }
1573
1574    /// Adds a listener for the `dialog` event on this browser context.
1575    ///
1576    /// The handler fires whenever a JavaScript dialog (alert, confirm, prompt,
1577    /// or beforeunload) is triggered from **any** page in the context. Context-level
1578    /// handlers fire before page-level handlers.
1579    ///
1580    /// The dialog must be explicitly accepted or dismissed; otherwise the page
1581    /// will freeze waiting for a response.
1582    ///
1583    /// # Arguments
1584    ///
1585    /// * `handler` - Async function that receives the [`Dialog`](crate::protocol::Dialog) and calls
1586    ///   `dialog.accept()` or `dialog.dismiss()`.
1587    ///
1588    /// # Errors
1589    ///
1590    /// Returns error if communication with the browser process fails.
1591    ///
1592    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-dialog>
1593    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1594    pub async fn on_dialog<F, Fut>(&self, handler: F) -> Result<()>
1595    where
1596        F: Fn(crate::protocol::Dialog) -> Fut + Send + Sync + 'static,
1597        Fut: Future<Output = Result<()>> + Send + 'static,
1598    {
1599        let handler: Handler<crate::protocol::Dialog> =
1600            Arc::new(move |dialog| Box::pin(handler(dialog)));
1601        self.dialog.add_handler(handler);
1602        Ok(())
1603    }
1604
1605    /// Registers a context-level console event handler.
1606    ///
1607    /// The handler fires for any console message emitted by any page in this context.
1608    /// Context-level handlers fire before page-level handlers.
1609    ///
1610    /// The server only sends console events after the first handler is registered
1611    /// (subscription is managed automatically per context channel).
1612    ///
1613    /// # Arguments
1614    ///
1615    /// * `handler` - Async closure that receives the [`ConsoleMessage`](crate::protocol::ConsoleMessage)
1616    ///
1617    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-console>
1618    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1619    pub async fn on_console<F, Fut>(&self, handler: F) -> Result<()>
1620    where
1621        F: Fn(crate::protocol::ConsoleMessage) -> Fut + Send + Sync + 'static,
1622        Fut: Future<Output = Result<()>> + Send + 'static,
1623    {
1624        let handler: Handler<crate::protocol::ConsoleMessage> =
1625            Arc::new(move |msg| Box::pin(handler(msg)));
1626
1627        self.subscribe_if_idle(&self.console).await;
1628        self.console.add_handler(handler);
1629
1630        Ok(())
1631    }
1632
1633    /// Registers a context-level handler for uncaught JavaScript exceptions.
1634    ///
1635    /// The handler fires whenever a page in this context throws an unhandled
1636    /// JavaScript error (i.e. an exception that propagates to `window.onerror`
1637    /// or an unhandled promise rejection). The [`WebError`](crate::protocol::WebError)
1638    /// passed to the handler contains the error message and an optional back-reference
1639    /// to the originating page.
1640    ///
1641    /// # Arguments
1642    ///
1643    /// * `handler` - Async closure that receives a [`WebError`](crate::protocol::WebError).
1644    ///
1645    /// # Errors
1646    ///
1647    /// Returns error if communication with the browser process fails.
1648    ///
1649    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-web-error>
1650    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1651    pub async fn on_weberror<F, Fut>(&self, handler: F) -> Result<()>
1652    where
1653        F: Fn(crate::protocol::WebError) -> Fut + Send + Sync + 'static,
1654        Fut: Future<Output = Result<()>> + Send + 'static,
1655    {
1656        let handler: Handler<crate::protocol::WebError> =
1657            Arc::new(move |web_error| Box::pin(handler(web_error)));
1658        self.weberror.add_handler(handler);
1659        Ok(())
1660    }
1661
1662    /// Registers a handler for the `serviceWorker` event.
1663    ///
1664    /// The handler is called when a new service worker is registered in the browser context.
1665    ///
1666    /// Note: Service worker testing typically requires HTTPS and a registered service worker.
1667    ///
1668    /// # Arguments
1669    ///
1670    /// * `handler` - Async closure called with the new [`Worker`](crate::protocol::Worker) object
1671    ///
1672    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-service-worker>
1673    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1674    pub async fn on_serviceworker<F, Fut>(&self, handler: F) -> Result<()>
1675    where
1676        F: Fn(crate::protocol::Worker) -> Fut + Send + Sync + 'static,
1677        Fut: Future<Output = Result<()>> + Send + 'static,
1678    {
1679        let handler = Arc::new(
1680            move |worker: crate::protocol::Worker| -> ServiceWorkerHandlerFuture {
1681                Box::pin(handler(worker))
1682            },
1683        );
1684        self.serviceworker_handlers.lock().unwrap().push(handler);
1685        Ok(())
1686    }
1687
1688    /// Exposes a Rust function to every page in this browser context as
1689    /// `window[name]` in JavaScript.
1690    ///
1691    /// When JavaScript code calls `window[name](arg1, arg2, …)` the Playwright
1692    /// server fires a `bindingCall` event that invokes `callback` with the
1693    /// deserialized arguments. The return value of `callback` is serialized back
1694    /// to JavaScript so the `await window[name](…)` expression resolves with it.
1695    ///
1696    /// The binding is injected into every existing page and every new page
1697    /// created in this context.
1698    ///
1699    /// # Arguments
1700    ///
1701    /// * `name`     – JavaScript identifier that will be available as `window[name]`.
1702    /// * `callback` – Async closure called with `Vec<serde_json::Value>` (the JS
1703    ///   arguments) and returning `serde_json::Value` (the result).
1704    ///
1705    /// # Errors
1706    ///
1707    /// Returns error if:
1708    /// - The context has been closed.
1709    /// - Communication with the browser process fails.
1710    ///
1711    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-expose-function>
1712    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), name = %name))]
1713    pub async fn expose_function<F, Fut>(&self, name: &str, callback: F) -> Result<()>
1714    where
1715        F: Fn(Vec<serde_json::Value>) -> Fut + Send + Sync + 'static,
1716        Fut: Future<Output = serde_json::Value> + Send + 'static,
1717    {
1718        self.expose_binding_internal(name, callback).await
1719    }
1720
1721    /// Exposes a Rust function to every page in this browser context as
1722    /// `window[name]` in JavaScript.
1723    ///
1724    /// Currently identical to [`expose_function`](Self::expose_function):
1725    /// arguments arrive as plain serialized values. Upstream Playwright's
1726    /// `exposeBinding` can additionally hand the callback a source
1727    /// (page/frame) descriptor, which this crate does not surface yet.
1728    ///
1729    /// # Arguments
1730    ///
1731    /// * `name`     – JavaScript identifier.
1732    /// * `callback` – Async closure with `Vec<serde_json::Value>` → `serde_json::Value`.
1733    ///
1734    /// # Errors
1735    ///
1736    /// Returns error if:
1737    /// - The context has been closed.
1738    /// - Communication with the browser process fails.
1739    ///
1740    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-expose-binding>
1741    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), name = %name))]
1742    pub async fn expose_binding<F, Fut>(&self, name: &str, callback: F) -> Result<()>
1743    where
1744        F: Fn(Vec<serde_json::Value>) -> Fut + Send + Sync + 'static,
1745        Fut: Future<Output = serde_json::Value> + Send + 'static,
1746    {
1747        self.expose_binding_internal(name, callback).await
1748    }
1749
1750    /// Internal implementation shared by expose_function and expose_binding.
1751    async fn expose_binding_internal<F, Fut>(&self, name: &str, callback: F) -> Result<()>
1752    where
1753        F: Fn(Vec<serde_json::Value>) -> Fut + Send + Sync + 'static,
1754        Fut: Future<Output = serde_json::Value> + Send + 'static,
1755    {
1756        // Wrap callback with type erasure
1757        let callback: BindingCallback = Arc::new(move |args: Vec<serde_json::Value>| {
1758            Box::pin(callback(args)) as BindingCallbackFuture
1759        });
1760
1761        // Store the callback before sending the RPC so that a race-condition
1762        // where a bindingCall arrives before we finish registering is avoided.
1763        self.binding_callbacks
1764            .lock()
1765            .unwrap()
1766            .insert(name.to_string(), callback);
1767
1768        // Tell the Playwright server to inject window[name] into every page.
1769        //
1770        // The protocol also accepts `noGlobal`, which suppresses that
1771        // injection. It is deliberately not exposed: it exists to support
1772        // passing functions as evaluate arguments, where the binding is
1773        // called through the bindings controller rather than off `window`.
1774        self.channel()
1775            .send_no_result("exposeBinding", serde_json::json!({ "name": name }))
1776            .await
1777    }
1778
1779    /// Waits for a new page to be created in this browser context.
1780    ///
1781    /// Creates a one-shot waiter that resolves when the next `page` event fires.
1782    /// The waiter **must** be created before the action that triggers the new page
1783    /// (e.g. `new_page()` or a user action that opens a popup) to avoid a race
1784    /// condition.
1785    ///
1786    /// # Arguments
1787    ///
1788    /// * `timeout` - Timeout in milliseconds. Defaults to 30 000 ms if `None`.
1789    ///
1790    /// # Errors
1791    ///
1792    /// Returns [`crate::error::Error::Timeout`] if no page is created within the timeout.
1793    ///
1794    /// # Example
1795    ///
1796    /// ```no_run
1797    /// # use playwright_rs::Playwright;
1798    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1799    /// # let pw = Playwright::launch().await?;
1800    /// # let browser = pw.chromium().launch().await?;
1801    /// # let context = browser.new_context().await?;
1802    /// // Set up the waiter BEFORE the triggering action
1803    /// let waiter = context.expect_page(None).await?;
1804    /// let _page = context.new_page().await?;
1805    /// let new_page = waiter.wait().await?;
1806    /// # Ok(())
1807    /// # }
1808    /// ```
1809    ///
1810    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-wait-for-event>
1811    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1812    pub async fn expect_page(&self, timeout: Option<f64>) -> Result<EventWaiter<Page>> {
1813        let rx = self.page_events.wait();
1814        Ok(EventWaiter::new(rx, timeout.or(Some(30_000.0))))
1815    }
1816
1817    /// Waits for this browser context to be closed.
1818    ///
1819    /// Creates a one-shot waiter that resolves when the `close` event fires.
1820    /// The waiter **must** be created before the action that closes the context
1821    /// to avoid a race condition.
1822    ///
1823    /// # Arguments
1824    ///
1825    /// * `timeout` - Timeout in milliseconds. Defaults to 30 000 ms if `None`.
1826    ///
1827    /// # Errors
1828    ///
1829    /// Returns [`crate::error::Error::Timeout`] if the context is not closed within the timeout.
1830    ///
1831    /// # Example
1832    ///
1833    /// ```no_run
1834    /// # use playwright_rs::Playwright;
1835    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1836    /// # let pw = Playwright::launch().await?;
1837    /// # let browser = pw.chromium().launch().await?;
1838    /// # let context = browser.new_context().await?;
1839    /// // Set up the waiter BEFORE closing
1840    /// let waiter = context.expect_close(None).await?;
1841    /// context.close().await?;
1842    /// waiter.wait().await?;
1843    /// # Ok(())
1844    /// # }
1845    /// ```
1846    ///
1847    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-wait-for-event>
1848    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1849    pub async fn expect_close(&self, timeout: Option<f64>) -> Result<EventWaiter<()>> {
1850        let rx = self.close_events.wait();
1851        Ok(EventWaiter::new(rx, timeout.or(Some(30_000.0))))
1852    }
1853
1854    /// Waits for a console message from any page in this context.
1855    ///
1856    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-console>
1857    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1858    pub async fn expect_console_message(
1859        &self,
1860        timeout: Option<f64>,
1861    ) -> Result<EventWaiter<crate::protocol::ConsoleMessage>> {
1862        self.subscribe_if_idle(&self.console).await;
1863        let rx = self.console.wait();
1864        Ok(EventWaiter::new(rx, timeout.or(Some(30_000.0))))
1865    }
1866
1867    /// Waits for the given event to fire and returns a typed `EventValue`.
1868    ///
1869    /// This is the generic version of the specific `expect_*` methods. It matches
1870    /// the playwright-python / playwright-js `context.expect_event(event_name)` API.
1871    ///
1872    /// The waiter **must** be created before the action that triggers the event.
1873    ///
1874    /// # Supported event names
1875    ///
1876    /// `"page"`, `"close"`, `"console"`, `"request"`, `"response"`,
1877    /// `"weberror"`, `"serviceworker"`
1878    ///
1879    /// # Arguments
1880    ///
1881    /// * `event` - Event name (case-sensitive, matches Playwright protocol names).
1882    /// * `timeout` - Timeout in milliseconds. Defaults to 30 000 ms if `None`.
1883    ///
1884    /// # Errors
1885    ///
1886    /// Returns [`crate::error::Error::InvalidArgument`] for unknown event names.
1887    /// Returns [`crate::error::Error::Timeout`] if the event does not fire within the timeout.
1888    ///
1889    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-wait-for-event>
1890    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1891    pub async fn expect_event(
1892        &self,
1893        event: &str,
1894        timeout: Option<f64>,
1895    ) -> crate::error::Result<EventWaiter<crate::protocol::EventValue>> {
1896        use crate::protocol::EventValue;
1897        use tokio::sync::oneshot;
1898
1899        let timeout_ms = timeout.or(Some(30_000.0));
1900
1901        match event {
1902            "page" => {
1903                let (mut tx, rx) = oneshot::channel::<EventValue>();
1904                let inner_rx = self.page_events.wait();
1905
1906                // select: drop the registry receiver when the caller times
1907                // out, or a stale FIFO waiter swallows the next event.
1908                tokio::spawn(async move {
1909                    tokio::select! {
1910                        v = inner_rx => {
1911                            if let Ok(v) = v { let _ = tx.send(EventValue::Page(v)); }
1912                        }
1913                        () = tx.closed() => {}
1914                    }
1915                });
1916
1917                Ok(EventWaiter::new(rx, timeout_ms))
1918            }
1919
1920            "close" => {
1921                let (mut tx, rx) = oneshot::channel::<EventValue>();
1922                let inner_rx = self.close_events.wait();
1923
1924                // select: drop the registry receiver when the caller times
1925                // out, or a stale FIFO waiter swallows the next event.
1926                tokio::spawn(async move {
1927                    tokio::select! {
1928                        v = inner_rx => {
1929                            if v.is_ok() { let _ = tx.send(EventValue::Close); }
1930                        }
1931                        () = tx.closed() => {}
1932                    }
1933                });
1934
1935                Ok(EventWaiter::new(rx, timeout_ms))
1936            }
1937
1938            "console" => {
1939                let (mut tx, rx) = oneshot::channel::<EventValue>();
1940
1941                self.subscribe_if_idle(&self.console).await;
1942                let inner_rx = self.console.wait();
1943
1944                // select: drop the registry receiver when the caller times
1945                // out, or a stale FIFO waiter swallows the next event.
1946                tokio::spawn(async move {
1947                    tokio::select! {
1948                        v = inner_rx => {
1949                            if let Ok(v) = v { let _ = tx.send(EventValue::ConsoleMessage(v)); }
1950                        }
1951                        () = tx.closed() => {}
1952                    }
1953                });
1954
1955                Ok(EventWaiter::new(rx, timeout_ms))
1956            }
1957
1958            "request" => {
1959                let (mut tx, rx) = oneshot::channel::<EventValue>();
1960
1961                self.subscribe_if_idle(&self.request).await;
1962                let inner_rx = self.request.wait();
1963
1964                // select: drop the registry receiver when the caller times
1965                // out, or a stale FIFO waiter swallows the next event.
1966                tokio::spawn(async move {
1967                    tokio::select! {
1968                        v = inner_rx => {
1969                            if let Ok(v) = v { let _ = tx.send(EventValue::Request(v)); }
1970                        }
1971                        () = tx.closed() => {}
1972                    }
1973                });
1974
1975                Ok(EventWaiter::new(rx, timeout_ms))
1976            }
1977
1978            "response" => {
1979                let (mut tx, rx) = oneshot::channel::<EventValue>();
1980
1981                self.subscribe_if_idle(&self.response).await;
1982                let inner_rx = self.response.wait();
1983
1984                // select: drop the registry receiver when the caller times
1985                // out, or a stale FIFO waiter swallows the next event.
1986                tokio::spawn(async move {
1987                    tokio::select! {
1988                        v = inner_rx => {
1989                            if let Ok(v) = v { let _ = tx.send(EventValue::Response(v)); }
1990                        }
1991                        () = tx.closed() => {}
1992                    }
1993                });
1994
1995                Ok(EventWaiter::new(rx, timeout_ms))
1996            }
1997
1998            "weberror" => {
1999                let (mut tx, rx) = oneshot::channel::<EventValue>();
2000                let inner_rx = self.weberror.wait();
2001
2002                // select: drop the registry receiver when the caller times
2003                // out, or a stale FIFO waiter swallows the next event.
2004                tokio::spawn(async move {
2005                    tokio::select! {
2006                        v = inner_rx => {
2007                            if let Ok(v) = v { let _ = tx.send(EventValue::WebError(v)); }
2008                        }
2009                        () = tx.closed() => {}
2010                    }
2011                });
2012
2013                Ok(EventWaiter::new(rx, timeout_ms))
2014            }
2015
2016            "serviceworker" => {
2017                let (tx, rx) = oneshot::channel::<EventValue>();
2018                let (inner_tx, inner_rx) = oneshot::channel::<crate::protocol::Worker>();
2019                self.serviceworker_waiters.lock().unwrap().push(inner_tx);
2020
2021                tokio::spawn(async move {
2022                    if let Ok(v) = inner_rx.await {
2023                        let _ = tx.send(EventValue::Worker(v));
2024                    }
2025                });
2026
2027                Ok(EventWaiter::new(rx, timeout_ms))
2028            }
2029
2030            other => Err(crate::error::Error::InvalidArgument(format!(
2031                "Unknown event name '{}'. Supported: page, close, console, request, response, \
2032                 weberror, serviceworker",
2033                other
2034            ))),
2035        }
2036    }
2037
2038    /// Intercepts WebSocket connections matching the given URL pattern for all pages in this context.
2039    ///
2040    /// When a WebSocket connection from any page in this context matches `url`,
2041    /// the `handler` is called with a [`WebSocketRoute`](crate::protocol::WebSocketRoute) object.
2042    /// The handler must call [`connect_to_server`](crate::protocol::WebSocketRoute::connect_to_server)
2043    /// to forward the connection to the real server, or
2044    /// [`close`](crate::protocol::WebSocketRoute::close) to terminate it.
2045    ///
2046    /// # Arguments
2047    ///
2048    /// * `url` — URL glob pattern (e.g. `"ws://**"` or `"wss://example.com/ws"`).
2049    /// * `handler` — Async closure receiving a `WebSocketRoute`.
2050    ///
2051    /// # Errors
2052    ///
2053    /// Returns an error if the RPC call to enable interception fails.
2054    ///
2055    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-route-web-socket>
2056    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), url = %url))]
2057    pub async fn route_web_socket<F, Fut>(&self, url: &str, handler: F) -> Result<()>
2058    where
2059        F: Fn(crate::protocol::WebSocketRoute) -> Fut + Send + Sync + 'static,
2060        Fut: Future<Output = Result<()>> + Send + 'static,
2061    {
2062        let handler = Arc::new(
2063            move |route: crate::protocol::WebSocketRoute| -> WsRouteHandlerFuture {
2064                Box::pin(handler(route))
2065            },
2066        );
2067
2068        self.ws_route_handlers
2069            .lock()
2070            .unwrap()
2071            .push(ContextWsRouteHandlerEntry {
2072                pattern: url.to_string(),
2073                handler,
2074            });
2075
2076        self.enable_ws_interception().await
2077    }
2078
2079    /// Updates WebSocket interception patterns for this context.
2080    async fn enable_ws_interception(&self) -> Result<()> {
2081        let patterns: Vec<serde_json::Value> = self
2082            .ws_route_handlers
2083            .lock()
2084            .unwrap()
2085            .iter()
2086            .map(|entry| serde_json::json!({ "glob": entry.pattern }))
2087            .collect();
2088
2089        self.channel()
2090            .send_no_result(
2091                "setWebSocketInterceptionPatterns",
2092                serde_json::json!({ "patterns": patterns }),
2093            )
2094            .await
2095    }
2096
2097    /// Updates network interception patterns for this context
2098    async fn enable_network_interception(&self) -> Result<()> {
2099        let patterns: Vec<serde_json::Value> = self
2100            .route_handlers
2101            .lock()
2102            .unwrap()
2103            .iter()
2104            .map(|entry| serde_json::json!({ "glob": entry.pattern }))
2105            .collect();
2106
2107        self.channel()
2108            .send_no_result(
2109                "setNetworkInterceptionPatterns",
2110                serde_json::json!({ "patterns": patterns }),
2111            )
2112            .await
2113    }
2114
2115    /// Deserializes binding call arguments from Playwright's protocol format.
2116    ///
2117    /// The `args` field in the BindingCall initializer is a JSON array where each
2118    /// element is in `serialize_argument` format: `{"value": <tagged>, "handles": []}`.
2119    /// This helper extracts the inner "value" from each entry and parses it.
2120    ///
2121    /// This is `pub` so that `Page::on_event("bindingCall")` can reuse it without
2122    /// duplicating the deserialization logic.
2123    pub fn deserialize_binding_args_pub(raw_args: &Value) -> Vec<Value> {
2124        Self::deserialize_binding_args(raw_args)
2125    }
2126
2127    fn deserialize_binding_args(raw_args: &Value) -> Vec<Value> {
2128        let Some(arr) = raw_args.as_array() else {
2129            return vec![];
2130        };
2131
2132        arr.iter()
2133            .map(|arg| {
2134                // Each arg is a direct Playwright type-tagged value, e.g. {"n": 3} or {"s": "hello"}
2135                // (NOT wrapped in {"value": ..., "handles": []} — that format is only for evaluate args)
2136                crate::protocol::evaluate_conversion::parse_value(arg, None)
2137            })
2138            .collect()
2139    }
2140
2141    /// Handles a route event from the protocol
2142    async fn on_route_event(route_handlers: Arc<Mutex<Vec<RouteHandlerEntry>>>, route: Route) {
2143        let handlers = route_handlers.lock().unwrap().clone();
2144        let url = route.request().url().to_string();
2145
2146        for entry in handlers.iter().rev() {
2147            if crate::protocol::route::matches_pattern(&entry.pattern, &url) {
2148                let handler = entry.handler.clone();
2149                if let Err(e) = handler(route.clone()).await {
2150                    tracing::warn!("Context route handler error: {}", e);
2151                    break;
2152                }
2153                if !route.was_handled() {
2154                    continue;
2155                }
2156                break;
2157            }
2158        }
2159    }
2160
2161    fn dispatch_request_event(&self, method: &str, params: Value) {
2162        if let Some(request_guid) = params
2163            .get("request")
2164            .and_then(|v| v.get("guid"))
2165            .and_then(|v| v.as_str())
2166        {
2167            let connection = self.connection();
2168            let request_guid_owned = request_guid.to_owned();
2169            let page_guid_owned = params
2170                .get("page")
2171                .and_then(|v| v.get("guid"))
2172                .and_then(|v| v.as_str())
2173                .map(|v| v.to_owned());
2174            // Extract failureText for requestFailed events
2175            let failure_text = params
2176                .get("failureText")
2177                .and_then(|v| v.as_str())
2178                .map(|s| s.to_owned());
2179            // Extract response GUID for requestFinished events (to read timing)
2180            let response_guid_owned = params
2181                .get("response")
2182                .and_then(|v| v.get("guid"))
2183                .and_then(|v| v.as_str())
2184                .map(|s| s.to_owned());
2185            // Extract responseEndTiming from requestFinished event params
2186            let response_end_timing = params.get("responseEndTiming").and_then(|v| v.as_f64());
2187            let method = method.to_owned();
2188            // Clone context-level handler vecs for use in spawn
2189            let ctx_request = self.request.clone();
2190            let ctx_request_finished = self.request_finished.clone();
2191            let ctx_request_failed = self.request_failed.clone();
2192            tokio::spawn(async move {
2193                let request: Request =
2194                    match connection.get_typed::<Request>(&request_guid_owned).await {
2195                        Ok(r) => r,
2196                        Err(_) => return,
2197                    };
2198
2199                // Set failure text on the request before dispatching to handlers
2200                if let Some(text) = failure_text {
2201                    request.set_failure_text(text);
2202                }
2203
2204                // For requestFinished, extract timing from the Response object's initializer
2205                if method == "requestFinished"
2206                    && let Some(timing) =
2207                        extract_timing(&connection, response_guid_owned, response_end_timing).await
2208                {
2209                    request.set_timing(timing);
2210                }
2211
2212                // Dispatch to context-level handlers first (matching playwright-python behavior)
2213                match method.as_str() {
2214                    "request" => ctx_request.dispatch(request.clone()).await,
2215                    "requestFinished" => ctx_request_finished.dispatch(request.clone()).await,
2216                    "requestFailed" => ctx_request_failed.dispatch(request.clone()).await,
2217                    _ => {}
2218                }
2219
2220                // Then dispatch to page-level handlers
2221                if let Some(page_guid) = page_guid_owned {
2222                    let page: Page = match connection.get_typed::<Page>(&page_guid).await {
2223                        Ok(p) => p,
2224                        Err(_) => return,
2225                    };
2226                    match method.as_str() {
2227                        "request" => page.trigger_request_event(request).await,
2228                        "requestFailed" => page.trigger_request_failed_event(request).await,
2229                        "requestFinished" => page.trigger_request_finished_event(request).await,
2230                        _ => unreachable!("Unreachable method {}", method),
2231                    }
2232                }
2233            });
2234        }
2235    }
2236
2237    fn dispatch_response_event(&self, _method: &str, params: Value) {
2238        if let Some(response_guid) = params
2239            .get("response")
2240            .and_then(|v| v.get("guid"))
2241            .and_then(|v| v.as_str())
2242        {
2243            let connection = self.connection();
2244            let response_guid_owned = response_guid.to_owned();
2245            let page_guid_owned = params
2246                .get("page")
2247                .and_then(|v| v.get("guid"))
2248                .and_then(|v| v.as_str())
2249                .map(|v| v.to_owned());
2250            let ctx_response = self.response.clone();
2251            tokio::spawn(async move {
2252                let response: ResponseObject = match connection
2253                    .get_typed::<ResponseObject>(&response_guid_owned)
2254                    .await
2255                {
2256                    Ok(r) => r,
2257                    Err(_) => return,
2258                };
2259
2260                // Dispatch to context-level handlers first (matching playwright-python behavior)
2261                ctx_response.dispatch(response.clone()).await;
2262
2263                // Then dispatch to page-level handlers
2264                if let Some(page_guid) = page_guid_owned {
2265                    let page: Page = match connection.get_typed::<Page>(&page_guid).await {
2266                        Ok(p) => p,
2267                        Err(_) => return,
2268                    };
2269                    page.trigger_response_event(response).await;
2270                }
2271            });
2272        }
2273    }
2274}
2275
2276impl ChannelOwner for BrowserContext {
2277    fn guid(&self) -> &str {
2278        self.base.guid()
2279    }
2280
2281    fn type_name(&self) -> &str {
2282        self.base.type_name()
2283    }
2284
2285    fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
2286        self.base.parent()
2287    }
2288
2289    fn connection(&self) -> Arc<dyn crate::server::connection::ConnectionLike> {
2290        self.base.connection()
2291    }
2292
2293    fn initializer(&self) -> &Value {
2294        self.base.initializer()
2295    }
2296
2297    fn channel(&self) -> &Channel {
2298        self.base.channel()
2299    }
2300
2301    fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
2302        self.base.dispose(reason)
2303    }
2304
2305    fn adopt(&self, child: Arc<dyn ChannelOwner>) {
2306        self.base.adopt(child)
2307    }
2308
2309    fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
2310        self.base.add_child(guid, child)
2311    }
2312
2313    fn remove_child(&self, guid: &str) {
2314        self.base.remove_child(guid)
2315    }
2316
2317    fn on_event(&self, method: &str, params: Value) {
2318        match method {
2319            "request" | "requestFailed" | "requestFinished" => {
2320                self.dispatch_request_event(method, params)
2321            }
2322            "response" => self.dispatch_response_event(method, params),
2323            "close" => {
2324                // BrowserContext close event — mark as closed and fire registered close handlers
2325                self.is_closed.store(true, Ordering::Relaxed);
2326                let close_events = self.close_events.clone();
2327                tokio::spawn(async move {
2328                    close_events.dispatch_all(()).await;
2329                });
2330            }
2331            "page" => {
2332                // Page events are triggered when pages are created, including:
2333                // - Initial page in persistent context with --app mode
2334                // - Popup pages opened through user interactions
2335                // Event format: {page: {guid: "..."}}
2336                if let Some(page_guid) = params
2337                    .get("page")
2338                    .and_then(|v| v.get("guid"))
2339                    .and_then(|v| v.as_str())
2340                {
2341                    let connection = self.connection();
2342                    let page_guid_owned = page_guid.to_string();
2343                    let pages = self.pages.clone();
2344                    let page_events = self.page_events.clone();
2345                    let download_handlers = self.download_handlers.clone();
2346                    let frame_attached_handlers = self.frame_attached_handlers.clone();
2347                    let frame_detached_handlers = self.frame_detached_handlers.clone();
2348                    let frame_navigated_handlers = self.frame_navigated_handlers.clone();
2349                    let page_load_handlers = self.page_load_handlers.clone();
2350                    let page_close_handlers = self.page_close_handlers.clone();
2351
2352                    tokio::spawn(async move {
2353                        // Get and downcast the Page object
2354                        let page: Page = match connection.get_typed::<Page>(&page_guid_owned).await
2355                        {
2356                            Ok(p) => p,
2357                            Err(_) => return,
2358                        };
2359
2360                        // Track the page
2361                        pages.lock().unwrap().push(page.clone());
2362
2363                        // Forward this new page's lifecycle events to any
2364                        // context-level handlers already registered.
2365                        if !download_handlers.lock().unwrap().is_empty() {
2366                            Self::wire_download(&page, download_handlers.clone()).await;
2367                        }
2368                        if !frame_attached_handlers.lock().unwrap().is_empty() {
2369                            Self::wire_frame_attached(&page, frame_attached_handlers.clone()).await;
2370                        }
2371                        if !frame_detached_handlers.lock().unwrap().is_empty() {
2372                            Self::wire_frame_detached(&page, frame_detached_handlers.clone()).await;
2373                        }
2374                        if !frame_navigated_handlers.lock().unwrap().is_empty() {
2375                            Self::wire_frame_navigated(&page, frame_navigated_handlers.clone())
2376                                .await;
2377                        }
2378                        if !page_load_handlers.lock().unwrap().is_empty() {
2379                            Self::wire_page_load(&page, page_load_handlers.clone()).await;
2380                        }
2381                        if !page_close_handlers.lock().unwrap().is_empty() {
2382                            Self::wire_page_close(&page, page_close_handlers.clone()).await;
2383                        }
2384
2385                        // If this page has an opener, dispatch popup event to opener's handlers.
2386                        // The opener guid is in the page's initializer: {"opener": {"guid": "..."}}
2387                        if let Some(opener_guid) = page
2388                            .initializer()
2389                            .get("opener")
2390                            .and_then(|v| v.get("guid"))
2391                            .and_then(|v| v.as_str())
2392                            && let Ok(opener) = connection.get_typed::<Page>(opener_guid).await
2393                        {
2394                            opener.trigger_popup_event(page.clone()).await;
2395                        }
2396
2397                        // Dispatch to context-level page handlers, then the
2398                        // longest-waiting expect_page() caller.
2399                        page_events.dispatch(page).await;
2400                    });
2401                }
2402            }
2403            "pageError" => {
2404                // pageError event: fired when an uncaught JS exception occurs on a page.
2405                // Event format:
2406                //   { "error": { "error": { "message": "...", "name": "...", "stack": "..." } },
2407                //     "page": { "guid": "page@..." } }
2408                //
2409                // Dispatch path:
2410                //  1. Construct WebError and fire context-level on_weberror handlers.
2411                //  2. Forward the raw message to the page's on_pageerror handlers.
2412                let message = params
2413                    .get("error")
2414                    .and_then(|e| e.get("error"))
2415                    .and_then(|e| e.get("message"))
2416                    .and_then(|m| m.as_str())
2417                    .unwrap_or("")
2418                    .to_string();
2419
2420                let page_guid_owned = params
2421                    .get("page")
2422                    .and_then(|v| v.get("guid"))
2423                    .and_then(|v| v.as_str())
2424                    .map(|s| s.to_string());
2425
2426                let location =
2427                    params
2428                        .get("location")
2429                        .map(|loc| crate::protocol::WebErrorLocation {
2430                            url: loc
2431                                .get("url")
2432                                .and_then(|v| v.as_str())
2433                                .unwrap_or("")
2434                                .to_string(),
2435                            line: loc.get("line").and_then(|v| v.as_i64()).unwrap_or(0) as i32,
2436                            column: loc.get("column").and_then(|v| v.as_i64()).unwrap_or(0) as i32,
2437                        });
2438
2439                let connection = self.connection();
2440                let weberror = self.weberror.clone();
2441
2442                tokio::spawn(async move {
2443                    // Resolve page (optional — may be None if page already closed)
2444                    let page = if let Some(ref guid) = page_guid_owned {
2445                        connection.get_typed::<Page>(guid).await.ok()
2446                    } else {
2447                        None
2448                    };
2449
2450                    // 1. Dispatch to context-level weberror handlers
2451                    let web_error = crate::protocol::WebError::new(
2452                        message.clone(),
2453                        page.clone(),
2454                        location.clone(),
2455                    );
2456                    weberror.dispatch(web_error).await;
2457
2458                    // 2. Forward to page-level pageerror handlers
2459                    if let Some(p) = page {
2460                        p.trigger_pageerror_event(message).await;
2461                    }
2462                });
2463            }
2464            "dialog" => {
2465                // Dialog events come to BrowserContext.
2466                // Dispatch to context-level handlers first, then forward to the Page.
2467                // Event format: {dialog: {guid: "..."}}
2468                // The Dialog protocol object has the Page as its parent
2469                if let Some(dialog_guid) = params
2470                    .get("dialog")
2471                    .and_then(|v| v.get("guid"))
2472                    .and_then(|v| v.as_str())
2473                {
2474                    let connection = self.connection();
2475                    let dialog_guid_owned = dialog_guid.to_string();
2476                    let ctx_dialog = self.dialog.clone();
2477
2478                    tokio::spawn(async move {
2479                        // Get and downcast the Dialog object
2480                        let dialog: crate::protocol::Dialog = match connection
2481                            .get_typed::<crate::protocol::Dialog>(&dialog_guid_owned)
2482                            .await
2483                        {
2484                            Ok(d) => d,
2485                            Err(_) => return,
2486                        };
2487
2488                        // Dispatch to context-level dialog handlers first
2489                        ctx_dialog.dispatch(dialog.clone()).await;
2490
2491                        // Then forward to the Page's dialog handlers
2492                        let page: Page =
2493                            match crate::server::connection::downcast_parent::<Page>(&dialog) {
2494                                Some(p) => p,
2495                                None => return,
2496                            };
2497
2498                        page.trigger_dialog_event(dialog).await;
2499                    });
2500                }
2501            }
2502            "bindingCall" => {
2503                // A JS caller invoked an exposed function. Dispatch to the registered
2504                // callback and send the result back via BindingCall::fulfill.
2505                // Event format: {binding: {guid: "..."}}
2506                if let Some(binding_guid) = params
2507                    .get("binding")
2508                    .and_then(|v| v.get("guid"))
2509                    .and_then(|v| v.as_str())
2510                {
2511                    let connection = self.connection();
2512                    let binding_guid_owned = binding_guid.to_string();
2513                    let binding_callbacks = self.binding_callbacks.clone();
2514
2515                    tokio::spawn(async move {
2516                        let binding_call: crate::protocol::BindingCall = match connection
2517                            .get_typed::<crate::protocol::BindingCall>(&binding_guid_owned)
2518                            .await
2519                        {
2520                            Ok(bc) => bc,
2521                            Err(e) => {
2522                                tracing::warn!("Failed to get BindingCall object: {}", e);
2523                                return;
2524                            }
2525                        };
2526
2527                        let name = binding_call.name().to_string();
2528
2529                        // Look up the registered callback
2530                        let callback = {
2531                            let callbacks = binding_callbacks.lock().unwrap();
2532                            callbacks.get(&name).cloned()
2533                        };
2534
2535                        let Some(callback) = callback else {
2536                            tracing::warn!("No callback registered for binding '{}'", name);
2537                            let _ = binding_call
2538                                .reject(&format!("No Rust handler for binding '{name}'"))
2539                                .await;
2540                            return;
2541                        };
2542
2543                        // Deserialize the args from Playwright protocol format
2544                        let raw_args = binding_call.args();
2545                        let args = Self::deserialize_binding_args(raw_args);
2546
2547                        // Call the callback and serialize the result
2548                        let result_value = callback(args).await;
2549                        let serialized =
2550                            crate::protocol::evaluate_conversion::serialize_argument(&result_value);
2551
2552                        if let Err(e) = binding_call.resolve(serialized).await {
2553                            tracing::warn!("Failed to resolve BindingCall '{}': {}", name, e);
2554                        }
2555                    });
2556                }
2557            }
2558            "route" => {
2559                // Handle context-level network routing event
2560                if let Some(route_guid) = params
2561                    .get("route")
2562                    .and_then(|v| v.get("guid"))
2563                    .and_then(|v| v.as_str())
2564                {
2565                    let connection = self.connection();
2566                    let route_guid_owned = route_guid.to_string();
2567                    let route_handlers = self.route_handlers.clone();
2568                    let request_context_guid = self.request_context_guid.clone();
2569
2570                    tokio::spawn(async move {
2571                        let route: Route =
2572                            match connection.get_typed::<Route>(&route_guid_owned).await {
2573                                Ok(r) => r,
2574                                Err(e) => {
2575                                    tracing::warn!("Failed to get route object: {}", e);
2576                                    return;
2577                                }
2578                            };
2579
2580                        // Set APIRequestContext on the route for fetch() support
2581                        if let Some(ref guid) = request_context_guid
2582                            && let Ok(api_ctx) =
2583                                connection.get_typed::<APIRequestContext>(guid).await
2584                        {
2585                            route.set_api_request_context(api_ctx);
2586                        }
2587
2588                        BrowserContext::on_route_event(route_handlers, route).await;
2589                    });
2590                }
2591            }
2592            "console" => {
2593                // Console events are sent to BrowserContext.
2594                // Construct ConsoleMessage from params, dispatch to context-level handlers,
2595                // then forward to the Page's on_console handlers.
2596                //
2597                // Event params format:
2598                // {
2599                //   type: "log"|"error"|"warning"|...,
2600                //   text: "rendered text",
2601                //   location: { url: "...", lineNumber: N, columnNumber: N },
2602                //   page: { guid: "page@..." },
2603                //   args: [ { guid: "JSHandle@..." }, ... ]  -- resolved to Arc<JSHandle>
2604                //   timestamp: <f64 milliseconds since Unix epoch>
2605                // }
2606                let type_ = params
2607                    .get("type")
2608                    .and_then(|v| v.as_str())
2609                    .unwrap_or("log")
2610                    .to_string();
2611                let text = params
2612                    .get("text")
2613                    .and_then(|v| v.as_str())
2614                    .unwrap_or("")
2615                    .to_string();
2616                let loc_url = params
2617                    .get("location")
2618                    .and_then(|v| v.get("url"))
2619                    .and_then(|v| v.as_str())
2620                    .unwrap_or("")
2621                    .to_string();
2622                // 1.60 emits `line`/`column`; older drivers used
2623                // `lineNumber`/`columnNumber` (deprecated, may be removed). Prefer
2624                // the new keys, fall back to the legacy ones.
2625                let loc_line = params
2626                    .get("location")
2627                    .and_then(|v| v.get("line").or_else(|| v.get("lineNumber")))
2628                    .and_then(|v| v.as_i64())
2629                    .unwrap_or(0) as i32;
2630                let loc_col = params
2631                    .get("location")
2632                    .and_then(|v| v.get("column").or_else(|| v.get("columnNumber")))
2633                    .and_then(|v| v.as_i64())
2634                    .unwrap_or(0) as i32;
2635                let page_guid_owned = params
2636                    .get("page")
2637                    .and_then(|v| v.get("guid"))
2638                    .and_then(|v| v.as_str())
2639                    .map(|s| s.to_string());
2640                // Collect arg GUIDs before spawning.
2641                let arg_guids: Vec<String> = params
2642                    .get("args")
2643                    .and_then(|v| v.as_array())
2644                    .map(|arr| {
2645                        arr.iter()
2646                            .filter_map(|v| {
2647                                v.get("guid")
2648                                    .and_then(|g| g.as_str())
2649                                    .map(|s| s.to_string())
2650                            })
2651                            .collect()
2652                    })
2653                    .unwrap_or_default();
2654                let timestamp = params
2655                    .get("timestamp")
2656                    .and_then(|v| v.as_f64())
2657                    .unwrap_or(0.0);
2658
2659                let connection = self.connection();
2660                let ctx_console = self.console.clone();
2661
2662                tokio::spawn(async move {
2663                    use crate::protocol::JSHandle;
2664                    use crate::protocol::console_message::{
2665                        ConsoleMessage, ConsoleMessageLocation,
2666                    };
2667
2668                    // Optionally resolve the page back-reference
2669                    let page = if let Some(ref guid) = page_guid_owned {
2670                        connection.get_typed::<Page>(guid).await.ok()
2671                    } else {
2672                        None
2673                    };
2674
2675                    // Resolve JSHandle args from the connection registry.
2676                    let args: Vec<std::sync::Arc<JSHandle>> = {
2677                        let mut resolved = Vec::with_capacity(arg_guids.len());
2678                        for guid in &arg_guids {
2679                            if let Ok(handle) = connection.get_typed::<JSHandle>(guid).await {
2680                                resolved.push(std::sync::Arc::new(handle));
2681                            }
2682                        }
2683                        resolved
2684                    };
2685
2686                    let location = ConsoleMessageLocation {
2687                        url: loc_url,
2688                        line_number: loc_line,
2689                        column_number: loc_col,
2690                    };
2691
2692                    let msg =
2693                        ConsoleMessage::new(type_, text, location, page.clone(), args, timestamp);
2694
2695                    // Handlers first, then the longest-waiting
2696                    // expect_console_message() caller — the same order as
2697                    // every other event since the registry migration.
2698                    ctx_console.dispatch(msg.clone()).await;
2699
2700                    // Forward to page-level handlers
2701                    if let Some(p) = page {
2702                        p.trigger_console_event(msg).await;
2703                    }
2704                });
2705            }
2706            "serviceWorker" => {
2707                // A new service worker was registered in this context.
2708                // Event format: {worker: {guid: "Worker@..."}}
2709                if let Some(worker_guid) = params
2710                    .get("worker")
2711                    .and_then(|v| v.get("guid"))
2712                    .and_then(|v| v.as_str())
2713                {
2714                    let connection = self.connection();
2715                    let worker_guid_owned = worker_guid.to_string();
2716                    let serviceworker_handlers = self.serviceworker_handlers.clone();
2717                    let serviceworker_waiters = self.serviceworker_waiters.clone();
2718                    let service_workers_list = self.service_workers_list.clone();
2719
2720                    tokio::spawn(async move {
2721                        let worker: crate::protocol::Worker = match connection
2722                            .get_typed::<crate::protocol::Worker>(&worker_guid_owned)
2723                            .await
2724                        {
2725                            Ok(w) => w,
2726                            Err(e) => {
2727                                tracing::warn!(
2728                                    "Failed to get Worker object for serviceWorker event: {}",
2729                                    e
2730                                );
2731                                return;
2732                            }
2733                        };
2734
2735                        // Track for service_workers() accessor
2736                        service_workers_list.lock().unwrap().push(worker.clone());
2737
2738                        let handlers = serviceworker_handlers.lock().unwrap().clone();
2739                        for handler in handlers {
2740                            let worker_clone = worker.clone();
2741                            tokio::spawn(async move {
2742                                if let Err(e) = handler(worker_clone).await {
2743                                    tracing::error!("Error in serviceworker handler: {}", e);
2744                                }
2745                            });
2746                        }
2747                        // Notify expect_event("serviceworker") waiters
2748                        if let Some(tx) = serviceworker_waiters.lock().unwrap().pop() {
2749                            let _ = tx.send(worker);
2750                        }
2751                    });
2752                }
2753            }
2754            "webSocketRoute" => {
2755                // A WebSocket matched a route_web_socket pattern on the context.
2756                // Event format: {webSocketRoute: {guid: "WebSocketRoute@..."}}
2757                if let Some(wsr_guid) = params
2758                    .get("webSocketRoute")
2759                    .and_then(|v| v.get("guid"))
2760                    .and_then(|v| v.as_str())
2761                {
2762                    let connection = self.connection();
2763                    let wsr_guid_owned = wsr_guid.to_string();
2764                    let ws_route_handlers = self.ws_route_handlers.clone();
2765
2766                    tokio::spawn(async move {
2767                        let route: crate::protocol::WebSocketRoute = match connection
2768                            .get_typed::<crate::protocol::WebSocketRoute>(&wsr_guid_owned)
2769                            .await
2770                        {
2771                            Ok(r) => r,
2772                            Err(e) => {
2773                                tracing::warn!("Failed to get WebSocketRoute object: {}", e);
2774                                return;
2775                            }
2776                        };
2777
2778                        let url = route.url().to_string();
2779                        let handlers = ws_route_handlers.lock().unwrap().clone();
2780                        for entry in handlers.iter().rev() {
2781                            if crate::protocol::route::matches_pattern(&entry.pattern, &url) {
2782                                let handler = entry.handler.clone();
2783                                let route_clone = route.clone();
2784                                tokio::spawn(async move {
2785                                    if let Err(e) = handler(route_clone).await {
2786                                        tracing::error!(
2787                                            "Error in context webSocketRoute handler: {}",
2788                                            e
2789                                        );
2790                                    }
2791                                });
2792                                break;
2793                            }
2794                        }
2795                    });
2796                }
2797            }
2798            _ => {
2799                // Other events will be handled in future phases
2800            }
2801        }
2802    }
2803
2804    fn was_collected(&self) -> bool {
2805        self.base.was_collected()
2806    }
2807
2808    fn as_any(&self) -> &dyn Any {
2809        self
2810    }
2811}
2812
2813impl std::fmt::Debug for BrowserContext {
2814    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2815        f.debug_struct("BrowserContext")
2816            .field("guid", &self.guid())
2817            .finish()
2818    }
2819}
2820
2821/// Viewport dimensions for browser context.
2822///
2823/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
2824#[derive(Debug, Clone, Serialize, Deserialize)]
2825pub struct Viewport {
2826    /// Page width in pixels
2827    pub width: u32,
2828    /// Page height in pixels
2829    pub height: u32,
2830}
2831
2832/// Geolocation coordinates.
2833///
2834/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
2835#[derive(Debug, Clone, Serialize, Deserialize)]
2836pub struct Geolocation {
2837    /// Latitude between -90 and 90
2838    pub latitude: f64,
2839    /// Longitude between -180 and 180
2840    pub longitude: f64,
2841    /// Optional accuracy in meters (default: 0)
2842    #[serde(skip_serializing_if = "Option::is_none")]
2843    pub accuracy: Option<f64>,
2844}
2845
2846/// Cookie information for storage state.
2847///
2848/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
2849#[derive(Debug, Clone, Serialize, Deserialize)]
2850#[serde(rename_all = "camelCase")]
2851#[non_exhaustive]
2852pub struct Cookie {
2853    /// Cookie name
2854    pub name: String,
2855    /// Cookie value
2856    pub value: String,
2857    /// Cookie domain (use dot prefix for subdomain matching, e.g., ".example.com")
2858    pub domain: String,
2859    /// Cookie path
2860    pub path: String,
2861    /// Unix timestamp in seconds; -1 for session cookies
2862    pub expires: f64,
2863    /// HTTP-only flag
2864    pub http_only: bool,
2865    /// Secure flag
2866    pub secure: bool,
2867    /// SameSite attribute ("Strict", "Lax", "None")
2868    #[serde(skip_serializing_if = "Option::is_none")]
2869    pub same_site: Option<String>,
2870}
2871
2872impl Cookie {
2873    /// Create a session cookie (no expiry) with the given name and value.
2874    /// Set `domain`+`path` (or serve it for a URL) before adding it.
2875    pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
2876        Self {
2877            name: name.into(),
2878            value: value.into(),
2879            domain: String::new(),
2880            path: "/".to_string(),
2881            expires: -1.0,
2882            http_only: false,
2883            secure: false,
2884            same_site: None,
2885        }
2886    }
2887    /// Cookie domain (e.g. "example.com").
2888    pub fn domain(mut self, domain: impl Into<String>) -> Self {
2889        self.domain = domain.into();
2890        self
2891    }
2892    /// Cookie path.
2893    pub fn path(mut self, path: impl Into<String>) -> Self {
2894        self.path = path.into();
2895        self
2896    }
2897    /// Expiry as Unix time in seconds (-1 for a session cookie).
2898    pub fn expires(mut self, expires: f64) -> Self {
2899        self.expires = expires;
2900        self
2901    }
2902    /// Mark the cookie HttpOnly.
2903    pub fn http_only(mut self, http_only: bool) -> Self {
2904        self.http_only = http_only;
2905        self
2906    }
2907    /// Mark the cookie Secure.
2908    pub fn secure(mut self, secure: bool) -> Self {
2909        self.secure = secure;
2910        self
2911    }
2912    /// SameSite attribute ("Strict", "Lax", or "None").
2913    pub fn same_site(mut self, same_site: impl Into<String>) -> Self {
2914        self.same_site = Some(same_site.into());
2915        self
2916    }
2917}
2918
2919/// Local storage item for storage state.
2920///
2921/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
2922#[derive(Debug, Clone, Serialize, Deserialize)]
2923#[non_exhaustive]
2924pub struct LocalStorageItem {
2925    /// Storage key
2926    pub name: String,
2927    /// Storage value
2928    pub value: String,
2929}
2930
2931impl LocalStorageItem {
2932    /// A single localStorage key/value pair.
2933    pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
2934        Self {
2935            name: name.into(),
2936            value: value.into(),
2937        }
2938    }
2939}
2940
2941/// Origin with local storage items for storage state.
2942///
2943/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
2944#[derive(Debug, Clone, Serialize, Deserialize)]
2945#[serde(rename_all = "camelCase")]
2946#[non_exhaustive]
2947pub struct Origin {
2948    /// Origin URL (e.g., `https://example.com`)
2949    pub origin: String,
2950    /// Local storage items for this origin
2951    pub local_storage: Vec<LocalStorageItem>,
2952    /// IndexedDB contents for this origin, as the driver's opaque payload.
2953    ///
2954    /// Populated when the state was captured with
2955    /// [`StorageStateOptions::indexed_db`], and passed back verbatim on
2956    /// restore. Kept as raw JSON rather than modelled: the shape is an
2957    /// implementation detail of the driver's snapshot format, and the only
2958    /// supported operation is carrying it back unchanged.
2959    #[serde(rename = "indexedDB", default, skip_serializing_if = "Option::is_none")]
2960    pub indexed_db: Option<serde_json::Value>,
2961}
2962
2963impl Origin {
2964    /// Storage entries for one origin.
2965    pub fn new(origin: impl Into<String>, local_storage: Vec<LocalStorageItem>) -> Self {
2966        Self {
2967            origin: origin.into(),
2968            local_storage,
2969            indexed_db: None,
2970        }
2971    }
2972}
2973
2974/// Storage state containing cookies and local storage.
2975///
2976/// Used to populate a browser context with saved authentication state,
2977/// enabling session persistence across context instances.
2978///
2979/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
2980#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2981#[non_exhaustive]
2982pub struct StorageState {
2983    /// List of cookies
2984    pub cookies: Vec<Cookie>,
2985    /// List of origins with local storage
2986    pub origins: Vec<Origin>,
2987    /// WebAuthn passkeys held by the context's virtual authenticator.
2988    /// Only populated when the state was captured with
2989    /// [`StorageStateOptions::credentials`]; omitted from the wire when empty
2990    /// so a state captured without them is unchanged.
2991    #[serde(default, skip_serializing_if = "Option::is_none")]
2992    pub credentials: Option<Vec<crate::protocol::VirtualCredential>>,
2993}
2994
2995impl StorageState {
2996    /// Cookies to seed the context with.
2997    pub fn cookies(mut self, cookies: Vec<Cookie>) -> Self {
2998        self.cookies = cookies;
2999        self
3000    }
3001    /// Per-origin storage (localStorage) to seed the context with.
3002    pub fn origins(mut self, origins: Vec<Origin>) -> Self {
3003        self.origins = origins;
3004        self
3005    }
3006    /// WebAuthn passkeys to seed the context's virtual authenticator with.
3007    pub fn credentials(mut self, credentials: Vec<crate::protocol::VirtualCredential>) -> Self {
3008        self.credentials = Some(credentials);
3009        self
3010    }
3011}
3012
3013/// Options for [`BrowserContext::storage_state`].
3014///
3015/// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-storage-state>
3016#[derive(Debug, Clone, Default, Serialize)]
3017#[serde(rename_all = "camelCase")]
3018#[non_exhaustive]
3019pub struct StorageStateOptions {
3020    /// Include IndexedDB contents in the captured state.
3021    // camelCase would render this "indexedDb"; the protocol field is
3022    // "indexedDB", and the driver drops unknown parameters silently, so the
3023    // wrong casing is not an error but a no-op.
3024    #[serde(rename = "indexedDB", skip_serializing_if = "Option::is_none")]
3025    pub indexed_db: Option<bool>,
3026    /// Include the virtual authenticator's WebAuthn passkeys.
3027    #[serde(skip_serializing_if = "Option::is_none")]
3028    pub credentials: Option<bool>,
3029}
3030
3031impl StorageStateOptions {
3032    /// Include IndexedDB contents in the captured state.
3033    pub fn indexed_db(mut self, include: bool) -> Self {
3034        self.indexed_db = Some(include);
3035        self
3036    }
3037    /// Include the virtual authenticator's WebAuthn passkeys.
3038    pub fn credentials(mut self, include: bool) -> Self {
3039        self.credentials = Some(include);
3040        self
3041    }
3042}
3043
3044/// Options for filtering which cookies to clear with `BrowserContext::clear_cookies()`.
3045///
3046/// All fields are optional; when provided they act as AND-combined filters.
3047///
3048/// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-clear-cookies>
3049#[derive(Debug, Clone, Default, Serialize)]
3050#[serde(rename_all = "camelCase")]
3051#[non_exhaustive]
3052pub struct ClearCookiesOptions {
3053    /// Filter by cookie name (exact match).
3054    #[serde(skip_serializing_if = "Option::is_none")]
3055    pub name: Option<String>,
3056    /// Filter by cookie domain.
3057    #[serde(skip_serializing_if = "Option::is_none")]
3058    pub domain: Option<String>,
3059    /// Filter by cookie path.
3060    #[serde(skip_serializing_if = "Option::is_none")]
3061    pub path: Option<String>,
3062}
3063
3064impl ClearCookiesOptions {
3065    /// Only clear cookies with this name.
3066    pub fn name(mut self, name: impl Into<String>) -> Self {
3067        self.name = Some(name.into());
3068        self
3069    }
3070    /// Only clear cookies for this domain.
3071    pub fn domain(mut self, domain: impl Into<String>) -> Self {
3072        self.domain = Some(domain.into());
3073        self
3074    }
3075    /// Only clear cookies for this path.
3076    pub fn path(mut self, path: impl Into<String>) -> Self {
3077        self.path = Some(path.into());
3078        self
3079    }
3080}
3081
3082/// Options for `BrowserContext::grant_permissions()`.
3083///
3084/// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-grant-permissions>
3085#[derive(Debug, Clone, Default)]
3086#[non_exhaustive]
3087pub struct GrantPermissionsOptions {
3088    /// Optional origin to restrict the permission grant to.
3089    ///
3090    /// For example `"https://example.com"`.
3091    pub origin: Option<String>,
3092}
3093
3094impl GrantPermissionsOptions {
3095    /// Restrict the grant to the given origin.
3096    pub fn origin(mut self, origin: impl Into<String>) -> Self {
3097        self.origin = Some(origin.into());
3098        self
3099    }
3100}
3101
3102/// Options for recording HAR.
3103///
3104/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-record-har>
3105#[derive(Debug, Clone, Serialize, Default)]
3106#[serde(rename_all = "camelCase")]
3107#[non_exhaustive]
3108pub struct RecordHar {
3109    /// Path on the filesystem to write the HAR file to.
3110    pub path: String,
3111    /// Optional setting to control whether to omit request content from the HAR.
3112    #[serde(skip_serializing_if = "Option::is_none")]
3113    pub omit_content: Option<bool>,
3114    /// Optional setting to control resource content management.
3115    /// "omit" | "embed" | "attach"
3116    #[serde(skip_serializing_if = "Option::is_none")]
3117    pub content: Option<String>,
3118    /// "full" | "minimal"
3119    #[serde(skip_serializing_if = "Option::is_none")]
3120    pub mode: Option<String>,
3121    /// A glob or regex pattern to filter requests that are stored in the HAR.
3122    #[serde(skip_serializing_if = "Option::is_none")]
3123    pub url_filter: Option<String>,
3124}
3125
3126impl RecordHar {
3127    /// Record a HAR to the given path.
3128    pub fn new(path: impl Into<String>) -> Self {
3129        Self {
3130            path: path.into(),
3131            omit_content: None,
3132            content: None,
3133            mode: None,
3134            url_filter: None,
3135        }
3136    }
3137    /// Omit response bodies from the HAR.
3138    pub fn omit_content(mut self, omit_content: bool) -> Self {
3139        self.omit_content = Some(omit_content);
3140        self
3141    }
3142    /// Content mode ("embed", "attach", or "omit").
3143    pub fn content(mut self, content: impl Into<String>) -> Self {
3144        self.content = Some(content.into());
3145        self
3146    }
3147    /// Recording mode ("full" or "minimal").
3148    pub fn mode(mut self, mode: impl Into<String>) -> Self {
3149        self.mode = Some(mode.into());
3150        self
3151    }
3152    /// Only record requests matching this URL glob.
3153    pub fn url_filter(mut self, url_filter: impl Into<String>) -> Self {
3154        self.url_filter = Some(url_filter.into());
3155        self
3156    }
3157}
3158
3159/// Options for recording video.
3160///
3161/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-record-video>
3162#[derive(Debug, Clone, Serialize, Default)]
3163#[non_exhaustive]
3164pub struct RecordVideo {
3165    /// Path to the directory to put videos into.
3166    pub dir: String,
3167    /// Optional dimensions of the recorded videos.
3168    #[serde(skip_serializing_if = "Option::is_none")]
3169    pub size: Option<Viewport>,
3170}
3171
3172impl RecordVideo {
3173    /// Record videos into the given directory.
3174    pub fn new(dir: impl Into<String>) -> Self {
3175        Self {
3176            dir: dir.into(),
3177            size: None,
3178        }
3179    }
3180    /// Recorded video size.
3181    pub fn size(mut self, size: Viewport) -> Self {
3182        self.size = Some(size);
3183        self
3184    }
3185}
3186
3187/// Options for creating a new browser context.
3188///
3189/// Controls how downloads are handled in a [`BrowserContext`].
3190///
3191/// See the `accept_downloads` field of [`BrowserContextOptions`].
3192#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
3193#[non_exhaustive]
3194pub enum AcceptDownloads {
3195    /// Allow and capture downloads via the `download` event.
3196    #[serde(rename = "accept")]
3197    Accept,
3198    /// Block downloads.
3199    #[serde(rename = "deny")]
3200    Deny,
3201    /// Let the browser handle downloads natively without routing through Playwright.
3202    #[serde(rename = "internal")]
3203    Internal,
3204}
3205
3206impl From<bool> for AcceptDownloads {
3207    fn from(value: bool) -> Self {
3208        if value { Self::Accept } else { Self::Deny }
3209    }
3210}
3211
3212/// Allows customizing viewport, user agent, locale, timezone, geolocation,
3213/// permissions, and other browser context settings.
3214///
3215/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
3216#[derive(Debug, Clone, Default, Serialize)]
3217#[serde(rename_all = "camelCase")]
3218#[non_exhaustive]
3219pub struct BrowserContextOptions {
3220    /// Sets consistent viewport for all pages in the context.
3221    /// Set to null via `no_viewport(true)` to disable viewport emulation.
3222    #[serde(skip_serializing_if = "Option::is_none")]
3223    pub viewport: Option<Viewport>,
3224
3225    /// Disables viewport emulation when set to true.
3226    /// Note: Playwright's public API calls this `noViewport`, but the protocol
3227    /// expects `noDefaultViewport`. playwright-python applies this transformation
3228    /// in `_prepare_browser_context_params`.
3229    #[serde(skip_serializing_if = "Option::is_none")]
3230    #[serde(rename = "noDefaultViewport")]
3231    pub no_viewport: Option<bool>,
3232
3233    /// Custom user agent string
3234    #[serde(skip_serializing_if = "Option::is_none")]
3235    pub user_agent: Option<String>,
3236
3237    /// Locale for the context (e.g., "en-GB", "de-DE", "fr-FR")
3238    #[serde(skip_serializing_if = "Option::is_none")]
3239    pub locale: Option<String>,
3240
3241    /// Timezone identifier (e.g., "America/New_York", "Europe/Berlin")
3242    #[serde(skip_serializing_if = "Option::is_none")]
3243    pub timezone_id: Option<String>,
3244
3245    /// Geolocation coordinates
3246    #[serde(skip_serializing_if = "Option::is_none")]
3247    pub geolocation: Option<Geolocation>,
3248
3249    /// List of permissions to grant (e.g., "geolocation", "notifications")
3250    #[serde(skip_serializing_if = "Option::is_none")]
3251    pub permissions: Option<Vec<String>>,
3252
3253    /// Network proxy settings
3254    #[serde(skip_serializing_if = "Option::is_none")]
3255    pub proxy: Option<ProxySettings>,
3256
3257    /// Emulates 'prefers-colors-scheme' media feature ("light", "dark", "no-preference")
3258    #[serde(skip_serializing_if = "Option::is_none")]
3259    pub color_scheme: Option<String>,
3260
3261    /// Whether the viewport supports touch events
3262    #[serde(skip_serializing_if = "Option::is_none")]
3263    pub has_touch: Option<bool>,
3264
3265    /// Whether the meta viewport tag is respected
3266    #[serde(skip_serializing_if = "Option::is_none")]
3267    pub is_mobile: Option<bool>,
3268
3269    /// Whether JavaScript is enabled in the context
3270    #[serde(skip_serializing_if = "Option::is_none")]
3271    pub javascript_enabled: Option<bool>,
3272
3273    /// Emulates network being offline
3274    #[serde(skip_serializing_if = "Option::is_none")]
3275    pub offline: Option<bool>,
3276
3277    /// How to handle downloads. See [`AcceptDownloads`] for options.
3278    #[serde(skip_serializing_if = "Option::is_none")]
3279    pub accept_downloads: Option<AcceptDownloads>,
3280
3281    /// Whether to bypass Content-Security-Policy
3282    #[serde(skip_serializing_if = "Option::is_none")]
3283    pub bypass_csp: Option<bool>,
3284
3285    /// Whether to ignore HTTPS errors
3286    #[serde(skip_serializing_if = "Option::is_none")]
3287    pub ignore_https_errors: Option<bool>,
3288
3289    /// Device scale factor (default: 1)
3290    #[serde(skip_serializing_if = "Option::is_none")]
3291    pub device_scale_factor: Option<f64>,
3292
3293    /// Extra HTTP headers to send with every request
3294    #[serde(skip_serializing_if = "Option::is_none")]
3295    pub extra_http_headers: Option<HashMap<String, String>>,
3296
3297    /// Base URL for relative navigation
3298    #[serde(skip_serializing_if = "Option::is_none")]
3299    pub base_url: Option<String>,
3300
3301    /// Storage state to populate the context (cookies, localStorage, sessionStorage).
3302    /// Can be an inline StorageState object or a file path string.
3303    /// Use builder methods `storage_state()` for inline or `storage_state_path()` for file path.
3304    #[serde(skip_serializing_if = "Option::is_none")]
3305    pub storage_state: Option<StorageState>,
3306
3307    /// Storage state file path (alternative to inline storage_state).
3308    /// This is handled by the builder and converted to storage_state during serialization.
3309    #[serde(skip_serializing_if = "Option::is_none")]
3310    pub storage_state_path: Option<String>,
3311
3312    // Launch options (for launch_persistent_context)
3313    /// Additional arguments to pass to browser instance
3314    #[serde(skip_serializing_if = "Option::is_none")]
3315    pub args: Option<Vec<String>>,
3316
3317    /// Browser distribution channel (e.g., "chrome", "msedge")
3318    #[serde(skip_serializing_if = "Option::is_none")]
3319    pub channel: Option<String>,
3320
3321    /// Enable Chromium sandboxing (default: false on Linux)
3322    #[serde(skip_serializing_if = "Option::is_none")]
3323    pub chromium_sandbox: Option<bool>,
3324
3325    /// Auto-open DevTools (deprecated, default: false)
3326    #[serde(skip_serializing_if = "Option::is_none")]
3327    pub devtools: Option<bool>,
3328
3329    /// Directory to save downloads
3330    #[serde(skip_serializing_if = "Option::is_none")]
3331    pub downloads_path: Option<String>,
3332
3333    /// Path to custom browser executable
3334    #[serde(skip_serializing_if = "Option::is_none")]
3335    pub executable_path: Option<String>,
3336
3337    /// Firefox user preferences (Firefox only)
3338    #[serde(skip_serializing_if = "Option::is_none")]
3339    pub firefox_user_prefs: Option<HashMap<String, serde_json::Value>>,
3340
3341    /// Run in headless mode (default: true unless devtools=true)
3342    #[serde(skip_serializing_if = "Option::is_none")]
3343    pub headless: Option<bool>,
3344
3345    /// Filter or disable default browser arguments.
3346    /// When `true`, Playwright does not pass its own default args.
3347    /// When an array, filters out the given default arguments.
3348    ///
3349    /// See: <https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context>
3350    #[serde(skip_serializing_if = "Option::is_none")]
3351    pub ignore_default_args: Option<IgnoreDefaultArgs>,
3352
3353    /// Slow down operations by N milliseconds
3354    #[serde(skip_serializing_if = "Option::is_none")]
3355    pub slow_mo: Option<f64>,
3356
3357    /// Timeout for browser launch in milliseconds
3358    #[serde(skip_serializing_if = "Option::is_none")]
3359    pub timeout: Option<f64>,
3360
3361    /// Directory to save traces
3362    #[serde(skip_serializing_if = "Option::is_none")]
3363    pub traces_dir: Option<String>,
3364
3365    /// Check if strict selectors mode is enabled
3366    #[serde(skip_serializing_if = "Option::is_none")]
3367    pub strict_selectors: Option<bool>,
3368
3369    /// Emulates 'prefers-reduced-motion' media feature
3370    #[serde(skip_serializing_if = "Option::is_none")]
3371    pub reduced_motion: Option<String>,
3372
3373    /// Emulates 'forced-colors' media feature
3374    #[serde(skip_serializing_if = "Option::is_none")]
3375    pub forced_colors: Option<String>,
3376
3377    /// Whether to allow sites to register Service workers
3378    #[serde(skip_serializing_if = "Option::is_none")]
3379    pub service_workers: Option<String>,
3380
3381    /// Options for recording HAR
3382    #[serde(skip_serializing_if = "Option::is_none")]
3383    pub record_har: Option<RecordHar>,
3384
3385    /// Options for recording video
3386    #[serde(skip_serializing_if = "Option::is_none")]
3387    pub record_video: Option<RecordVideo>,
3388}
3389
3390impl BrowserContextOptions {
3391    /// Creates a new builder for BrowserContextOptions
3392    pub fn builder() -> BrowserContextOptionsBuilder {
3393        BrowserContextOptionsBuilder::default()
3394    }
3395}
3396
3397/// Builder for BrowserContextOptions
3398#[derive(Debug, Clone, Default)]
3399pub struct BrowserContextOptionsBuilder {
3400    viewport: Option<Viewport>,
3401    no_viewport: Option<bool>,
3402    user_agent: Option<String>,
3403    locale: Option<String>,
3404    timezone_id: Option<String>,
3405    geolocation: Option<Geolocation>,
3406    permissions: Option<Vec<String>>,
3407    proxy: Option<ProxySettings>,
3408    color_scheme: Option<String>,
3409    has_touch: Option<bool>,
3410    is_mobile: Option<bool>,
3411    javascript_enabled: Option<bool>,
3412    offline: Option<bool>,
3413    accept_downloads: Option<AcceptDownloads>,
3414    bypass_csp: Option<bool>,
3415    ignore_https_errors: Option<bool>,
3416    device_scale_factor: Option<f64>,
3417    extra_http_headers: Option<HashMap<String, String>>,
3418    base_url: Option<String>,
3419    storage_state: Option<StorageState>,
3420    storage_state_path: Option<String>,
3421    // Launch options
3422    args: Option<Vec<String>>,
3423    channel: Option<String>,
3424    chromium_sandbox: Option<bool>,
3425    devtools: Option<bool>,
3426    downloads_path: Option<String>,
3427    executable_path: Option<String>,
3428    firefox_user_prefs: Option<HashMap<String, serde_json::Value>>,
3429    headless: Option<bool>,
3430    ignore_default_args: Option<IgnoreDefaultArgs>,
3431    slow_mo: Option<f64>,
3432    timeout: Option<f64>,
3433    traces_dir: Option<String>,
3434    strict_selectors: Option<bool>,
3435    reduced_motion: Option<String>,
3436    forced_colors: Option<String>,
3437    service_workers: Option<String>,
3438    record_har: Option<RecordHar>,
3439    record_video: Option<RecordVideo>,
3440}
3441
3442impl BrowserContextOptionsBuilder {
3443    /// Sets the viewport dimensions
3444    pub fn viewport(mut self, viewport: Viewport) -> Self {
3445        self.viewport = Some(viewport);
3446        self.no_viewport = None; // Clear no_viewport if setting viewport
3447        self
3448    }
3449
3450    /// Disables viewport emulation
3451    pub fn no_viewport(mut self, no_viewport: bool) -> Self {
3452        self.no_viewport = Some(no_viewport);
3453        if no_viewport {
3454            self.viewport = None; // Clear viewport if setting no_viewport
3455        }
3456        self
3457    }
3458
3459    /// Sets the user agent string
3460    pub fn user_agent(mut self, user_agent: String) -> Self {
3461        self.user_agent = Some(user_agent);
3462        self
3463    }
3464
3465    /// Sets the locale
3466    pub fn locale(mut self, locale: String) -> Self {
3467        self.locale = Some(locale);
3468        self
3469    }
3470
3471    /// Sets the timezone identifier
3472    pub fn timezone_id(mut self, timezone_id: String) -> Self {
3473        self.timezone_id = Some(timezone_id);
3474        self
3475    }
3476
3477    /// Sets the geolocation
3478    pub fn geolocation(mut self, geolocation: Geolocation) -> Self {
3479        self.geolocation = Some(geolocation);
3480        self
3481    }
3482
3483    /// Sets the permissions to grant
3484    pub fn permissions(mut self, permissions: Vec<String>) -> Self {
3485        self.permissions = Some(permissions);
3486        self
3487    }
3488
3489    /// Sets the network proxy settings for this context.
3490    ///
3491    /// This allows routing all network traffic through a proxy server,
3492    /// useful for rotating proxies without creating new browsers.
3493    ///
3494    /// # Example
3495    ///
3496    /// ```no_run
3497    /// use playwright_rs::protocol::{BrowserContextOptions, ProxySettings};
3498    ///
3499    /// let options = BrowserContextOptions::builder()
3500    ///     .proxy(
3501    ///         ProxySettings::new("http://proxy.example.com:8080")
3502    ///             .bypass(".example.com")
3503    ///             .username("user")
3504    ///             .password("pass"),
3505    ///     )
3506    ///     .build();
3507    /// ```
3508    ///
3509    /// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
3510    pub fn proxy(mut self, proxy: ProxySettings) -> Self {
3511        self.proxy = Some(proxy);
3512        self
3513    }
3514
3515    /// Sets the color scheme preference
3516    pub fn color_scheme(mut self, color_scheme: String) -> Self {
3517        self.color_scheme = Some(color_scheme);
3518        self
3519    }
3520
3521    /// Sets whether the viewport supports touch events
3522    pub fn has_touch(mut self, has_touch: bool) -> Self {
3523        self.has_touch = Some(has_touch);
3524        self
3525    }
3526
3527    /// Sets whether this is a mobile viewport
3528    pub fn is_mobile(mut self, is_mobile: bool) -> Self {
3529        self.is_mobile = Some(is_mobile);
3530        self
3531    }
3532
3533    /// Sets whether JavaScript is enabled
3534    pub fn javascript_enabled(mut self, javascript_enabled: bool) -> Self {
3535        self.javascript_enabled = Some(javascript_enabled);
3536        self
3537    }
3538
3539    /// Sets whether to emulate offline network
3540    pub fn offline(mut self, offline: bool) -> Self {
3541        self.offline = Some(offline);
3542        self
3543    }
3544
3545    /// Sets how to handle downloads. Accepts `AcceptDownloads` or `bool`
3546    /// (`true` → `Accept`, `false` → `Deny`).
3547    pub fn accept_downloads(mut self, accept_downloads: impl Into<AcceptDownloads>) -> Self {
3548        self.accept_downloads = Some(accept_downloads.into());
3549        self
3550    }
3551
3552    /// Sets whether to bypass Content-Security-Policy
3553    pub fn bypass_csp(mut self, bypass_csp: bool) -> Self {
3554        self.bypass_csp = Some(bypass_csp);
3555        self
3556    }
3557
3558    /// Sets whether to ignore HTTPS errors
3559    pub fn ignore_https_errors(mut self, ignore_https_errors: bool) -> Self {
3560        self.ignore_https_errors = Some(ignore_https_errors);
3561        self
3562    }
3563
3564    /// Sets the device scale factor
3565    pub fn device_scale_factor(mut self, device_scale_factor: f64) -> Self {
3566        self.device_scale_factor = Some(device_scale_factor);
3567        self
3568    }
3569
3570    /// Sets extra HTTP headers
3571    pub fn extra_http_headers(mut self, extra_http_headers: HashMap<String, String>) -> Self {
3572        self.extra_http_headers = Some(extra_http_headers);
3573        self
3574    }
3575
3576    /// Sets the base URL for relative navigation
3577    pub fn base_url(mut self, base_url: String) -> Self {
3578        self.base_url = Some(base_url);
3579        self
3580    }
3581
3582    /// Sets the storage state inline (cookies, localStorage).
3583    ///
3584    /// Populates the browser context with the provided storage state, including
3585    /// cookies and local storage. This is useful for initializing a context with
3586    /// a saved authentication state.
3587    ///
3588    /// Mutually exclusive with `storage_state_path()`.
3589    ///
3590    /// # Example
3591    ///
3592    /// ```rust
3593    /// use playwright_rs::protocol::{BrowserContextOptions, Cookie, StorageState, Origin, LocalStorageItem};
3594    ///
3595    /// let storage_state = StorageState::default()
3596    ///     .cookies(vec![
3597    ///         Cookie::new("session_id", "abc123")
3598    ///             .domain(".example.com")
3599    ///             .http_only(true)
3600    ///             .secure(true)
3601    ///             .same_site("Lax"),
3602    ///     ])
3603    ///     .origins(vec![Origin::new(
3604    ///         "https://example.com",
3605    ///         vec![LocalStorageItem::new("user_prefs", "{\"theme\":\"dark\"}")],
3606    ///     )]);
3607    ///
3608    /// let options = BrowserContextOptions::builder()
3609    ///     .storage_state(storage_state)
3610    ///     .build();
3611    /// ```
3612    ///
3613    /// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
3614    pub fn storage_state(mut self, storage_state: StorageState) -> Self {
3615        self.storage_state = Some(storage_state);
3616        self.storage_state_path = None; // Clear path if setting inline
3617        self
3618    }
3619
3620    /// Sets the storage state from a file path.
3621    ///
3622    /// The file should contain a JSON representation of StorageState with cookies
3623    /// and origins. This is useful for loading authentication state saved from a
3624    /// previous session.
3625    ///
3626    /// Mutually exclusive with `storage_state()`.
3627    ///
3628    /// # Example
3629    ///
3630    /// ```rust
3631    /// use playwright_rs::protocol::BrowserContextOptions;
3632    ///
3633    /// let options = BrowserContextOptions::builder()
3634    ///     .storage_state_path("auth.json".to_string())
3635    ///     .build();
3636    /// ```
3637    ///
3638    /// The file should have this format:
3639    /// ```json
3640    /// {
3641    ///   "cookies": [{
3642    ///     "name": "session_id",
3643    ///     "value": "abc123",
3644    ///     "domain": ".example.com",
3645    ///     "path": "/",
3646    ///     "expires": -1,
3647    ///     "httpOnly": true,
3648    ///     "secure": true,
3649    ///     "sameSite": "Lax"
3650    ///   }],
3651    ///   "origins": [{
3652    ///     "origin": "https://example.com",
3653    ///     "localStorage": [{
3654    ///       "name": "user_prefs",
3655    ///       "value": "{\"theme\":\"dark\"}"
3656    ///     }]
3657    ///   }]
3658    /// }
3659    /// ```
3660    ///
3661    /// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
3662    pub fn storage_state_path(mut self, path: String) -> Self {
3663        self.storage_state_path = Some(path);
3664        self.storage_state = None; // Clear inline if setting path
3665        self
3666    }
3667
3668    /// Sets additional arguments to pass to browser instance (for launch_persistent_context)
3669    pub fn args(mut self, args: Vec<String>) -> Self {
3670        self.args = Some(args);
3671        self
3672    }
3673
3674    /// Sets browser distribution channel (for launch_persistent_context)
3675    pub fn channel(mut self, channel: String) -> Self {
3676        self.channel = Some(channel);
3677        self
3678    }
3679
3680    /// Enables or disables Chromium sandboxing (for launch_persistent_context)
3681    pub fn chromium_sandbox(mut self, enabled: bool) -> Self {
3682        self.chromium_sandbox = Some(enabled);
3683        self
3684    }
3685
3686    /// Auto-open DevTools (for launch_persistent_context)
3687    pub fn devtools(mut self, enabled: bool) -> Self {
3688        self.devtools = Some(enabled);
3689        self
3690    }
3691
3692    /// Sets directory to save downloads (for launch_persistent_context)
3693    pub fn downloads_path(mut self, path: String) -> Self {
3694        self.downloads_path = Some(path);
3695        self
3696    }
3697
3698    /// Sets path to custom browser executable (for launch_persistent_context)
3699    pub fn executable_path(mut self, path: String) -> Self {
3700        self.executable_path = Some(path);
3701        self
3702    }
3703
3704    /// Sets Firefox user preferences (for launch_persistent_context, Firefox only)
3705    pub fn firefox_user_prefs(mut self, prefs: HashMap<String, serde_json::Value>) -> Self {
3706        self.firefox_user_prefs = Some(prefs);
3707        self
3708    }
3709
3710    /// Run in headless mode (for launch_persistent_context)
3711    pub fn headless(mut self, enabled: bool) -> Self {
3712        self.headless = Some(enabled);
3713        self
3714    }
3715
3716    /// Filter or disable default browser arguments (for launch_persistent_context).
3717    ///
3718    /// When `IgnoreDefaultArgs::Bool(true)`, Playwright does not pass its own
3719    /// default arguments and only uses the ones from `args`.
3720    /// When `IgnoreDefaultArgs::Array(vec)`, filters out the given default arguments.
3721    ///
3722    /// See: <https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context>
3723    pub fn ignore_default_args(mut self, args: IgnoreDefaultArgs) -> Self {
3724        self.ignore_default_args = Some(args);
3725        self
3726    }
3727
3728    /// Slow down operations by N milliseconds (for launch_persistent_context)
3729    pub fn slow_mo(mut self, ms: f64) -> Self {
3730        self.slow_mo = Some(ms);
3731        self
3732    }
3733
3734    /// Set timeout for browser launch in milliseconds (for launch_persistent_context)
3735    pub fn timeout(mut self, ms: f64) -> Self {
3736        self.timeout = Some(ms);
3737        self
3738    }
3739
3740    /// Set directory to save traces (for launch_persistent_context)
3741    pub fn traces_dir(mut self, path: String) -> Self {
3742        self.traces_dir = Some(path);
3743        self
3744    }
3745
3746    /// Check if strict selectors mode is enabled
3747    pub fn strict_selectors(mut self, enabled: bool) -> Self {
3748        self.strict_selectors = Some(enabled);
3749        self
3750    }
3751
3752    /// Emulates 'prefers-reduced-motion' media feature
3753    pub fn reduced_motion(mut self, value: String) -> Self {
3754        self.reduced_motion = Some(value);
3755        self
3756    }
3757
3758    /// Emulates 'forced-colors' media feature
3759    pub fn forced_colors(mut self, value: String) -> Self {
3760        self.forced_colors = Some(value);
3761        self
3762    }
3763
3764    /// Whether to allow sites to register Service workers ("allow" | "block")
3765    pub fn service_workers(mut self, value: String) -> Self {
3766        self.service_workers = Some(value);
3767        self
3768    }
3769
3770    /// Sets options for recording HAR
3771    pub fn record_har(mut self, record_har: RecordHar) -> Self {
3772        self.record_har = Some(record_har);
3773        self
3774    }
3775
3776    /// Sets options for recording video
3777    pub fn record_video(mut self, record_video: RecordVideo) -> Self {
3778        self.record_video = Some(record_video);
3779        self
3780    }
3781
3782    /// Builds the BrowserContextOptions
3783    pub fn build(self) -> BrowserContextOptions {
3784        BrowserContextOptions {
3785            viewport: self.viewport,
3786            no_viewport: self.no_viewport,
3787            user_agent: self.user_agent,
3788            locale: self.locale,
3789            timezone_id: self.timezone_id,
3790            geolocation: self.geolocation,
3791            permissions: self.permissions,
3792            proxy: self.proxy,
3793            color_scheme: self.color_scheme,
3794            has_touch: self.has_touch,
3795            is_mobile: self.is_mobile,
3796            javascript_enabled: self.javascript_enabled,
3797            offline: self.offline,
3798            accept_downloads: self.accept_downloads,
3799            bypass_csp: self.bypass_csp,
3800            ignore_https_errors: self.ignore_https_errors,
3801            device_scale_factor: self.device_scale_factor,
3802            extra_http_headers: self.extra_http_headers,
3803            base_url: self.base_url,
3804            storage_state: self.storage_state,
3805            storage_state_path: self.storage_state_path,
3806            // Launch options
3807            args: self.args,
3808            channel: self.channel,
3809            chromium_sandbox: self.chromium_sandbox,
3810            devtools: self.devtools,
3811            downloads_path: self.downloads_path,
3812            executable_path: self.executable_path,
3813            firefox_user_prefs: self.firefox_user_prefs,
3814            headless: self.headless,
3815            ignore_default_args: self.ignore_default_args,
3816            slow_mo: self.slow_mo,
3817            timeout: self.timeout,
3818            traces_dir: self.traces_dir,
3819            strict_selectors: self.strict_selectors,
3820            reduced_motion: self.reduced_motion,
3821            forced_colors: self.forced_colors,
3822            service_workers: self.service_workers,
3823            record_har: self.record_har,
3824            record_video: self.record_video,
3825        }
3826    }
3827}
3828
3829/// Extracts timing data from a Response object's initializer, patching in
3830/// `responseEnd` from the event's `responseEndTiming` if available.
3831async fn extract_timing(
3832    connection: &std::sync::Arc<dyn crate::server::connection::ConnectionLike>,
3833    response_guid: Option<String>,
3834    response_end_timing: Option<f64>,
3835) -> Option<serde_json::Value> {
3836    let resp_guid = response_guid?;
3837    let resp_obj: crate::protocol::ResponseObject = connection
3838        .get_typed::<crate::protocol::ResponseObject>(&resp_guid)
3839        .await
3840        .ok()?;
3841    let mut timing = resp_obj.initializer().get("timing")?.clone();
3842    crate::protocol::ResourceTiming::merge_response_end(&mut timing, response_end_timing);
3843    Some(timing)
3844}
3845
3846#[cfg(test)]
3847mod tests {
3848    use super::*;
3849    use crate::api::launch_options::IgnoreDefaultArgs;
3850
3851    #[test]
3852    fn storage_state_options_serialize_with_protocol_casing() {
3853        // The driver validates "indexedDB" and silently drops unknown keys,
3854        // so serde's camelCase ("indexedDb") would make the flag a no-op
3855        // that reports success.
3856        let opts = StorageStateOptions::default()
3857            .indexed_db(true)
3858            .credentials(true);
3859        let value = serde_json::to_value(&opts).unwrap();
3860        assert_eq!(
3861            value,
3862            serde_json::json!({ "indexedDB": true, "credentials": true })
3863        );
3864    }
3865
3866    #[test]
3867    fn storage_state_round_trips_credentials_content() {
3868        // A captured state's value is in being restorable; this pins that a
3869        // save/load through JSON preserves credential content, not just
3870        // count.
3871        // Exactly the driver's VirtualCredential schema: five required
3872        // string fields.
3873        let json = serde_json::json!({
3874            "cookies": [],
3875            "origins": [],
3876            "credentials": [{
3877                "id": "Y3JlZA",
3878                "rpId": "example.com",
3879                "userHandle": "dXNlcg",
3880                "privateKey": "cGtleQ",
3881                "publicKey": "cHVi"
3882            }]
3883        });
3884        let state: StorageState = serde_json::from_value(json.clone()).unwrap();
3885        let back = serde_json::to_value(&state).unwrap();
3886        assert_eq!(back["credentials"], json["credentials"]);
3887    }
3888
3889    #[test]
3890    fn origin_round_trips_indexed_db_payload_verbatim() {
3891        // The payload is the driver's opaque snapshot format; the contract
3892        // is carrying it back unchanged, under the protocol's exact casing.
3893        let json = serde_json::json!({
3894            "origin": "https://example.com",
3895            "localStorage": [],
3896            "indexedDB": [{"name": "db", "version": 1, "stores": []}]
3897        });
3898        let origin: Origin = serde_json::from_value(json.clone()).unwrap();
3899        let back = serde_json::to_value(&origin).unwrap();
3900        assert_eq!(back["indexedDB"], json["indexedDB"]);
3901        assert!(back.get("indexedDb").is_none());
3902    }
3903
3904    #[test]
3905    fn test_browser_context_options_ignore_default_args_bool_serialization() {
3906        let options = BrowserContextOptions::builder()
3907            .ignore_default_args(IgnoreDefaultArgs::Bool(true))
3908            .build();
3909
3910        let value = serde_json::to_value(&options).unwrap();
3911        assert_eq!(value["ignoreDefaultArgs"], serde_json::json!(true));
3912    }
3913
3914    #[test]
3915    fn test_browser_context_options_ignore_default_args_array_serialization() {
3916        let options = BrowserContextOptions::builder()
3917            .ignore_default_args(IgnoreDefaultArgs::Array(vec!["--foo".to_string()]))
3918            .build();
3919
3920        let value = serde_json::to_value(&options).unwrap();
3921        assert_eq!(value["ignoreDefaultArgs"], serde_json::json!(["--foo"]));
3922    }
3923
3924    #[test]
3925    fn test_browser_context_options_ignore_default_args_absent() {
3926        let options = BrowserContextOptions::builder().build();
3927
3928        let value = serde_json::to_value(&options).unwrap();
3929        assert!(value.get("ignoreDefaultArgs").is_none());
3930    }
3931
3932    #[test]
3933    fn test_accept_downloads_serializes_as_protocol_string() {
3934        for (variant, expected) in [
3935            (AcceptDownloads::Accept, "accept"),
3936            (AcceptDownloads::Deny, "deny"),
3937            (AcceptDownloads::Internal, "internal"),
3938        ] {
3939            let options = BrowserContextOptions::builder()
3940                .accept_downloads(variant)
3941                .build();
3942            let value = serde_json::to_value(&options).unwrap();
3943            assert_eq!(value["acceptDownloads"], serde_json::json!(expected));
3944        }
3945    }
3946
3947    #[test]
3948    fn test_accept_downloads_bool_compatibility() {
3949        let opts = BrowserContextOptions::builder()
3950            .accept_downloads(true)
3951            .build();
3952        assert_eq!(opts.accept_downloads, Some(AcceptDownloads::Accept));
3953
3954        let opts = BrowserContextOptions::builder()
3955            .accept_downloads(false)
3956            .build();
3957        assert_eq!(opts.accept_downloads, Some(AcceptDownloads::Deny));
3958    }
3959}