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