Skip to main content

playwright_rs/protocol/browser_context/
options.rs

1use super::{Geolocation, HttpCredentials, StorageState};
2use crate::api::launch_options::IgnoreDefaultArgs;
3use crate::protocol::ProxySettings;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7/// Viewport dimensions for browser context.
8///
9/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct Viewport {
12    /// Page width in pixels
13    pub width: u32,
14    /// Page height in pixels
15    pub height: u32,
16}
17
18impl From<crate::protocol::DeviceViewport> for Viewport {
19    fn from(size: crate::protocol::DeviceViewport) -> Self {
20        Self {
21            width: size.width,
22            height: size.height,
23        }
24    }
25}
26
27/// Options for recording HAR.
28///
29/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-record-har>
30#[derive(Debug, Clone, Serialize, Default)]
31#[serde(rename_all = "camelCase")]
32#[non_exhaustive]
33pub struct RecordHar {
34    /// Path on the filesystem to write the HAR file to.
35    pub path: String,
36    /// Optional setting to control whether to omit request content from the HAR.
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub omit_content: Option<bool>,
39    /// Optional setting to control resource content management.
40    /// "omit" | "embed" | "attach"
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub content: Option<String>,
43    /// "full" | "minimal"
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub mode: Option<String>,
46    /// A URL glob; only requests whose URL matches are stored in the HAR.
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub url_filter: Option<String>,
49}
50
51impl RecordHar {
52    /// Record a HAR to the given path.
53    pub fn new(path: impl Into<String>) -> Self {
54        Self {
55            path: path.into(),
56            omit_content: None,
57            content: None,
58            mode: None,
59            url_filter: None,
60        }
61    }
62    /// Omit response bodies from the HAR.
63    pub fn omit_content(mut self, omit_content: bool) -> Self {
64        self.omit_content = Some(omit_content);
65        self
66    }
67    /// Content mode ("embed", "attach", or "omit").
68    pub fn content(mut self, content: impl Into<String>) -> Self {
69        self.content = Some(content.into());
70        self
71    }
72    /// Recording mode ("full" or "minimal").
73    pub fn mode(mut self, mode: impl Into<String>) -> Self {
74        self.mode = Some(mode.into());
75        self
76    }
77    /// Only record requests matching this URL glob.
78    pub fn url_filter(mut self, url_filter: impl Into<String>) -> Self {
79        self.url_filter = Some(url_filter.into());
80        self
81    }
82}
83
84/// Options for recording video.
85///
86/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-record-video>
87#[derive(Debug, Clone, Serialize, Default)]
88#[serde(rename_all = "camelCase")]
89#[non_exhaustive]
90pub struct RecordVideo {
91    /// Path to the directory to put videos into.
92    pub dir: String,
93    /// Optional dimensions of the recorded videos.
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub size: Option<Viewport>,
96    /// Frame rate of the recorded videos. Must be positive. Firefox and
97    /// WebKit capture at most 25 frames per second.
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub fps: Option<u32>,
100    /// Decorate each action in the recorded videos, as
101    /// [`Screencast::show_actions`](crate::protocol::Screencast::show_actions)
102    /// does for a screencast.
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub show_actions: Option<crate::protocol::ShowActionsOptions>,
105}
106
107impl RecordVideo {
108    /// Record videos into the given directory.
109    pub fn new(dir: impl Into<String>) -> Self {
110        Self {
111            dir: dir.into(),
112            ..Self::default()
113        }
114    }
115    /// Recorded video size.
116    pub fn size(mut self, size: Viewport) -> Self {
117        self.size = Some(size);
118        self
119    }
120    /// Recorded video frame rate.
121    pub fn fps(mut self, fps: u32) -> Self {
122        self.fps = Some(fps);
123        self
124    }
125    /// Decorate each action in the recorded videos.
126    pub fn show_actions(mut self, show_actions: crate::protocol::ShowActionsOptions) -> Self {
127        self.show_actions = Some(show_actions);
128        self
129    }
130}
131
132/// Controls how downloads are handled in a [`BrowserContext`](super::BrowserContext).
133///
134/// See the `accept_downloads` field of [`BrowserContextOptions`].
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
136#[non_exhaustive]
137pub enum AcceptDownloads {
138    /// Allow and capture downloads via the `download` event.
139    #[serde(rename = "accept")]
140    Accept,
141    /// Block downloads.
142    #[serde(rename = "deny")]
143    Deny,
144    /// Let the browser handle downloads natively without routing through Playwright.
145    #[serde(rename = "internal")]
146    Internal,
147}
148
149impl From<bool> for AcceptDownloads {
150    fn from(value: bool) -> Self {
151        if value { Self::Accept } else { Self::Deny }
152    }
153}
154
155/// `extraHTTPHeaders` goes to the driver as its `NameValue[]` list, not as
156/// the map the builder takes. The pairs are streamed from the map rather
157/// than copied into an intermediate value.
158fn serialize_header_pairs<S: serde::Serializer>(
159    headers: &Option<HashMap<String, String>>,
160    serializer: S,
161) -> std::result::Result<S::Ok, S::Error> {
162    #[derive(Serialize)]
163    struct NameValue<'a> {
164        name: &'a str,
165        value: &'a str,
166    }
167    match headers {
168        Some(headers) => serializer.collect_seq(
169            headers
170                .iter()
171                .map(|(name, value)| NameValue { name, value }),
172        ),
173        None => serializer.serialize_none(),
174    }
175}
176
177/// Options for creating a new browser context.
178///
179/// Allows customizing viewport, user agent, locale, timezone, geolocation,
180/// permissions, and other browser context settings.
181///
182/// A proxy and a saved session, built inline; `storage_state_path` loads the
183/// same state from a file written by `BrowserContext::storage_state`:
184///
185/// ```rust
186/// use playwright_rs::protocol::{
187///     BrowserContextOptions, Cookie, LocalStorageItem, Origin, ProxySettings, StorageState,
188/// };
189///
190/// let session = StorageState::default()
191///     .cookies(vec![
192///         Cookie::new("session_id", "abc123")
193///             .domain(".example.com")
194///             .http_only(true)
195///             .secure(true)
196///             .same_site("Lax"),
197///     ])
198///     .origins(vec![Origin::new(
199///         "https://example.com",
200///         vec![LocalStorageItem::new("user_prefs", "{\"theme\":\"dark\"}")],
201///     )]);
202///
203/// let options = BrowserContextOptions::builder()
204///     .proxy(
205///         ProxySettings::new("http://proxy.example.com:8080")
206///             .bypass(".example.com")
207///             .username("user")
208///             .password("pass"),
209///     )
210///     .storage_state(session)
211///     .build();
212/// ```
213///
214/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
215#[derive(Debug, Clone, Default, Serialize)]
216#[serde(rename_all = "camelCase")]
217#[non_exhaustive]
218pub struct BrowserContextOptions {
219    /// Sets consistent viewport for all pages in the context.
220    /// Set to null via `no_viewport(true)` to disable viewport emulation.
221    #[serde(skip_serializing_if = "Option::is_none")]
222    pub viewport: Option<Viewport>,
223
224    /// Disables viewport emulation when set to true.
225    /// Note: Playwright's public API calls this `noViewport`, but the protocol
226    /// expects `noDefaultViewport`. playwright-python applies this transformation
227    /// in `_prepare_browser_context_params`.
228    #[serde(skip_serializing_if = "Option::is_none")]
229    #[serde(rename = "noDefaultViewport")]
230    pub no_viewport: Option<bool>,
231
232    /// The screen size `window.screen` and media queries report, separate
233    /// from the viewport. Defaults to the viewport's size, and is used only
234    /// when a viewport is set, so it has no effect with `no_viewport`. A
235    /// device descriptor's [`screen`](crate::protocol::DeviceDescriptor::screen)
236    /// belongs here when emulating that device.
237    #[serde(skip_serializing_if = "Option::is_none")]
238    pub screen: Option<Viewport>,
239
240    /// Custom user agent string
241    #[serde(skip_serializing_if = "Option::is_none")]
242    pub user_agent: Option<String>,
243
244    /// Locale for the context (e.g., "en-GB", "de-DE", "fr-FR")
245    #[serde(skip_serializing_if = "Option::is_none")]
246    pub locale: Option<String>,
247
248    /// Timezone identifier (e.g., "America/New_York", "Europe/Berlin")
249    #[serde(skip_serializing_if = "Option::is_none")]
250    pub timezone_id: Option<String>,
251
252    /// Geolocation coordinates
253    #[serde(skip_serializing_if = "Option::is_none")]
254    pub geolocation: Option<Geolocation>,
255
256    /// Credentials for HTTP authentication, matched per request origin.
257    #[serde(rename = "httpCredentials", skip_serializing_if = "Option::is_none")]
258    pub http_credentials: Option<Vec<HttpCredentials>>,
259
260    /// List of permissions to grant (e.g., "geolocation", "notifications")
261    #[serde(skip_serializing_if = "Option::is_none")]
262    pub permissions: Option<Vec<String>>,
263
264    /// Network proxy settings
265    #[serde(skip_serializing_if = "Option::is_none")]
266    pub proxy: Option<ProxySettings>,
267
268    /// Emulates 'prefers-colors-scheme' media feature ("light", "dark", "no-preference")
269    #[serde(skip_serializing_if = "Option::is_none")]
270    pub color_scheme: Option<String>,
271
272    /// Whether the viewport supports touch events
273    #[serde(skip_serializing_if = "Option::is_none")]
274    pub has_touch: Option<bool>,
275
276    /// Whether the meta viewport tag is respected
277    #[serde(skip_serializing_if = "Option::is_none")]
278    pub is_mobile: Option<bool>,
279
280    /// Whether JavaScript is enabled in the context
281    #[serde(rename = "javaScriptEnabled", skip_serializing_if = "Option::is_none")]
282    pub javascript_enabled: Option<bool>,
283
284    /// Emulates network being offline
285    #[serde(skip_serializing_if = "Option::is_none")]
286    pub offline: Option<bool>,
287
288    /// How to handle downloads. See [`AcceptDownloads`] for options.
289    #[serde(skip_serializing_if = "Option::is_none")]
290    pub accept_downloads: Option<AcceptDownloads>,
291
292    /// Whether to bypass Content-Security-Policy
293    #[serde(rename = "bypassCSP", skip_serializing_if = "Option::is_none")]
294    pub bypass_csp: Option<bool>,
295
296    /// Whether to ignore HTTPS errors
297    #[serde(rename = "ignoreHTTPSErrors", skip_serializing_if = "Option::is_none")]
298    pub ignore_https_errors: Option<bool>,
299
300    /// Device scale factor (default: 1)
301    #[serde(skip_serializing_if = "Option::is_none")]
302    pub device_scale_factor: Option<f64>,
303
304    /// Extra HTTP headers to send with every request
305    #[serde(
306        rename = "extraHTTPHeaders",
307        serialize_with = "serialize_header_pairs",
308        skip_serializing_if = "Option::is_none"
309    )]
310    pub extra_http_headers: Option<HashMap<String, String>>,
311
312    /// Base URL for relative navigation
313    #[serde(rename = "baseURL", skip_serializing_if = "Option::is_none")]
314    pub base_url: Option<String>,
315
316    /// Storage state to populate the context (cookies, localStorage, sessionStorage).
317    /// Can be an inline StorageState object or a file path string.
318    /// Use builder methods `storage_state()` for inline or `storage_state_path()` for file path.
319    #[serde(skip_serializing_if = "Option::is_none")]
320    pub storage_state: Option<StorageState>,
321
322    /// Storage state file path (alternative to inline storage_state).
323    /// This is handled by the builder and converted to storage_state during serialization.
324    #[serde(skip_serializing_if = "Option::is_none")]
325    pub storage_state_path: Option<String>,
326
327    // Launch options (for launch_persistent_context)
328    /// Additional arguments to pass to browser instance
329    #[serde(skip_serializing_if = "Option::is_none")]
330    pub args: Option<Vec<String>>,
331
332    /// Browser distribution channel (e.g., "chrome", "msedge")
333    #[serde(skip_serializing_if = "Option::is_none")]
334    pub channel: Option<String>,
335
336    /// Enable Chromium sandboxing (default: false on Linux)
337    #[serde(skip_serializing_if = "Option::is_none")]
338    pub chromium_sandbox: Option<bool>,
339
340    /// Auto-open DevTools.
341    ///
342    /// Playwright 1.58 removed this option. The driver drops undeclared
343    /// parameters without erroring, so setting it has no effect and no longer
344    /// reaches the wire. Nor does it imply a headed browser any more. For
345    /// Chromium, pass `--auto-open-devtools-for-tabs` in `args` and set
346    /// `headless(false)`. Both apply only to
347    /// `BrowserType::launch_persistent_context`; for a context from
348    /// `Browser::new_context`, set them on the [`LaunchOptions`] the browser
349    /// was launched with.
350    ///
351    /// [`LaunchOptions`]: crate::api::LaunchOptions
352    #[deprecated(
353        since = "0.20.0",
354        note = "removed from the protocol in Playwright 1.58; setting it has no effect. For Chromium, pass `--auto-open-devtools-for-tabs` in `args` (persistent contexts) or `LaunchOptions::args` instead"
355    )]
356    #[serde(skip_serializing)]
357    pub devtools: Option<bool>,
358
359    /// Directory to save downloads
360    #[serde(skip_serializing_if = "Option::is_none")]
361    pub downloads_path: Option<String>,
362
363    /// Path to custom browser executable
364    #[serde(skip_serializing_if = "Option::is_none")]
365    pub executable_path: Option<String>,
366
367    /// Firefox user preferences (Firefox only)
368    #[serde(skip_serializing_if = "Option::is_none")]
369    pub firefox_user_prefs: Option<HashMap<String, serde_json::Value>>,
370
371    /// Run in headless mode (default: true)
372    #[serde(skip_serializing_if = "Option::is_none")]
373    pub headless: Option<bool>,
374
375    /// Filter or disable default browser arguments.
376    /// When `true`, Playwright does not pass its own default args.
377    /// When an array, filters out the given default arguments.
378    ///
379    /// See: <https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context>
380    #[serde(skip_serializing_if = "Option::is_none")]
381    pub ignore_default_args: Option<IgnoreDefaultArgs>,
382
383    /// Slow down operations by N milliseconds
384    #[serde(skip_serializing_if = "Option::is_none")]
385    pub slow_mo: Option<f64>,
386
387    /// Timeout for browser launch in milliseconds, for
388    /// `launch_persistent_context` (default:
389    /// [`DEFAULT_LAUNCH_TIMEOUT_MS`](crate::DEFAULT_LAUNCH_TIMEOUT_MS))
390    #[serde(skip_serializing_if = "Option::is_none")]
391    pub timeout: Option<f64>,
392
393    /// Directory to save traces
394    #[serde(skip_serializing_if = "Option::is_none")]
395    pub traces_dir: Option<String>,
396
397    /// Check if strict selectors mode is enabled
398    #[serde(skip_serializing_if = "Option::is_none")]
399    pub strict_selectors: Option<bool>,
400
401    /// Emulates 'prefers-reduced-motion' media feature
402    #[serde(skip_serializing_if = "Option::is_none")]
403    pub reduced_motion: Option<String>,
404
405    /// Emulates 'forced-colors' media feature
406    #[serde(skip_serializing_if = "Option::is_none")]
407    pub forced_colors: Option<String>,
408
409    /// Whether to allow sites to register Service workers
410    #[serde(skip_serializing_if = "Option::is_none")]
411    pub service_workers: Option<String>,
412
413    /// Records the context's network traffic as a HAR file. Not a driver
414    /// parameter: the recording starts once the context exists and is written
415    /// when it closes (see [`BrowserContextOptionsBuilder::record_har`]).
416    #[serde(skip)]
417    pub record_har: Option<RecordHar>,
418
419    /// Options for recording video
420    #[serde(skip_serializing_if = "Option::is_none")]
421    pub record_video: Option<RecordVideo>,
422}
423
424impl BrowserContextOptions {
425    /// Creates a new builder for BrowserContextOptions
426    pub fn builder() -> BrowserContextOptionsBuilder {
427        BrowserContextOptionsBuilder::default()
428    }
429}
430
431/// Builder for BrowserContextOptions
432#[derive(Debug, Clone, Default)]
433pub struct BrowserContextOptionsBuilder {
434    viewport: Option<Viewport>,
435    no_viewport: Option<bool>,
436    screen: Option<Viewport>,
437    user_agent: Option<String>,
438    locale: Option<String>,
439    timezone_id: Option<String>,
440    geolocation: Option<Geolocation>,
441    http_credentials: Option<Vec<HttpCredentials>>,
442    permissions: Option<Vec<String>>,
443    proxy: Option<ProxySettings>,
444    color_scheme: Option<String>,
445    has_touch: Option<bool>,
446    is_mobile: Option<bool>,
447    javascript_enabled: Option<bool>,
448    offline: Option<bool>,
449    accept_downloads: Option<AcceptDownloads>,
450    bypass_csp: Option<bool>,
451    ignore_https_errors: Option<bool>,
452    device_scale_factor: Option<f64>,
453    extra_http_headers: Option<HashMap<String, String>>,
454    base_url: Option<String>,
455    storage_state: Option<StorageState>,
456    storage_state_path: Option<String>,
457    // Launch options
458    args: Option<Vec<String>>,
459    channel: Option<String>,
460    chromium_sandbox: Option<bool>,
461    devtools: Option<bool>,
462    downloads_path: Option<String>,
463    executable_path: Option<String>,
464    firefox_user_prefs: Option<HashMap<String, serde_json::Value>>,
465    headless: Option<bool>,
466    ignore_default_args: Option<IgnoreDefaultArgs>,
467    slow_mo: Option<f64>,
468    timeout: Option<f64>,
469    traces_dir: Option<String>,
470    strict_selectors: Option<bool>,
471    reduced_motion: Option<String>,
472    forced_colors: Option<String>,
473    service_workers: Option<String>,
474    record_har: Option<RecordHar>,
475    record_video: Option<RecordVideo>,
476}
477
478impl BrowserContextOptionsBuilder {
479    /// Sets the viewport dimensions
480    pub fn viewport(mut self, viewport: Viewport) -> Self {
481        self.viewport = Some(viewport);
482        self.no_viewport = None; // Clear no_viewport if setting viewport
483        self
484    }
485
486    /// Disables viewport emulation
487    pub fn no_viewport(mut self, no_viewport: bool) -> Self {
488        self.no_viewport = Some(no_viewport);
489        if no_viewport {
490            self.viewport = None; // Clear viewport if setting no_viewport
491        }
492        self
493    }
494
495    /// Sets the screen size `window.screen` and media queries report,
496    /// separate from the viewport. Used only when a viewport is set. Takes a
497    /// device descriptor's `screen` directly.
498    ///
499    /// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-screen>
500    pub fn screen(mut self, screen: impl Into<Viewport>) -> Self {
501        self.screen = Some(screen.into());
502        self
503    }
504
505    /// Sets the user agent string
506    pub fn user_agent(mut self, user_agent: String) -> Self {
507        self.user_agent = Some(user_agent);
508        self
509    }
510
511    /// Sets the locale
512    pub fn locale(mut self, locale: String) -> Self {
513        self.locale = Some(locale);
514        self
515    }
516
517    /// Sets the timezone identifier
518    pub fn timezone_id(mut self, timezone_id: String) -> Self {
519        self.timezone_id = Some(timezone_id);
520        self
521    }
522
523    /// Sets the geolocation
524    pub fn geolocation(mut self, geolocation: Geolocation) -> Self {
525        self.geolocation = Some(geolocation);
526        self
527    }
528
529    /// Sets credentials for HTTP authentication.
530    ///
531    /// Each request uses the first entry whose `origin` matches it; an entry
532    /// without an origin matches anything.
533    pub fn http_credentials(mut self, credentials: Vec<HttpCredentials>) -> Self {
534        self.http_credentials = Some(credentials);
535        self
536    }
537
538    /// Sets the permissions to grant
539    pub fn permissions(mut self, permissions: Vec<String>) -> Self {
540        self.permissions = Some(permissions);
541        self
542    }
543
544    /// Sets the network proxy settings for this context.
545    ///
546    /// This allows routing all network traffic through a proxy server,
547    /// useful for rotating proxies without creating new browsers.
548    ///
549    /// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
550    pub fn proxy(mut self, proxy: ProxySettings) -> Self {
551        self.proxy = Some(proxy);
552        self
553    }
554
555    /// Sets the color scheme preference
556    pub fn color_scheme(mut self, color_scheme: String) -> Self {
557        self.color_scheme = Some(color_scheme);
558        self
559    }
560
561    /// Sets whether the viewport supports touch events
562    pub fn has_touch(mut self, has_touch: bool) -> Self {
563        self.has_touch = Some(has_touch);
564        self
565    }
566
567    /// Sets whether this is a mobile viewport
568    pub fn is_mobile(mut self, is_mobile: bool) -> Self {
569        self.is_mobile = Some(is_mobile);
570        self
571    }
572
573    /// Sets whether JavaScript is enabled
574    pub fn javascript_enabled(mut self, javascript_enabled: bool) -> Self {
575        self.javascript_enabled = Some(javascript_enabled);
576        self
577    }
578
579    /// Sets whether to emulate offline network
580    pub fn offline(mut self, offline: bool) -> Self {
581        self.offline = Some(offline);
582        self
583    }
584
585    /// Sets how to handle downloads. Accepts `AcceptDownloads` or `bool`
586    /// (`true` → `Accept`, `false` → `Deny`).
587    pub fn accept_downloads(mut self, accept_downloads: impl Into<AcceptDownloads>) -> Self {
588        self.accept_downloads = Some(accept_downloads.into());
589        self
590    }
591
592    /// Sets whether to bypass Content-Security-Policy
593    pub fn bypass_csp(mut self, bypass_csp: bool) -> Self {
594        self.bypass_csp = Some(bypass_csp);
595        self
596    }
597
598    /// Sets whether to ignore HTTPS errors
599    pub fn ignore_https_errors(mut self, ignore_https_errors: bool) -> Self {
600        self.ignore_https_errors = Some(ignore_https_errors);
601        self
602    }
603
604    /// Sets the device scale factor
605    pub fn device_scale_factor(mut self, device_scale_factor: f64) -> Self {
606        self.device_scale_factor = Some(device_scale_factor);
607        self
608    }
609
610    /// Sets extra HTTP headers
611    pub fn extra_http_headers(mut self, extra_http_headers: HashMap<String, String>) -> Self {
612        self.extra_http_headers = Some(extra_http_headers);
613        self
614    }
615
616    /// Sets the base URL for relative navigation
617    pub fn base_url(mut self, base_url: String) -> Self {
618        self.base_url = Some(base_url);
619        self
620    }
621
622    /// Sets the storage state inline (cookies, localStorage).
623    ///
624    /// Populates the browser context with the provided storage state, including
625    /// cookies and local storage. This is useful for initializing a context with
626    /// a saved authentication state.
627    ///
628    /// Mutually exclusive with `storage_state_path()`.
629    ///
630    /// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
631    pub fn storage_state(mut self, storage_state: StorageState) -> Self {
632        self.storage_state = Some(storage_state);
633        self.storage_state_path = None; // Clear path if setting inline
634        self
635    }
636
637    /// Sets the storage state from a file path.
638    ///
639    /// The file should contain a JSON representation of StorageState with cookies
640    /// and origins. This is useful for loading authentication state saved from a
641    /// previous session.
642    ///
643    /// Mutually exclusive with `storage_state()`.
644    ///
645    /// The file should have this format:
646    /// ```json
647    /// {
648    ///   "cookies": [{
649    ///     "name": "session_id",
650    ///     "value": "abc123",
651    ///     "domain": ".example.com",
652    ///     "path": "/",
653    ///     "expires": -1,
654    ///     "httpOnly": true,
655    ///     "secure": true,
656    ///     "sameSite": "Lax"
657    ///   }],
658    ///   "origins": [{
659    ///     "origin": "https://example.com",
660    ///     "localStorage": [{
661    ///       "name": "user_prefs",
662    ///       "value": "{\"theme\":\"dark\"}"
663    ///     }]
664    ///   }]
665    /// }
666    /// ```
667    ///
668    /// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
669    pub fn storage_state_path(mut self, path: String) -> Self {
670        self.storage_state_path = Some(path);
671        self.storage_state = None; // Clear inline if setting path
672        self
673    }
674
675    /// Sets additional arguments to pass to browser instance (for launch_persistent_context)
676    pub fn args(mut self, args: Vec<String>) -> Self {
677        self.args = Some(args);
678        self
679    }
680
681    /// Sets browser distribution channel (for launch_persistent_context)
682    pub fn channel(mut self, channel: String) -> Self {
683        self.channel = Some(channel);
684        self
685    }
686
687    /// Enables or disables Chromium sandboxing (for launch_persistent_context)
688    pub fn chromium_sandbox(mut self, enabled: bool) -> Self {
689        self.chromium_sandbox = Some(enabled);
690        self
691    }
692
693    /// Auto-open DevTools. Playwright 1.58 removed the option, so this has no effect.
694    #[deprecated(
695        since = "0.20.0",
696        note = "removed from the protocol in Playwright 1.58; setting it has no effect. For Chromium, pass `--auto-open-devtools-for-tabs` in `args` (persistent contexts) or `LaunchOptions::args` instead"
697    )]
698    pub fn devtools(mut self, enabled: bool) -> Self {
699        self.devtools = Some(enabled);
700        self
701    }
702
703    /// Sets directory to save downloads (for launch_persistent_context)
704    pub fn downloads_path(mut self, path: String) -> Self {
705        self.downloads_path = Some(path);
706        self
707    }
708
709    /// Sets path to custom browser executable (for launch_persistent_context)
710    pub fn executable_path(mut self, path: String) -> Self {
711        self.executable_path = Some(path);
712        self
713    }
714
715    /// Sets Firefox user preferences (for launch_persistent_context, Firefox only)
716    pub fn firefox_user_prefs(mut self, prefs: HashMap<String, serde_json::Value>) -> Self {
717        self.firefox_user_prefs = Some(prefs);
718        self
719    }
720
721    /// Run in headless mode (for launch_persistent_context)
722    pub fn headless(mut self, enabled: bool) -> Self {
723        self.headless = Some(enabled);
724        self
725    }
726
727    /// Filter or disable default browser arguments (for launch_persistent_context).
728    ///
729    /// When `IgnoreDefaultArgs::Bool(true)`, Playwright does not pass its own
730    /// default arguments and only uses the ones from `args`.
731    /// When `IgnoreDefaultArgs::Array(vec)`, filters out the given default arguments.
732    ///
733    /// See: <https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context>
734    pub fn ignore_default_args(mut self, args: IgnoreDefaultArgs) -> Self {
735        self.ignore_default_args = Some(args);
736        self
737    }
738
739    /// Slow down operations by N milliseconds (for launch_persistent_context)
740    pub fn slow_mo(mut self, ms: f64) -> Self {
741        self.slow_mo = Some(ms);
742        self
743    }
744
745    /// Set timeout for browser launch in milliseconds (for launch_persistent_context)
746    pub fn timeout(mut self, ms: f64) -> Self {
747        self.timeout = Some(ms);
748        self
749    }
750
751    /// Set directory to save traces (for launch_persistent_context)
752    pub fn traces_dir(mut self, path: String) -> Self {
753        self.traces_dir = Some(path);
754        self
755    }
756
757    /// Check if strict selectors mode is enabled
758    pub fn strict_selectors(mut self, enabled: bool) -> Self {
759        self.strict_selectors = Some(enabled);
760        self
761    }
762
763    /// Emulates 'prefers-reduced-motion' media feature
764    pub fn reduced_motion(mut self, value: String) -> Self {
765        self.reduced_motion = Some(value);
766        self
767    }
768
769    /// Emulates 'forced-colors' media feature
770    pub fn forced_colors(mut self, value: String) -> Self {
771        self.forced_colors = Some(value);
772        self
773    }
774
775    /// Whether to allow sites to register Service workers ("allow" | "block")
776    pub fn service_workers(mut self, value: String) -> Self {
777        self.service_workers = Some(value);
778        self
779    }
780
781    /// Records the context's network traffic as a HAR file at the path in
782    /// `record_har`.
783    ///
784    /// The file is written when the context closes, by
785    /// [`BrowserContext::close`](crate::protocol::BrowserContext::close), so
786    /// close the context to get it: a browser closed without closing the
787    /// context first writes nothing. A `.zip` path stores resource bodies as
788    /// separate entries; any other path embeds them in the `.har`.
789    ///
790    /// # Errors
791    ///
792    /// Creating the context fails with
793    /// [`Error::InvalidArgument`](crate::Error::InvalidArgument), before any
794    /// context exists, if the `content` or `mode` string is not one
795    /// Playwright accepts.
796    ///
797    /// If the HAR cannot be written, `close` returns the error and leaves the
798    /// context open; a second `close` closes it.
799    ///
800    /// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-record-har>
801    pub fn record_har(mut self, record_har: RecordHar) -> Self {
802        self.record_har = Some(record_har);
803        self
804    }
805
806    /// Sets options for recording video
807    pub fn record_video(mut self, record_video: RecordVideo) -> Self {
808        self.record_video = Some(record_video);
809        self
810    }
811
812    /// Builds the BrowserContextOptions
813    pub fn build(self) -> BrowserContextOptions {
814        BrowserContextOptions {
815            viewport: self.viewport,
816            no_viewport: self.no_viewport,
817            screen: self.screen,
818            user_agent: self.user_agent,
819            locale: self.locale,
820            timezone_id: self.timezone_id,
821            geolocation: self.geolocation,
822            http_credentials: self.http_credentials,
823            permissions: self.permissions,
824            proxy: self.proxy,
825            color_scheme: self.color_scheme,
826            has_touch: self.has_touch,
827            is_mobile: self.is_mobile,
828            javascript_enabled: self.javascript_enabled,
829            offline: self.offline,
830            accept_downloads: self.accept_downloads,
831            bypass_csp: self.bypass_csp,
832            ignore_https_errors: self.ignore_https_errors,
833            device_scale_factor: self.device_scale_factor,
834            extra_http_headers: self.extra_http_headers,
835            base_url: self.base_url,
836            storage_state: self.storage_state,
837            storage_state_path: self.storage_state_path,
838            // Launch options
839            args: self.args,
840            channel: self.channel,
841            chromium_sandbox: self.chromium_sandbox,
842            #[allow(deprecated)]
843            devtools: self.devtools,
844            downloads_path: self.downloads_path,
845            executable_path: self.executable_path,
846            firefox_user_prefs: self.firefox_user_prefs,
847            headless: self.headless,
848            ignore_default_args: self.ignore_default_args,
849            slow_mo: self.slow_mo,
850            timeout: self.timeout,
851            traces_dir: self.traces_dir,
852            strict_selectors: self.strict_selectors,
853            reduced_motion: self.reduced_motion,
854            forced_colors: self.forced_colors,
855            service_workers: self.service_workers,
856            record_har: self.record_har,
857            record_video: self.record_video,
858        }
859    }
860}
861
862#[cfg(test)]
863mod tests {
864    use super::*;
865
866    #[test]
867    fn test_browser_context_options_ignore_default_args_bool_serialization() {
868        let options = BrowserContextOptions::builder()
869            .ignore_default_args(IgnoreDefaultArgs::Bool(true))
870            .build();
871
872        let value = serde_json::to_value(&options).unwrap();
873        assert_eq!(value["ignoreDefaultArgs"], serde_json::json!(true));
874    }
875
876    #[test]
877    fn test_browser_context_options_ignore_default_args_array_serialization() {
878        let options = BrowserContextOptions::builder()
879            .ignore_default_args(IgnoreDefaultArgs::Array(vec!["--foo".to_string()]))
880            .build();
881
882        let value = serde_json::to_value(&options).unwrap();
883        assert_eq!(value["ignoreDefaultArgs"], serde_json::json!(["--foo"]));
884    }
885
886    #[test]
887    fn test_browser_context_options_ignore_default_args_absent() {
888        let options = BrowserContextOptions::builder().build();
889
890        let value = serde_json::to_value(&options).unwrap();
891        assert!(value.get("ignoreDefaultArgs").is_none());
892    }
893
894    #[test]
895    fn every_key_sent_is_a_parameter_the_driver_declares() {
896        use crate::protocol_spec::{
897            BROWSER, BROWSER_TYPE, command_parameters, keys, mixin_properties, nested_properties,
898        };
899
900        // A literal with no `..Default::default()`, so a new field fails to
901        // compile here until it is added and checked.
902        let options = BrowserContextOptions {
903            viewport: Some(Viewport {
904                width: 1,
905                height: 1,
906            }),
907            no_viewport: Some(false),
908            screen: Some(Viewport {
909                width: 1,
910                height: 1,
911            }),
912            user_agent: Some(String::new()),
913            locale: Some(String::new()),
914            timezone_id: Some(String::new()),
915            geolocation: Some(crate::protocol::Geolocation {
916                latitude: 0.0,
917                longitude: 0.0,
918                accuracy: None,
919            }),
920            http_credentials: Some(vec![crate::protocol::HttpCredentials::new("u", "p")]),
921            permissions: Some(vec![]),
922            proxy: Some(crate::protocol::ProxySettings::new("http://proxy")),
923            color_scheme: Some(String::new()),
924            has_touch: Some(false),
925            is_mobile: Some(false),
926            javascript_enabled: Some(false),
927            offline: Some(false),
928            accept_downloads: Some(AcceptDownloads::Accept),
929            bypass_csp: Some(false),
930            ignore_https_errors: Some(false),
931            device_scale_factor: Some(1.0),
932            extra_http_headers: Some(HashMap::new()),
933            base_url: Some(String::new()),
934            storage_state: Some(crate::protocol::StorageState::default()),
935            storage_state_path: Some(String::new()),
936            args: Some(vec![]),
937            channel: Some(String::new()),
938            chromium_sandbox: Some(false),
939            #[allow(deprecated)]
940            devtools: Some(false),
941            downloads_path: Some(String::new()),
942            executable_path: Some(String::new()),
943            firefox_user_prefs: Some(HashMap::new()),
944            headless: Some(true),
945            ignore_default_args: Some(IgnoreDefaultArgs::Array(vec![])),
946            slow_mo: Some(0.0),
947            timeout: Some(0.0),
948            traces_dir: Some(String::new()),
949            strict_selectors: Some(false),
950            reduced_motion: Some(String::new()),
951            forced_colors: Some(String::new()),
952            service_workers: Some(String::new()),
953            record_har: Some(RecordHar::new("h.har")),
954            record_video: Some(
955                RecordVideo::new("videos")
956                    .size(Viewport {
957                        width: 1,
958                        height: 1,
959                    })
960                    .fps(30)
961                    .show_actions(
962                        crate::protocol::ShowActionsOptions::default()
963                            .duration(1.0)
964                            .style(crate::protocol::ShowActionsStyle::default().title("x")),
965                    ),
966            ),
967        };
968
969        let value = serde_json::to_value(&options).unwrap();
970        let video = &value["recordVideo"];
971        assert!(keys(video).is_subset(&nested_properties("ContextOptions", "recordVideo")));
972        assert!(keys(&video["showActions"]).is_subset(&mixin_properties("ShowActionsOptions")));
973        let sent = keys(&value);
974        let mut declared = command_parameters(BROWSER, "newContext");
975        declared.extend(command_parameters(BROWSER_TYPE, "launchPersistentContext"));
976        let undeclared: Vec<&str> = sent
977            .iter()
978            .filter(|key| !declared.contains(*key))
979            .map(String::as_str)
980            .collect();
981        // Two of these never reach the driver as parameters: storageStatePath
982        // is read and sent inline as storageState, and timeout moves into the
983        // message metadata, where the server takes a call's deadline from.
984        assert_eq!(undeclared, ["storageStatePath", "timeout"]);
985    }
986
987    #[test]
988    fn acronym_options_use_the_driver_spelling() {
989        let options = BrowserContextOptions::builder()
990            .javascript_enabled(false)
991            .bypass_csp(true)
992            .ignore_https_errors(true)
993            .base_url("http://localhost".to_string())
994            .build();
995
996        let value = serde_json::to_value(&options).unwrap();
997        assert_eq!(value["javaScriptEnabled"], serde_json::json!(false));
998        assert_eq!(value["bypassCSP"], serde_json::json!(true));
999        assert_eq!(value["ignoreHTTPSErrors"], serde_json::json!(true));
1000        assert_eq!(value["baseURL"], serde_json::json!("http://localhost"));
1001        for wrong in [
1002            "javascriptEnabled",
1003            "bypassCsp",
1004            "ignoreHttpsErrors",
1005            "baseUrl",
1006        ] {
1007            assert!(value.get(wrong).is_none(), "sent the unknown key {wrong}");
1008        }
1009    }
1010
1011    #[test]
1012    fn extra_http_headers_serialize_as_name_value_pairs() {
1013        let headers = HashMap::from([
1014            ("X-One".to_string(), "1".to_string()),
1015            ("X-Two".to_string(), "2".to_string()),
1016        ]);
1017        let options = BrowserContextOptions::builder()
1018            .extra_http_headers(headers)
1019            .build();
1020
1021        let value = serde_json::to_value(&options).unwrap();
1022        assert!(value.get("extraHttpHeaders").is_none());
1023        let mut pairs = value["extraHTTPHeaders"]
1024            .as_array()
1025            .expect("extraHTTPHeaders is a list")
1026            .clone();
1027        pairs.sort_by_key(|pair| pair["name"].as_str().unwrap_or_default().to_string());
1028        assert_eq!(
1029            pairs,
1030            vec![
1031                serde_json::json!({"name": "X-One", "value": "1"}),
1032                serde_json::json!({"name": "X-Two", "value": "2"}),
1033            ]
1034        );
1035    }
1036
1037    #[test]
1038    fn test_accept_downloads_serializes_as_protocol_string() {
1039        for (variant, expected) in [
1040            (AcceptDownloads::Accept, "accept"),
1041            (AcceptDownloads::Deny, "deny"),
1042            (AcceptDownloads::Internal, "internal"),
1043        ] {
1044            let options = BrowserContextOptions::builder()
1045                .accept_downloads(variant)
1046                .build();
1047            let value = serde_json::to_value(&options).unwrap();
1048            assert_eq!(value["acceptDownloads"], serde_json::json!(expected));
1049        }
1050    }
1051
1052    #[test]
1053    fn test_accept_downloads_bool_compatibility() {
1054        let opts = BrowserContextOptions::builder()
1055            .accept_downloads(true)
1056            .build();
1057        assert_eq!(opts.accept_downloads, Some(AcceptDownloads::Accept));
1058
1059        let opts = BrowserContextOptions::builder()
1060            .accept_downloads(false)
1061            .build();
1062        assert_eq!(opts.accept_downloads, Some(AcceptDownloads::Deny));
1063    }
1064}