Skip to main content

playwright_rs/protocol/
browser_context.rs

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