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    /// Returns storage state for this browser context.
289    ///
290    /// Contains current cookies and local storage snapshots.
291    ///
292    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-storage-state>
293    pub async fn storage_state(&self) -> Result<StorageState> {
294        let response: StorageState = self
295            .channel()
296            .send("storageState", serde_json::json!({}))
297            .await?;
298        Ok(response)
299    }
300
301    /// Adds cookies into this browser context.
302    ///
303    /// All pages within this context will have these cookies installed. Cookies can be granularly specified
304    /// with `name`, `value`, `url`, `domain`, `path`, `expires`, `httpOnly`, `secure`, `sameSite`.
305    ///
306    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-add-cookies>
307    pub async fn add_cookies(&self, cookies: &[Cookie]) -> Result<()> {
308        self.channel()
309            .send_no_result(
310                "addCookies",
311                serde_json::json!({
312                    "cookies": cookies
313                }),
314            )
315            .await
316    }
317}
318
319impl ChannelOwner for BrowserContext {
320    fn guid(&self) -> &str {
321        self.base.guid()
322    }
323
324    fn type_name(&self) -> &str {
325        self.base.type_name()
326    }
327
328    fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
329        self.base.parent()
330    }
331
332    fn connection(&self) -> Arc<dyn crate::server::connection::ConnectionLike> {
333        self.base.connection()
334    }
335
336    fn initializer(&self) -> &Value {
337        self.base.initializer()
338    }
339
340    fn channel(&self) -> &Channel {
341        self.base.channel()
342    }
343
344    fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
345        self.base.dispose(reason)
346    }
347
348    fn adopt(&self, child: Arc<dyn ChannelOwner>) {
349        self.base.adopt(child)
350    }
351
352    fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
353        self.base.add_child(guid, child)
354    }
355
356    fn remove_child(&self, guid: &str) {
357        self.base.remove_child(guid)
358    }
359
360    fn on_event(&self, method: &str, params: Value) {
361        match method {
362            "page" => {
363                // Page events are triggered when pages are created, including:
364                // - Initial page in persistent context with --app mode
365                // - Popup pages opened through user interactions
366                // Event format: {page: {guid: "..."}}
367                if let Some(page_guid) = params
368                    .get("page")
369                    .and_then(|v| v.get("guid"))
370                    .and_then(|v| v.as_str())
371                {
372                    let connection = self.connection();
373                    let page_guid_owned = page_guid.to_string();
374                    let pages = self.pages.clone();
375
376                    tokio::spawn(async move {
377                        // Get the Page object
378                        let page_arc = match connection.get_object(&page_guid_owned).await {
379                            Ok(obj) => obj,
380                            Err(_) => return,
381                        };
382
383                        // Downcast to Page
384                        let page = match page_arc.as_any().downcast_ref::<Page>() {
385                            Some(p) => p.clone(),
386                            None => return,
387                        };
388
389                        // Track the page
390                        pages.lock().unwrap().push(page);
391                    });
392                }
393            }
394            "dialog" => {
395                // Dialog events come to BrowserContext, need to forward to the associated Page
396                // Event format: {dialog: {guid: "..."}}
397                // The Dialog protocol object has the Page as its parent
398                if let Some(dialog_guid) = params
399                    .get("dialog")
400                    .and_then(|v| v.get("guid"))
401                    .and_then(|v| v.as_str())
402                {
403                    let connection = self.connection();
404                    let dialog_guid_owned = dialog_guid.to_string();
405
406                    tokio::spawn(async move {
407                        // Get the Dialog object
408                        let dialog_arc = match connection.get_object(&dialog_guid_owned).await {
409                            Ok(obj) => obj,
410                            Err(_) => return,
411                        };
412
413                        // Downcast to Dialog
414                        let dialog = match dialog_arc
415                            .as_any()
416                            .downcast_ref::<crate::protocol::Dialog>()
417                        {
418                            Some(d) => d.clone(),
419                            None => return,
420                        };
421
422                        // Get the Page from the Dialog's parent
423                        let page_arc = match dialog_arc.parent() {
424                            Some(parent) => parent,
425                            None => return,
426                        };
427
428                        // Downcast to Page
429                        let page = match page_arc.as_any().downcast_ref::<Page>() {
430                            Some(p) => p.clone(),
431                            None => return,
432                        };
433
434                        // Forward to Page's dialog handlers
435                        page.trigger_dialog_event(dialog).await;
436                    });
437                }
438            }
439            _ => {
440                // Other events will be handled in future phases
441            }
442        }
443    }
444
445    fn was_collected(&self) -> bool {
446        self.base.was_collected()
447    }
448
449    fn as_any(&self) -> &dyn Any {
450        self
451    }
452}
453
454impl std::fmt::Debug for BrowserContext {
455    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
456        f.debug_struct("BrowserContext")
457            .field("guid", &self.guid())
458            .finish()
459    }
460}
461
462/// Viewport dimensions for browser context.
463///
464/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
465#[derive(Debug, Clone, Serialize, Deserialize)]
466pub struct Viewport {
467    /// Page width in pixels
468    pub width: u32,
469    /// Page height in pixels
470    pub height: u32,
471}
472
473/// Geolocation coordinates.
474///
475/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
476#[derive(Debug, Clone, Serialize, Deserialize)]
477pub struct Geolocation {
478    /// Latitude between -90 and 90
479    pub latitude: f64,
480    /// Longitude between -180 and 180
481    pub longitude: f64,
482    /// Optional accuracy in meters (default: 0)
483    #[serde(skip_serializing_if = "Option::is_none")]
484    pub accuracy: Option<f64>,
485}
486
487/// Cookie information for storage state.
488///
489/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
490#[derive(Debug, Clone, Serialize, Deserialize)]
491#[serde(rename_all = "camelCase")]
492pub struct Cookie {
493    /// Cookie name
494    pub name: String,
495    /// Cookie value
496    pub value: String,
497    /// Cookie domain (use dot prefix for subdomain matching, e.g., ".example.com")
498    pub domain: String,
499    /// Cookie path
500    pub path: String,
501    /// Unix timestamp in seconds; -1 for session cookies
502    pub expires: f64,
503    /// HTTP-only flag
504    pub http_only: bool,
505    /// Secure flag
506    pub secure: bool,
507    /// SameSite attribute ("Strict", "Lax", "None")
508    #[serde(skip_serializing_if = "Option::is_none")]
509    pub same_site: Option<String>,
510}
511
512/// Local storage item for storage state.
513///
514/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
515#[derive(Debug, Clone, Serialize, Deserialize)]
516pub struct LocalStorageItem {
517    /// Storage key
518    pub name: String,
519    /// Storage value
520    pub value: String,
521}
522
523/// Origin with local storage items for storage state.
524///
525/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
526#[derive(Debug, Clone, Serialize, Deserialize)]
527#[serde(rename_all = "camelCase")]
528pub struct Origin {
529    /// Origin URL (e.g., "https://example.com")
530    pub origin: String,
531    /// Local storage items for this origin
532    pub local_storage: Vec<LocalStorageItem>,
533}
534
535/// Storage state containing cookies and local storage.
536///
537/// Used to populate a browser context with saved authentication state,
538/// enabling session persistence across context instances.
539///
540/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
541#[derive(Debug, Clone, Serialize, Deserialize)]
542pub struct StorageState {
543    /// List of cookies
544    pub cookies: Vec<Cookie>,
545    /// List of origins with local storage
546    pub origins: Vec<Origin>,
547}
548
549/// Options for recording HAR.
550///
551/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-record-har>
552#[derive(Debug, Clone, Serialize, Default)]
553#[serde(rename_all = "camelCase")]
554pub struct RecordHar {
555    /// Path on the filesystem to write the HAR file to.
556    pub path: String,
557    /// Optional setting to control whether to omit request content from the HAR.
558    #[serde(skip_serializing_if = "Option::is_none")]
559    pub omit_content: Option<bool>,
560    /// Optional setting to control resource content management.
561    /// "omit" | "embed" | "attach"
562    #[serde(skip_serializing_if = "Option::is_none")]
563    pub content: Option<String>,
564    /// "full" | "minimal"
565    #[serde(skip_serializing_if = "Option::is_none")]
566    pub mode: Option<String>,
567    /// A glob or regex pattern to filter requests that are stored in the HAR.
568    #[serde(skip_serializing_if = "Option::is_none")]
569    pub url_filter: Option<String>,
570}
571
572/// Options for recording video.
573///
574/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-record-video>
575#[derive(Debug, Clone, Serialize, Default)]
576pub struct RecordVideo {
577    /// Path to the directory to put videos into.
578    pub dir: String,
579    /// Optional dimensions of the recorded videos.
580    #[serde(skip_serializing_if = "Option::is_none")]
581    pub size: Option<Viewport>,
582}
583
584/// Options for creating a new browser context.
585///
586/// Allows customizing viewport, user agent, locale, timezone, geolocation,
587/// permissions, and other browser context settings.
588///
589/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
590#[derive(Debug, Clone, Default, Serialize)]
591#[serde(rename_all = "camelCase")]
592pub struct BrowserContextOptions {
593    /// Sets consistent viewport for all pages in the context.
594    /// Set to null via `no_viewport(true)` to disable viewport emulation.
595    #[serde(skip_serializing_if = "Option::is_none")]
596    pub viewport: Option<Viewport>,
597
598    /// Disables viewport emulation when set to true.
599    #[serde(skip_serializing_if = "Option::is_none")]
600    pub no_viewport: Option<bool>,
601
602    /// Custom user agent string
603    #[serde(skip_serializing_if = "Option::is_none")]
604    pub user_agent: Option<String>,
605
606    /// Locale for the context (e.g., "en-GB", "de-DE", "fr-FR")
607    #[serde(skip_serializing_if = "Option::is_none")]
608    pub locale: Option<String>,
609
610    /// Timezone identifier (e.g., "America/New_York", "Europe/Berlin")
611    #[serde(skip_serializing_if = "Option::is_none")]
612    pub timezone_id: Option<String>,
613
614    /// Geolocation coordinates
615    #[serde(skip_serializing_if = "Option::is_none")]
616    pub geolocation: Option<Geolocation>,
617
618    /// List of permissions to grant (e.g., "geolocation", "notifications")
619    #[serde(skip_serializing_if = "Option::is_none")]
620    pub permissions: Option<Vec<String>>,
621
622    /// Emulates 'prefers-colors-scheme' media feature ("light", "dark", "no-preference")
623    #[serde(skip_serializing_if = "Option::is_none")]
624    pub color_scheme: Option<String>,
625
626    /// Whether the viewport supports touch events
627    #[serde(skip_serializing_if = "Option::is_none")]
628    pub has_touch: Option<bool>,
629
630    /// Whether the meta viewport tag is respected
631    #[serde(skip_serializing_if = "Option::is_none")]
632    pub is_mobile: Option<bool>,
633
634    /// Whether JavaScript is enabled in the context
635    #[serde(skip_serializing_if = "Option::is_none")]
636    pub javascript_enabled: Option<bool>,
637
638    /// Emulates network being offline
639    #[serde(skip_serializing_if = "Option::is_none")]
640    pub offline: Option<bool>,
641
642    /// Whether to automatically download attachments
643    #[serde(skip_serializing_if = "Option::is_none")]
644    pub accept_downloads: Option<bool>,
645
646    /// Whether to bypass Content-Security-Policy
647    #[serde(skip_serializing_if = "Option::is_none")]
648    pub bypass_csp: Option<bool>,
649
650    /// Whether to ignore HTTPS errors
651    #[serde(skip_serializing_if = "Option::is_none")]
652    pub ignore_https_errors: Option<bool>,
653
654    /// Device scale factor (default: 1)
655    #[serde(skip_serializing_if = "Option::is_none")]
656    pub device_scale_factor: Option<f64>,
657
658    /// Extra HTTP headers to send with every request
659    #[serde(skip_serializing_if = "Option::is_none")]
660    pub extra_http_headers: Option<HashMap<String, String>>,
661
662    /// Base URL for relative navigation
663    #[serde(skip_serializing_if = "Option::is_none")]
664    pub base_url: Option<String>,
665
666    /// Storage state to populate the context (cookies, localStorage, sessionStorage).
667    /// Can be an inline StorageState object or a file path string.
668    /// Use builder methods `storage_state()` for inline or `storage_state_path()` for file path.
669    #[serde(skip_serializing_if = "Option::is_none")]
670    pub storage_state: Option<StorageState>,
671
672    /// Storage state file path (alternative to inline storage_state).
673    /// This is handled by the builder and converted to storage_state during serialization.
674    #[serde(skip_serializing_if = "Option::is_none")]
675    pub storage_state_path: Option<String>,
676
677    // Launch options (for launch_persistent_context)
678    /// Additional arguments to pass to browser instance
679    #[serde(skip_serializing_if = "Option::is_none")]
680    pub args: Option<Vec<String>>,
681
682    /// Browser distribution channel (e.g., "chrome", "msedge")
683    #[serde(skip_serializing_if = "Option::is_none")]
684    pub channel: Option<String>,
685
686    /// Enable Chromium sandboxing (default: false on Linux)
687    #[serde(skip_serializing_if = "Option::is_none")]
688    pub chromium_sandbox: Option<bool>,
689
690    /// Auto-open DevTools (deprecated, default: false)
691    #[serde(skip_serializing_if = "Option::is_none")]
692    pub devtools: Option<bool>,
693
694    /// Directory to save downloads
695    #[serde(skip_serializing_if = "Option::is_none")]
696    pub downloads_path: Option<String>,
697
698    /// Path to custom browser executable
699    #[serde(skip_serializing_if = "Option::is_none")]
700    pub executable_path: Option<String>,
701
702    /// Firefox user preferences (Firefox only)
703    #[serde(skip_serializing_if = "Option::is_none")]
704    pub firefox_user_prefs: Option<HashMap<String, serde_json::Value>>,
705
706    /// Run in headless mode (default: true unless devtools=true)
707    #[serde(skip_serializing_if = "Option::is_none")]
708    pub headless: Option<bool>,
709
710    /// Slow down operations by N milliseconds
711    #[serde(skip_serializing_if = "Option::is_none")]
712    pub slow_mo: Option<f64>,
713
714    /// Timeout for browser launch in milliseconds
715    #[serde(skip_serializing_if = "Option::is_none")]
716    pub timeout: Option<f64>,
717
718    /// Directory to save traces
719    #[serde(skip_serializing_if = "Option::is_none")]
720    pub traces_dir: Option<String>,
721
722    /// Check if strict selectors mode is enabled
723    #[serde(skip_serializing_if = "Option::is_none")]
724    pub strict_selectors: Option<bool>,
725
726    /// Emulates 'prefers-reduced-motion' media feature
727    #[serde(skip_serializing_if = "Option::is_none")]
728    pub reduced_motion: Option<String>,
729
730    /// Emulates 'forced-colors' media feature
731    #[serde(skip_serializing_if = "Option::is_none")]
732    pub forced_colors: Option<String>,
733
734    /// Whether to allow sites to register Service workers
735    #[serde(skip_serializing_if = "Option::is_none")]
736    pub service_workers: Option<String>,
737
738    /// Options for recording HAR
739    #[serde(skip_serializing_if = "Option::is_none")]
740    pub record_har: Option<RecordHar>,
741
742    /// Options for recording video
743    #[serde(skip_serializing_if = "Option::is_none")]
744    pub record_video: Option<RecordVideo>,
745}
746
747impl BrowserContextOptions {
748    /// Creates a new builder for BrowserContextOptions
749    pub fn builder() -> BrowserContextOptionsBuilder {
750        BrowserContextOptionsBuilder::default()
751    }
752}
753
754/// Builder for BrowserContextOptions
755#[derive(Debug, Clone, Default)]
756pub struct BrowserContextOptionsBuilder {
757    viewport: Option<Viewport>,
758    no_viewport: Option<bool>,
759    user_agent: Option<String>,
760    locale: Option<String>,
761    timezone_id: Option<String>,
762    geolocation: Option<Geolocation>,
763    permissions: Option<Vec<String>>,
764    color_scheme: Option<String>,
765    has_touch: Option<bool>,
766    is_mobile: Option<bool>,
767    javascript_enabled: Option<bool>,
768    offline: Option<bool>,
769    accept_downloads: Option<bool>,
770    bypass_csp: Option<bool>,
771    ignore_https_errors: Option<bool>,
772    device_scale_factor: Option<f64>,
773    extra_http_headers: Option<HashMap<String, String>>,
774    base_url: Option<String>,
775    storage_state: Option<StorageState>,
776    storage_state_path: Option<String>,
777    // Launch options
778    args: Option<Vec<String>>,
779    channel: Option<String>,
780    chromium_sandbox: Option<bool>,
781    devtools: Option<bool>,
782    downloads_path: Option<String>,
783    executable_path: Option<String>,
784    firefox_user_prefs: Option<HashMap<String, serde_json::Value>>,
785    headless: Option<bool>,
786    slow_mo: Option<f64>,
787    timeout: Option<f64>,
788    traces_dir: Option<String>,
789    strict_selectors: Option<bool>,
790    reduced_motion: Option<String>,
791    forced_colors: Option<String>,
792    service_workers: Option<String>,
793    record_har: Option<RecordHar>,
794    record_video: Option<RecordVideo>,
795}
796
797impl BrowserContextOptionsBuilder {
798    /// Sets the viewport dimensions
799    pub fn viewport(mut self, viewport: Viewport) -> Self {
800        self.viewport = Some(viewport);
801        self.no_viewport = None; // Clear no_viewport if setting viewport
802        self
803    }
804
805    /// Disables viewport emulation
806    pub fn no_viewport(mut self, no_viewport: bool) -> Self {
807        self.no_viewport = Some(no_viewport);
808        if no_viewport {
809            self.viewport = None; // Clear viewport if setting no_viewport
810        }
811        self
812    }
813
814    /// Sets the user agent string
815    pub fn user_agent(mut self, user_agent: String) -> Self {
816        self.user_agent = Some(user_agent);
817        self
818    }
819
820    /// Sets the locale
821    pub fn locale(mut self, locale: String) -> Self {
822        self.locale = Some(locale);
823        self
824    }
825
826    /// Sets the timezone identifier
827    pub fn timezone_id(mut self, timezone_id: String) -> Self {
828        self.timezone_id = Some(timezone_id);
829        self
830    }
831
832    /// Sets the geolocation
833    pub fn geolocation(mut self, geolocation: Geolocation) -> Self {
834        self.geolocation = Some(geolocation);
835        self
836    }
837
838    /// Sets the permissions to grant
839    pub fn permissions(mut self, permissions: Vec<String>) -> Self {
840        self.permissions = Some(permissions);
841        self
842    }
843
844    /// Sets the color scheme preference
845    pub fn color_scheme(mut self, color_scheme: String) -> Self {
846        self.color_scheme = Some(color_scheme);
847        self
848    }
849
850    /// Sets whether the viewport supports touch events
851    pub fn has_touch(mut self, has_touch: bool) -> Self {
852        self.has_touch = Some(has_touch);
853        self
854    }
855
856    /// Sets whether this is a mobile viewport
857    pub fn is_mobile(mut self, is_mobile: bool) -> Self {
858        self.is_mobile = Some(is_mobile);
859        self
860    }
861
862    /// Sets whether JavaScript is enabled
863    pub fn javascript_enabled(mut self, javascript_enabled: bool) -> Self {
864        self.javascript_enabled = Some(javascript_enabled);
865        self
866    }
867
868    /// Sets whether to emulate offline network
869    pub fn offline(mut self, offline: bool) -> Self {
870        self.offline = Some(offline);
871        self
872    }
873
874    /// Sets whether to automatically download attachments
875    pub fn accept_downloads(mut self, accept_downloads: bool) -> Self {
876        self.accept_downloads = Some(accept_downloads);
877        self
878    }
879
880    /// Sets whether to bypass Content-Security-Policy
881    pub fn bypass_csp(mut self, bypass_csp: bool) -> Self {
882        self.bypass_csp = Some(bypass_csp);
883        self
884    }
885
886    /// Sets whether to ignore HTTPS errors
887    pub fn ignore_https_errors(mut self, ignore_https_errors: bool) -> Self {
888        self.ignore_https_errors = Some(ignore_https_errors);
889        self
890    }
891
892    /// Sets the device scale factor
893    pub fn device_scale_factor(mut self, device_scale_factor: f64) -> Self {
894        self.device_scale_factor = Some(device_scale_factor);
895        self
896    }
897
898    /// Sets extra HTTP headers
899    pub fn extra_http_headers(mut self, extra_http_headers: HashMap<String, String>) -> Self {
900        self.extra_http_headers = Some(extra_http_headers);
901        self
902    }
903
904    /// Sets the base URL for relative navigation
905    pub fn base_url(mut self, base_url: String) -> Self {
906        self.base_url = Some(base_url);
907        self
908    }
909
910    /// Sets the storage state inline (cookies, localStorage).
911    ///
912    /// Populates the browser context with the provided storage state, including
913    /// cookies and local storage. This is useful for initializing a context with
914    /// a saved authentication state.
915    ///
916    /// Mutually exclusive with `storage_state_path()`.
917    ///
918    /// # Example
919    ///
920    /// ```rust
921    /// use playwright_rs::protocol::{BrowserContextOptions, Cookie, StorageState, Origin, LocalStorageItem};
922    ///
923    /// let storage_state = StorageState {
924    ///     cookies: vec![Cookie {
925    ///         name: "session_id".to_string(),
926    ///         value: "abc123".to_string(),
927    ///         domain: ".example.com".to_string(),
928    ///         path: "/".to_string(),
929    ///         expires: -1.0,
930    ///         http_only: true,
931    ///         secure: true,
932    ///         same_site: Some("Lax".to_string()),
933    ///     }],
934    ///     origins: vec![Origin {
935    ///         origin: "https://example.com".to_string(),
936    ///         local_storage: vec![LocalStorageItem {
937    ///             name: "user_prefs".to_string(),
938    ///             value: "{\"theme\":\"dark\"}".to_string(),
939    ///         }],
940    ///     }],
941    /// };
942    ///
943    /// let options = BrowserContextOptions::builder()
944    ///     .storage_state(storage_state)
945    ///     .build();
946    /// ```
947    ///
948    /// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
949    pub fn storage_state(mut self, storage_state: StorageState) -> Self {
950        self.storage_state = Some(storage_state);
951        self.storage_state_path = None; // Clear path if setting inline
952        self
953    }
954
955    /// Sets the storage state from a file path.
956    ///
957    /// The file should contain a JSON representation of StorageState with cookies
958    /// and origins. This is useful for loading authentication state saved from a
959    /// previous session.
960    ///
961    /// Mutually exclusive with `storage_state()`.
962    ///
963    /// # Example
964    ///
965    /// ```rust
966    /// use playwright_rs::protocol::BrowserContextOptions;
967    ///
968    /// let options = BrowserContextOptions::builder()
969    ///     .storage_state_path("auth.json".to_string())
970    ///     .build();
971    /// ```
972    ///
973    /// The file should have this format:
974    /// ```json
975    /// {
976    ///   "cookies": [{
977    ///     "name": "session_id",
978    ///     "value": "abc123",
979    ///     "domain": ".example.com",
980    ///     "path": "/",
981    ///     "expires": -1,
982    ///     "httpOnly": true,
983    ///     "secure": true,
984    ///     "sameSite": "Lax"
985    ///   }],
986    ///   "origins": [{
987    ///     "origin": "https://example.com",
988    ///     "localStorage": [{
989    ///       "name": "user_prefs",
990    ///       "value": "{\"theme\":\"dark\"}"
991    ///     }]
992    ///   }]
993    /// }
994    /// ```
995    ///
996    /// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
997    pub fn storage_state_path(mut self, path: String) -> Self {
998        self.storage_state_path = Some(path);
999        self.storage_state = None; // Clear inline if setting path
1000        self
1001    }
1002
1003    /// Sets additional arguments to pass to browser instance (for launch_persistent_context)
1004    pub fn args(mut self, args: Vec<String>) -> Self {
1005        self.args = Some(args);
1006        self
1007    }
1008
1009    /// Sets browser distribution channel (for launch_persistent_context)
1010    pub fn channel(mut self, channel: String) -> Self {
1011        self.channel = Some(channel);
1012        self
1013    }
1014
1015    /// Enables or disables Chromium sandboxing (for launch_persistent_context)
1016    pub fn chromium_sandbox(mut self, enabled: bool) -> Self {
1017        self.chromium_sandbox = Some(enabled);
1018        self
1019    }
1020
1021    /// Auto-open DevTools (for launch_persistent_context)
1022    pub fn devtools(mut self, enabled: bool) -> Self {
1023        self.devtools = Some(enabled);
1024        self
1025    }
1026
1027    /// Sets directory to save downloads (for launch_persistent_context)
1028    pub fn downloads_path(mut self, path: String) -> Self {
1029        self.downloads_path = Some(path);
1030        self
1031    }
1032
1033    /// Sets path to custom browser executable (for launch_persistent_context)
1034    pub fn executable_path(mut self, path: String) -> Self {
1035        self.executable_path = Some(path);
1036        self
1037    }
1038
1039    /// Sets Firefox user preferences (for launch_persistent_context, Firefox only)
1040    pub fn firefox_user_prefs(mut self, prefs: HashMap<String, serde_json::Value>) -> Self {
1041        self.firefox_user_prefs = Some(prefs);
1042        self
1043    }
1044
1045    /// Run in headless mode (for launch_persistent_context)
1046    pub fn headless(mut self, enabled: bool) -> Self {
1047        self.headless = Some(enabled);
1048        self
1049    }
1050
1051    /// Slow down operations by N milliseconds (for launch_persistent_context)
1052    pub fn slow_mo(mut self, ms: f64) -> Self {
1053        self.slow_mo = Some(ms);
1054        self
1055    }
1056
1057    /// Set timeout for browser launch in milliseconds (for launch_persistent_context)
1058    pub fn timeout(mut self, ms: f64) -> Self {
1059        self.timeout = Some(ms);
1060        self
1061    }
1062
1063    /// Set directory to save traces (for launch_persistent_context)
1064    pub fn traces_dir(mut self, path: String) -> Self {
1065        self.traces_dir = Some(path);
1066        self
1067    }
1068
1069    /// Check if strict selectors mode is enabled
1070    pub fn strict_selectors(mut self, enabled: bool) -> Self {
1071        self.strict_selectors = Some(enabled);
1072        self
1073    }
1074
1075    /// Emulates 'prefers-reduced-motion' media feature
1076    pub fn reduced_motion(mut self, value: String) -> Self {
1077        self.reduced_motion = Some(value);
1078        self
1079    }
1080
1081    /// Emulates 'forced-colors' media feature
1082    pub fn forced_colors(mut self, value: String) -> Self {
1083        self.forced_colors = Some(value);
1084        self
1085    }
1086
1087    /// Whether to allow sites to register Service workers ("allow" | "block")
1088    pub fn service_workers(mut self, value: String) -> Self {
1089        self.service_workers = Some(value);
1090        self
1091    }
1092
1093    /// Sets options for recording HAR
1094    pub fn record_har(mut self, record_har: RecordHar) -> Self {
1095        self.record_har = Some(record_har);
1096        self
1097    }
1098
1099    /// Sets options for recording video
1100    pub fn record_video(mut self, record_video: RecordVideo) -> Self {
1101        self.record_video = Some(record_video);
1102        self
1103    }
1104
1105    /// Builds the BrowserContextOptions
1106    pub fn build(self) -> BrowserContextOptions {
1107        BrowserContextOptions {
1108            viewport: self.viewport,
1109            no_viewport: self.no_viewport,
1110            user_agent: self.user_agent,
1111            locale: self.locale,
1112            timezone_id: self.timezone_id,
1113            geolocation: self.geolocation,
1114            permissions: self.permissions,
1115            color_scheme: self.color_scheme,
1116            has_touch: self.has_touch,
1117            is_mobile: self.is_mobile,
1118            javascript_enabled: self.javascript_enabled,
1119            offline: self.offline,
1120            accept_downloads: self.accept_downloads,
1121            bypass_csp: self.bypass_csp,
1122            ignore_https_errors: self.ignore_https_errors,
1123            device_scale_factor: self.device_scale_factor,
1124            extra_http_headers: self.extra_http_headers,
1125            base_url: self.base_url,
1126            storage_state: self.storage_state,
1127            storage_state_path: self.storage_state_path,
1128            // Launch options
1129            args: self.args,
1130            channel: self.channel,
1131            chromium_sandbox: self.chromium_sandbox,
1132            devtools: self.devtools,
1133            downloads_path: self.downloads_path,
1134            executable_path: self.executable_path,
1135            firefox_user_prefs: self.firefox_user_prefs,
1136            headless: self.headless,
1137            slow_mo: self.slow_mo,
1138            timeout: self.timeout,
1139            traces_dir: self.traces_dir,
1140            strict_selectors: self.strict_selectors,
1141            reduced_motion: self.reduced_motion,
1142            forced_colors: self.forced_colors,
1143            service_workers: self.service_workers,
1144            record_har: self.record_har,
1145            record_video: self.record_video,
1146        }
1147    }
1148}