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