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::Result;
9use crate::protocol::api_request_context::APIRequestContext;
10use crate::protocol::cdp_session::CDPSession;
11use crate::protocol::event_waiter::EventWaiter;
12use crate::protocol::route::UnrouteBehavior;
13use crate::protocol::tracing::Tracing;
14use crate::protocol::{Browser, Page, ProxySettings, Request, ResponseObject, Route};
15use crate::server::channel::Channel;
16use crate::server::channel_owner::{ChannelOwner, ChannelOwnerImpl, ParentOrConnection};
17use crate::server::connection::ConnectionExt;
18use serde::{Deserialize, Serialize};
19use serde_json::Value;
20use std::any::Any;
21use std::collections::HashMap;
22use std::future::Future;
23use std::pin::Pin;
24use std::sync::{Arc, Mutex};
25use tokio::sync::oneshot;
26
27/// BrowserContext represents an isolated browser session.
28///
29/// Contexts are isolated environments within a browser instance. Each context
30/// has its own cookies, cache, and local storage, enabling independent sessions
31/// without interference.
32///
33/// # Example
34///
35/// ```ignore
36/// use playwright_rs::protocol::Playwright;
37///
38/// #[tokio::main]
39/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
40///     let playwright = Playwright::launch().await?;
41///     let browser = playwright.chromium().launch().await?;
42///
43///     // Create isolated contexts
44///     let context1 = browser.new_context().await?;
45///     let context2 = browser.new_context().await?;
46///
47///     // Create pages in each context
48///     let page1 = context1.new_page().await?;
49///     let page2 = context2.new_page().await?;
50///
51///     // Access all pages in a context
52///     let pages = context1.pages();
53///     assert_eq!(pages.len(), 1);
54///
55///     // Access the browser from a context
56///     let ctx_browser = context1.browser().unwrap();
57///     assert_eq!(ctx_browser.name(), browser.name());
58///
59///     // App mode: access initial page created automatically
60///     let chromium = playwright.chromium();
61///     let app_context = chromium
62///         .launch_persistent_context_with_options(
63///             "/tmp/app-data",
64///             playwright_rs::protocol::BrowserContextOptions::builder()
65///                 .args(vec!["--app=https://example.com".to_string()])
66///                 .headless(true)
67///                 .build()
68///         )
69///         .await?;
70///
71///     // Get the initial page (don't create a new one!)
72///     let app_pages = app_context.pages();
73///     if !app_pages.is_empty() {
74///         let initial_page = &app_pages[0];
75///         // Use the initial page...
76///     }
77///
78///     // Cleanup
79///     context1.close().await?;
80///     context2.close().await?;
81///     app_context.close().await?;
82///     browser.close().await?;
83///     Ok(())
84/// }
85/// ```
86///
87/// See: <https://playwright.dev/docs/api/class-browsercontext>
88/// Type alias for boxed route handler future
89type RouteHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
90
91/// Type alias for boxed page handler future
92type PageHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
93
94/// Type alias for boxed close handler future
95type CloseHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
96
97/// Type alias for boxed request handler future
98type RequestHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
99
100/// Type alias for boxed response handler future
101type ResponseHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
102
103/// Type alias for boxed dialog handler future
104type DialogHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
105
106/// Type alias for boxed binding callback future
107type BindingCallbackFuture = Pin<Box<dyn Future<Output = serde_json::Value> + Send>>;
108
109/// Context-level page event handler
110type PageHandler = Arc<dyn Fn(Page) -> PageHandlerFuture + Send + Sync>;
111
112/// Context-level close event handler
113type CloseHandler = Arc<dyn Fn() -> CloseHandlerFuture + Send + Sync>;
114
115/// Context-level request event handler
116type RequestHandler = Arc<dyn Fn(Request) -> RequestHandlerFuture + Send + Sync>;
117
118/// Context-level response event handler
119type ResponseHandler = Arc<dyn Fn(ResponseObject) -> ResponseHandlerFuture + Send + Sync>;
120
121/// Context-level dialog event handler
122type DialogHandler = Arc<dyn Fn(crate::protocol::Dialog) -> DialogHandlerFuture + Send + Sync>;
123
124/// Binding callback: receives deserialized JS args, returns a JSON value
125type BindingCallback = Arc<dyn Fn(Vec<serde_json::Value>) -> BindingCallbackFuture + Send + Sync>;
126
127/// Storage for a single route handler
128#[derive(Clone)]
129struct RouteHandlerEntry {
130    pattern: String,
131    handler: Arc<dyn Fn(Route) -> RouteHandlerFuture + Send + Sync>,
132}
133
134#[derive(Clone)]
135pub struct BrowserContext {
136    base: ChannelOwnerImpl,
137    /// Browser instance that owns this context (None for persistent contexts)
138    browser: Option<Browser>,
139    /// All open pages in this context
140    pages: Arc<Mutex<Vec<Page>>>,
141    /// Route handlers for context-level network interception
142    route_handlers: Arc<Mutex<Vec<RouteHandlerEntry>>>,
143    /// APIRequestContext GUID from initializer (resolved lazily)
144    request_context_guid: Option<String>,
145    /// Tracing GUID from initializer (resolved lazily)
146    tracing_guid: Option<String>,
147    /// Default action timeout for all pages in this context (milliseconds), stored as f64 bits.
148    default_timeout_ms: Arc<std::sync::atomic::AtomicU64>,
149    /// Default navigation timeout for all pages in this context (milliseconds), stored as f64 bits.
150    default_navigation_timeout_ms: Arc<std::sync::atomic::AtomicU64>,
151    /// Context-level page event handlers (fired when a new page is created)
152    page_handlers: Arc<Mutex<Vec<PageHandler>>>,
153    /// Context-level close event handlers (fired when the context is closed)
154    close_handlers: Arc<Mutex<Vec<CloseHandler>>>,
155    /// Context-level request event handlers
156    request_handlers: Arc<Mutex<Vec<RequestHandler>>>,
157    /// Context-level request finished event handlers
158    request_finished_handlers: Arc<Mutex<Vec<RequestHandler>>>,
159    /// Context-level request failed event handlers
160    request_failed_handlers: Arc<Mutex<Vec<RequestHandler>>>,
161    /// Context-level response event handlers
162    response_handlers: Arc<Mutex<Vec<ResponseHandler>>>,
163    /// One-shot senders waiting for the next "page" event (expect_page)
164    page_waiters: Arc<Mutex<Vec<oneshot::Sender<Page>>>>,
165    /// One-shot senders waiting for the next "close" event (expect_close)
166    close_waiters: Arc<Mutex<Vec<oneshot::Sender<()>>>>,
167    /// Context-level dialog event handlers (fired for dialogs on any page in the context)
168    dialog_handlers: Arc<Mutex<Vec<DialogHandler>>>,
169    /// Registered binding callbacks keyed by name (for expose_function / expose_binding)
170    binding_callbacks: Arc<Mutex<HashMap<String, BindingCallback>>>,
171}
172
173impl BrowserContext {
174    /// Creates a new BrowserContext from protocol initialization
175    ///
176    /// This is called by the object factory when the server sends a `__create__` message
177    /// for a BrowserContext object.
178    ///
179    /// # Arguments
180    ///
181    /// * `parent` - The parent Browser object
182    /// * `type_name` - The protocol type name ("BrowserContext")
183    /// * `guid` - The unique identifier for this context
184    /// * `initializer` - The initialization data from the server
185    ///
186    /// # Errors
187    ///
188    /// Returns error if initializer is malformed
189    pub fn new(
190        parent: Arc<dyn ChannelOwner>,
191        type_name: String,
192        guid: Arc<str>,
193        initializer: Value,
194    ) -> Result<Self> {
195        // Extract APIRequestContext GUID from initializer before moving it
196        let request_context_guid = initializer
197            .get("requestContext")
198            .and_then(|v| v.get("guid"))
199            .and_then(|v| v.as_str())
200            .map(|s| s.to_string());
201
202        // Extract Tracing GUID from initializer before moving it
203        let tracing_guid = initializer
204            .get("tracing")
205            .and_then(|v| v.get("guid"))
206            .and_then(|v| v.as_str())
207            .map(|s| s.to_string());
208
209        let base = ChannelOwnerImpl::new(
210            ParentOrConnection::Parent(parent.clone()),
211            type_name,
212            guid,
213            initializer,
214        );
215
216        // Store browser reference if parent is a Browser
217        // Returns None only for special contexts (Android, Electron) where parent is not a Browser
218        // For both regular contexts and persistent contexts, parent is a Browser instance
219        let browser = parent.as_any().downcast_ref::<Browser>().cloned();
220
221        let context = Self {
222            base,
223            browser,
224            pages: Arc::new(Mutex::new(Vec::new())),
225            route_handlers: Arc::new(Mutex::new(Vec::new())),
226            request_context_guid,
227            tracing_guid,
228            default_timeout_ms: Arc::new(std::sync::atomic::AtomicU64::new(
229                crate::DEFAULT_TIMEOUT_MS.to_bits(),
230            )),
231            default_navigation_timeout_ms: Arc::new(std::sync::atomic::AtomicU64::new(
232                crate::DEFAULT_TIMEOUT_MS.to_bits(),
233            )),
234            page_handlers: Arc::new(Mutex::new(Vec::new())),
235            close_handlers: Arc::new(Mutex::new(Vec::new())),
236            request_handlers: Arc::new(Mutex::new(Vec::new())),
237            request_finished_handlers: Arc::new(Mutex::new(Vec::new())),
238            request_failed_handlers: Arc::new(Mutex::new(Vec::new())),
239            response_handlers: Arc::new(Mutex::new(Vec::new())),
240            page_waiters: Arc::new(Mutex::new(Vec::new())),
241            close_waiters: Arc::new(Mutex::new(Vec::new())),
242            dialog_handlers: Arc::new(Mutex::new(Vec::new())),
243            binding_callbacks: Arc::new(Mutex::new(HashMap::new())),
244        };
245
246        // Enable dialog event subscription
247        // Dialog events need to be explicitly subscribed to via updateSubscription command
248        let channel = context.channel().clone();
249        tokio::spawn(async move {
250            _ = channel.update_subscription("dialog", true).await;
251        });
252
253        Ok(context)
254    }
255
256    /// Returns the channel for sending protocol messages
257    ///
258    /// Used internally for sending RPC calls to the context.
259    fn channel(&self) -> &Channel {
260        self.base.channel()
261    }
262
263    /// Adds a script which would be evaluated in one of the following scenarios:
264    ///
265    /// - Whenever a page is created in the browser context or is navigated.
266    /// - Whenever a child frame is attached or navigated in any page in the browser context.
267    ///
268    /// The script is evaluated after the document was created but before any of its scripts
269    /// were run. This is useful to amend the JavaScript environment, e.g. to seed Math.random.
270    ///
271    /// # Arguments
272    ///
273    /// * `script` - Script to be evaluated in all pages in the browser context.
274    ///
275    /// # Errors
276    ///
277    /// Returns error if:
278    /// - Context has been closed
279    /// - Communication with browser process fails
280    ///
281    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-add-init-script>
282    pub async fn add_init_script(&self, script: &str) -> Result<()> {
283        self.channel()
284            .send_no_result("addInitScript", serde_json::json!({ "source": script }))
285            .await
286    }
287
288    /// Creates a new page in this browser context.
289    ///
290    /// Pages are isolated tabs/windows within a context. Each page starts
291    /// at "about:blank" and can be navigated independently.
292    ///
293    /// # Errors
294    ///
295    /// Returns error if:
296    /// - Context has been closed
297    /// - Communication with browser process fails
298    ///
299    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-new-page>
300    pub async fn new_page(&self) -> Result<Page> {
301        // Response contains the GUID of the created Page
302        #[derive(Deserialize)]
303        struct NewPageResponse {
304            page: GuidRef,
305        }
306
307        #[derive(Deserialize)]
308        struct GuidRef {
309            #[serde(deserialize_with = "crate::server::connection::deserialize_arc_str")]
310            guid: Arc<str>,
311        }
312
313        // Send newPage RPC to server
314        let response: NewPageResponse = self
315            .channel()
316            .send("newPage", serde_json::json!({}))
317            .await?;
318
319        // Retrieve and downcast the Page object from the connection registry
320        let page: Page = self
321            .connection()
322            .get_typed::<Page>(&response.page.guid)
323            .await?;
324
325        // Note: Don't track the page here - it will be tracked via the "page" event
326        // that Playwright server sends automatically when a page is created.
327        // Tracking it here would create duplicates.
328
329        // Propagate context-level timeout defaults to the new page
330        let ctx_timeout = self.default_timeout_ms();
331        let ctx_nav_timeout = self.default_navigation_timeout_ms();
332        if ctx_timeout.to_bits() != crate::DEFAULT_TIMEOUT_MS.to_bits() {
333            page.set_default_timeout(ctx_timeout).await;
334        }
335        if ctx_nav_timeout.to_bits() != crate::DEFAULT_TIMEOUT_MS.to_bits() {
336            page.set_default_navigation_timeout(ctx_nav_timeout).await;
337        }
338
339        Ok(page)
340    }
341
342    /// Returns all open pages in the context.
343    ///
344    /// This method provides a snapshot of all currently active pages that belong
345    /// to this browser context instance. Pages created via `new_page()` and popup
346    /// pages opened through user interactions are included.
347    ///
348    /// In persistent contexts launched with `--app=url`, this will include the
349    /// initial page created automatically by Playwright.
350    ///
351    /// # Errors
352    ///
353    /// This method does not return errors. It provides a snapshot of pages at
354    /// the time of invocation.
355    ///
356    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-pages>
357    pub fn pages(&self) -> Vec<Page> {
358        self.pages.lock().unwrap().clone()
359    }
360
361    /// Returns the browser instance that owns this context.
362    ///
363    /// Returns `None` only for contexts created outside of normal browser
364    /// (e.g., Android or Electron contexts). For both regular contexts and
365    /// persistent contexts, this returns the owning Browser instance.
366    ///
367    /// # Errors
368    ///
369    /// This method does not return errors.
370    ///
371    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-browser>
372    pub fn browser(&self) -> Option<Browser> {
373        self.browser.clone()
374    }
375
376    /// Returns the APIRequestContext associated with this context.
377    ///
378    /// The APIRequestContext is created automatically by the server for each
379    /// BrowserContext. It enables performing HTTP requests and is used internally
380    /// by `Route::fetch()`.
381    ///
382    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-request>
383    pub async fn request(&self) -> Result<APIRequestContext> {
384        let guid = self.request_context_guid.as_ref().ok_or_else(|| {
385            crate::error::Error::ProtocolError(
386                "No APIRequestContext available for this context".to_string(),
387            )
388        })?;
389
390        self.connection().get_typed::<APIRequestContext>(guid).await
391    }
392
393    /// Creates a new Chrome DevTools Protocol session for the given page.
394    ///
395    /// CDPSession provides low-level access to the Chrome DevTools Protocol.
396    /// This method is only available in Chromium-based browsers.
397    ///
398    /// # Arguments
399    ///
400    /// * `page` - The page to create a CDP session for
401    ///
402    /// # Errors
403    ///
404    /// Returns error if:
405    /// - The browser is not Chromium-based
406    /// - Context has been closed
407    /// - Communication with browser process fails
408    ///
409    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-new-cdp-session>
410    pub async fn new_cdp_session(&self, page: &Page) -> Result<CDPSession> {
411        #[derive(serde::Deserialize)]
412        struct NewCDPSessionResponse {
413            session: GuidRef,
414        }
415
416        #[derive(serde::Deserialize)]
417        struct GuidRef {
418            #[serde(deserialize_with = "crate::server::connection::deserialize_arc_str")]
419            guid: Arc<str>,
420        }
421
422        let response: NewCDPSessionResponse = self
423            .channel()
424            .send(
425                "newCDPSession",
426                serde_json::json!({ "page": { "guid": page.guid() } }),
427            )
428            .await?;
429
430        self.connection()
431            .get_typed::<CDPSession>(&response.session.guid)
432            .await
433    }
434
435    /// Returns the Tracing object for this browser context.
436    ///
437    /// The Tracing object is created automatically by the Playwright server for each
438    /// BrowserContext. Use it to start and stop trace recording.
439    ///
440    /// # Errors
441    ///
442    /// Returns error if no Tracing object is available for this context (rare,
443    /// should not happen in normal usage).
444    ///
445    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-tracing>
446    pub async fn tracing(&self) -> Result<Tracing> {
447        let guid = self.tracing_guid.as_ref().ok_or_else(|| {
448            crate::error::Error::ProtocolError(
449                "No Tracing object available for this context".to_string(),
450            )
451        })?;
452
453        self.connection().get_typed::<Tracing>(guid).await
454    }
455
456    /// Closes the browser context and all its pages.
457    ///
458    /// This is a graceful operation that sends a close command to the context
459    /// and waits for it to shut down properly.
460    ///
461    /// # Errors
462    ///
463    /// Returns error if:
464    /// - Context has already been closed
465    /// - Communication with browser process fails
466    ///
467    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-close>
468    pub async fn close(&self) -> Result<()> {
469        // Send close RPC to server
470        self.channel()
471            .send_no_result("close", serde_json::json!({}))
472            .await
473    }
474
475    /// Sets the default timeout for all operations in this browser context.
476    ///
477    /// This applies to all pages already open in this context as well as pages
478    /// created subsequently. Pass `0` to disable timeouts.
479    ///
480    /// # Arguments
481    ///
482    /// * `timeout` - Timeout in milliseconds
483    ///
484    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-timeout>
485    pub async fn set_default_timeout(&self, timeout: f64) {
486        self.default_timeout_ms
487            .store(timeout.to_bits(), std::sync::atomic::Ordering::Relaxed);
488        let pages: Vec<Page> = self.pages.lock().unwrap().clone();
489        for page in pages {
490            page.set_default_timeout(timeout).await;
491        }
492        crate::protocol::page::set_timeout_and_notify(
493            self.channel(),
494            "setDefaultTimeoutNoReply",
495            timeout,
496        )
497        .await;
498    }
499
500    /// Sets the default timeout for navigation operations in this browser context.
501    ///
502    /// This applies to all pages already open in this context as well as pages
503    /// created subsequently. Pass `0` to disable timeouts.
504    ///
505    /// # Arguments
506    ///
507    /// * `timeout` - Timeout in milliseconds
508    ///
509    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-navigation-timeout>
510    pub async fn set_default_navigation_timeout(&self, timeout: f64) {
511        self.default_navigation_timeout_ms
512            .store(timeout.to_bits(), std::sync::atomic::Ordering::Relaxed);
513        let pages: Vec<Page> = self.pages.lock().unwrap().clone();
514        for page in pages {
515            page.set_default_navigation_timeout(timeout).await;
516        }
517        crate::protocol::page::set_timeout_and_notify(
518            self.channel(),
519            "setDefaultNavigationTimeoutNoReply",
520            timeout,
521        )
522        .await;
523    }
524
525    /// Returns the context's current default action timeout in milliseconds.
526    fn default_timeout_ms(&self) -> f64 {
527        f64::from_bits(
528            self.default_timeout_ms
529                .load(std::sync::atomic::Ordering::Relaxed),
530        )
531    }
532
533    /// Returns the context's current default navigation timeout in milliseconds.
534    fn default_navigation_timeout_ms(&self) -> f64 {
535        f64::from_bits(
536            self.default_navigation_timeout_ms
537                .load(std::sync::atomic::Ordering::Relaxed),
538        )
539    }
540
541    /// Pauses the browser context.
542    ///
543    /// This pauses the execution of all pages in the context.
544    pub async fn pause(&self) -> Result<()> {
545        self.channel()
546            .send_no_result("pause", serde_json::Value::Null)
547            .await
548    }
549
550    /// Returns storage state for this browser context.
551    ///
552    /// Contains current cookies and local storage snapshots.
553    ///
554    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-storage-state>
555    pub async fn storage_state(&self) -> Result<StorageState> {
556        let response: StorageState = self
557            .channel()
558            .send("storageState", serde_json::json!({}))
559            .await?;
560        Ok(response)
561    }
562
563    /// Adds cookies into this browser context.
564    ///
565    /// All pages within this context will have these cookies installed. Cookies can be granularly specified
566    /// with `name`, `value`, `url`, `domain`, `path`, `expires`, `httpOnly`, `secure`, `sameSite`.
567    ///
568    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-add-cookies>
569    pub async fn add_cookies(&self, cookies: &[Cookie]) -> Result<()> {
570        self.channel()
571            .send_no_result(
572                "addCookies",
573                serde_json::json!({
574                    "cookies": cookies
575                }),
576            )
577            .await
578    }
579
580    /// Returns cookies for this browser context, optionally filtered by URLs.
581    ///
582    /// If `urls` is `None` or empty, all cookies are returned.
583    ///
584    /// # Arguments
585    ///
586    /// * `urls` - Optional list of URLs to filter cookies by
587    ///
588    /// # Errors
589    ///
590    /// Returns error if:
591    /// - Context has been closed
592    /// - Communication with browser process fails
593    ///
594    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-cookies>
595    pub async fn cookies(&self, urls: Option<&[&str]>) -> Result<Vec<Cookie>> {
596        let url_list: Vec<&str> = urls.unwrap_or(&[]).to_vec();
597        #[derive(serde::Deserialize)]
598        struct CookiesResponse {
599            cookies: Vec<Cookie>,
600        }
601        let response: CookiesResponse = self
602            .channel()
603            .send("cookies", serde_json::json!({ "urls": url_list }))
604            .await?;
605        Ok(response.cookies)
606    }
607
608    /// Clears cookies from this browser context, with optional filters.
609    ///
610    /// When called with no options, all cookies are removed. Use `ClearCookiesOptions`
611    /// to filter which cookies to clear by name, domain, or path.
612    ///
613    /// # Arguments
614    ///
615    /// * `options` - Optional filters for which cookies to clear
616    ///
617    /// # Errors
618    ///
619    /// Returns error if:
620    /// - Context has been closed
621    /// - Communication with browser process fails
622    ///
623    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-clear-cookies>
624    pub async fn clear_cookies(&self, options: Option<ClearCookiesOptions>) -> Result<()> {
625        let params = match options {
626            None => serde_json::json!({}),
627            Some(opts) => serde_json::to_value(opts).unwrap_or(serde_json::json!({})),
628        };
629        self.channel().send_no_result("clearCookies", params).await
630    }
631
632    /// Sets extra HTTP headers that will be sent with every request from this context.
633    ///
634    /// These headers are merged with per-page extra headers set with `page.set_extra_http_headers()`.
635    /// If the page has specific headers that conflict, page-level headers take precedence.
636    ///
637    /// # Arguments
638    ///
639    /// * `headers` - Map of header names to values. All header names are lowercased.
640    ///
641    /// # Errors
642    ///
643    /// Returns error if:
644    /// - Context has been closed
645    /// - Communication with browser process fails
646    ///
647    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-set-extra-http-headers>
648    pub async fn set_extra_http_headers(&self, headers: HashMap<String, String>) -> Result<()> {
649        // Playwright protocol expects an array of {name, value} objects
650        let headers_array: Vec<serde_json::Value> = headers
651            .into_iter()
652            .map(|(name, value)| serde_json::json!({ "name": name, "value": value }))
653            .collect();
654        self.channel()
655            .send_no_result(
656                "setExtraHTTPHeaders",
657                serde_json::json!({ "headers": headers_array }),
658            )
659            .await
660    }
661
662    /// Grants browser permissions to the context.
663    ///
664    /// Permissions are granted for all pages in the context. The optional `origin`
665    /// in `GrantPermissionsOptions` restricts the grant to a specific URL origin.
666    ///
667    /// Common permissions: `"geolocation"`, `"notifications"`, `"camera"`,
668    /// `"microphone"`, `"clipboard-read"`, `"clipboard-write"`.
669    ///
670    /// # Arguments
671    ///
672    /// * `permissions` - List of permission strings to grant
673    /// * `options` - Optional options, including `origin` to restrict the grant
674    ///
675    /// # Errors
676    ///
677    /// Returns error if:
678    /// - Permission name is not recognised
679    /// - Context has been closed
680    /// - Communication with browser process fails
681    ///
682    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-grant-permissions>
683    pub async fn grant_permissions(
684        &self,
685        permissions: &[&str],
686        options: Option<GrantPermissionsOptions>,
687    ) -> Result<()> {
688        let mut params = serde_json::json!({ "permissions": permissions });
689        if let Some(opts) = options
690            && let Some(origin) = opts.origin
691        {
692            params["origin"] = serde_json::Value::String(origin);
693        }
694        self.channel()
695            .send_no_result("grantPermissions", params)
696            .await
697    }
698
699    /// Clears all permission overrides for this browser context.
700    ///
701    /// Reverts all permissions previously set with `grant_permissions()` back to
702    /// the browser default state.
703    ///
704    /// # Errors
705    ///
706    /// Returns error if:
707    /// - Context has been closed
708    /// - Communication with browser process fails
709    ///
710    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-clear-permissions>
711    pub async fn clear_permissions(&self) -> Result<()> {
712        self.channel()
713            .send_no_result("clearPermissions", serde_json::json!({}))
714            .await
715    }
716
717    /// Sets or clears the geolocation for all pages in this context.
718    ///
719    /// Pass `Some(Geolocation { ... })` to set a specific location, or `None` to
720    /// clear the override and let the browser handle location requests naturally.
721    ///
722    /// Note: Geolocation access requires the `"geolocation"` permission to be granted
723    /// via `grant_permissions()` for navigator.geolocation to succeed.
724    ///
725    /// # Arguments
726    ///
727    /// * `geolocation` - Location to set, or `None` to clear
728    ///
729    /// # Errors
730    ///
731    /// Returns error if:
732    /// - Latitude or longitude is out of range
733    /// - Context has been closed
734    /// - Communication with browser process fails
735    ///
736    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-set-geolocation>
737    pub async fn set_geolocation(&self, geolocation: Option<Geolocation>) -> Result<()> {
738        // Playwright protocol: omit the "geolocation" key entirely to clear;
739        // passing null causes a validation error on the server side.
740        let params = match geolocation {
741            Some(geo) => serde_json::json!({ "geolocation": geo }),
742            None => serde_json::json!({}),
743        };
744        self.channel()
745            .send_no_result("setGeolocation", params)
746            .await
747    }
748
749    /// Toggles the offline mode for this browser context.
750    ///
751    /// When `true`, all network requests from pages in this context will fail with
752    /// a network error. Set to `false` to restore network connectivity.
753    ///
754    /// # Arguments
755    ///
756    /// * `offline` - `true` to go offline, `false` to go back online
757    ///
758    /// # Errors
759    ///
760    /// Returns error if:
761    /// - Context has been closed
762    /// - Communication with browser process fails
763    ///
764    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-set-offline>
765    pub async fn set_offline(&self, offline: bool) -> Result<()> {
766        self.channel()
767            .send_no_result("setOffline", serde_json::json!({ "offline": offline }))
768            .await
769    }
770
771    /// Registers a route handler for context-level network interception.
772    ///
773    /// Routes registered on a context apply to all pages within the context.
774    /// Page-level routes take precedence over context-level routes.
775    ///
776    /// # Arguments
777    ///
778    /// * `pattern` - URL pattern to match (supports glob patterns like "**/*.png")
779    /// * `handler` - Async closure that handles the route
780    ///
781    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-route>
782    pub async fn route<F, Fut>(&self, pattern: &str, handler: F) -> Result<()>
783    where
784        F: Fn(Route) -> Fut + Send + Sync + 'static,
785        Fut: Future<Output = Result<()>> + Send + 'static,
786    {
787        let handler =
788            Arc::new(move |route: Route| -> RouteHandlerFuture { Box::pin(handler(route)) });
789
790        self.route_handlers.lock().unwrap().push(RouteHandlerEntry {
791            pattern: pattern.to_string(),
792            handler,
793        });
794
795        self.enable_network_interception().await
796    }
797
798    /// Removes route handler(s) matching the given URL pattern.
799    ///
800    /// # Arguments
801    ///
802    /// * `pattern` - URL pattern to remove handlers for
803    ///
804    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-unroute>
805    pub async fn unroute(&self, pattern: &str) -> Result<()> {
806        self.route_handlers
807            .lock()
808            .unwrap()
809            .retain(|entry| entry.pattern != pattern);
810        self.enable_network_interception().await
811    }
812
813    /// Removes all registered route handlers.
814    ///
815    /// # Arguments
816    ///
817    /// * `behavior` - Optional behavior for in-flight handlers
818    ///
819    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-unroute-all>
820    pub async fn unroute_all(&self, _behavior: Option<UnrouteBehavior>) -> Result<()> {
821        self.route_handlers.lock().unwrap().clear();
822        self.enable_network_interception().await
823    }
824
825    /// Adds a listener for the `page` event.
826    ///
827    /// The handler is called whenever a new page is created in this context,
828    /// including popup pages opened through user interactions.
829    ///
830    /// # Arguments
831    ///
832    /// * `handler` - Async function that receives the new `Page`
833    ///
834    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-page>
835    pub async fn on_page<F, Fut>(&self, handler: F) -> Result<()>
836    where
837        F: Fn(Page) -> Fut + Send + Sync + 'static,
838        Fut: Future<Output = Result<()>> + Send + 'static,
839    {
840        let handler = Arc::new(move |page: Page| -> PageHandlerFuture { Box::pin(handler(page)) });
841        self.page_handlers.lock().unwrap().push(handler);
842        Ok(())
843    }
844
845    /// Adds a listener for the `close` event.
846    ///
847    /// The handler is called when the browser context is closed.
848    ///
849    /// # Arguments
850    ///
851    /// * `handler` - Async function called with no arguments when the context closes
852    ///
853    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-close>
854    pub async fn on_close<F, Fut>(&self, handler: F) -> Result<()>
855    where
856        F: Fn() -> Fut + Send + Sync + 'static,
857        Fut: Future<Output = Result<()>> + Send + 'static,
858    {
859        let handler = Arc::new(move || -> CloseHandlerFuture { Box::pin(handler()) });
860        self.close_handlers.lock().unwrap().push(handler);
861        Ok(())
862    }
863
864    /// Adds a listener for the `request` event.
865    ///
866    /// The handler fires whenever a request is issued from any page in the context.
867    /// This is equivalent to subscribing to `on_request` on each individual page,
868    /// but covers all current and future pages of the context.
869    ///
870    /// Context-level handlers fire before page-level handlers.
871    ///
872    /// # Arguments
873    ///
874    /// * `handler` - Async function that receives the `Request`
875    ///
876    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-request>
877    pub async fn on_request<F, Fut>(&self, handler: F) -> Result<()>
878    where
879        F: Fn(Request) -> Fut + Send + Sync + 'static,
880        Fut: Future<Output = Result<()>> + Send + 'static,
881    {
882        let handler = Arc::new(move |request: Request| -> RequestHandlerFuture {
883            Box::pin(handler(request))
884        });
885        let needs_subscription = self.request_handlers.lock().unwrap().is_empty();
886        if needs_subscription {
887            _ = self.channel().update_subscription("request", true).await;
888        }
889        self.request_handlers.lock().unwrap().push(handler);
890        Ok(())
891    }
892
893    /// Adds a listener for the `requestFinished` event.
894    ///
895    /// The handler fires after the request has been successfully received by the server
896    /// and a response has been fully downloaded for any page in the context.
897    ///
898    /// Context-level handlers fire before page-level handlers.
899    ///
900    /// # Arguments
901    ///
902    /// * `handler` - Async function that receives the completed `Request`
903    ///
904    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-request-finished>
905    pub async fn on_request_finished<F, Fut>(&self, handler: F) -> Result<()>
906    where
907        F: Fn(Request) -> Fut + Send + Sync + 'static,
908        Fut: Future<Output = Result<()>> + Send + 'static,
909    {
910        let handler = Arc::new(move |request: Request| -> RequestHandlerFuture {
911            Box::pin(handler(request))
912        });
913        let needs_subscription = self.request_finished_handlers.lock().unwrap().is_empty();
914        if needs_subscription {
915            _ = self
916                .channel()
917                .update_subscription("requestFinished", true)
918                .await;
919        }
920        self.request_finished_handlers.lock().unwrap().push(handler);
921        Ok(())
922    }
923
924    /// Adds a listener for the `requestFailed` event.
925    ///
926    /// The handler fires when a request from any page in the context fails,
927    /// for example due to a network error or if the server returned an error response.
928    ///
929    /// Context-level handlers fire before page-level handlers.
930    ///
931    /// # Arguments
932    ///
933    /// * `handler` - Async function that receives the failed `Request`
934    ///
935    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-request-failed>
936    pub async fn on_request_failed<F, Fut>(&self, handler: F) -> Result<()>
937    where
938        F: Fn(Request) -> Fut + Send + Sync + 'static,
939        Fut: Future<Output = Result<()>> + Send + 'static,
940    {
941        let handler = Arc::new(move |request: Request| -> RequestHandlerFuture {
942            Box::pin(handler(request))
943        });
944        let needs_subscription = self.request_failed_handlers.lock().unwrap().is_empty();
945        if needs_subscription {
946            _ = self
947                .channel()
948                .update_subscription("requestFailed", true)
949                .await;
950        }
951        self.request_failed_handlers.lock().unwrap().push(handler);
952        Ok(())
953    }
954
955    /// Adds a listener for the `response` event.
956    ///
957    /// The handler fires whenever a response is received from any page in the context.
958    ///
959    /// Context-level handlers fire before page-level handlers.
960    ///
961    /// # Arguments
962    ///
963    /// * `handler` - Async function that receives the `ResponseObject`
964    ///
965    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-response>
966    pub async fn on_response<F, Fut>(&self, handler: F) -> Result<()>
967    where
968        F: Fn(ResponseObject) -> Fut + Send + Sync + 'static,
969        Fut: Future<Output = Result<()>> + Send + 'static,
970    {
971        let handler = Arc::new(move |response: ResponseObject| -> ResponseHandlerFuture {
972            Box::pin(handler(response))
973        });
974        let needs_subscription = self.response_handlers.lock().unwrap().is_empty();
975        if needs_subscription {
976            _ = self.channel().update_subscription("response", true).await;
977        }
978        self.response_handlers.lock().unwrap().push(handler);
979        Ok(())
980    }
981
982    /// Adds a listener for the `dialog` event on this browser context.
983    ///
984    /// The handler fires whenever a JavaScript dialog (alert, confirm, prompt,
985    /// or beforeunload) is triggered from **any** page in the context. Context-level
986    /// handlers fire before page-level handlers.
987    ///
988    /// The dialog must be explicitly accepted or dismissed; otherwise the page
989    /// will freeze waiting for a response.
990    ///
991    /// # Arguments
992    ///
993    /// * `handler` - Async function that receives the [`Dialog`](crate::protocol::Dialog) and calls
994    ///   `dialog.accept()` or `dialog.dismiss()`.
995    ///
996    /// # Errors
997    ///
998    /// Returns error if communication with the browser process fails.
999    ///
1000    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-dialog>
1001    pub async fn on_dialog<F, Fut>(&self, handler: F) -> Result<()>
1002    where
1003        F: Fn(crate::protocol::Dialog) -> Fut + Send + Sync + 'static,
1004        Fut: Future<Output = Result<()>> + Send + 'static,
1005    {
1006        let handler = Arc::new(
1007            move |dialog: crate::protocol::Dialog| -> DialogHandlerFuture {
1008                Box::pin(handler(dialog))
1009            },
1010        );
1011        self.dialog_handlers.lock().unwrap().push(handler);
1012        Ok(())
1013    }
1014
1015    /// Exposes a Rust function to every page in this browser context as
1016    /// `window[name]` in JavaScript.
1017    ///
1018    /// When JavaScript code calls `window[name](arg1, arg2, …)` the Playwright
1019    /// server fires a `bindingCall` event that invokes `callback` with the
1020    /// deserialized arguments. The return value of `callback` is serialized back
1021    /// to JavaScript so the `await window[name](…)` expression resolves with it.
1022    ///
1023    /// The binding is injected into every existing page and every new page
1024    /// created in this context.
1025    ///
1026    /// # Arguments
1027    ///
1028    /// * `name`     – JavaScript identifier that will be available as `window[name]`.
1029    /// * `callback` – Async closure called with `Vec<serde_json::Value>` (the JS
1030    ///   arguments) and returning `serde_json::Value` (the result).
1031    ///
1032    /// # Errors
1033    ///
1034    /// Returns error if:
1035    /// - The context has been closed.
1036    /// - Communication with the browser process fails.
1037    ///
1038    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-expose-function>
1039    pub async fn expose_function<F, Fut>(&self, name: &str, callback: F) -> Result<()>
1040    where
1041        F: Fn(Vec<serde_json::Value>) -> Fut + Send + Sync + 'static,
1042        Fut: Future<Output = serde_json::Value> + Send + 'static,
1043    {
1044        self.expose_binding_internal(name, false, callback).await
1045    }
1046
1047    /// Exposes a Rust function to every page in this browser context as
1048    /// `window[name]` in JavaScript, with `needsHandle: true`.
1049    ///
1050    /// Identical to [`expose_function`](Self::expose_function) but the Playwright
1051    /// server passes the first argument as a `JSHandle` object rather than a plain
1052    /// value.  Use this when the JS caller passes complex objects that you want to
1053    /// inspect on the Rust side.
1054    ///
1055    /// # Arguments
1056    ///
1057    /// * `name`     – JavaScript identifier.
1058    /// * `callback` – Async closure with `Vec<serde_json::Value>` → `serde_json::Value`.
1059    ///
1060    /// # Errors
1061    ///
1062    /// Returns error if:
1063    /// - The context has been closed.
1064    /// - Communication with the browser process fails.
1065    ///
1066    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-expose-binding>
1067    pub async fn expose_binding<F, Fut>(&self, name: &str, callback: F) -> Result<()>
1068    where
1069        F: Fn(Vec<serde_json::Value>) -> Fut + Send + Sync + 'static,
1070        Fut: Future<Output = serde_json::Value> + Send + 'static,
1071    {
1072        self.expose_binding_internal(name, true, callback).await
1073    }
1074
1075    /// Internal implementation shared by expose_function and expose_binding.
1076    ///
1077    /// Both `expose_function` and `expose_binding` use `needsHandle: false` because
1078    /// the current implementation does not support JSHandle objects. Using
1079    /// `needsHandle: true` would cause the Playwright server to wrap the first
1080    /// argument as a `JSHandle`, which requires a JSHandle protocol object that
1081    /// is not yet implemented.
1082    async fn expose_binding_internal<F, Fut>(
1083        &self,
1084        name: &str,
1085        _needs_handle: bool,
1086        callback: F,
1087    ) -> Result<()>
1088    where
1089        F: Fn(Vec<serde_json::Value>) -> Fut + Send + Sync + 'static,
1090        Fut: Future<Output = serde_json::Value> + Send + 'static,
1091    {
1092        // Wrap callback with type erasure
1093        let callback: BindingCallback = Arc::new(move |args: Vec<serde_json::Value>| {
1094            Box::pin(callback(args)) as BindingCallbackFuture
1095        });
1096
1097        // Store the callback before sending the RPC so that a race-condition
1098        // where a bindingCall arrives before we finish registering is avoided.
1099        self.binding_callbacks
1100            .lock()
1101            .unwrap()
1102            .insert(name.to_string(), callback);
1103
1104        // Tell the Playwright server to inject window[name] into every page.
1105        // Always use needsHandle: false — see note above.
1106        self.channel()
1107            .send_no_result(
1108                "exposeBinding",
1109                serde_json::json!({ "name": name, "needsHandle": false }),
1110            )
1111            .await
1112    }
1113
1114    /// Waits for a new page to be created in this browser context.
1115    ///
1116    /// Creates a one-shot waiter that resolves when the next `page` event fires.
1117    /// The waiter **must** be created before the action that triggers the new page
1118    /// (e.g. `new_page()` or a user action that opens a popup) to avoid a race
1119    /// condition.
1120    ///
1121    /// # Arguments
1122    ///
1123    /// * `timeout` - Timeout in milliseconds. Defaults to 30 000 ms if `None`.
1124    ///
1125    /// # Errors
1126    ///
1127    /// Returns [`Error::Timeout`](crate::error::Error::Timeout) if no page is created within the timeout.
1128    ///
1129    /// # Example
1130    ///
1131    /// ```ignore
1132    /// // Set up the waiter BEFORE the triggering action
1133    /// let waiter = context.expect_page(None).await?;
1134    /// let _page = context.new_page().await?;
1135    /// let new_page = waiter.wait().await?;
1136    /// ```
1137    ///
1138    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-wait-for-event>
1139    pub async fn expect_page(&self, timeout: Option<f64>) -> Result<EventWaiter<Page>> {
1140        let (tx, rx) = oneshot::channel();
1141        self.page_waiters.lock().unwrap().push(tx);
1142        Ok(EventWaiter::new(rx, timeout.or(Some(30_000.0))))
1143    }
1144
1145    /// Waits for this browser context to be closed.
1146    ///
1147    /// Creates a one-shot waiter that resolves when the `close` event fires.
1148    /// The waiter **must** be created before the action that closes the context
1149    /// to avoid a race condition.
1150    ///
1151    /// # Arguments
1152    ///
1153    /// * `timeout` - Timeout in milliseconds. Defaults to 30 000 ms if `None`.
1154    ///
1155    /// # Errors
1156    ///
1157    /// Returns [`Error::Timeout`](crate::error::Error::Timeout) if the context is not closed within the timeout.
1158    ///
1159    /// # Example
1160    ///
1161    /// ```ignore
1162    /// // Set up the waiter BEFORE closing
1163    /// let waiter = context.expect_close(None).await?;
1164    /// context.close().await?;
1165    /// waiter.wait().await?;
1166    /// ```
1167    ///
1168    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-wait-for-event>
1169    pub async fn expect_close(&self, timeout: Option<f64>) -> Result<EventWaiter<()>> {
1170        let (tx, rx) = oneshot::channel();
1171        self.close_waiters.lock().unwrap().push(tx);
1172        Ok(EventWaiter::new(rx, timeout.or(Some(30_000.0))))
1173    }
1174
1175    /// Updates network interception patterns for this context
1176    async fn enable_network_interception(&self) -> Result<()> {
1177        let patterns: Vec<serde_json::Value> = self
1178            .route_handlers
1179            .lock()
1180            .unwrap()
1181            .iter()
1182            .map(|entry| serde_json::json!({ "glob": entry.pattern }))
1183            .collect();
1184
1185        self.channel()
1186            .send_no_result(
1187                "setNetworkInterceptionPatterns",
1188                serde_json::json!({ "patterns": patterns }),
1189            )
1190            .await
1191    }
1192
1193    /// Deserializes binding call arguments from Playwright's protocol format.
1194    ///
1195    /// The `args` field in the BindingCall initializer is a JSON array where each
1196    /// element is in `serialize_argument` format: `{"value": <tagged>, "handles": []}`.
1197    /// This helper extracts the inner "value" from each entry and parses it.
1198    ///
1199    /// This is `pub` so that `Page::on_event("bindingCall")` can reuse it without
1200    /// duplicating the deserialization logic.
1201    pub fn deserialize_binding_args_pub(raw_args: &Value) -> Vec<Value> {
1202        Self::deserialize_binding_args(raw_args)
1203    }
1204
1205    fn deserialize_binding_args(raw_args: &Value) -> Vec<Value> {
1206        let Some(arr) = raw_args.as_array() else {
1207            return vec![];
1208        };
1209
1210        arr.iter()
1211            .map(|arg| {
1212                // Each arg is a direct Playwright type-tagged value, e.g. {"n": 3} or {"s": "hello"}
1213                // (NOT wrapped in {"value": ..., "handles": []} — that format is only for evaluate args)
1214                crate::protocol::evaluate_conversion::parse_value(arg, None)
1215            })
1216            .collect()
1217    }
1218
1219    /// Handles a route event from the protocol
1220    async fn on_route_event(route_handlers: Arc<Mutex<Vec<RouteHandlerEntry>>>, route: Route) {
1221        let handlers = route_handlers.lock().unwrap().clone();
1222        let url = route.request().url().to_string();
1223
1224        for entry in handlers.iter().rev() {
1225            if crate::protocol::route::matches_pattern(&entry.pattern, &url) {
1226                let handler = entry.handler.clone();
1227                if let Err(e) = handler(route.clone()).await {
1228                    tracing::warn!("Context route handler error: {}", e);
1229                    break;
1230                }
1231                if !route.was_handled() {
1232                    continue;
1233                }
1234                break;
1235            }
1236        }
1237    }
1238
1239    fn dispatch_request_event(&self, method: &str, params: Value) {
1240        if let Some(request_guid) = params
1241            .get("request")
1242            .and_then(|v| v.get("guid"))
1243            .and_then(|v| v.as_str())
1244        {
1245            let connection = self.connection();
1246            let request_guid_owned = request_guid.to_owned();
1247            let page_guid_owned = params
1248                .get("page")
1249                .and_then(|v| v.get("guid"))
1250                .and_then(|v| v.as_str())
1251                .map(|v| v.to_owned());
1252            // Extract failureText for requestFailed events
1253            let failure_text = params
1254                .get("failureText")
1255                .and_then(|v| v.as_str())
1256                .map(|s| s.to_owned());
1257            // Extract response GUID for requestFinished events (to read timing)
1258            let response_guid_owned = params
1259                .get("response")
1260                .and_then(|v| v.get("guid"))
1261                .and_then(|v| v.as_str())
1262                .map(|s| s.to_owned());
1263            // Extract responseEndTiming from requestFinished event params
1264            let response_end_timing = params.get("responseEndTiming").and_then(|v| v.as_f64());
1265            let method = method.to_owned();
1266            // Clone context-level handler vecs for use in spawn
1267            let ctx_request_handlers = self.request_handlers.clone();
1268            let ctx_request_finished_handlers = self.request_finished_handlers.clone();
1269            let ctx_request_failed_handlers = self.request_failed_handlers.clone();
1270            tokio::spawn(async move {
1271                let request: Request =
1272                    match connection.get_typed::<Request>(&request_guid_owned).await {
1273                        Ok(r) => r,
1274                        Err(_) => return,
1275                    };
1276
1277                // Set failure text on the request before dispatching to handlers
1278                if let Some(text) = failure_text {
1279                    request.set_failure_text(text);
1280                }
1281
1282                // For requestFinished, extract timing from the Response object's initializer
1283                if method == "requestFinished"
1284                    && let Some(timing) =
1285                        extract_timing(&connection, response_guid_owned, response_end_timing).await
1286                {
1287                    request.set_timing(timing);
1288                }
1289
1290                // Dispatch to context-level handlers first (matching playwright-python behavior)
1291                let ctx_handlers = match method.as_str() {
1292                    "request" => ctx_request_handlers.lock().unwrap().clone(),
1293                    "requestFinished" => ctx_request_finished_handlers.lock().unwrap().clone(),
1294                    "requestFailed" => ctx_request_failed_handlers.lock().unwrap().clone(),
1295                    _ => vec![],
1296                };
1297                for handler in ctx_handlers {
1298                    if let Err(e) = handler(request.clone()).await {
1299                        tracing::warn!("Context {} handler error: {}", method, e);
1300                    }
1301                }
1302
1303                // Then dispatch to page-level handlers
1304                if let Some(page_guid) = page_guid_owned {
1305                    let page: Page = match connection.get_typed::<Page>(&page_guid).await {
1306                        Ok(p) => p,
1307                        Err(_) => return,
1308                    };
1309                    match method.as_str() {
1310                        "request" => page.trigger_request_event(request).await,
1311                        "requestFailed" => page.trigger_request_failed_event(request).await,
1312                        "requestFinished" => page.trigger_request_finished_event(request).await,
1313                        _ => unreachable!("Unreachable method {}", method),
1314                    }
1315                }
1316            });
1317        }
1318    }
1319
1320    fn dispatch_response_event(&self, _method: &str, params: Value) {
1321        if let Some(response_guid) = params
1322            .get("response")
1323            .and_then(|v| v.get("guid"))
1324            .and_then(|v| v.as_str())
1325        {
1326            let connection = self.connection();
1327            let response_guid_owned = response_guid.to_owned();
1328            let page_guid_owned = params
1329                .get("page")
1330                .and_then(|v| v.get("guid"))
1331                .and_then(|v| v.as_str())
1332                .map(|v| v.to_owned());
1333            let ctx_response_handlers = self.response_handlers.clone();
1334            tokio::spawn(async move {
1335                let response: ResponseObject = match connection
1336                    .get_typed::<ResponseObject>(&response_guid_owned)
1337                    .await
1338                {
1339                    Ok(r) => r,
1340                    Err(_) => return,
1341                };
1342
1343                // Dispatch to context-level handlers first (matching playwright-python behavior)
1344                let ctx_handlers = ctx_response_handlers.lock().unwrap().clone();
1345                for handler in ctx_handlers {
1346                    if let Err(e) = handler(response.clone()).await {
1347                        tracing::warn!("Context response handler error: {}", e);
1348                    }
1349                }
1350
1351                // Then dispatch to page-level handlers
1352                if let Some(page_guid) = page_guid_owned {
1353                    let page: Page = match connection.get_typed::<Page>(&page_guid).await {
1354                        Ok(p) => p,
1355                        Err(_) => return,
1356                    };
1357                    page.trigger_response_event(response).await;
1358                }
1359            });
1360        }
1361    }
1362}
1363
1364impl ChannelOwner for BrowserContext {
1365    fn guid(&self) -> &str {
1366        self.base.guid()
1367    }
1368
1369    fn type_name(&self) -> &str {
1370        self.base.type_name()
1371    }
1372
1373    fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
1374        self.base.parent()
1375    }
1376
1377    fn connection(&self) -> Arc<dyn crate::server::connection::ConnectionLike> {
1378        self.base.connection()
1379    }
1380
1381    fn initializer(&self) -> &Value {
1382        self.base.initializer()
1383    }
1384
1385    fn channel(&self) -> &Channel {
1386        self.base.channel()
1387    }
1388
1389    fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
1390        self.base.dispose(reason)
1391    }
1392
1393    fn adopt(&self, child: Arc<dyn ChannelOwner>) {
1394        self.base.adopt(child)
1395    }
1396
1397    fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
1398        self.base.add_child(guid, child)
1399    }
1400
1401    fn remove_child(&self, guid: &str) {
1402        self.base.remove_child(guid)
1403    }
1404
1405    fn on_event(&self, method: &str, params: Value) {
1406        match method {
1407            "request" | "requestFailed" | "requestFinished" => {
1408                self.dispatch_request_event(method, params)
1409            }
1410            "response" => self.dispatch_response_event(method, params),
1411            "close" => {
1412                // BrowserContext close event — fire registered close handlers
1413                let close_handlers = self.close_handlers.clone();
1414                let close_waiters = self.close_waiters.clone();
1415                tokio::spawn(async move {
1416                    let handlers = close_handlers.lock().unwrap().clone();
1417                    for handler in handlers {
1418                        if let Err(e) = handler().await {
1419                            tracing::warn!("Context close handler error: {}", e);
1420                        }
1421                    }
1422
1423                    // Notify all expect_close() waiters
1424                    let waiters: Vec<_> = close_waiters.lock().unwrap().drain(..).collect();
1425                    for tx in waiters {
1426                        let _ = tx.send(());
1427                    }
1428                });
1429            }
1430            "page" => {
1431                // Page events are triggered when pages are created, including:
1432                // - Initial page in persistent context with --app mode
1433                // - Popup pages opened through user interactions
1434                // Event format: {page: {guid: "..."}}
1435                if let Some(page_guid) = params
1436                    .get("page")
1437                    .and_then(|v| v.get("guid"))
1438                    .and_then(|v| v.as_str())
1439                {
1440                    let connection = self.connection();
1441                    let page_guid_owned = page_guid.to_string();
1442                    let pages = self.pages.clone();
1443                    let page_handlers = self.page_handlers.clone();
1444                    let page_waiters = self.page_waiters.clone();
1445
1446                    tokio::spawn(async move {
1447                        // Get and downcast the Page object
1448                        let page: Page = match connection.get_typed::<Page>(&page_guid_owned).await
1449                        {
1450                            Ok(p) => p,
1451                            Err(_) => return,
1452                        };
1453
1454                        // Track the page
1455                        pages.lock().unwrap().push(page.clone());
1456
1457                        // Dispatch to context-level page handlers
1458                        let handlers = page_handlers.lock().unwrap().clone();
1459                        for handler in handlers {
1460                            if let Err(e) = handler(page.clone()).await {
1461                                tracing::warn!("Context page handler error: {}", e);
1462                            }
1463                        }
1464
1465                        // Notify the first expect_page() waiter (FIFO order)
1466                        if let Some(tx) = page_waiters.lock().unwrap().pop() {
1467                            let _ = tx.send(page);
1468                        }
1469                    });
1470                }
1471            }
1472            "dialog" => {
1473                // Dialog events come to BrowserContext.
1474                // Dispatch to context-level handlers first, then forward to the Page.
1475                // Event format: {dialog: {guid: "..."}}
1476                // The Dialog protocol object has the Page as its parent
1477                if let Some(dialog_guid) = params
1478                    .get("dialog")
1479                    .and_then(|v| v.get("guid"))
1480                    .and_then(|v| v.as_str())
1481                {
1482                    let connection = self.connection();
1483                    let dialog_guid_owned = dialog_guid.to_string();
1484                    let dialog_handlers = self.dialog_handlers.clone();
1485
1486                    tokio::spawn(async move {
1487                        // Get and downcast the Dialog object
1488                        let dialog: crate::protocol::Dialog = match connection
1489                            .get_typed::<crate::protocol::Dialog>(&dialog_guid_owned)
1490                            .await
1491                        {
1492                            Ok(d) => d,
1493                            Err(_) => return,
1494                        };
1495
1496                        // Dispatch to context-level dialog handlers first
1497                        let ctx_handlers = dialog_handlers.lock().unwrap().clone();
1498                        for handler in ctx_handlers {
1499                            if let Err(e) = handler(dialog.clone()).await {
1500                                tracing::warn!("Context dialog handler error: {}", e);
1501                            }
1502                        }
1503
1504                        // Then forward to the Page's dialog handlers
1505                        let page: Page =
1506                            match crate::server::connection::downcast_parent::<Page>(&dialog) {
1507                                Some(p) => p,
1508                                None => return,
1509                            };
1510
1511                        page.trigger_dialog_event(dialog).await;
1512                    });
1513                }
1514            }
1515            "bindingCall" => {
1516                // A JS caller invoked an exposed function. Dispatch to the registered
1517                // callback and send the result back via BindingCall::fulfill.
1518                // Event format: {binding: {guid: "..."}}
1519                if let Some(binding_guid) = params
1520                    .get("binding")
1521                    .and_then(|v| v.get("guid"))
1522                    .and_then(|v| v.as_str())
1523                {
1524                    let connection = self.connection();
1525                    let binding_guid_owned = binding_guid.to_string();
1526                    let binding_callbacks = self.binding_callbacks.clone();
1527
1528                    tokio::spawn(async move {
1529                        let binding_call: crate::protocol::BindingCall = match connection
1530                            .get_typed::<crate::protocol::BindingCall>(&binding_guid_owned)
1531                            .await
1532                        {
1533                            Ok(bc) => bc,
1534                            Err(e) => {
1535                                tracing::warn!("Failed to get BindingCall object: {}", e);
1536                                return;
1537                            }
1538                        };
1539
1540                        let name = binding_call.name().to_string();
1541
1542                        // Look up the registered callback
1543                        let callback = {
1544                            let callbacks = binding_callbacks.lock().unwrap();
1545                            callbacks.get(&name).cloned()
1546                        };
1547
1548                        let Some(callback) = callback else {
1549                            tracing::warn!("No callback registered for binding '{}'", name);
1550                            let _ = binding_call
1551                                .reject(&format!("No Rust handler for binding '{name}'"))
1552                                .await;
1553                            return;
1554                        };
1555
1556                        // Deserialize the args from Playwright protocol format
1557                        let raw_args = binding_call.args();
1558                        let args = Self::deserialize_binding_args(raw_args);
1559
1560                        // Call the callback and serialize the result
1561                        let result_value = callback(args).await;
1562                        let serialized =
1563                            crate::protocol::evaluate_conversion::serialize_argument(&result_value);
1564
1565                        if let Err(e) = binding_call.resolve(serialized).await {
1566                            tracing::warn!("Failed to resolve BindingCall '{}': {}", name, e);
1567                        }
1568                    });
1569                }
1570            }
1571            "route" => {
1572                // Handle context-level network routing event
1573                if let Some(route_guid) = params
1574                    .get("route")
1575                    .and_then(|v| v.get("guid"))
1576                    .and_then(|v| v.as_str())
1577                {
1578                    let connection = self.connection();
1579                    let route_guid_owned = route_guid.to_string();
1580                    let route_handlers = self.route_handlers.clone();
1581                    let request_context_guid = self.request_context_guid.clone();
1582
1583                    tokio::spawn(async move {
1584                        let route: Route =
1585                            match connection.get_typed::<Route>(&route_guid_owned).await {
1586                                Ok(r) => r,
1587                                Err(e) => {
1588                                    tracing::warn!("Failed to get route object: {}", e);
1589                                    return;
1590                                }
1591                            };
1592
1593                        // Set APIRequestContext on the route for fetch() support
1594                        if let Some(ref guid) = request_context_guid
1595                            && let Ok(api_ctx) =
1596                                connection.get_typed::<APIRequestContext>(guid).await
1597                        {
1598                            route.set_api_request_context(api_ctx);
1599                        }
1600
1601                        BrowserContext::on_route_event(route_handlers, route).await;
1602                    });
1603                }
1604            }
1605            _ => {
1606                // Other events will be handled in future phases
1607            }
1608        }
1609    }
1610
1611    fn was_collected(&self) -> bool {
1612        self.base.was_collected()
1613    }
1614
1615    fn as_any(&self) -> &dyn Any {
1616        self
1617    }
1618}
1619
1620impl std::fmt::Debug for BrowserContext {
1621    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1622        f.debug_struct("BrowserContext")
1623            .field("guid", &self.guid())
1624            .finish()
1625    }
1626}
1627
1628/// Viewport dimensions for browser context.
1629///
1630/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
1631#[derive(Debug, Clone, Serialize, Deserialize)]
1632pub struct Viewport {
1633    /// Page width in pixels
1634    pub width: u32,
1635    /// Page height in pixels
1636    pub height: u32,
1637}
1638
1639/// Geolocation coordinates.
1640///
1641/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
1642#[derive(Debug, Clone, Serialize, Deserialize)]
1643pub struct Geolocation {
1644    /// Latitude between -90 and 90
1645    pub latitude: f64,
1646    /// Longitude between -180 and 180
1647    pub longitude: f64,
1648    /// Optional accuracy in meters (default: 0)
1649    #[serde(skip_serializing_if = "Option::is_none")]
1650    pub accuracy: Option<f64>,
1651}
1652
1653/// Cookie information for storage state.
1654///
1655/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
1656#[derive(Debug, Clone, Serialize, Deserialize)]
1657#[serde(rename_all = "camelCase")]
1658pub struct Cookie {
1659    /// Cookie name
1660    pub name: String,
1661    /// Cookie value
1662    pub value: String,
1663    /// Cookie domain (use dot prefix for subdomain matching, e.g., ".example.com")
1664    pub domain: String,
1665    /// Cookie path
1666    pub path: String,
1667    /// Unix timestamp in seconds; -1 for session cookies
1668    pub expires: f64,
1669    /// HTTP-only flag
1670    pub http_only: bool,
1671    /// Secure flag
1672    pub secure: bool,
1673    /// SameSite attribute ("Strict", "Lax", "None")
1674    #[serde(skip_serializing_if = "Option::is_none")]
1675    pub same_site: Option<String>,
1676}
1677
1678/// Local storage item for storage state.
1679///
1680/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
1681#[derive(Debug, Clone, Serialize, Deserialize)]
1682pub struct LocalStorageItem {
1683    /// Storage key
1684    pub name: String,
1685    /// Storage value
1686    pub value: String,
1687}
1688
1689/// Origin with local storage items for storage state.
1690///
1691/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
1692#[derive(Debug, Clone, Serialize, Deserialize)]
1693#[serde(rename_all = "camelCase")]
1694pub struct Origin {
1695    /// Origin URL (e.g., `https://example.com`)
1696    pub origin: String,
1697    /// Local storage items for this origin
1698    pub local_storage: Vec<LocalStorageItem>,
1699}
1700
1701/// Storage state containing cookies and local storage.
1702///
1703/// Used to populate a browser context with saved authentication state,
1704/// enabling session persistence across context instances.
1705///
1706/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
1707#[derive(Debug, Clone, Serialize, Deserialize)]
1708pub struct StorageState {
1709    /// List of cookies
1710    pub cookies: Vec<Cookie>,
1711    /// List of origins with local storage
1712    pub origins: Vec<Origin>,
1713}
1714
1715/// Options for filtering which cookies to clear with `BrowserContext::clear_cookies()`.
1716///
1717/// All fields are optional; when provided they act as AND-combined filters.
1718///
1719/// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-clear-cookies>
1720#[derive(Debug, Clone, Default, Serialize)]
1721#[serde(rename_all = "camelCase")]
1722pub struct ClearCookiesOptions {
1723    /// Filter by cookie name (exact match).
1724    #[serde(skip_serializing_if = "Option::is_none")]
1725    pub name: Option<String>,
1726    /// Filter by cookie domain.
1727    #[serde(skip_serializing_if = "Option::is_none")]
1728    pub domain: Option<String>,
1729    /// Filter by cookie path.
1730    #[serde(skip_serializing_if = "Option::is_none")]
1731    pub path: Option<String>,
1732}
1733
1734/// Options for `BrowserContext::grant_permissions()`.
1735///
1736/// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-grant-permissions>
1737#[derive(Debug, Clone, Default)]
1738pub struct GrantPermissionsOptions {
1739    /// Optional origin to restrict the permission grant to.
1740    ///
1741    /// For example `"https://example.com"`.
1742    pub origin: Option<String>,
1743}
1744
1745/// Options for recording HAR.
1746///
1747/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-record-har>
1748#[derive(Debug, Clone, Serialize, Default)]
1749#[serde(rename_all = "camelCase")]
1750pub struct RecordHar {
1751    /// Path on the filesystem to write the HAR file to.
1752    pub path: String,
1753    /// Optional setting to control whether to omit request content from the HAR.
1754    #[serde(skip_serializing_if = "Option::is_none")]
1755    pub omit_content: Option<bool>,
1756    /// Optional setting to control resource content management.
1757    /// "omit" | "embed" | "attach"
1758    #[serde(skip_serializing_if = "Option::is_none")]
1759    pub content: Option<String>,
1760    /// "full" | "minimal"
1761    #[serde(skip_serializing_if = "Option::is_none")]
1762    pub mode: Option<String>,
1763    /// A glob or regex pattern to filter requests that are stored in the HAR.
1764    #[serde(skip_serializing_if = "Option::is_none")]
1765    pub url_filter: Option<String>,
1766}
1767
1768/// Options for recording video.
1769///
1770/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-record-video>
1771#[derive(Debug, Clone, Serialize, Default)]
1772pub struct RecordVideo {
1773    /// Path to the directory to put videos into.
1774    pub dir: String,
1775    /// Optional dimensions of the recorded videos.
1776    #[serde(skip_serializing_if = "Option::is_none")]
1777    pub size: Option<Viewport>,
1778}
1779
1780/// Options for creating a new browser context.
1781///
1782/// Allows customizing viewport, user agent, locale, timezone, geolocation,
1783/// permissions, and other browser context settings.
1784///
1785/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
1786#[derive(Debug, Clone, Default, Serialize)]
1787#[serde(rename_all = "camelCase")]
1788pub struct BrowserContextOptions {
1789    /// Sets consistent viewport for all pages in the context.
1790    /// Set to null via `no_viewport(true)` to disable viewport emulation.
1791    #[serde(skip_serializing_if = "Option::is_none")]
1792    pub viewport: Option<Viewport>,
1793
1794    /// Disables viewport emulation when set to true.
1795    /// Note: Playwright's public API calls this `noViewport`, but the protocol
1796    /// expects `noDefaultViewport`. playwright-python applies this transformation
1797    /// in `_prepare_browser_context_params`.
1798    #[serde(skip_serializing_if = "Option::is_none")]
1799    #[serde(rename = "noDefaultViewport")]
1800    pub no_viewport: Option<bool>,
1801
1802    /// Custom user agent string
1803    #[serde(skip_serializing_if = "Option::is_none")]
1804    pub user_agent: Option<String>,
1805
1806    /// Locale for the context (e.g., "en-GB", "de-DE", "fr-FR")
1807    #[serde(skip_serializing_if = "Option::is_none")]
1808    pub locale: Option<String>,
1809
1810    /// Timezone identifier (e.g., "America/New_York", "Europe/Berlin")
1811    #[serde(skip_serializing_if = "Option::is_none")]
1812    pub timezone_id: Option<String>,
1813
1814    /// Geolocation coordinates
1815    #[serde(skip_serializing_if = "Option::is_none")]
1816    pub geolocation: Option<Geolocation>,
1817
1818    /// List of permissions to grant (e.g., "geolocation", "notifications")
1819    #[serde(skip_serializing_if = "Option::is_none")]
1820    pub permissions: Option<Vec<String>>,
1821
1822    /// Network proxy settings
1823    #[serde(skip_serializing_if = "Option::is_none")]
1824    pub proxy: Option<ProxySettings>,
1825
1826    /// Emulates 'prefers-colors-scheme' media feature ("light", "dark", "no-preference")
1827    #[serde(skip_serializing_if = "Option::is_none")]
1828    pub color_scheme: Option<String>,
1829
1830    /// Whether the viewport supports touch events
1831    #[serde(skip_serializing_if = "Option::is_none")]
1832    pub has_touch: Option<bool>,
1833
1834    /// Whether the meta viewport tag is respected
1835    #[serde(skip_serializing_if = "Option::is_none")]
1836    pub is_mobile: Option<bool>,
1837
1838    /// Whether JavaScript is enabled in the context
1839    #[serde(skip_serializing_if = "Option::is_none")]
1840    pub javascript_enabled: Option<bool>,
1841
1842    /// Emulates network being offline
1843    #[serde(skip_serializing_if = "Option::is_none")]
1844    pub offline: Option<bool>,
1845
1846    /// Whether to automatically download attachments
1847    #[serde(skip_serializing_if = "Option::is_none")]
1848    pub accept_downloads: Option<bool>,
1849
1850    /// Whether to bypass Content-Security-Policy
1851    #[serde(skip_serializing_if = "Option::is_none")]
1852    pub bypass_csp: Option<bool>,
1853
1854    /// Whether to ignore HTTPS errors
1855    #[serde(skip_serializing_if = "Option::is_none")]
1856    pub ignore_https_errors: Option<bool>,
1857
1858    /// Device scale factor (default: 1)
1859    #[serde(skip_serializing_if = "Option::is_none")]
1860    pub device_scale_factor: Option<f64>,
1861
1862    /// Extra HTTP headers to send with every request
1863    #[serde(skip_serializing_if = "Option::is_none")]
1864    pub extra_http_headers: Option<HashMap<String, String>>,
1865
1866    /// Base URL for relative navigation
1867    #[serde(skip_serializing_if = "Option::is_none")]
1868    pub base_url: Option<String>,
1869
1870    /// Storage state to populate the context (cookies, localStorage, sessionStorage).
1871    /// Can be an inline StorageState object or a file path string.
1872    /// Use builder methods `storage_state()` for inline or `storage_state_path()` for file path.
1873    #[serde(skip_serializing_if = "Option::is_none")]
1874    pub storage_state: Option<StorageState>,
1875
1876    /// Storage state file path (alternative to inline storage_state).
1877    /// This is handled by the builder and converted to storage_state during serialization.
1878    #[serde(skip_serializing_if = "Option::is_none")]
1879    pub storage_state_path: Option<String>,
1880
1881    // Launch options (for launch_persistent_context)
1882    /// Additional arguments to pass to browser instance
1883    #[serde(skip_serializing_if = "Option::is_none")]
1884    pub args: Option<Vec<String>>,
1885
1886    /// Browser distribution channel (e.g., "chrome", "msedge")
1887    #[serde(skip_serializing_if = "Option::is_none")]
1888    pub channel: Option<String>,
1889
1890    /// Enable Chromium sandboxing (default: false on Linux)
1891    #[serde(skip_serializing_if = "Option::is_none")]
1892    pub chromium_sandbox: Option<bool>,
1893
1894    /// Auto-open DevTools (deprecated, default: false)
1895    #[serde(skip_serializing_if = "Option::is_none")]
1896    pub devtools: Option<bool>,
1897
1898    /// Directory to save downloads
1899    #[serde(skip_serializing_if = "Option::is_none")]
1900    pub downloads_path: Option<String>,
1901
1902    /// Path to custom browser executable
1903    #[serde(skip_serializing_if = "Option::is_none")]
1904    pub executable_path: Option<String>,
1905
1906    /// Firefox user preferences (Firefox only)
1907    #[serde(skip_serializing_if = "Option::is_none")]
1908    pub firefox_user_prefs: Option<HashMap<String, serde_json::Value>>,
1909
1910    /// Run in headless mode (default: true unless devtools=true)
1911    #[serde(skip_serializing_if = "Option::is_none")]
1912    pub headless: Option<bool>,
1913
1914    /// Filter or disable default browser arguments.
1915    /// When `true`, Playwright does not pass its own default args.
1916    /// When an array, filters out the given default arguments.
1917    ///
1918    /// See: <https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context>
1919    #[serde(skip_serializing_if = "Option::is_none")]
1920    pub ignore_default_args: Option<IgnoreDefaultArgs>,
1921
1922    /// Slow down operations by N milliseconds
1923    #[serde(skip_serializing_if = "Option::is_none")]
1924    pub slow_mo: Option<f64>,
1925
1926    /// Timeout for browser launch in milliseconds
1927    #[serde(skip_serializing_if = "Option::is_none")]
1928    pub timeout: Option<f64>,
1929
1930    /// Directory to save traces
1931    #[serde(skip_serializing_if = "Option::is_none")]
1932    pub traces_dir: Option<String>,
1933
1934    /// Check if strict selectors mode is enabled
1935    #[serde(skip_serializing_if = "Option::is_none")]
1936    pub strict_selectors: Option<bool>,
1937
1938    /// Emulates 'prefers-reduced-motion' media feature
1939    #[serde(skip_serializing_if = "Option::is_none")]
1940    pub reduced_motion: Option<String>,
1941
1942    /// Emulates 'forced-colors' media feature
1943    #[serde(skip_serializing_if = "Option::is_none")]
1944    pub forced_colors: Option<String>,
1945
1946    /// Whether to allow sites to register Service workers
1947    #[serde(skip_serializing_if = "Option::is_none")]
1948    pub service_workers: Option<String>,
1949
1950    /// Options for recording HAR
1951    #[serde(skip_serializing_if = "Option::is_none")]
1952    pub record_har: Option<RecordHar>,
1953
1954    /// Options for recording video
1955    #[serde(skip_serializing_if = "Option::is_none")]
1956    pub record_video: Option<RecordVideo>,
1957}
1958
1959impl BrowserContextOptions {
1960    /// Creates a new builder for BrowserContextOptions
1961    pub fn builder() -> BrowserContextOptionsBuilder {
1962        BrowserContextOptionsBuilder::default()
1963    }
1964}
1965
1966/// Builder for BrowserContextOptions
1967#[derive(Debug, Clone, Default)]
1968pub struct BrowserContextOptionsBuilder {
1969    viewport: Option<Viewport>,
1970    no_viewport: Option<bool>,
1971    user_agent: Option<String>,
1972    locale: Option<String>,
1973    timezone_id: Option<String>,
1974    geolocation: Option<Geolocation>,
1975    permissions: Option<Vec<String>>,
1976    proxy: Option<ProxySettings>,
1977    color_scheme: Option<String>,
1978    has_touch: Option<bool>,
1979    is_mobile: Option<bool>,
1980    javascript_enabled: Option<bool>,
1981    offline: Option<bool>,
1982    accept_downloads: Option<bool>,
1983    bypass_csp: Option<bool>,
1984    ignore_https_errors: Option<bool>,
1985    device_scale_factor: Option<f64>,
1986    extra_http_headers: Option<HashMap<String, String>>,
1987    base_url: Option<String>,
1988    storage_state: Option<StorageState>,
1989    storage_state_path: Option<String>,
1990    // Launch options
1991    args: Option<Vec<String>>,
1992    channel: Option<String>,
1993    chromium_sandbox: Option<bool>,
1994    devtools: Option<bool>,
1995    downloads_path: Option<String>,
1996    executable_path: Option<String>,
1997    firefox_user_prefs: Option<HashMap<String, serde_json::Value>>,
1998    headless: Option<bool>,
1999    ignore_default_args: Option<IgnoreDefaultArgs>,
2000    slow_mo: Option<f64>,
2001    timeout: Option<f64>,
2002    traces_dir: Option<String>,
2003    strict_selectors: Option<bool>,
2004    reduced_motion: Option<String>,
2005    forced_colors: Option<String>,
2006    service_workers: Option<String>,
2007    record_har: Option<RecordHar>,
2008    record_video: Option<RecordVideo>,
2009}
2010
2011impl BrowserContextOptionsBuilder {
2012    /// Sets the viewport dimensions
2013    pub fn viewport(mut self, viewport: Viewport) -> Self {
2014        self.viewport = Some(viewport);
2015        self.no_viewport = None; // Clear no_viewport if setting viewport
2016        self
2017    }
2018
2019    /// Disables viewport emulation
2020    pub fn no_viewport(mut self, no_viewport: bool) -> Self {
2021        self.no_viewport = Some(no_viewport);
2022        if no_viewport {
2023            self.viewport = None; // Clear viewport if setting no_viewport
2024        }
2025        self
2026    }
2027
2028    /// Sets the user agent string
2029    pub fn user_agent(mut self, user_agent: String) -> Self {
2030        self.user_agent = Some(user_agent);
2031        self
2032    }
2033
2034    /// Sets the locale
2035    pub fn locale(mut self, locale: String) -> Self {
2036        self.locale = Some(locale);
2037        self
2038    }
2039
2040    /// Sets the timezone identifier
2041    pub fn timezone_id(mut self, timezone_id: String) -> Self {
2042        self.timezone_id = Some(timezone_id);
2043        self
2044    }
2045
2046    /// Sets the geolocation
2047    pub fn geolocation(mut self, geolocation: Geolocation) -> Self {
2048        self.geolocation = Some(geolocation);
2049        self
2050    }
2051
2052    /// Sets the permissions to grant
2053    pub fn permissions(mut self, permissions: Vec<String>) -> Self {
2054        self.permissions = Some(permissions);
2055        self
2056    }
2057
2058    /// Sets the network proxy settings for this context.
2059    ///
2060    /// This allows routing all network traffic through a proxy server,
2061    /// useful for rotating proxies without creating new browsers.
2062    ///
2063    /// # Example
2064    ///
2065    /// ```ignore
2066    /// use playwright_rs::protocol::{BrowserContextOptions, ProxySettings};
2067    ///
2068    /// let options = BrowserContextOptions::builder()
2069    ///     .proxy(ProxySettings {
2070    ///         server: "http://proxy.example.com:8080".to_string(),
2071    ///         bypass: Some(".example.com".to_string()),
2072    ///         username: Some("user".to_string()),
2073    ///         password: Some("pass".to_string()),
2074    ///     })
2075    ///     .build();
2076    /// ```
2077    ///
2078    /// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
2079    pub fn proxy(mut self, proxy: ProxySettings) -> Self {
2080        self.proxy = Some(proxy);
2081        self
2082    }
2083
2084    /// Sets the color scheme preference
2085    pub fn color_scheme(mut self, color_scheme: String) -> Self {
2086        self.color_scheme = Some(color_scheme);
2087        self
2088    }
2089
2090    /// Sets whether the viewport supports touch events
2091    pub fn has_touch(mut self, has_touch: bool) -> Self {
2092        self.has_touch = Some(has_touch);
2093        self
2094    }
2095
2096    /// Sets whether this is a mobile viewport
2097    pub fn is_mobile(mut self, is_mobile: bool) -> Self {
2098        self.is_mobile = Some(is_mobile);
2099        self
2100    }
2101
2102    /// Sets whether JavaScript is enabled
2103    pub fn javascript_enabled(mut self, javascript_enabled: bool) -> Self {
2104        self.javascript_enabled = Some(javascript_enabled);
2105        self
2106    }
2107
2108    /// Sets whether to emulate offline network
2109    pub fn offline(mut self, offline: bool) -> Self {
2110        self.offline = Some(offline);
2111        self
2112    }
2113
2114    /// Sets whether to automatically download attachments
2115    pub fn accept_downloads(mut self, accept_downloads: bool) -> Self {
2116        self.accept_downloads = Some(accept_downloads);
2117        self
2118    }
2119
2120    /// Sets whether to bypass Content-Security-Policy
2121    pub fn bypass_csp(mut self, bypass_csp: bool) -> Self {
2122        self.bypass_csp = Some(bypass_csp);
2123        self
2124    }
2125
2126    /// Sets whether to ignore HTTPS errors
2127    pub fn ignore_https_errors(mut self, ignore_https_errors: bool) -> Self {
2128        self.ignore_https_errors = Some(ignore_https_errors);
2129        self
2130    }
2131
2132    /// Sets the device scale factor
2133    pub fn device_scale_factor(mut self, device_scale_factor: f64) -> Self {
2134        self.device_scale_factor = Some(device_scale_factor);
2135        self
2136    }
2137
2138    /// Sets extra HTTP headers
2139    pub fn extra_http_headers(mut self, extra_http_headers: HashMap<String, String>) -> Self {
2140        self.extra_http_headers = Some(extra_http_headers);
2141        self
2142    }
2143
2144    /// Sets the base URL for relative navigation
2145    pub fn base_url(mut self, base_url: String) -> Self {
2146        self.base_url = Some(base_url);
2147        self
2148    }
2149
2150    /// Sets the storage state inline (cookies, localStorage).
2151    ///
2152    /// Populates the browser context with the provided storage state, including
2153    /// cookies and local storage. This is useful for initializing a context with
2154    /// a saved authentication state.
2155    ///
2156    /// Mutually exclusive with `storage_state_path()`.
2157    ///
2158    /// # Example
2159    ///
2160    /// ```rust
2161    /// use playwright_rs::protocol::{BrowserContextOptions, Cookie, StorageState, Origin, LocalStorageItem};
2162    ///
2163    /// let storage_state = StorageState {
2164    ///     cookies: vec![Cookie {
2165    ///         name: "session_id".to_string(),
2166    ///         value: "abc123".to_string(),
2167    ///         domain: ".example.com".to_string(),
2168    ///         path: "/".to_string(),
2169    ///         expires: -1.0,
2170    ///         http_only: true,
2171    ///         secure: true,
2172    ///         same_site: Some("Lax".to_string()),
2173    ///     }],
2174    ///     origins: vec![Origin {
2175    ///         origin: "https://example.com".to_string(),
2176    ///         local_storage: vec![LocalStorageItem {
2177    ///             name: "user_prefs".to_string(),
2178    ///             value: "{\"theme\":\"dark\"}".to_string(),
2179    ///         }],
2180    ///     }],
2181    /// };
2182    ///
2183    /// let options = BrowserContextOptions::builder()
2184    ///     .storage_state(storage_state)
2185    ///     .build();
2186    /// ```
2187    ///
2188    /// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
2189    pub fn storage_state(mut self, storage_state: StorageState) -> Self {
2190        self.storage_state = Some(storage_state);
2191        self.storage_state_path = None; // Clear path if setting inline
2192        self
2193    }
2194
2195    /// Sets the storage state from a file path.
2196    ///
2197    /// The file should contain a JSON representation of StorageState with cookies
2198    /// and origins. This is useful for loading authentication state saved from a
2199    /// previous session.
2200    ///
2201    /// Mutually exclusive with `storage_state()`.
2202    ///
2203    /// # Example
2204    ///
2205    /// ```rust
2206    /// use playwright_rs::protocol::BrowserContextOptions;
2207    ///
2208    /// let options = BrowserContextOptions::builder()
2209    ///     .storage_state_path("auth.json".to_string())
2210    ///     .build();
2211    /// ```
2212    ///
2213    /// The file should have this format:
2214    /// ```json
2215    /// {
2216    ///   "cookies": [{
2217    ///     "name": "session_id",
2218    ///     "value": "abc123",
2219    ///     "domain": ".example.com",
2220    ///     "path": "/",
2221    ///     "expires": -1,
2222    ///     "httpOnly": true,
2223    ///     "secure": true,
2224    ///     "sameSite": "Lax"
2225    ///   }],
2226    ///   "origins": [{
2227    ///     "origin": "https://example.com",
2228    ///     "localStorage": [{
2229    ///       "name": "user_prefs",
2230    ///       "value": "{\"theme\":\"dark\"}"
2231    ///     }]
2232    ///   }]
2233    /// }
2234    /// ```
2235    ///
2236    /// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
2237    pub fn storage_state_path(mut self, path: String) -> Self {
2238        self.storage_state_path = Some(path);
2239        self.storage_state = None; // Clear inline if setting path
2240        self
2241    }
2242
2243    /// Sets additional arguments to pass to browser instance (for launch_persistent_context)
2244    pub fn args(mut self, args: Vec<String>) -> Self {
2245        self.args = Some(args);
2246        self
2247    }
2248
2249    /// Sets browser distribution channel (for launch_persistent_context)
2250    pub fn channel(mut self, channel: String) -> Self {
2251        self.channel = Some(channel);
2252        self
2253    }
2254
2255    /// Enables or disables Chromium sandboxing (for launch_persistent_context)
2256    pub fn chromium_sandbox(mut self, enabled: bool) -> Self {
2257        self.chromium_sandbox = Some(enabled);
2258        self
2259    }
2260
2261    /// Auto-open DevTools (for launch_persistent_context)
2262    pub fn devtools(mut self, enabled: bool) -> Self {
2263        self.devtools = Some(enabled);
2264        self
2265    }
2266
2267    /// Sets directory to save downloads (for launch_persistent_context)
2268    pub fn downloads_path(mut self, path: String) -> Self {
2269        self.downloads_path = Some(path);
2270        self
2271    }
2272
2273    /// Sets path to custom browser executable (for launch_persistent_context)
2274    pub fn executable_path(mut self, path: String) -> Self {
2275        self.executable_path = Some(path);
2276        self
2277    }
2278
2279    /// Sets Firefox user preferences (for launch_persistent_context, Firefox only)
2280    pub fn firefox_user_prefs(mut self, prefs: HashMap<String, serde_json::Value>) -> Self {
2281        self.firefox_user_prefs = Some(prefs);
2282        self
2283    }
2284
2285    /// Run in headless mode (for launch_persistent_context)
2286    pub fn headless(mut self, enabled: bool) -> Self {
2287        self.headless = Some(enabled);
2288        self
2289    }
2290
2291    /// Filter or disable default browser arguments (for launch_persistent_context).
2292    ///
2293    /// When `IgnoreDefaultArgs::Bool(true)`, Playwright does not pass its own
2294    /// default arguments and only uses the ones from `args`.
2295    /// When `IgnoreDefaultArgs::Array(vec)`, filters out the given default arguments.
2296    ///
2297    /// See: <https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context>
2298    pub fn ignore_default_args(mut self, args: IgnoreDefaultArgs) -> Self {
2299        self.ignore_default_args = Some(args);
2300        self
2301    }
2302
2303    /// Slow down operations by N milliseconds (for launch_persistent_context)
2304    pub fn slow_mo(mut self, ms: f64) -> Self {
2305        self.slow_mo = Some(ms);
2306        self
2307    }
2308
2309    /// Set timeout for browser launch in milliseconds (for launch_persistent_context)
2310    pub fn timeout(mut self, ms: f64) -> Self {
2311        self.timeout = Some(ms);
2312        self
2313    }
2314
2315    /// Set directory to save traces (for launch_persistent_context)
2316    pub fn traces_dir(mut self, path: String) -> Self {
2317        self.traces_dir = Some(path);
2318        self
2319    }
2320
2321    /// Check if strict selectors mode is enabled
2322    pub fn strict_selectors(mut self, enabled: bool) -> Self {
2323        self.strict_selectors = Some(enabled);
2324        self
2325    }
2326
2327    /// Emulates 'prefers-reduced-motion' media feature
2328    pub fn reduced_motion(mut self, value: String) -> Self {
2329        self.reduced_motion = Some(value);
2330        self
2331    }
2332
2333    /// Emulates 'forced-colors' media feature
2334    pub fn forced_colors(mut self, value: String) -> Self {
2335        self.forced_colors = Some(value);
2336        self
2337    }
2338
2339    /// Whether to allow sites to register Service workers ("allow" | "block")
2340    pub fn service_workers(mut self, value: String) -> Self {
2341        self.service_workers = Some(value);
2342        self
2343    }
2344
2345    /// Sets options for recording HAR
2346    pub fn record_har(mut self, record_har: RecordHar) -> Self {
2347        self.record_har = Some(record_har);
2348        self
2349    }
2350
2351    /// Sets options for recording video
2352    pub fn record_video(mut self, record_video: RecordVideo) -> Self {
2353        self.record_video = Some(record_video);
2354        self
2355    }
2356
2357    /// Builds the BrowserContextOptions
2358    pub fn build(self) -> BrowserContextOptions {
2359        BrowserContextOptions {
2360            viewport: self.viewport,
2361            no_viewport: self.no_viewport,
2362            user_agent: self.user_agent,
2363            locale: self.locale,
2364            timezone_id: self.timezone_id,
2365            geolocation: self.geolocation,
2366            permissions: self.permissions,
2367            proxy: self.proxy,
2368            color_scheme: self.color_scheme,
2369            has_touch: self.has_touch,
2370            is_mobile: self.is_mobile,
2371            javascript_enabled: self.javascript_enabled,
2372            offline: self.offline,
2373            accept_downloads: self.accept_downloads,
2374            bypass_csp: self.bypass_csp,
2375            ignore_https_errors: self.ignore_https_errors,
2376            device_scale_factor: self.device_scale_factor,
2377            extra_http_headers: self.extra_http_headers,
2378            base_url: self.base_url,
2379            storage_state: self.storage_state,
2380            storage_state_path: self.storage_state_path,
2381            // Launch options
2382            args: self.args,
2383            channel: self.channel,
2384            chromium_sandbox: self.chromium_sandbox,
2385            devtools: self.devtools,
2386            downloads_path: self.downloads_path,
2387            executable_path: self.executable_path,
2388            firefox_user_prefs: self.firefox_user_prefs,
2389            headless: self.headless,
2390            ignore_default_args: self.ignore_default_args,
2391            slow_mo: self.slow_mo,
2392            timeout: self.timeout,
2393            traces_dir: self.traces_dir,
2394            strict_selectors: self.strict_selectors,
2395            reduced_motion: self.reduced_motion,
2396            forced_colors: self.forced_colors,
2397            service_workers: self.service_workers,
2398            record_har: self.record_har,
2399            record_video: self.record_video,
2400        }
2401    }
2402}
2403
2404/// Extracts timing data from a Response object's initializer, patching in
2405/// `responseEnd` from the event's `responseEndTiming` if available.
2406async fn extract_timing(
2407    connection: &std::sync::Arc<dyn crate::server::connection::ConnectionLike>,
2408    response_guid: Option<String>,
2409    response_end_timing: Option<f64>,
2410) -> Option<serde_json::Value> {
2411    let resp_guid = response_guid?;
2412    let resp_obj: crate::protocol::ResponseObject = connection
2413        .get_typed::<crate::protocol::ResponseObject>(&resp_guid)
2414        .await
2415        .ok()?;
2416    let mut timing = resp_obj.initializer().get("timing")?.clone();
2417    if let (Some(end), Some(obj)) = (response_end_timing, timing.as_object_mut())
2418        && let Some(n) = serde_json::Number::from_f64(end)
2419    {
2420        obj.insert("responseEnd".to_string(), serde_json::Value::Number(n));
2421    }
2422    Some(timing)
2423}
2424
2425#[cfg(test)]
2426mod tests {
2427    use super::*;
2428    use crate::api::launch_options::IgnoreDefaultArgs;
2429
2430    #[test]
2431    fn test_browser_context_options_ignore_default_args_bool_serialization() {
2432        let options = BrowserContextOptions::builder()
2433            .ignore_default_args(IgnoreDefaultArgs::Bool(true))
2434            .build();
2435
2436        let value = serde_json::to_value(&options).unwrap();
2437        assert_eq!(value["ignoreDefaultArgs"], serde_json::json!(true));
2438    }
2439
2440    #[test]
2441    fn test_browser_context_options_ignore_default_args_array_serialization() {
2442        let options = BrowserContextOptions::builder()
2443            .ignore_default_args(IgnoreDefaultArgs::Array(vec!["--foo".to_string()]))
2444            .build();
2445
2446        let value = serde_json::to_value(&options).unwrap();
2447        assert_eq!(value["ignoreDefaultArgs"], serde_json::json!(["--foo"]));
2448    }
2449
2450    #[test]
2451    fn test_browser_context_options_ignore_default_args_absent() {
2452        let options = BrowserContextOptions::builder().build();
2453
2454        let value = serde_json::to_value(&options).unwrap();
2455        assert!(value.get("ignoreDefaultArgs").is_none());
2456    }
2457}