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::error::Result;
8use crate::protocol::{Browser, Page};
9use crate::server::channel::Channel;
10use crate::server::channel_owner::{ChannelOwner, ChannelOwnerImpl, ParentOrConnection};
11use serde::{Deserialize, Serialize};
12use serde_json::Value;
13use std::any::Any;
14use std::collections::HashMap;
15use std::sync::{Arc, Mutex};
16
17/// BrowserContext represents an isolated browser session.
18///
19/// Contexts are isolated environments within a browser instance. Each context
20/// has its own cookies, cache, and local storage, enabling independent sessions
21/// without interference.
22///
23/// # Example
24///
25/// ```ignore
26/// use playwright_rs::protocol::Playwright;
27///
28/// #[tokio::main]
29/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
30///     let playwright = Playwright::launch().await?;
31///     let browser = playwright.chromium().launch().await?;
32///
33///     // Create isolated contexts
34///     let context1 = browser.new_context().await?;
35///     let context2 = browser.new_context().await?;
36///
37///     // Create pages in each context
38///     let page1 = context1.new_page().await?;
39///     let page2 = context2.new_page().await?;
40///
41///     // Access all pages in a context
42///     let pages = context1.pages();
43///     assert_eq!(pages.len(), 1);
44///
45///     // Access the browser from a context
46///     let ctx_browser = context1.browser().unwrap();
47///     assert_eq!(ctx_browser.name(), browser.name());
48///
49///     // App mode: access initial page created automatically
50///     let chromium = playwright.chromium();
51///     let app_context = chromium
52///         .launch_persistent_context_with_options(
53///             "/tmp/app-data",
54///             playwright_rs::protocol::BrowserContextOptions::builder()
55///                 .args(vec!["--app=https://example.com".to_string()])
56///                 .headless(true)
57///                 .build()
58///         )
59///         .await?;
60///
61///     // Get the initial page (don't create a new one!)
62///     let app_pages = app_context.pages();
63///     if !app_pages.is_empty() {
64///         let initial_page = &app_pages[0];
65///         // Use the initial page...
66///     }
67///
68///     // Cleanup
69///     context1.close().await?;
70///     context2.close().await?;
71///     app_context.close().await?;
72///     browser.close().await?;
73///     Ok(())
74/// }
75/// ```
76///
77/// See: <https://playwright.dev/docs/api/class-browsercontext>
78#[derive(Clone)]
79pub struct BrowserContext {
80    base: ChannelOwnerImpl,
81    /// Browser instance that owns this context (None for persistent contexts)
82    browser: Option<Browser>,
83    /// All open pages in this context
84    pages: Arc<Mutex<Vec<Page>>>,
85}
86
87impl BrowserContext {
88    /// Creates a new BrowserContext from protocol initialization
89    ///
90    /// This is called by the object factory when the server sends a `__create__` message
91    /// for a BrowserContext object.
92    ///
93    /// # Arguments
94    ///
95    /// * `parent` - The parent Browser object
96    /// * `type_name` - The protocol type name ("BrowserContext")
97    /// * `guid` - The unique identifier for this context
98    /// * `initializer` - The initialization data from the server
99    ///
100    /// # Errors
101    ///
102    /// Returns error if initializer is malformed
103    pub fn new(
104        parent: Arc<dyn ChannelOwner>,
105        type_name: String,
106        guid: Arc<str>,
107        initializer: Value,
108    ) -> Result<Self> {
109        let base = ChannelOwnerImpl::new(
110            ParentOrConnection::Parent(parent.clone()),
111            type_name,
112            guid,
113            initializer,
114        );
115
116        // Store browser reference if parent is a Browser
117        // Returns None only for special contexts (Android, Electron) where parent is not a Browser
118        // For both regular contexts and persistent contexts, parent is a Browser instance
119        let browser = parent.as_any().downcast_ref::<Browser>().cloned();
120
121        let context = Self {
122            base,
123            browser,
124            pages: Arc::new(Mutex::new(Vec::new())),
125        };
126
127        // Enable dialog event subscription
128        // Dialog events need to be explicitly subscribed to via updateSubscription command
129        let channel = context.channel().clone();
130        tokio::spawn(async move {
131            let _ = channel
132                .send_no_result(
133                    "updateSubscription",
134                    serde_json::json!({
135                        "event": "dialog",
136                        "enabled": true
137                    }),
138                )
139                .await;
140        });
141
142        Ok(context)
143    }
144
145    /// Returns the channel for sending protocol messages
146    ///
147    /// Used internally for sending RPC calls to the context.
148    fn channel(&self) -> &Channel {
149        self.base.channel()
150    }
151
152    /// Adds a script which would be evaluated in one of the following scenarios:
153    ///
154    /// - Whenever a page is created in the browser context or is navigated.
155    /// - Whenever a child frame is attached or navigated in any page in the browser context.
156    ///
157    /// The script is evaluated after the document was created but before any of its scripts
158    /// were run. This is useful to amend the JavaScript environment, e.g. to seed Math.random.
159    ///
160    /// # Arguments
161    ///
162    /// * `script` - Script to be evaluated in all pages in the browser context.
163    ///
164    /// # Errors
165    ///
166    /// Returns error if:
167    /// - Context has been closed
168    /// - Communication with browser process fails
169    ///
170    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-add-init-script>
171    pub async fn add_init_script(&self, script: &str) -> Result<()> {
172        self.channel()
173            .send_no_result("addInitScript", serde_json::json!({ "source": script }))
174            .await
175    }
176
177    /// Creates a new page in this browser context.
178    ///
179    /// Pages are isolated tabs/windows within a context. Each page starts
180    /// at "about:blank" and can be navigated independently.
181    ///
182    /// # Errors
183    ///
184    /// Returns error if:
185    /// - Context has been closed
186    /// - Communication with browser process fails
187    ///
188    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-new-page>
189    pub async fn new_page(&self) -> Result<Page> {
190        // Response contains the GUID of the created Page
191        #[derive(Deserialize)]
192        struct NewPageResponse {
193            page: GuidRef,
194        }
195
196        #[derive(Deserialize)]
197        struct GuidRef {
198            #[serde(deserialize_with = "crate::server::connection::deserialize_arc_str")]
199            guid: Arc<str>,
200        }
201
202        // Send newPage RPC to server
203        let response: NewPageResponse = self
204            .channel()
205            .send("newPage", serde_json::json!({}))
206            .await?;
207
208        // Retrieve the Page object from the connection registry
209        let page_arc = self.connection().get_object(&response.page.guid).await?;
210
211        // Downcast to Page
212        let page = page_arc.as_any().downcast_ref::<Page>().ok_or_else(|| {
213            crate::error::Error::ProtocolError(format!(
214                "Expected Page object, got {}",
215                page_arc.type_name()
216            ))
217        })?;
218
219        // Note: Don't track the page here - it will be tracked via the "page" event
220        // that Playwright server sends automatically when a page is created.
221        // Tracking it here would create duplicates.
222
223        Ok(page.clone())
224    }
225
226    /// Returns all open pages in the context.
227    ///
228    /// This method provides a snapshot of all currently active pages that belong
229    /// to this browser context instance. Pages created via `new_page()` and popup
230    /// pages opened through user interactions are included.
231    ///
232    /// In persistent contexts launched with `--app=url`, this will include the
233    /// initial page created automatically by Playwright.
234    ///
235    /// # Errors
236    ///
237    /// This method does not return errors. It provides a snapshot of pages at
238    /// the time of invocation.
239    ///
240    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-pages>
241    pub fn pages(&self) -> Vec<Page> {
242        self.pages.lock().unwrap().clone()
243    }
244
245    /// Returns the browser instance that owns this context.
246    ///
247    /// Returns `None` only for contexts created outside of normal browser
248    /// (e.g., Android or Electron contexts). For both regular contexts and
249    /// persistent contexts, this returns the owning Browser instance.
250    ///
251    /// # Errors
252    ///
253    /// This method does not return errors.
254    ///
255    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-browser>
256    pub fn browser(&self) -> Option<Browser> {
257        self.browser.clone()
258    }
259
260    /// Closes the browser context and all its pages.
261    ///
262    /// This is a graceful operation that sends a close command to the context
263    /// and waits for it to shut down properly.
264    ///
265    /// # Errors
266    ///
267    /// Returns error if:
268    /// - Context has already been closed
269    /// - Communication with browser process fails
270    ///
271    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-close>
272    pub async fn close(&self) -> Result<()> {
273        // Send close RPC to server
274        self.channel()
275            .send_no_result("close", serde_json::json!({}))
276            .await
277    }
278
279    /// Pauses the browser context.
280    ///
281    /// This pauses the execution of all pages in the context.
282    pub async fn pause(&self) -> Result<()> {
283        self.channel()
284            .send_no_result("pause", serde_json::Value::Null)
285            .await
286    }
287}
288
289impl ChannelOwner for BrowserContext {
290    fn guid(&self) -> &str {
291        self.base.guid()
292    }
293
294    fn type_name(&self) -> &str {
295        self.base.type_name()
296    }
297
298    fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
299        self.base.parent()
300    }
301
302    fn connection(&self) -> Arc<dyn crate::server::connection::ConnectionLike> {
303        self.base.connection()
304    }
305
306    fn initializer(&self) -> &Value {
307        self.base.initializer()
308    }
309
310    fn channel(&self) -> &Channel {
311        self.base.channel()
312    }
313
314    fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
315        self.base.dispose(reason)
316    }
317
318    fn adopt(&self, child: Arc<dyn ChannelOwner>) {
319        self.base.adopt(child)
320    }
321
322    fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
323        self.base.add_child(guid, child)
324    }
325
326    fn remove_child(&self, guid: &str) {
327        self.base.remove_child(guid)
328    }
329
330    fn on_event(&self, method: &str, params: Value) {
331        match method {
332            "page" => {
333                // Page events are triggered when pages are created, including:
334                // - Initial page in persistent context with --app mode
335                // - Popup pages opened through user interactions
336                // Event format: {page: {guid: "..."}}
337                if let Some(page_guid) = params
338                    .get("page")
339                    .and_then(|v| v.get("guid"))
340                    .and_then(|v| v.as_str())
341                {
342                    let connection = self.connection();
343                    let page_guid_owned = page_guid.to_string();
344                    let pages = self.pages.clone();
345
346                    tokio::spawn(async move {
347                        // Get the Page object
348                        let page_arc = match connection.get_object(&page_guid_owned).await {
349                            Ok(obj) => obj,
350                            Err(_) => return,
351                        };
352
353                        // Downcast to Page
354                        let page = match page_arc.as_any().downcast_ref::<Page>() {
355                            Some(p) => p.clone(),
356                            None => return,
357                        };
358
359                        // Track the page
360                        pages.lock().unwrap().push(page);
361                    });
362                }
363            }
364            "dialog" => {
365                // Dialog events come to BrowserContext, need to forward to the associated Page
366                // Event format: {dialog: {guid: "..."}}
367                // The Dialog protocol object has the Page as its parent
368                if let Some(dialog_guid) = params
369                    .get("dialog")
370                    .and_then(|v| v.get("guid"))
371                    .and_then(|v| v.as_str())
372                {
373                    let connection = self.connection();
374                    let dialog_guid_owned = dialog_guid.to_string();
375
376                    tokio::spawn(async move {
377                        // Get the Dialog object
378                        let dialog_arc = match connection.get_object(&dialog_guid_owned).await {
379                            Ok(obj) => obj,
380                            Err(_) => return,
381                        };
382
383                        // Downcast to Dialog
384                        let dialog = match dialog_arc
385                            .as_any()
386                            .downcast_ref::<crate::protocol::Dialog>()
387                        {
388                            Some(d) => d.clone(),
389                            None => return,
390                        };
391
392                        // Get the Page from the Dialog's parent
393                        let page_arc = match dialog_arc.parent() {
394                            Some(parent) => parent,
395                            None => return,
396                        };
397
398                        // Downcast to Page
399                        let page = match page_arc.as_any().downcast_ref::<Page>() {
400                            Some(p) => p.clone(),
401                            None => return,
402                        };
403
404                        // Forward to Page's dialog handlers
405                        page.trigger_dialog_event(dialog).await;
406                    });
407                }
408            }
409            _ => {
410                // Other events will be handled in future phases
411            }
412        }
413    }
414
415    fn was_collected(&self) -> bool {
416        self.base.was_collected()
417    }
418
419    fn as_any(&self) -> &dyn Any {
420        self
421    }
422}
423
424impl std::fmt::Debug for BrowserContext {
425    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
426        f.debug_struct("BrowserContext")
427            .field("guid", &self.guid())
428            .finish()
429    }
430}
431
432/// Viewport dimensions for browser context.
433///
434/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
435#[derive(Debug, Clone, Serialize, Deserialize)]
436pub struct Viewport {
437    /// Page width in pixels
438    pub width: u32,
439    /// Page height in pixels
440    pub height: u32,
441}
442
443/// Geolocation coordinates.
444///
445/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
446#[derive(Debug, Clone, Serialize, Deserialize)]
447pub struct Geolocation {
448    /// Latitude between -90 and 90
449    pub latitude: f64,
450    /// Longitude between -180 and 180
451    pub longitude: f64,
452    /// Optional accuracy in meters (default: 0)
453    #[serde(skip_serializing_if = "Option::is_none")]
454    pub accuracy: Option<f64>,
455}
456
457/// Cookie information for storage state.
458///
459/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
460#[derive(Debug, Clone, Serialize, Deserialize)]
461#[serde(rename_all = "camelCase")]
462pub struct Cookie {
463    /// Cookie name
464    pub name: String,
465    /// Cookie value
466    pub value: String,
467    /// Cookie domain (use dot prefix for subdomain matching, e.g., ".example.com")
468    pub domain: String,
469    /// Cookie path
470    pub path: String,
471    /// Unix timestamp in seconds; -1 for session cookies
472    pub expires: f64,
473    /// HTTP-only flag
474    pub http_only: bool,
475    /// Secure flag
476    pub secure: bool,
477    /// SameSite attribute ("Strict", "Lax", "None")
478    #[serde(skip_serializing_if = "Option::is_none")]
479    pub same_site: Option<String>,
480}
481
482/// Local storage item for storage state.
483///
484/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
485#[derive(Debug, Clone, Serialize, Deserialize)]
486pub struct LocalStorageItem {
487    /// Storage key
488    pub name: String,
489    /// Storage value
490    pub value: String,
491}
492
493/// Origin with local storage items for storage state.
494///
495/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
496#[derive(Debug, Clone, Serialize, Deserialize)]
497#[serde(rename_all = "camelCase")]
498pub struct Origin {
499    /// Origin URL (e.g., "https://example.com")
500    pub origin: String,
501    /// Local storage items for this origin
502    pub local_storage: Vec<LocalStorageItem>,
503}
504
505/// Storage state containing cookies and local storage.
506///
507/// Used to populate a browser context with saved authentication state,
508/// enabling session persistence across context instances.
509///
510/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
511#[derive(Debug, Clone, Serialize, Deserialize)]
512pub struct StorageState {
513    /// List of cookies
514    pub cookies: Vec<Cookie>,
515    /// List of origins with local storage
516    pub origins: Vec<Origin>,
517}
518
519/// Options for creating a new browser context.
520///
521/// Allows customizing viewport, user agent, locale, timezone, geolocation,
522/// permissions, and other browser context settings.
523///
524/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
525#[derive(Debug, Clone, Default, Serialize)]
526#[serde(rename_all = "camelCase")]
527pub struct BrowserContextOptions {
528    /// Sets consistent viewport for all pages in the context.
529    /// Set to null via `no_viewport(true)` to disable viewport emulation.
530    #[serde(skip_serializing_if = "Option::is_none")]
531    pub viewport: Option<Viewport>,
532
533    /// Disables viewport emulation when set to true.
534    #[serde(skip_serializing_if = "Option::is_none")]
535    pub no_viewport: Option<bool>,
536
537    /// Custom user agent string
538    #[serde(skip_serializing_if = "Option::is_none")]
539    pub user_agent: Option<String>,
540
541    /// Locale for the context (e.g., "en-GB", "de-DE", "fr-FR")
542    #[serde(skip_serializing_if = "Option::is_none")]
543    pub locale: Option<String>,
544
545    /// Timezone identifier (e.g., "America/New_York", "Europe/Berlin")
546    #[serde(skip_serializing_if = "Option::is_none")]
547    pub timezone_id: Option<String>,
548
549    /// Geolocation coordinates
550    #[serde(skip_serializing_if = "Option::is_none")]
551    pub geolocation: Option<Geolocation>,
552
553    /// List of permissions to grant (e.g., "geolocation", "notifications")
554    #[serde(skip_serializing_if = "Option::is_none")]
555    pub permissions: Option<Vec<String>>,
556
557    /// Emulates 'prefers-colors-scheme' media feature ("light", "dark", "no-preference")
558    #[serde(skip_serializing_if = "Option::is_none")]
559    pub color_scheme: Option<String>,
560
561    /// Whether the viewport supports touch events
562    #[serde(skip_serializing_if = "Option::is_none")]
563    pub has_touch: Option<bool>,
564
565    /// Whether the meta viewport tag is respected
566    #[serde(skip_serializing_if = "Option::is_none")]
567    pub is_mobile: Option<bool>,
568
569    /// Whether JavaScript is enabled in the context
570    #[serde(skip_serializing_if = "Option::is_none")]
571    pub javascript_enabled: Option<bool>,
572
573    /// Emulates network being offline
574    #[serde(skip_serializing_if = "Option::is_none")]
575    pub offline: Option<bool>,
576
577    /// Whether to automatically download attachments
578    #[serde(skip_serializing_if = "Option::is_none")]
579    pub accept_downloads: Option<bool>,
580
581    /// Whether to bypass Content-Security-Policy
582    #[serde(skip_serializing_if = "Option::is_none")]
583    pub bypass_csp: Option<bool>,
584
585    /// Whether to ignore HTTPS errors
586    #[serde(skip_serializing_if = "Option::is_none")]
587    pub ignore_https_errors: Option<bool>,
588
589    /// Device scale factor (default: 1)
590    #[serde(skip_serializing_if = "Option::is_none")]
591    pub device_scale_factor: Option<f64>,
592
593    /// Extra HTTP headers to send with every request
594    #[serde(skip_serializing_if = "Option::is_none")]
595    pub extra_http_headers: Option<HashMap<String, String>>,
596
597    /// Base URL for relative navigation
598    #[serde(skip_serializing_if = "Option::is_none")]
599    pub base_url: Option<String>,
600
601    /// Storage state to populate the context (cookies, localStorage, sessionStorage).
602    /// Can be an inline StorageState object or a file path string.
603    /// Use builder methods `storage_state()` for inline or `storage_state_path()` for file path.
604    #[serde(skip_serializing_if = "Option::is_none")]
605    pub storage_state: Option<StorageState>,
606
607    /// Storage state file path (alternative to inline storage_state).
608    /// This is handled by the builder and converted to storage_state during serialization.
609    #[serde(skip_serializing_if = "Option::is_none")]
610    pub storage_state_path: Option<String>,
611
612    // Launch options (for launch_persistent_context)
613    /// Additional arguments to pass to browser instance
614    #[serde(skip_serializing_if = "Option::is_none")]
615    pub args: Option<Vec<String>>,
616
617    /// Browser distribution channel (e.g., "chrome", "msedge")
618    #[serde(skip_serializing_if = "Option::is_none")]
619    pub channel: Option<String>,
620
621    /// Enable Chromium sandboxing (default: false on Linux)
622    #[serde(skip_serializing_if = "Option::is_none")]
623    pub chromium_sandbox: Option<bool>,
624
625    /// Auto-open DevTools (deprecated, default: false)
626    #[serde(skip_serializing_if = "Option::is_none")]
627    pub devtools: Option<bool>,
628
629    /// Directory to save downloads
630    #[serde(skip_serializing_if = "Option::is_none")]
631    pub downloads_path: Option<String>,
632
633    /// Path to custom browser executable
634    #[serde(skip_serializing_if = "Option::is_none")]
635    pub executable_path: Option<String>,
636
637    /// Firefox user preferences (Firefox only)
638    #[serde(skip_serializing_if = "Option::is_none")]
639    pub firefox_user_prefs: Option<HashMap<String, serde_json::Value>>,
640
641    /// Run in headless mode (default: true unless devtools=true)
642    #[serde(skip_serializing_if = "Option::is_none")]
643    pub headless: Option<bool>,
644
645    /// Slow down operations by N milliseconds
646    #[serde(skip_serializing_if = "Option::is_none")]
647    pub slow_mo: Option<f64>,
648
649    /// Timeout for browser launch in milliseconds
650    #[serde(skip_serializing_if = "Option::is_none")]
651    pub timeout: Option<f64>,
652
653    /// Directory to save traces
654    #[serde(skip_serializing_if = "Option::is_none")]
655    pub traces_dir: Option<String>,
656}
657
658impl BrowserContextOptions {
659    /// Creates a new builder for BrowserContextOptions
660    pub fn builder() -> BrowserContextOptionsBuilder {
661        BrowserContextOptionsBuilder::default()
662    }
663}
664
665/// Builder for BrowserContextOptions
666#[derive(Debug, Clone, Default)]
667pub struct BrowserContextOptionsBuilder {
668    viewport: Option<Viewport>,
669    no_viewport: Option<bool>,
670    user_agent: Option<String>,
671    locale: Option<String>,
672    timezone_id: Option<String>,
673    geolocation: Option<Geolocation>,
674    permissions: Option<Vec<String>>,
675    color_scheme: Option<String>,
676    has_touch: Option<bool>,
677    is_mobile: Option<bool>,
678    javascript_enabled: Option<bool>,
679    offline: Option<bool>,
680    accept_downloads: Option<bool>,
681    bypass_csp: Option<bool>,
682    ignore_https_errors: Option<bool>,
683    device_scale_factor: Option<f64>,
684    extra_http_headers: Option<HashMap<String, String>>,
685    base_url: Option<String>,
686    storage_state: Option<StorageState>,
687    storage_state_path: Option<String>,
688    // Launch options
689    args: Option<Vec<String>>,
690    channel: Option<String>,
691    chromium_sandbox: Option<bool>,
692    devtools: Option<bool>,
693    downloads_path: Option<String>,
694    executable_path: Option<String>,
695    firefox_user_prefs: Option<HashMap<String, serde_json::Value>>,
696    headless: Option<bool>,
697    slow_mo: Option<f64>,
698    timeout: Option<f64>,
699    traces_dir: Option<String>,
700}
701
702impl BrowserContextOptionsBuilder {
703    /// Sets the viewport dimensions
704    pub fn viewport(mut self, viewport: Viewport) -> Self {
705        self.viewport = Some(viewport);
706        self.no_viewport = None; // Clear no_viewport if setting viewport
707        self
708    }
709
710    /// Disables viewport emulation
711    pub fn no_viewport(mut self, no_viewport: bool) -> Self {
712        self.no_viewport = Some(no_viewport);
713        if no_viewport {
714            self.viewport = None; // Clear viewport if setting no_viewport
715        }
716        self
717    }
718
719    /// Sets the user agent string
720    pub fn user_agent(mut self, user_agent: String) -> Self {
721        self.user_agent = Some(user_agent);
722        self
723    }
724
725    /// Sets the locale
726    pub fn locale(mut self, locale: String) -> Self {
727        self.locale = Some(locale);
728        self
729    }
730
731    /// Sets the timezone identifier
732    pub fn timezone_id(mut self, timezone_id: String) -> Self {
733        self.timezone_id = Some(timezone_id);
734        self
735    }
736
737    /// Sets the geolocation
738    pub fn geolocation(mut self, geolocation: Geolocation) -> Self {
739        self.geolocation = Some(geolocation);
740        self
741    }
742
743    /// Sets the permissions to grant
744    pub fn permissions(mut self, permissions: Vec<String>) -> Self {
745        self.permissions = Some(permissions);
746        self
747    }
748
749    /// Sets the color scheme preference
750    pub fn color_scheme(mut self, color_scheme: String) -> Self {
751        self.color_scheme = Some(color_scheme);
752        self
753    }
754
755    /// Sets whether the viewport supports touch events
756    pub fn has_touch(mut self, has_touch: bool) -> Self {
757        self.has_touch = Some(has_touch);
758        self
759    }
760
761    /// Sets whether this is a mobile viewport
762    pub fn is_mobile(mut self, is_mobile: bool) -> Self {
763        self.is_mobile = Some(is_mobile);
764        self
765    }
766
767    /// Sets whether JavaScript is enabled
768    pub fn javascript_enabled(mut self, javascript_enabled: bool) -> Self {
769        self.javascript_enabled = Some(javascript_enabled);
770        self
771    }
772
773    /// Sets whether to emulate offline network
774    pub fn offline(mut self, offline: bool) -> Self {
775        self.offline = Some(offline);
776        self
777    }
778
779    /// Sets whether to automatically download attachments
780    pub fn accept_downloads(mut self, accept_downloads: bool) -> Self {
781        self.accept_downloads = Some(accept_downloads);
782        self
783    }
784
785    /// Sets whether to bypass Content-Security-Policy
786    pub fn bypass_csp(mut self, bypass_csp: bool) -> Self {
787        self.bypass_csp = Some(bypass_csp);
788        self
789    }
790
791    /// Sets whether to ignore HTTPS errors
792    pub fn ignore_https_errors(mut self, ignore_https_errors: bool) -> Self {
793        self.ignore_https_errors = Some(ignore_https_errors);
794        self
795    }
796
797    /// Sets the device scale factor
798    pub fn device_scale_factor(mut self, device_scale_factor: f64) -> Self {
799        self.device_scale_factor = Some(device_scale_factor);
800        self
801    }
802
803    /// Sets extra HTTP headers
804    pub fn extra_http_headers(mut self, extra_http_headers: HashMap<String, String>) -> Self {
805        self.extra_http_headers = Some(extra_http_headers);
806        self
807    }
808
809    /// Sets the base URL for relative navigation
810    pub fn base_url(mut self, base_url: String) -> Self {
811        self.base_url = Some(base_url);
812        self
813    }
814
815    /// Sets the storage state inline (cookies, localStorage).
816    ///
817    /// Populates the browser context with the provided storage state, including
818    /// cookies and local storage. This is useful for initializing a context with
819    /// a saved authentication state.
820    ///
821    /// Mutually exclusive with `storage_state_path()`.
822    ///
823    /// # Example
824    ///
825    /// ```rust
826    /// use playwright_rs::protocol::{BrowserContextOptions, Cookie, StorageState, Origin, LocalStorageItem};
827    ///
828    /// let storage_state = StorageState {
829    ///     cookies: vec![Cookie {
830    ///         name: "session_id".to_string(),
831    ///         value: "abc123".to_string(),
832    ///         domain: ".example.com".to_string(),
833    ///         path: "/".to_string(),
834    ///         expires: -1.0,
835    ///         http_only: true,
836    ///         secure: true,
837    ///         same_site: Some("Lax".to_string()),
838    ///     }],
839    ///     origins: vec![Origin {
840    ///         origin: "https://example.com".to_string(),
841    ///         local_storage: vec![LocalStorageItem {
842    ///             name: "user_prefs".to_string(),
843    ///             value: "{\"theme\":\"dark\"}".to_string(),
844    ///         }],
845    ///     }],
846    /// };
847    ///
848    /// let options = BrowserContextOptions::builder()
849    ///     .storage_state(storage_state)
850    ///     .build();
851    /// ```
852    ///
853    /// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
854    pub fn storage_state(mut self, storage_state: StorageState) -> Self {
855        self.storage_state = Some(storage_state);
856        self.storage_state_path = None; // Clear path if setting inline
857        self
858    }
859
860    /// Sets the storage state from a file path.
861    ///
862    /// The file should contain a JSON representation of StorageState with cookies
863    /// and origins. This is useful for loading authentication state saved from a
864    /// previous session.
865    ///
866    /// Mutually exclusive with `storage_state()`.
867    ///
868    /// # Example
869    ///
870    /// ```rust
871    /// use playwright_rs::protocol::BrowserContextOptions;
872    ///
873    /// let options = BrowserContextOptions::builder()
874    ///     .storage_state_path("auth.json".to_string())
875    ///     .build();
876    /// ```
877    ///
878    /// The file should have this format:
879    /// ```json
880    /// {
881    ///   "cookies": [{
882    ///     "name": "session_id",
883    ///     "value": "abc123",
884    ///     "domain": ".example.com",
885    ///     "path": "/",
886    ///     "expires": -1,
887    ///     "httpOnly": true,
888    ///     "secure": true,
889    ///     "sameSite": "Lax"
890    ///   }],
891    ///   "origins": [{
892    ///     "origin": "https://example.com",
893    ///     "localStorage": [{
894    ///       "name": "user_prefs",
895    ///       "value": "{\"theme\":\"dark\"}"
896    ///     }]
897    ///   }]
898    /// }
899    /// ```
900    ///
901    /// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
902    pub fn storage_state_path(mut self, path: String) -> Self {
903        self.storage_state_path = Some(path);
904        self.storage_state = None; // Clear inline if setting path
905        self
906    }
907
908    /// Sets additional arguments to pass to browser instance (for launch_persistent_context)
909    pub fn args(mut self, args: Vec<String>) -> Self {
910        self.args = Some(args);
911        self
912    }
913
914    /// Sets browser distribution channel (for launch_persistent_context)
915    pub fn channel(mut self, channel: String) -> Self {
916        self.channel = Some(channel);
917        self
918    }
919
920    /// Enables or disables Chromium sandboxing (for launch_persistent_context)
921    pub fn chromium_sandbox(mut self, enabled: bool) -> Self {
922        self.chromium_sandbox = Some(enabled);
923        self
924    }
925
926    /// Auto-open DevTools (for launch_persistent_context)
927    pub fn devtools(mut self, enabled: bool) -> Self {
928        self.devtools = Some(enabled);
929        self
930    }
931
932    /// Sets directory to save downloads (for launch_persistent_context)
933    pub fn downloads_path(mut self, path: String) -> Self {
934        self.downloads_path = Some(path);
935        self
936    }
937
938    /// Sets path to custom browser executable (for launch_persistent_context)
939    pub fn executable_path(mut self, path: String) -> Self {
940        self.executable_path = Some(path);
941        self
942    }
943
944    /// Sets Firefox user preferences (for launch_persistent_context, Firefox only)
945    pub fn firefox_user_prefs(mut self, prefs: HashMap<String, serde_json::Value>) -> Self {
946        self.firefox_user_prefs = Some(prefs);
947        self
948    }
949
950    /// Run in headless mode (for launch_persistent_context)
951    pub fn headless(mut self, enabled: bool) -> Self {
952        self.headless = Some(enabled);
953        self
954    }
955
956    /// Slow down operations by N milliseconds (for launch_persistent_context)
957    pub fn slow_mo(mut self, ms: f64) -> Self {
958        self.slow_mo = Some(ms);
959        self
960    }
961
962    /// Set timeout for browser launch in milliseconds (for launch_persistent_context)
963    pub fn timeout(mut self, ms: f64) -> Self {
964        self.timeout = Some(ms);
965        self
966    }
967
968    /// Set directory to save traces (for launch_persistent_context)
969    pub fn traces_dir(mut self, path: String) -> Self {
970        self.traces_dir = Some(path);
971        self
972    }
973
974    /// Builds the BrowserContextOptions
975    pub fn build(self) -> BrowserContextOptions {
976        BrowserContextOptions {
977            viewport: self.viewport,
978            no_viewport: self.no_viewport,
979            user_agent: self.user_agent,
980            locale: self.locale,
981            timezone_id: self.timezone_id,
982            geolocation: self.geolocation,
983            permissions: self.permissions,
984            color_scheme: self.color_scheme,
985            has_touch: self.has_touch,
986            is_mobile: self.is_mobile,
987            javascript_enabled: self.javascript_enabled,
988            offline: self.offline,
989            accept_downloads: self.accept_downloads,
990            bypass_csp: self.bypass_csp,
991            ignore_https_errors: self.ignore_https_errors,
992            device_scale_factor: self.device_scale_factor,
993            extra_http_headers: self.extra_http_headers,
994            base_url: self.base_url,
995            storage_state: self.storage_state,
996            storage_state_path: self.storage_state_path,
997            // Launch options
998            args: self.args,
999            channel: self.channel,
1000            chromium_sandbox: self.chromium_sandbox,
1001            devtools: self.devtools,
1002            downloads_path: self.downloads_path,
1003            executable_path: self.executable_path,
1004            firefox_user_prefs: self.firefox_user_prefs,
1005            headless: self.headless,
1006            slow_mo: self.slow_mo,
1007            timeout: self.timeout,
1008            traces_dir: self.traces_dir,
1009        }
1010    }
1011}