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