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::route::UnrouteBehavior;
11use crate::protocol::{Browser, Page, ProxySettings, Request, ResponseObject, Route};
12use crate::server::channel::Channel;
13use crate::server::channel_owner::{ChannelOwner, ChannelOwnerImpl, ParentOrConnection};
14use serde::{Deserialize, Serialize};
15use serde_json::Value;
16use std::any::Any;
17use std::collections::HashMap;
18use std::future::Future;
19use std::pin::Pin;
20use std::sync::{Arc, Mutex};
21
22/// BrowserContext represents an isolated browser session.
23///
24/// Contexts are isolated environments within a browser instance. Each context
25/// has its own cookies, cache, and local storage, enabling independent sessions
26/// without interference.
27///
28/// # Example
29///
30/// ```ignore
31/// use playwright_rs::protocol::Playwright;
32///
33/// #[tokio::main]
34/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
35///     let playwright = Playwright::launch().await?;
36///     let browser = playwright.chromium().launch().await?;
37///
38///     // Create isolated contexts
39///     let context1 = browser.new_context().await?;
40///     let context2 = browser.new_context().await?;
41///
42///     // Create pages in each context
43///     let page1 = context1.new_page().await?;
44///     let page2 = context2.new_page().await?;
45///
46///     // Access all pages in a context
47///     let pages = context1.pages();
48///     assert_eq!(pages.len(), 1);
49///
50///     // Access the browser from a context
51///     let ctx_browser = context1.browser().unwrap();
52///     assert_eq!(ctx_browser.name(), browser.name());
53///
54///     // App mode: access initial page created automatically
55///     let chromium = playwright.chromium();
56///     let app_context = chromium
57///         .launch_persistent_context_with_options(
58///             "/tmp/app-data",
59///             playwright_rs::protocol::BrowserContextOptions::builder()
60///                 .args(vec!["--app=https://example.com".to_string()])
61///                 .headless(true)
62///                 .build()
63///         )
64///         .await?;
65///
66///     // Get the initial page (don't create a new one!)
67///     let app_pages = app_context.pages();
68///     if !app_pages.is_empty() {
69///         let initial_page = &app_pages[0];
70///         // Use the initial page...
71///     }
72///
73///     // Cleanup
74///     context1.close().await?;
75///     context2.close().await?;
76///     app_context.close().await?;
77///     browser.close().await?;
78///     Ok(())
79/// }
80/// ```
81///
82/// See: <https://playwright.dev/docs/api/class-browsercontext>
83/// Type alias for boxed route handler future
84type RouteHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
85
86/// Storage for a single route handler
87#[derive(Clone)]
88struct RouteHandlerEntry {
89    pattern: String,
90    handler: Arc<dyn Fn(Route) -> RouteHandlerFuture + Send + Sync>,
91}
92
93#[derive(Clone)]
94pub struct BrowserContext {
95    base: ChannelOwnerImpl,
96    /// Browser instance that owns this context (None for persistent contexts)
97    browser: Option<Browser>,
98    /// All open pages in this context
99    pages: Arc<Mutex<Vec<Page>>>,
100    /// Route handlers for context-level network interception
101    route_handlers: Arc<Mutex<Vec<RouteHandlerEntry>>>,
102    /// APIRequestContext GUID from initializer (resolved lazily)
103    request_context_guid: Option<String>,
104    /// Default action timeout for all pages in this context (milliseconds), stored as f64 bits.
105    default_timeout_ms: Arc<std::sync::atomic::AtomicU64>,
106    /// Default navigation timeout for all pages in this context (milliseconds), stored as f64 bits.
107    default_navigation_timeout_ms: Arc<std::sync::atomic::AtomicU64>,
108}
109
110impl BrowserContext {
111    /// Creates a new BrowserContext from protocol initialization
112    ///
113    /// This is called by the object factory when the server sends a `__create__` message
114    /// for a BrowserContext object.
115    ///
116    /// # Arguments
117    ///
118    /// * `parent` - The parent Browser object
119    /// * `type_name` - The protocol type name ("BrowserContext")
120    /// * `guid` - The unique identifier for this context
121    /// * `initializer` - The initialization data from the server
122    ///
123    /// # Errors
124    ///
125    /// Returns error if initializer is malformed
126    pub fn new(
127        parent: Arc<dyn ChannelOwner>,
128        type_name: String,
129        guid: Arc<str>,
130        initializer: Value,
131    ) -> Result<Self> {
132        // Extract APIRequestContext GUID from initializer before moving it
133        let request_context_guid = initializer
134            .get("requestContext")
135            .and_then(|v| v.get("guid"))
136            .and_then(|v| v.as_str())
137            .map(|s| s.to_string());
138
139        let base = ChannelOwnerImpl::new(
140            ParentOrConnection::Parent(parent.clone()),
141            type_name,
142            guid,
143            initializer,
144        );
145
146        // Store browser reference if parent is a Browser
147        // Returns None only for special contexts (Android, Electron) where parent is not a Browser
148        // For both regular contexts and persistent contexts, parent is a Browser instance
149        let browser = parent.as_any().downcast_ref::<Browser>().cloned();
150
151        let context = Self {
152            base,
153            browser,
154            pages: Arc::new(Mutex::new(Vec::new())),
155            route_handlers: Arc::new(Mutex::new(Vec::new())),
156            request_context_guid,
157            default_timeout_ms: Arc::new(std::sync::atomic::AtomicU64::new(
158                crate::DEFAULT_TIMEOUT_MS.to_bits(),
159            )),
160            default_navigation_timeout_ms: Arc::new(std::sync::atomic::AtomicU64::new(
161                crate::DEFAULT_TIMEOUT_MS.to_bits(),
162            )),
163        };
164
165        // Enable dialog event subscription
166        // Dialog events need to be explicitly subscribed to via updateSubscription command
167        let channel = context.channel().clone();
168        tokio::spawn(async move {
169            _ = channel.update_subscription("dialog", true).await;
170        });
171
172        Ok(context)
173    }
174
175    /// Returns the channel for sending protocol messages
176    ///
177    /// Used internally for sending RPC calls to the context.
178    fn channel(&self) -> &Channel {
179        self.base.channel()
180    }
181
182    /// Adds a script which would be evaluated in one of the following scenarios:
183    ///
184    /// - Whenever a page is created in the browser context or is navigated.
185    /// - Whenever a child frame is attached or navigated in any page in the browser context.
186    ///
187    /// The script is evaluated after the document was created but before any of its scripts
188    /// were run. This is useful to amend the JavaScript environment, e.g. to seed Math.random.
189    ///
190    /// # Arguments
191    ///
192    /// * `script` - Script to be evaluated in all pages in the browser context.
193    ///
194    /// # Errors
195    ///
196    /// Returns error if:
197    /// - Context has been closed
198    /// - Communication with browser process fails
199    ///
200    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-add-init-script>
201    pub async fn add_init_script(&self, script: &str) -> Result<()> {
202        self.channel()
203            .send_no_result("addInitScript", serde_json::json!({ "source": script }))
204            .await
205    }
206
207    /// Creates a new page in this browser context.
208    ///
209    /// Pages are isolated tabs/windows within a context. Each page starts
210    /// at "about:blank" and can be navigated independently.
211    ///
212    /// # Errors
213    ///
214    /// Returns error if:
215    /// - Context has been closed
216    /// - Communication with browser process fails
217    ///
218    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-new-page>
219    pub async fn new_page(&self) -> Result<Page> {
220        // Response contains the GUID of the created Page
221        #[derive(Deserialize)]
222        struct NewPageResponse {
223            page: GuidRef,
224        }
225
226        #[derive(Deserialize)]
227        struct GuidRef {
228            #[serde(deserialize_with = "crate::server::connection::deserialize_arc_str")]
229            guid: Arc<str>,
230        }
231
232        // Send newPage RPC to server
233        let response: NewPageResponse = self
234            .channel()
235            .send("newPage", serde_json::json!({}))
236            .await?;
237
238        // Retrieve the Page object from the connection registry
239        let page_arc = self.connection().get_object(&response.page.guid).await?;
240
241        // Downcast to Page
242        let page = page_arc.as_any().downcast_ref::<Page>().ok_or_else(|| {
243            crate::error::Error::ProtocolError(format!(
244                "Expected Page object, got {}",
245                page_arc.type_name()
246            ))
247        })?;
248
249        // Note: Don't track the page here - it will be tracked via the "page" event
250        // that Playwright server sends automatically when a page is created.
251        // Tracking it here would create duplicates.
252
253        let page = page.clone();
254
255        // Propagate context-level timeout defaults to the new page
256        let ctx_timeout = self.default_timeout_ms();
257        let ctx_nav_timeout = self.default_navigation_timeout_ms();
258        if ctx_timeout.to_bits() != crate::DEFAULT_TIMEOUT_MS.to_bits() {
259            page.set_default_timeout(ctx_timeout).await;
260        }
261        if ctx_nav_timeout.to_bits() != crate::DEFAULT_TIMEOUT_MS.to_bits() {
262            page.set_default_navigation_timeout(ctx_nav_timeout).await;
263        }
264
265        Ok(page)
266    }
267
268    /// Returns all open pages in the context.
269    ///
270    /// This method provides a snapshot of all currently active pages that belong
271    /// to this browser context instance. Pages created via `new_page()` and popup
272    /// pages opened through user interactions are included.
273    ///
274    /// In persistent contexts launched with `--app=url`, this will include the
275    /// initial page created automatically by Playwright.
276    ///
277    /// # Errors
278    ///
279    /// This method does not return errors. It provides a snapshot of pages at
280    /// the time of invocation.
281    ///
282    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-pages>
283    pub fn pages(&self) -> Vec<Page> {
284        self.pages.lock().unwrap().clone()
285    }
286
287    /// Returns the browser instance that owns this context.
288    ///
289    /// Returns `None` only for contexts created outside of normal browser
290    /// (e.g., Android or Electron contexts). For both regular contexts and
291    /// persistent contexts, this returns the owning Browser instance.
292    ///
293    /// # Errors
294    ///
295    /// This method does not return errors.
296    ///
297    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-browser>
298    pub fn browser(&self) -> Option<Browser> {
299        self.browser.clone()
300    }
301
302    /// Returns the APIRequestContext associated with this context.
303    ///
304    /// The APIRequestContext is created automatically by the server for each
305    /// BrowserContext. It enables performing HTTP requests and is used internally
306    /// by `Route::fetch()`.
307    ///
308    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-request>
309    pub async fn request(&self) -> Result<APIRequestContext> {
310        let guid = self.request_context_guid.as_ref().ok_or_else(|| {
311            crate::error::Error::ProtocolError(
312                "No APIRequestContext available for this context".to_string(),
313            )
314        })?;
315
316        let obj = self.connection().get_object(guid).await?;
317        obj.as_any()
318            .downcast_ref::<APIRequestContext>()
319            .cloned()
320            .ok_or_else(|| {
321                crate::error::Error::ProtocolError(format!(
322                    "Expected APIRequestContext, got {}",
323                    obj.type_name()
324                ))
325            })
326    }
327
328    /// Closes the browser context and all its pages.
329    ///
330    /// This is a graceful operation that sends a close command to the context
331    /// and waits for it to shut down properly.
332    ///
333    /// # Errors
334    ///
335    /// Returns error if:
336    /// - Context has already been closed
337    /// - Communication with browser process fails
338    ///
339    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-close>
340    pub async fn close(&self) -> Result<()> {
341        // Send close RPC to server
342        self.channel()
343            .send_no_result("close", serde_json::json!({}))
344            .await
345    }
346
347    /// Sets the default timeout for all operations in this browser context.
348    ///
349    /// This applies to all pages already open in this context as well as pages
350    /// created subsequently. Pass `0` to disable timeouts.
351    ///
352    /// # Arguments
353    ///
354    /// * `timeout` - Timeout in milliseconds
355    ///
356    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-timeout>
357    pub async fn set_default_timeout(&self, timeout: f64) {
358        self.default_timeout_ms
359            .store(timeout.to_bits(), std::sync::atomic::Ordering::Relaxed);
360        let pages: Vec<Page> = self.pages.lock().unwrap().clone();
361        for page in pages {
362            page.set_default_timeout(timeout).await;
363        }
364        crate::protocol::page::set_timeout_and_notify(
365            self.channel(),
366            "setDefaultTimeoutNoReply",
367            timeout,
368        )
369        .await;
370    }
371
372    /// Sets the default timeout for navigation operations in this browser context.
373    ///
374    /// This applies to all pages already open in this context as well as pages
375    /// created subsequently. Pass `0` to disable timeouts.
376    ///
377    /// # Arguments
378    ///
379    /// * `timeout` - Timeout in milliseconds
380    ///
381    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-navigation-timeout>
382    pub async fn set_default_navigation_timeout(&self, timeout: f64) {
383        self.default_navigation_timeout_ms
384            .store(timeout.to_bits(), std::sync::atomic::Ordering::Relaxed);
385        let pages: Vec<Page> = self.pages.lock().unwrap().clone();
386        for page in pages {
387            page.set_default_navigation_timeout(timeout).await;
388        }
389        crate::protocol::page::set_timeout_and_notify(
390            self.channel(),
391            "setDefaultNavigationTimeoutNoReply",
392            timeout,
393        )
394        .await;
395    }
396
397    /// Returns the context's current default action timeout in milliseconds.
398    fn default_timeout_ms(&self) -> f64 {
399        f64::from_bits(
400            self.default_timeout_ms
401                .load(std::sync::atomic::Ordering::Relaxed),
402        )
403    }
404
405    /// Returns the context's current default navigation timeout in milliseconds.
406    fn default_navigation_timeout_ms(&self) -> f64 {
407        f64::from_bits(
408            self.default_navigation_timeout_ms
409                .load(std::sync::atomic::Ordering::Relaxed),
410        )
411    }
412
413    /// Pauses the browser context.
414    ///
415    /// This pauses the execution of all pages in the context.
416    pub async fn pause(&self) -> Result<()> {
417        self.channel()
418            .send_no_result("pause", serde_json::Value::Null)
419            .await
420    }
421
422    /// Returns storage state for this browser context.
423    ///
424    /// Contains current cookies and local storage snapshots.
425    ///
426    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-storage-state>
427    pub async fn storage_state(&self) -> Result<StorageState> {
428        let response: StorageState = self
429            .channel()
430            .send("storageState", serde_json::json!({}))
431            .await?;
432        Ok(response)
433    }
434
435    /// Adds cookies into this browser context.
436    ///
437    /// All pages within this context will have these cookies installed. Cookies can be granularly specified
438    /// with `name`, `value`, `url`, `domain`, `path`, `expires`, `httpOnly`, `secure`, `sameSite`.
439    ///
440    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-add-cookies>
441    pub async fn add_cookies(&self, cookies: &[Cookie]) -> Result<()> {
442        self.channel()
443            .send_no_result(
444                "addCookies",
445                serde_json::json!({
446                    "cookies": cookies
447                }),
448            )
449            .await
450    }
451
452    /// Returns cookies for this browser context, optionally filtered by URLs.
453    ///
454    /// If `urls` is `None` or empty, all cookies are returned.
455    ///
456    /// # Arguments
457    ///
458    /// * `urls` - Optional list of URLs to filter cookies by
459    ///
460    /// # Errors
461    ///
462    /// Returns error if:
463    /// - Context has been closed
464    /// - Communication with browser process fails
465    ///
466    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-cookies>
467    pub async fn cookies(&self, urls: Option<&[&str]>) -> Result<Vec<Cookie>> {
468        let url_list: Vec<&str> = urls.unwrap_or(&[]).to_vec();
469        #[derive(serde::Deserialize)]
470        struct CookiesResponse {
471            cookies: Vec<Cookie>,
472        }
473        let response: CookiesResponse = self
474            .channel()
475            .send("cookies", serde_json::json!({ "urls": url_list }))
476            .await?;
477        Ok(response.cookies)
478    }
479
480    /// Clears cookies from this browser context, with optional filters.
481    ///
482    /// When called with no options, all cookies are removed. Use `ClearCookiesOptions`
483    /// to filter which cookies to clear by name, domain, or path.
484    ///
485    /// # Arguments
486    ///
487    /// * `options` - Optional filters for which cookies to clear
488    ///
489    /// # Errors
490    ///
491    /// Returns error if:
492    /// - Context has been closed
493    /// - Communication with browser process fails
494    ///
495    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-clear-cookies>
496    pub async fn clear_cookies(&self, options: Option<ClearCookiesOptions>) -> Result<()> {
497        let params = match options {
498            None => serde_json::json!({}),
499            Some(opts) => serde_json::to_value(opts).unwrap_or(serde_json::json!({})),
500        };
501        self.channel().send_no_result("clearCookies", params).await
502    }
503
504    /// Sets extra HTTP headers that will be sent with every request from this context.
505    ///
506    /// These headers are merged with per-page extra headers set with `page.set_extra_http_headers()`.
507    /// If the page has specific headers that conflict, page-level headers take precedence.
508    ///
509    /// # Arguments
510    ///
511    /// * `headers` - Map of header names to values. All header names are lowercased.
512    ///
513    /// # Errors
514    ///
515    /// Returns error if:
516    /// - Context has been closed
517    /// - Communication with browser process fails
518    ///
519    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-set-extra-http-headers>
520    pub async fn set_extra_http_headers(&self, headers: HashMap<String, String>) -> Result<()> {
521        // Playwright protocol expects an array of {name, value} objects
522        let headers_array: Vec<serde_json::Value> = headers
523            .into_iter()
524            .map(|(name, value)| serde_json::json!({ "name": name, "value": value }))
525            .collect();
526        self.channel()
527            .send_no_result(
528                "setExtraHTTPHeaders",
529                serde_json::json!({ "headers": headers_array }),
530            )
531            .await
532    }
533
534    /// Grants browser permissions to the context.
535    ///
536    /// Permissions are granted for all pages in the context. The optional `origin`
537    /// in `GrantPermissionsOptions` restricts the grant to a specific URL origin.
538    ///
539    /// Common permissions: `"geolocation"`, `"notifications"`, `"camera"`,
540    /// `"microphone"`, `"clipboard-read"`, `"clipboard-write"`.
541    ///
542    /// # Arguments
543    ///
544    /// * `permissions` - List of permission strings to grant
545    /// * `options` - Optional options, including `origin` to restrict the grant
546    ///
547    /// # Errors
548    ///
549    /// Returns error if:
550    /// - Permission name is not recognised
551    /// - Context has been closed
552    /// - Communication with browser process fails
553    ///
554    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-grant-permissions>
555    pub async fn grant_permissions(
556        &self,
557        permissions: &[&str],
558        options: Option<GrantPermissionsOptions>,
559    ) -> Result<()> {
560        let mut params = serde_json::json!({ "permissions": permissions });
561        if let Some(opts) = options {
562            if let Some(origin) = opts.origin {
563                params["origin"] = serde_json::Value::String(origin);
564            }
565        }
566        self.channel()
567            .send_no_result("grantPermissions", params)
568            .await
569    }
570
571    /// Clears all permission overrides for this browser context.
572    ///
573    /// Reverts all permissions previously set with `grant_permissions()` back to
574    /// the browser default state.
575    ///
576    /// # Errors
577    ///
578    /// Returns error if:
579    /// - Context has been closed
580    /// - Communication with browser process fails
581    ///
582    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-clear-permissions>
583    pub async fn clear_permissions(&self) -> Result<()> {
584        self.channel()
585            .send_no_result("clearPermissions", serde_json::json!({}))
586            .await
587    }
588
589    /// Sets or clears the geolocation for all pages in this context.
590    ///
591    /// Pass `Some(Geolocation { ... })` to set a specific location, or `None` to
592    /// clear the override and let the browser handle location requests naturally.
593    ///
594    /// Note: Geolocation access requires the `"geolocation"` permission to be granted
595    /// via `grant_permissions()` for navigator.geolocation to succeed.
596    ///
597    /// # Arguments
598    ///
599    /// * `geolocation` - Location to set, or `None` to clear
600    ///
601    /// # Errors
602    ///
603    /// Returns error if:
604    /// - Latitude or longitude is out of range
605    /// - Context has been closed
606    /// - Communication with browser process fails
607    ///
608    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-set-geolocation>
609    pub async fn set_geolocation(&self, geolocation: Option<Geolocation>) -> Result<()> {
610        // Playwright protocol: omit the "geolocation" key entirely to clear;
611        // passing null causes a validation error on the server side.
612        let params = match geolocation {
613            Some(geo) => serde_json::json!({ "geolocation": geo }),
614            None => serde_json::json!({}),
615        };
616        self.channel()
617            .send_no_result("setGeolocation", params)
618            .await
619    }
620
621    /// Toggles the offline mode for this browser context.
622    ///
623    /// When `true`, all network requests from pages in this context will fail with
624    /// a network error. Set to `false` to restore network connectivity.
625    ///
626    /// # Arguments
627    ///
628    /// * `offline` - `true` to go offline, `false` to go back online
629    ///
630    /// # Errors
631    ///
632    /// Returns error if:
633    /// - Context has been closed
634    /// - Communication with browser process fails
635    ///
636    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-set-offline>
637    pub async fn set_offline(&self, offline: bool) -> Result<()> {
638        self.channel()
639            .send_no_result("setOffline", serde_json::json!({ "offline": offline }))
640            .await
641    }
642
643    /// Registers a route handler for context-level network interception.
644    ///
645    /// Routes registered on a context apply to all pages within the context.
646    /// Page-level routes take precedence over context-level routes.
647    ///
648    /// # Arguments
649    ///
650    /// * `pattern` - URL pattern to match (supports glob patterns like "**/*.png")
651    /// * `handler` - Async closure that handles the route
652    ///
653    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-route>
654    pub async fn route<F, Fut>(&self, pattern: &str, handler: F) -> Result<()>
655    where
656        F: Fn(Route) -> Fut + Send + Sync + 'static,
657        Fut: Future<Output = Result<()>> + Send + 'static,
658    {
659        let handler =
660            Arc::new(move |route: Route| -> RouteHandlerFuture { Box::pin(handler(route)) });
661
662        self.route_handlers.lock().unwrap().push(RouteHandlerEntry {
663            pattern: pattern.to_string(),
664            handler,
665        });
666
667        self.enable_network_interception().await
668    }
669
670    /// Removes route handler(s) matching the given URL pattern.
671    ///
672    /// # Arguments
673    ///
674    /// * `pattern` - URL pattern to remove handlers for
675    ///
676    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-unroute>
677    pub async fn unroute(&self, pattern: &str) -> Result<()> {
678        self.route_handlers
679            .lock()
680            .unwrap()
681            .retain(|entry| entry.pattern != pattern);
682        self.enable_network_interception().await
683    }
684
685    /// Removes all registered route handlers.
686    ///
687    /// # Arguments
688    ///
689    /// * `behavior` - Optional behavior for in-flight handlers
690    ///
691    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-unroute-all>
692    pub async fn unroute_all(&self, _behavior: Option<UnrouteBehavior>) -> Result<()> {
693        self.route_handlers.lock().unwrap().clear();
694        self.enable_network_interception().await
695    }
696
697    /// Updates network interception patterns for this context
698    async fn enable_network_interception(&self) -> Result<()> {
699        let patterns: Vec<serde_json::Value> = self
700            .route_handlers
701            .lock()
702            .unwrap()
703            .iter()
704            .map(|entry| serde_json::json!({ "glob": entry.pattern }))
705            .collect();
706
707        self.channel()
708            .send_no_result(
709                "setNetworkInterceptionPatterns",
710                serde_json::json!({ "patterns": patterns }),
711            )
712            .await
713    }
714
715    /// Handles a route event from the protocol
716    async fn on_route_event(route_handlers: Arc<Mutex<Vec<RouteHandlerEntry>>>, route: Route) {
717        let handlers = route_handlers.lock().unwrap().clone();
718        let url = route.request().url().to_string();
719
720        for entry in handlers.iter().rev() {
721            if crate::protocol::route::matches_pattern(&entry.pattern, &url) {
722                let handler = entry.handler.clone();
723                if let Err(e) = handler(route.clone()).await {
724                    tracing::warn!("Context route handler error: {}", e);
725                    break;
726                }
727                if !route.was_handled() {
728                    continue;
729                }
730                break;
731            }
732        }
733    }
734
735    fn dispatch_request_event(&self, method: &str, params: Value) {
736        if let Some(request_guid) = params
737            .get("request")
738            .and_then(|v| v.get("guid"))
739            .and_then(|v| v.as_str())
740        {
741            let connection = self.connection();
742            let request_guid_owned = request_guid.to_owned();
743            let page_guid_owned = params
744                .get("page")
745                .and_then(|v| v.get("guid"))
746                .and_then(|v| v.as_str())
747                .map(|v| v.to_owned());
748            // Extract failureText for requestFailed events
749            let failure_text = params
750                .get("failureText")
751                .and_then(|v| v.as_str())
752                .map(|s| s.to_owned());
753            // Extract response GUID for requestFinished events (to read timing)
754            let response_guid_owned = params
755                .get("response")
756                .and_then(|v| v.get("guid"))
757                .and_then(|v| v.as_str())
758                .map(|s| s.to_owned());
759            // Extract responseEndTiming from requestFinished event params
760            let response_end_timing = params.get("responseEndTiming").and_then(|v| v.as_f64());
761            let method = method.to_owned();
762            tokio::spawn(async move {
763                let request_arc = match connection.get_object(&request_guid_owned).await {
764                    Ok(obj) => obj,
765                    Err(_err) => return,
766                };
767
768                let request = match request_arc.as_any().downcast_ref::<Request>() {
769                    Some(v) => v.clone(),
770                    None => return,
771                };
772
773                // Set failure text on the request before dispatching to handlers
774                if let Some(text) = failure_text {
775                    request.set_failure_text(text);
776                }
777
778                // For requestFinished, extract timing from the Response object's initializer
779                if method == "requestFinished" {
780                    if let Some(timing) =
781                        extract_timing(&connection, response_guid_owned, response_end_timing).await
782                    {
783                        request.set_timing(timing);
784                    }
785                }
786
787                if let Some(page_guid) = page_guid_owned {
788                    let page_arc = match connection.get_object(&page_guid).await {
789                        Ok(v) => v,
790                        Err(_) => return,
791                    };
792                    let page = match page_arc.as_any().downcast_ref::<Page>() {
793                        Some(p) => p,
794                        None => return,
795                    };
796                    match method.as_str() {
797                        "request" => page.trigger_request_event(request).await,
798                        "requestFailed" => page.trigger_request_failed_event(request).await,
799                        "requestFinished" => page.trigger_request_finished_event(request).await,
800                        _ => unreachable!("Unreachable method {}", method),
801                    }
802                }
803            });
804        }
805    }
806
807    fn dispatch_response_event(&self, _method: &str, params: Value) {
808        if let Some(response_guid) = params
809            .get("response")
810            .and_then(|v| v.get("guid"))
811            .and_then(|v| v.as_str())
812        {
813            let connection = self.connection();
814            let response_guid_owned = response_guid.to_owned();
815            let page_guid_owned = params
816                .get("page")
817                .and_then(|v| v.get("guid"))
818                .and_then(|v| v.as_str())
819                .map(|v| v.to_owned());
820            tokio::spawn(async move {
821                let response_arc = match connection.get_object(&response_guid_owned).await {
822                    Ok(obj) => obj,
823                    Err(_err) => return,
824                };
825
826                let response = match response_arc.as_any().downcast_ref::<ResponseObject>() {
827                    Some(v) => v.clone(),
828                    None => return,
829                };
830
831                if let Some(page_guid) = page_guid_owned {
832                    let page_arc = match connection.get_object(&page_guid).await {
833                        Ok(v) => v,
834                        Err(_) => return,
835                    };
836                    let page = match page_arc.as_any().downcast_ref::<Page>() {
837                        Some(p) => p,
838                        None => return,
839                    };
840                    page.trigger_response_event(response).await;
841                }
842            });
843        }
844    }
845}
846
847impl ChannelOwner for BrowserContext {
848    fn guid(&self) -> &str {
849        self.base.guid()
850    }
851
852    fn type_name(&self) -> &str {
853        self.base.type_name()
854    }
855
856    fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
857        self.base.parent()
858    }
859
860    fn connection(&self) -> Arc<dyn crate::server::connection::ConnectionLike> {
861        self.base.connection()
862    }
863
864    fn initializer(&self) -> &Value {
865        self.base.initializer()
866    }
867
868    fn channel(&self) -> &Channel {
869        self.base.channel()
870    }
871
872    fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
873        self.base.dispose(reason)
874    }
875
876    fn adopt(&self, child: Arc<dyn ChannelOwner>) {
877        self.base.adopt(child)
878    }
879
880    fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
881        self.base.add_child(guid, child)
882    }
883
884    fn remove_child(&self, guid: &str) {
885        self.base.remove_child(guid)
886    }
887
888    fn on_event(&self, method: &str, params: Value) {
889        match method {
890            "request" | "requestFailed" | "requestFinished" => {
891                self.dispatch_request_event(method, params)
892            }
893            "response" => self.dispatch_response_event(method, params),
894            "page" => {
895                // Page events are triggered when pages are created, including:
896                // - Initial page in persistent context with --app mode
897                // - Popup pages opened through user interactions
898                // Event format: {page: {guid: "..."}}
899                if let Some(page_guid) = params
900                    .get("page")
901                    .and_then(|v| v.get("guid"))
902                    .and_then(|v| v.as_str())
903                {
904                    let connection = self.connection();
905                    let page_guid_owned = page_guid.to_string();
906                    let pages = self.pages.clone();
907
908                    tokio::spawn(async move {
909                        // Get the Page object
910                        let page_arc = match connection.get_object(&page_guid_owned).await {
911                            Ok(obj) => obj,
912                            Err(_) => return,
913                        };
914
915                        // Downcast to Page
916                        let page = match page_arc.as_any().downcast_ref::<Page>() {
917                            Some(p) => p.clone(),
918                            None => return,
919                        };
920
921                        // Track the page
922                        pages.lock().unwrap().push(page);
923                    });
924                }
925            }
926            "dialog" => {
927                // Dialog events come to BrowserContext, need to forward to the associated Page
928                // Event format: {dialog: {guid: "..."}}
929                // The Dialog protocol object has the Page as its parent
930                if let Some(dialog_guid) = params
931                    .get("dialog")
932                    .and_then(|v| v.get("guid"))
933                    .and_then(|v| v.as_str())
934                {
935                    let connection = self.connection();
936                    let dialog_guid_owned = dialog_guid.to_string();
937
938                    tokio::spawn(async move {
939                        // Get the Dialog object
940                        let dialog_arc = match connection.get_object(&dialog_guid_owned).await {
941                            Ok(obj) => obj,
942                            Err(_) => return,
943                        };
944
945                        // Downcast to Dialog
946                        let dialog = match dialog_arc
947                            .as_any()
948                            .downcast_ref::<crate::protocol::Dialog>()
949                        {
950                            Some(d) => d.clone(),
951                            None => return,
952                        };
953
954                        // Get the Page from the Dialog's parent
955                        let page_arc = match dialog_arc.parent() {
956                            Some(parent) => parent,
957                            None => return,
958                        };
959
960                        // Downcast to Page
961                        let page = match page_arc.as_any().downcast_ref::<Page>() {
962                            Some(p) => p.clone(),
963                            None => return,
964                        };
965
966                        // Forward to Page's dialog handlers
967                        page.trigger_dialog_event(dialog).await;
968                    });
969                }
970            }
971            "route" => {
972                // Handle context-level network routing event
973                if let Some(route_guid) = params
974                    .get("route")
975                    .and_then(|v| v.get("guid"))
976                    .and_then(|v| v.as_str())
977                {
978                    let connection = self.connection();
979                    let route_guid_owned = route_guid.to_string();
980                    let route_handlers = self.route_handlers.clone();
981                    let request_context_guid = self.request_context_guid.clone();
982
983                    tokio::spawn(async move {
984                        let route_arc = match connection.get_object(&route_guid_owned).await {
985                            Ok(obj) => obj,
986                            Err(e) => {
987                                tracing::warn!("Failed to get route object: {}", e);
988                                return;
989                            }
990                        };
991
992                        let route = match route_arc.as_any().downcast_ref::<Route>() {
993                            Some(r) => r.clone(),
994                            None => {
995                                tracing::warn!("Failed to downcast to Route");
996                                return;
997                            }
998                        };
999
1000                        // Set APIRequestContext on the route for fetch() support
1001                        if let Some(ref guid) = request_context_guid {
1002                            if let Ok(obj) = connection.get_object(guid).await {
1003                                if let Some(api_ctx) =
1004                                    obj.as_any().downcast_ref::<APIRequestContext>()
1005                                {
1006                                    route.set_api_request_context(api_ctx.clone());
1007                                }
1008                            }
1009                        }
1010
1011                        BrowserContext::on_route_event(route_handlers, route).await;
1012                    });
1013                }
1014            }
1015            _ => {
1016                // Other events will be handled in future phases
1017            }
1018        }
1019    }
1020
1021    fn was_collected(&self) -> bool {
1022        self.base.was_collected()
1023    }
1024
1025    fn as_any(&self) -> &dyn Any {
1026        self
1027    }
1028}
1029
1030impl std::fmt::Debug for BrowserContext {
1031    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1032        f.debug_struct("BrowserContext")
1033            .field("guid", &self.guid())
1034            .finish()
1035    }
1036}
1037
1038/// Viewport dimensions for browser context.
1039///
1040/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
1041#[derive(Debug, Clone, Serialize, Deserialize)]
1042pub struct Viewport {
1043    /// Page width in pixels
1044    pub width: u32,
1045    /// Page height in pixels
1046    pub height: u32,
1047}
1048
1049/// Geolocation coordinates.
1050///
1051/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
1052#[derive(Debug, Clone, Serialize, Deserialize)]
1053pub struct Geolocation {
1054    /// Latitude between -90 and 90
1055    pub latitude: f64,
1056    /// Longitude between -180 and 180
1057    pub longitude: f64,
1058    /// Optional accuracy in meters (default: 0)
1059    #[serde(skip_serializing_if = "Option::is_none")]
1060    pub accuracy: Option<f64>,
1061}
1062
1063/// Cookie information for storage state.
1064///
1065/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
1066#[derive(Debug, Clone, Serialize, Deserialize)]
1067#[serde(rename_all = "camelCase")]
1068pub struct Cookie {
1069    /// Cookie name
1070    pub name: String,
1071    /// Cookie value
1072    pub value: String,
1073    /// Cookie domain (use dot prefix for subdomain matching, e.g., ".example.com")
1074    pub domain: String,
1075    /// Cookie path
1076    pub path: String,
1077    /// Unix timestamp in seconds; -1 for session cookies
1078    pub expires: f64,
1079    /// HTTP-only flag
1080    pub http_only: bool,
1081    /// Secure flag
1082    pub secure: bool,
1083    /// SameSite attribute ("Strict", "Lax", "None")
1084    #[serde(skip_serializing_if = "Option::is_none")]
1085    pub same_site: Option<String>,
1086}
1087
1088/// Local storage item for storage state.
1089///
1090/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
1091#[derive(Debug, Clone, Serialize, Deserialize)]
1092pub struct LocalStorageItem {
1093    /// Storage key
1094    pub name: String,
1095    /// Storage value
1096    pub value: String,
1097}
1098
1099/// Origin with local storage items for storage state.
1100///
1101/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
1102#[derive(Debug, Clone, Serialize, Deserialize)]
1103#[serde(rename_all = "camelCase")]
1104pub struct Origin {
1105    /// Origin URL (e.g., `https://example.com`)
1106    pub origin: String,
1107    /// Local storage items for this origin
1108    pub local_storage: Vec<LocalStorageItem>,
1109}
1110
1111/// Storage state containing cookies and local storage.
1112///
1113/// Used to populate a browser context with saved authentication state,
1114/// enabling session persistence across context instances.
1115///
1116/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
1117#[derive(Debug, Clone, Serialize, Deserialize)]
1118pub struct StorageState {
1119    /// List of cookies
1120    pub cookies: Vec<Cookie>,
1121    /// List of origins with local storage
1122    pub origins: Vec<Origin>,
1123}
1124
1125/// Options for filtering which cookies to clear with `BrowserContext::clear_cookies()`.
1126///
1127/// All fields are optional; when provided they act as AND-combined filters.
1128///
1129/// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-clear-cookies>
1130#[derive(Debug, Clone, Default, Serialize)]
1131#[serde(rename_all = "camelCase")]
1132pub struct ClearCookiesOptions {
1133    /// Filter by cookie name (exact match).
1134    #[serde(skip_serializing_if = "Option::is_none")]
1135    pub name: Option<String>,
1136    /// Filter by cookie domain.
1137    #[serde(skip_serializing_if = "Option::is_none")]
1138    pub domain: Option<String>,
1139    /// Filter by cookie path.
1140    #[serde(skip_serializing_if = "Option::is_none")]
1141    pub path: Option<String>,
1142}
1143
1144/// Options for `BrowserContext::grant_permissions()`.
1145///
1146/// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-grant-permissions>
1147#[derive(Debug, Clone, Default)]
1148pub struct GrantPermissionsOptions {
1149    /// Optional origin to restrict the permission grant to.
1150    ///
1151    /// For example `"https://example.com"`.
1152    pub origin: Option<String>,
1153}
1154
1155/// Options for recording HAR.
1156///
1157/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-record-har>
1158#[derive(Debug, Clone, Serialize, Default)]
1159#[serde(rename_all = "camelCase")]
1160pub struct RecordHar {
1161    /// Path on the filesystem to write the HAR file to.
1162    pub path: String,
1163    /// Optional setting to control whether to omit request content from the HAR.
1164    #[serde(skip_serializing_if = "Option::is_none")]
1165    pub omit_content: Option<bool>,
1166    /// Optional setting to control resource content management.
1167    /// "omit" | "embed" | "attach"
1168    #[serde(skip_serializing_if = "Option::is_none")]
1169    pub content: Option<String>,
1170    /// "full" | "minimal"
1171    #[serde(skip_serializing_if = "Option::is_none")]
1172    pub mode: Option<String>,
1173    /// A glob or regex pattern to filter requests that are stored in the HAR.
1174    #[serde(skip_serializing_if = "Option::is_none")]
1175    pub url_filter: Option<String>,
1176}
1177
1178/// Options for recording video.
1179///
1180/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-record-video>
1181#[derive(Debug, Clone, Serialize, Default)]
1182pub struct RecordVideo {
1183    /// Path to the directory to put videos into.
1184    pub dir: String,
1185    /// Optional dimensions of the recorded videos.
1186    #[serde(skip_serializing_if = "Option::is_none")]
1187    pub size: Option<Viewport>,
1188}
1189
1190/// Options for creating a new browser context.
1191///
1192/// Allows customizing viewport, user agent, locale, timezone, geolocation,
1193/// permissions, and other browser context settings.
1194///
1195/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
1196#[derive(Debug, Clone, Default, Serialize)]
1197#[serde(rename_all = "camelCase")]
1198pub struct BrowserContextOptions {
1199    /// Sets consistent viewport for all pages in the context.
1200    /// Set to null via `no_viewport(true)` to disable viewport emulation.
1201    #[serde(skip_serializing_if = "Option::is_none")]
1202    pub viewport: Option<Viewport>,
1203
1204    /// Disables viewport emulation when set to true.
1205    /// Note: Playwright's public API calls this `noViewport`, but the protocol
1206    /// expects `noDefaultViewport`. playwright-python applies this transformation
1207    /// in `_prepare_browser_context_params`.
1208    #[serde(skip_serializing_if = "Option::is_none")]
1209    #[serde(rename = "noDefaultViewport")]
1210    pub no_viewport: Option<bool>,
1211
1212    /// Custom user agent string
1213    #[serde(skip_serializing_if = "Option::is_none")]
1214    pub user_agent: Option<String>,
1215
1216    /// Locale for the context (e.g., "en-GB", "de-DE", "fr-FR")
1217    #[serde(skip_serializing_if = "Option::is_none")]
1218    pub locale: Option<String>,
1219
1220    /// Timezone identifier (e.g., "America/New_York", "Europe/Berlin")
1221    #[serde(skip_serializing_if = "Option::is_none")]
1222    pub timezone_id: Option<String>,
1223
1224    /// Geolocation coordinates
1225    #[serde(skip_serializing_if = "Option::is_none")]
1226    pub geolocation: Option<Geolocation>,
1227
1228    /// List of permissions to grant (e.g., "geolocation", "notifications")
1229    #[serde(skip_serializing_if = "Option::is_none")]
1230    pub permissions: Option<Vec<String>>,
1231
1232    /// Network proxy settings
1233    #[serde(skip_serializing_if = "Option::is_none")]
1234    pub proxy: Option<ProxySettings>,
1235
1236    /// Emulates 'prefers-colors-scheme' media feature ("light", "dark", "no-preference")
1237    #[serde(skip_serializing_if = "Option::is_none")]
1238    pub color_scheme: Option<String>,
1239
1240    /// Whether the viewport supports touch events
1241    #[serde(skip_serializing_if = "Option::is_none")]
1242    pub has_touch: Option<bool>,
1243
1244    /// Whether the meta viewport tag is respected
1245    #[serde(skip_serializing_if = "Option::is_none")]
1246    pub is_mobile: Option<bool>,
1247
1248    /// Whether JavaScript is enabled in the context
1249    #[serde(skip_serializing_if = "Option::is_none")]
1250    pub javascript_enabled: Option<bool>,
1251
1252    /// Emulates network being offline
1253    #[serde(skip_serializing_if = "Option::is_none")]
1254    pub offline: Option<bool>,
1255
1256    /// Whether to automatically download attachments
1257    #[serde(skip_serializing_if = "Option::is_none")]
1258    pub accept_downloads: Option<bool>,
1259
1260    /// Whether to bypass Content-Security-Policy
1261    #[serde(skip_serializing_if = "Option::is_none")]
1262    pub bypass_csp: Option<bool>,
1263
1264    /// Whether to ignore HTTPS errors
1265    #[serde(skip_serializing_if = "Option::is_none")]
1266    pub ignore_https_errors: Option<bool>,
1267
1268    /// Device scale factor (default: 1)
1269    #[serde(skip_serializing_if = "Option::is_none")]
1270    pub device_scale_factor: Option<f64>,
1271
1272    /// Extra HTTP headers to send with every request
1273    #[serde(skip_serializing_if = "Option::is_none")]
1274    pub extra_http_headers: Option<HashMap<String, String>>,
1275
1276    /// Base URL for relative navigation
1277    #[serde(skip_serializing_if = "Option::is_none")]
1278    pub base_url: Option<String>,
1279
1280    /// Storage state to populate the context (cookies, localStorage, sessionStorage).
1281    /// Can be an inline StorageState object or a file path string.
1282    /// Use builder methods `storage_state()` for inline or `storage_state_path()` for file path.
1283    #[serde(skip_serializing_if = "Option::is_none")]
1284    pub storage_state: Option<StorageState>,
1285
1286    /// Storage state file path (alternative to inline storage_state).
1287    /// This is handled by the builder and converted to storage_state during serialization.
1288    #[serde(skip_serializing_if = "Option::is_none")]
1289    pub storage_state_path: Option<String>,
1290
1291    // Launch options (for launch_persistent_context)
1292    /// Additional arguments to pass to browser instance
1293    #[serde(skip_serializing_if = "Option::is_none")]
1294    pub args: Option<Vec<String>>,
1295
1296    /// Browser distribution channel (e.g., "chrome", "msedge")
1297    #[serde(skip_serializing_if = "Option::is_none")]
1298    pub channel: Option<String>,
1299
1300    /// Enable Chromium sandboxing (default: false on Linux)
1301    #[serde(skip_serializing_if = "Option::is_none")]
1302    pub chromium_sandbox: Option<bool>,
1303
1304    /// Auto-open DevTools (deprecated, default: false)
1305    #[serde(skip_serializing_if = "Option::is_none")]
1306    pub devtools: Option<bool>,
1307
1308    /// Directory to save downloads
1309    #[serde(skip_serializing_if = "Option::is_none")]
1310    pub downloads_path: Option<String>,
1311
1312    /// Path to custom browser executable
1313    #[serde(skip_serializing_if = "Option::is_none")]
1314    pub executable_path: Option<String>,
1315
1316    /// Firefox user preferences (Firefox only)
1317    #[serde(skip_serializing_if = "Option::is_none")]
1318    pub firefox_user_prefs: Option<HashMap<String, serde_json::Value>>,
1319
1320    /// Run in headless mode (default: true unless devtools=true)
1321    #[serde(skip_serializing_if = "Option::is_none")]
1322    pub headless: Option<bool>,
1323
1324    /// Filter or disable default browser arguments.
1325    /// When `true`, Playwright does not pass its own default args.
1326    /// When an array, filters out the given default arguments.
1327    ///
1328    /// See: <https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context>
1329    #[serde(skip_serializing_if = "Option::is_none")]
1330    pub ignore_default_args: Option<IgnoreDefaultArgs>,
1331
1332    /// Slow down operations by N milliseconds
1333    #[serde(skip_serializing_if = "Option::is_none")]
1334    pub slow_mo: Option<f64>,
1335
1336    /// Timeout for browser launch in milliseconds
1337    #[serde(skip_serializing_if = "Option::is_none")]
1338    pub timeout: Option<f64>,
1339
1340    /// Directory to save traces
1341    #[serde(skip_serializing_if = "Option::is_none")]
1342    pub traces_dir: Option<String>,
1343
1344    /// Check if strict selectors mode is enabled
1345    #[serde(skip_serializing_if = "Option::is_none")]
1346    pub strict_selectors: Option<bool>,
1347
1348    /// Emulates 'prefers-reduced-motion' media feature
1349    #[serde(skip_serializing_if = "Option::is_none")]
1350    pub reduced_motion: Option<String>,
1351
1352    /// Emulates 'forced-colors' media feature
1353    #[serde(skip_serializing_if = "Option::is_none")]
1354    pub forced_colors: Option<String>,
1355
1356    /// Whether to allow sites to register Service workers
1357    #[serde(skip_serializing_if = "Option::is_none")]
1358    pub service_workers: Option<String>,
1359
1360    /// Options for recording HAR
1361    #[serde(skip_serializing_if = "Option::is_none")]
1362    pub record_har: Option<RecordHar>,
1363
1364    /// Options for recording video
1365    #[serde(skip_serializing_if = "Option::is_none")]
1366    pub record_video: Option<RecordVideo>,
1367}
1368
1369impl BrowserContextOptions {
1370    /// Creates a new builder for BrowserContextOptions
1371    pub fn builder() -> BrowserContextOptionsBuilder {
1372        BrowserContextOptionsBuilder::default()
1373    }
1374}
1375
1376/// Builder for BrowserContextOptions
1377#[derive(Debug, Clone, Default)]
1378pub struct BrowserContextOptionsBuilder {
1379    viewport: Option<Viewport>,
1380    no_viewport: Option<bool>,
1381    user_agent: Option<String>,
1382    locale: Option<String>,
1383    timezone_id: Option<String>,
1384    geolocation: Option<Geolocation>,
1385    permissions: Option<Vec<String>>,
1386    proxy: Option<ProxySettings>,
1387    color_scheme: Option<String>,
1388    has_touch: Option<bool>,
1389    is_mobile: Option<bool>,
1390    javascript_enabled: Option<bool>,
1391    offline: Option<bool>,
1392    accept_downloads: Option<bool>,
1393    bypass_csp: Option<bool>,
1394    ignore_https_errors: Option<bool>,
1395    device_scale_factor: Option<f64>,
1396    extra_http_headers: Option<HashMap<String, String>>,
1397    base_url: Option<String>,
1398    storage_state: Option<StorageState>,
1399    storage_state_path: Option<String>,
1400    // Launch options
1401    args: Option<Vec<String>>,
1402    channel: Option<String>,
1403    chromium_sandbox: Option<bool>,
1404    devtools: Option<bool>,
1405    downloads_path: Option<String>,
1406    executable_path: Option<String>,
1407    firefox_user_prefs: Option<HashMap<String, serde_json::Value>>,
1408    headless: Option<bool>,
1409    ignore_default_args: Option<IgnoreDefaultArgs>,
1410    slow_mo: Option<f64>,
1411    timeout: Option<f64>,
1412    traces_dir: Option<String>,
1413    strict_selectors: Option<bool>,
1414    reduced_motion: Option<String>,
1415    forced_colors: Option<String>,
1416    service_workers: Option<String>,
1417    record_har: Option<RecordHar>,
1418    record_video: Option<RecordVideo>,
1419}
1420
1421impl BrowserContextOptionsBuilder {
1422    /// Sets the viewport dimensions
1423    pub fn viewport(mut self, viewport: Viewport) -> Self {
1424        self.viewport = Some(viewport);
1425        self.no_viewport = None; // Clear no_viewport if setting viewport
1426        self
1427    }
1428
1429    /// Disables viewport emulation
1430    pub fn no_viewport(mut self, no_viewport: bool) -> Self {
1431        self.no_viewport = Some(no_viewport);
1432        if no_viewport {
1433            self.viewport = None; // Clear viewport if setting no_viewport
1434        }
1435        self
1436    }
1437
1438    /// Sets the user agent string
1439    pub fn user_agent(mut self, user_agent: String) -> Self {
1440        self.user_agent = Some(user_agent);
1441        self
1442    }
1443
1444    /// Sets the locale
1445    pub fn locale(mut self, locale: String) -> Self {
1446        self.locale = Some(locale);
1447        self
1448    }
1449
1450    /// Sets the timezone identifier
1451    pub fn timezone_id(mut self, timezone_id: String) -> Self {
1452        self.timezone_id = Some(timezone_id);
1453        self
1454    }
1455
1456    /// Sets the geolocation
1457    pub fn geolocation(mut self, geolocation: Geolocation) -> Self {
1458        self.geolocation = Some(geolocation);
1459        self
1460    }
1461
1462    /// Sets the permissions to grant
1463    pub fn permissions(mut self, permissions: Vec<String>) -> Self {
1464        self.permissions = Some(permissions);
1465        self
1466    }
1467
1468    /// Sets the network proxy settings for this context.
1469    ///
1470    /// This allows routing all network traffic through a proxy server,
1471    /// useful for rotating proxies without creating new browsers.
1472    ///
1473    /// # Example
1474    ///
1475    /// ```ignore
1476    /// use playwright_rs::protocol::{BrowserContextOptions, ProxySettings};
1477    ///
1478    /// let options = BrowserContextOptions::builder()
1479    ///     .proxy(ProxySettings {
1480    ///         server: "http://proxy.example.com:8080".to_string(),
1481    ///         bypass: Some(".example.com".to_string()),
1482    ///         username: Some("user".to_string()),
1483    ///         password: Some("pass".to_string()),
1484    ///     })
1485    ///     .build();
1486    /// ```
1487    ///
1488    /// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
1489    pub fn proxy(mut self, proxy: ProxySettings) -> Self {
1490        self.proxy = Some(proxy);
1491        self
1492    }
1493
1494    /// Sets the color scheme preference
1495    pub fn color_scheme(mut self, color_scheme: String) -> Self {
1496        self.color_scheme = Some(color_scheme);
1497        self
1498    }
1499
1500    /// Sets whether the viewport supports touch events
1501    pub fn has_touch(mut self, has_touch: bool) -> Self {
1502        self.has_touch = Some(has_touch);
1503        self
1504    }
1505
1506    /// Sets whether this is a mobile viewport
1507    pub fn is_mobile(mut self, is_mobile: bool) -> Self {
1508        self.is_mobile = Some(is_mobile);
1509        self
1510    }
1511
1512    /// Sets whether JavaScript is enabled
1513    pub fn javascript_enabled(mut self, javascript_enabled: bool) -> Self {
1514        self.javascript_enabled = Some(javascript_enabled);
1515        self
1516    }
1517
1518    /// Sets whether to emulate offline network
1519    pub fn offline(mut self, offline: bool) -> Self {
1520        self.offline = Some(offline);
1521        self
1522    }
1523
1524    /// Sets whether to automatically download attachments
1525    pub fn accept_downloads(mut self, accept_downloads: bool) -> Self {
1526        self.accept_downloads = Some(accept_downloads);
1527        self
1528    }
1529
1530    /// Sets whether to bypass Content-Security-Policy
1531    pub fn bypass_csp(mut self, bypass_csp: bool) -> Self {
1532        self.bypass_csp = Some(bypass_csp);
1533        self
1534    }
1535
1536    /// Sets whether to ignore HTTPS errors
1537    pub fn ignore_https_errors(mut self, ignore_https_errors: bool) -> Self {
1538        self.ignore_https_errors = Some(ignore_https_errors);
1539        self
1540    }
1541
1542    /// Sets the device scale factor
1543    pub fn device_scale_factor(mut self, device_scale_factor: f64) -> Self {
1544        self.device_scale_factor = Some(device_scale_factor);
1545        self
1546    }
1547
1548    /// Sets extra HTTP headers
1549    pub fn extra_http_headers(mut self, extra_http_headers: HashMap<String, String>) -> Self {
1550        self.extra_http_headers = Some(extra_http_headers);
1551        self
1552    }
1553
1554    /// Sets the base URL for relative navigation
1555    pub fn base_url(mut self, base_url: String) -> Self {
1556        self.base_url = Some(base_url);
1557        self
1558    }
1559
1560    /// Sets the storage state inline (cookies, localStorage).
1561    ///
1562    /// Populates the browser context with the provided storage state, including
1563    /// cookies and local storage. This is useful for initializing a context with
1564    /// a saved authentication state.
1565    ///
1566    /// Mutually exclusive with `storage_state_path()`.
1567    ///
1568    /// # Example
1569    ///
1570    /// ```rust
1571    /// use playwright_rs::protocol::{BrowserContextOptions, Cookie, StorageState, Origin, LocalStorageItem};
1572    ///
1573    /// let storage_state = StorageState {
1574    ///     cookies: vec![Cookie {
1575    ///         name: "session_id".to_string(),
1576    ///         value: "abc123".to_string(),
1577    ///         domain: ".example.com".to_string(),
1578    ///         path: "/".to_string(),
1579    ///         expires: -1.0,
1580    ///         http_only: true,
1581    ///         secure: true,
1582    ///         same_site: Some("Lax".to_string()),
1583    ///     }],
1584    ///     origins: vec![Origin {
1585    ///         origin: "https://example.com".to_string(),
1586    ///         local_storage: vec![LocalStorageItem {
1587    ///             name: "user_prefs".to_string(),
1588    ///             value: "{\"theme\":\"dark\"}".to_string(),
1589    ///         }],
1590    ///     }],
1591    /// };
1592    ///
1593    /// let options = BrowserContextOptions::builder()
1594    ///     .storage_state(storage_state)
1595    ///     .build();
1596    /// ```
1597    ///
1598    /// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
1599    pub fn storage_state(mut self, storage_state: StorageState) -> Self {
1600        self.storage_state = Some(storage_state);
1601        self.storage_state_path = None; // Clear path if setting inline
1602        self
1603    }
1604
1605    /// Sets the storage state from a file path.
1606    ///
1607    /// The file should contain a JSON representation of StorageState with cookies
1608    /// and origins. This is useful for loading authentication state saved from a
1609    /// previous session.
1610    ///
1611    /// Mutually exclusive with `storage_state()`.
1612    ///
1613    /// # Example
1614    ///
1615    /// ```rust
1616    /// use playwright_rs::protocol::BrowserContextOptions;
1617    ///
1618    /// let options = BrowserContextOptions::builder()
1619    ///     .storage_state_path("auth.json".to_string())
1620    ///     .build();
1621    /// ```
1622    ///
1623    /// The file should have this format:
1624    /// ```json
1625    /// {
1626    ///   "cookies": [{
1627    ///     "name": "session_id",
1628    ///     "value": "abc123",
1629    ///     "domain": ".example.com",
1630    ///     "path": "/",
1631    ///     "expires": -1,
1632    ///     "httpOnly": true,
1633    ///     "secure": true,
1634    ///     "sameSite": "Lax"
1635    ///   }],
1636    ///   "origins": [{
1637    ///     "origin": "https://example.com",
1638    ///     "localStorage": [{
1639    ///       "name": "user_prefs",
1640    ///       "value": "{\"theme\":\"dark\"}"
1641    ///     }]
1642    ///   }]
1643    /// }
1644    /// ```
1645    ///
1646    /// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
1647    pub fn storage_state_path(mut self, path: String) -> Self {
1648        self.storage_state_path = Some(path);
1649        self.storage_state = None; // Clear inline if setting path
1650        self
1651    }
1652
1653    /// Sets additional arguments to pass to browser instance (for launch_persistent_context)
1654    pub fn args(mut self, args: Vec<String>) -> Self {
1655        self.args = Some(args);
1656        self
1657    }
1658
1659    /// Sets browser distribution channel (for launch_persistent_context)
1660    pub fn channel(mut self, channel: String) -> Self {
1661        self.channel = Some(channel);
1662        self
1663    }
1664
1665    /// Enables or disables Chromium sandboxing (for launch_persistent_context)
1666    pub fn chromium_sandbox(mut self, enabled: bool) -> Self {
1667        self.chromium_sandbox = Some(enabled);
1668        self
1669    }
1670
1671    /// Auto-open DevTools (for launch_persistent_context)
1672    pub fn devtools(mut self, enabled: bool) -> Self {
1673        self.devtools = Some(enabled);
1674        self
1675    }
1676
1677    /// Sets directory to save downloads (for launch_persistent_context)
1678    pub fn downloads_path(mut self, path: String) -> Self {
1679        self.downloads_path = Some(path);
1680        self
1681    }
1682
1683    /// Sets path to custom browser executable (for launch_persistent_context)
1684    pub fn executable_path(mut self, path: String) -> Self {
1685        self.executable_path = Some(path);
1686        self
1687    }
1688
1689    /// Sets Firefox user preferences (for launch_persistent_context, Firefox only)
1690    pub fn firefox_user_prefs(mut self, prefs: HashMap<String, serde_json::Value>) -> Self {
1691        self.firefox_user_prefs = Some(prefs);
1692        self
1693    }
1694
1695    /// Run in headless mode (for launch_persistent_context)
1696    pub fn headless(mut self, enabled: bool) -> Self {
1697        self.headless = Some(enabled);
1698        self
1699    }
1700
1701    /// Filter or disable default browser arguments (for launch_persistent_context).
1702    ///
1703    /// When `IgnoreDefaultArgs::Bool(true)`, Playwright does not pass its own
1704    /// default arguments and only uses the ones from `args`.
1705    /// When `IgnoreDefaultArgs::Array(vec)`, filters out the given default arguments.
1706    ///
1707    /// See: <https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context>
1708    pub fn ignore_default_args(mut self, args: IgnoreDefaultArgs) -> Self {
1709        self.ignore_default_args = Some(args);
1710        self
1711    }
1712
1713    /// Slow down operations by N milliseconds (for launch_persistent_context)
1714    pub fn slow_mo(mut self, ms: f64) -> Self {
1715        self.slow_mo = Some(ms);
1716        self
1717    }
1718
1719    /// Set timeout for browser launch in milliseconds (for launch_persistent_context)
1720    pub fn timeout(mut self, ms: f64) -> Self {
1721        self.timeout = Some(ms);
1722        self
1723    }
1724
1725    /// Set directory to save traces (for launch_persistent_context)
1726    pub fn traces_dir(mut self, path: String) -> Self {
1727        self.traces_dir = Some(path);
1728        self
1729    }
1730
1731    /// Check if strict selectors mode is enabled
1732    pub fn strict_selectors(mut self, enabled: bool) -> Self {
1733        self.strict_selectors = Some(enabled);
1734        self
1735    }
1736
1737    /// Emulates 'prefers-reduced-motion' media feature
1738    pub fn reduced_motion(mut self, value: String) -> Self {
1739        self.reduced_motion = Some(value);
1740        self
1741    }
1742
1743    /// Emulates 'forced-colors' media feature
1744    pub fn forced_colors(mut self, value: String) -> Self {
1745        self.forced_colors = Some(value);
1746        self
1747    }
1748
1749    /// Whether to allow sites to register Service workers ("allow" | "block")
1750    pub fn service_workers(mut self, value: String) -> Self {
1751        self.service_workers = Some(value);
1752        self
1753    }
1754
1755    /// Sets options for recording HAR
1756    pub fn record_har(mut self, record_har: RecordHar) -> Self {
1757        self.record_har = Some(record_har);
1758        self
1759    }
1760
1761    /// Sets options for recording video
1762    pub fn record_video(mut self, record_video: RecordVideo) -> Self {
1763        self.record_video = Some(record_video);
1764        self
1765    }
1766
1767    /// Builds the BrowserContextOptions
1768    pub fn build(self) -> BrowserContextOptions {
1769        BrowserContextOptions {
1770            viewport: self.viewport,
1771            no_viewport: self.no_viewport,
1772            user_agent: self.user_agent,
1773            locale: self.locale,
1774            timezone_id: self.timezone_id,
1775            geolocation: self.geolocation,
1776            permissions: self.permissions,
1777            proxy: self.proxy,
1778            color_scheme: self.color_scheme,
1779            has_touch: self.has_touch,
1780            is_mobile: self.is_mobile,
1781            javascript_enabled: self.javascript_enabled,
1782            offline: self.offline,
1783            accept_downloads: self.accept_downloads,
1784            bypass_csp: self.bypass_csp,
1785            ignore_https_errors: self.ignore_https_errors,
1786            device_scale_factor: self.device_scale_factor,
1787            extra_http_headers: self.extra_http_headers,
1788            base_url: self.base_url,
1789            storage_state: self.storage_state,
1790            storage_state_path: self.storage_state_path,
1791            // Launch options
1792            args: self.args,
1793            channel: self.channel,
1794            chromium_sandbox: self.chromium_sandbox,
1795            devtools: self.devtools,
1796            downloads_path: self.downloads_path,
1797            executable_path: self.executable_path,
1798            firefox_user_prefs: self.firefox_user_prefs,
1799            headless: self.headless,
1800            ignore_default_args: self.ignore_default_args,
1801            slow_mo: self.slow_mo,
1802            timeout: self.timeout,
1803            traces_dir: self.traces_dir,
1804            strict_selectors: self.strict_selectors,
1805            reduced_motion: self.reduced_motion,
1806            forced_colors: self.forced_colors,
1807            service_workers: self.service_workers,
1808            record_har: self.record_har,
1809            record_video: self.record_video,
1810        }
1811    }
1812}
1813
1814/// Extracts timing data from a Response object's initializer, patching in
1815/// `responseEnd` from the event's `responseEndTiming` if available.
1816async fn extract_timing(
1817    connection: &std::sync::Arc<dyn crate::server::connection::ConnectionLike>,
1818    response_guid: Option<String>,
1819    response_end_timing: Option<f64>,
1820) -> Option<serde_json::Value> {
1821    let resp_guid = response_guid?;
1822    let resp_arc = connection.get_object(&resp_guid).await.ok()?;
1823    let resp_obj = resp_arc
1824        .as_any()
1825        .downcast_ref::<crate::protocol::ResponseObject>()?;
1826    let mut timing = resp_obj.initializer().get("timing")?.clone();
1827    if let (Some(end), Some(obj)) = (response_end_timing, timing.as_object_mut()) {
1828        if let Some(n) = serde_json::Number::from_f64(end) {
1829            obj.insert("responseEnd".to_string(), serde_json::Value::Number(n));
1830        }
1831    }
1832    Some(timing)
1833}
1834
1835#[cfg(test)]
1836mod tests {
1837    use super::*;
1838    use crate::api::launch_options::IgnoreDefaultArgs;
1839
1840    #[test]
1841    fn test_browser_context_options_ignore_default_args_bool_serialization() {
1842        let options = BrowserContextOptions::builder()
1843            .ignore_default_args(IgnoreDefaultArgs::Bool(true))
1844            .build();
1845
1846        let value = serde_json::to_value(&options).unwrap();
1847        assert_eq!(value["ignoreDefaultArgs"], serde_json::json!(true));
1848    }
1849
1850    #[test]
1851    fn test_browser_context_options_ignore_default_args_array_serialization() {
1852        let options = BrowserContextOptions::builder()
1853            .ignore_default_args(IgnoreDefaultArgs::Array(vec!["--foo".to_string()]))
1854            .build();
1855
1856        let value = serde_json::to_value(&options).unwrap();
1857        assert_eq!(value["ignoreDefaultArgs"], serde_json::json!(["--foo"]));
1858    }
1859
1860    #[test]
1861    fn test_browser_context_options_ignore_default_args_absent() {
1862        let options = BrowserContextOptions::builder().build();
1863
1864        let value = serde_json::to_value(&options).unwrap();
1865        assert!(value.get("ignoreDefaultArgs").is_none());
1866    }
1867}