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