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