Skip to main content

playwright_rs/protocol/
browser_context.rs

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