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