Skip to main content

playwright_rs/api/
launch_options.rs

1// Launch options for BrowserType::launch()
2//
3// This module provides options for launching browsers, matching the Playwright API exactly.
4// See: https://playwright.dev/docs/api/class-browsertype#browser-type-launch
5
6use crate::protocol::proxy::ProxySettings;
7use serde::{Deserialize, Serialize};
8use serde_json::{Value, json};
9use std::collections::HashMap;
10
11/// Options for launching a browser
12///
13/// All options are optional and will use Playwright's defaults if not specified.
14#[derive(Debug, Clone, Default, Serialize, Deserialize)]
15#[serde(rename_all = "camelCase")]
16pub struct LaunchOptions {
17    /// Additional arguments to pass to browser instance
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub args: Option<Vec<String>>,
20
21    /// Directory to write artifacts (traces, video, downloads) to.
22    /// Surfaces Playwright's `artifactsDir` option; if unset, Playwright
23    /// chooses a temp directory.
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub artifacts_dir: Option<String>,
26
27    /// Browser distribution channel (e.g., "chrome", "msedge")
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub channel: Option<String>,
30
31    /// Enable Chromium sandboxing (default: false on Linux)
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub chromium_sandbox: Option<bool>,
34
35    /// Auto-open DevTools (deprecated, default: false)
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub devtools: Option<bool>,
38
39    /// Directory to save downloads
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub downloads_path: Option<String>,
42
43    /// Environment variables for browser process
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub env: Option<HashMap<String, String>>,
46
47    /// Path to custom browser executable
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub executable_path: Option<String>,
50
51    /// Firefox user preferences (Firefox only)
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub firefox_user_prefs: Option<HashMap<String, Value>>,
54
55    /// Close browser on SIGHUP (default: true)
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub handle_sighup: Option<bool>,
58
59    /// Close browser on SIGINT/Ctrl-C (default: true)
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub handle_sigint: Option<bool>,
62
63    /// Close browser on SIGTERM (default: true)
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub handle_sigterm: Option<bool>,
66
67    /// Run in headless mode (default: true unless devtools=true)
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub headless: Option<bool>,
70
71    /// Filter or disable default browser arguments
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub ignore_default_args: Option<IgnoreDefaultArgs>,
74
75    /// Network proxy settings
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub proxy: Option<ProxySettings>,
78
79    /// Slow down operations by N milliseconds
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub slow_mo: Option<f64>,
82
83    /// Timeout for browser launch in milliseconds (default: DEFAULT_TIMEOUT_MS)
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub timeout: Option<f64>,
86
87    /// Directory to save traces
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub traces_dir: Option<String>,
90}
91
92/// Filter or disable default browser arguments
93#[derive(Debug, Clone, Serialize, Deserialize)]
94#[serde(untagged)]
95pub enum IgnoreDefaultArgs {
96    /// Ignore all default arguments
97    Bool(bool),
98    /// Filter specific default arguments
99    Array(Vec<String>),
100}
101
102impl LaunchOptions {
103    /// Creates a new LaunchOptions with default values
104    pub fn new() -> Self {
105        Self::default()
106    }
107
108    /// Set additional arguments to pass to browser instance
109    pub fn args(mut self, args: Vec<String>) -> Self {
110        self.args = Some(args);
111        self
112    }
113
114    /// Set the directory to write artifacts (traces, video, downloads) to
115    pub fn artifacts_dir(mut self, path: String) -> Self {
116        self.artifacts_dir = Some(path);
117        self
118    }
119
120    /// Set browser distribution channel
121    pub fn channel(mut self, channel: String) -> Self {
122        self.channel = Some(channel);
123        self
124    }
125
126    /// Enable or disable Chromium sandboxing
127    pub fn chromium_sandbox(mut self, enabled: bool) -> Self {
128        self.chromium_sandbox = Some(enabled);
129        self
130    }
131
132    /// Auto-open DevTools
133    pub fn devtools(mut self, enabled: bool) -> Self {
134        self.devtools = Some(enabled);
135        self
136    }
137
138    /// Set directory to save downloads
139    pub fn downloads_path(mut self, path: String) -> Self {
140        self.downloads_path = Some(path);
141        self
142    }
143
144    /// Set environment variables for browser process
145    pub fn env(mut self, env: HashMap<String, String>) -> Self {
146        self.env = Some(env);
147        self
148    }
149
150    /// Set path to custom browser executable
151    pub fn executable_path(mut self, path: String) -> Self {
152        self.executable_path = Some(path);
153        self
154    }
155
156    /// Set Firefox user preferences (Firefox only)
157    pub fn firefox_user_prefs(mut self, prefs: HashMap<String, Value>) -> Self {
158        self.firefox_user_prefs = Some(prefs);
159        self
160    }
161
162    /// Set whether to close browser on SIGHUP
163    pub fn handle_sighup(mut self, enabled: bool) -> Self {
164        self.handle_sighup = Some(enabled);
165        self
166    }
167
168    /// Set whether to close browser on SIGINT/Ctrl-C
169    pub fn handle_sigint(mut self, enabled: bool) -> Self {
170        self.handle_sigint = Some(enabled);
171        self
172    }
173
174    /// Set whether to close browser on SIGTERM
175    pub fn handle_sigterm(mut self, enabled: bool) -> Self {
176        self.handle_sigterm = Some(enabled);
177        self
178    }
179
180    /// Run in headless mode
181    pub fn headless(mut self, enabled: bool) -> Self {
182        self.headless = Some(enabled);
183        self
184    }
185
186    /// Filter or disable default browser arguments
187    pub fn ignore_default_args(mut self, args: IgnoreDefaultArgs) -> Self {
188        self.ignore_default_args = Some(args);
189        self
190    }
191
192    /// Set network proxy settings
193    pub fn proxy(mut self, proxy: ProxySettings) -> Self {
194        self.proxy = Some(proxy);
195        self
196    }
197
198    /// Slow down operations by N milliseconds
199    pub fn slow_mo(mut self, ms: f64) -> Self {
200        self.slow_mo = Some(ms);
201        self
202    }
203
204    /// Set timeout for browser launch in milliseconds
205    pub fn timeout(mut self, ms: f64) -> Self {
206        self.timeout = Some(ms);
207        self
208    }
209
210    /// Set directory to save traces
211    pub fn traces_dir(mut self, path: String) -> Self {
212        self.traces_dir = Some(path);
213        self
214    }
215
216    /// Normalize options for protocol transmission
217    ///
218    /// This performs transformations required by the Playwright protocol:
219    /// 1. Set default timeout if not specified (required in 1.56.1+)
220    /// 2. Convert env HashMap to array of {name, value} objects
221    /// 3. Convert bool ignoreDefaultArgs to ignoreAllDefaultArgs
222    ///
223    /// This matches the behavior of playwright-python's parameter normalization.
224    pub(crate) fn normalize(self) -> Value {
225        let mut value =
226            serde_json::to_value(&self).expect("serialization of LaunchOptions cannot fail");
227
228        // Set default timeout if not specified
229        // Note: In Playwright 1.56.1+, timeout became a required parameter
230        if value.get("timeout").is_none() {
231            value["timeout"] = json!(crate::DEFAULT_TIMEOUT_MS);
232        }
233
234        // Convert env HashMap to array of {name, value} objects
235        if let Some(env_map) = value.get_mut("env")
236            && let Some(map) = env_map.as_object()
237        {
238            let env_array: Vec<_> = map
239                .iter()
240                .map(|(k, v)| json!({"name": k, "value": v}))
241                .collect();
242            *env_map = json!(env_array);
243        }
244
245        // Convert bool ignoreDefaultArgs to ignoreAllDefaultArgs
246        if let Some(ignore) = value.get("ignoreDefaultArgs")
247            && let Some(b) = ignore.as_bool()
248        {
249            if b {
250                value["ignoreAllDefaultArgs"] = json!(true);
251            }
252            value
253                .as_object_mut()
254                .expect("params is a JSON object")
255                .remove("ignoreDefaultArgs");
256        }
257
258        value
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265
266    #[test]
267    fn test_launch_options_default() {
268        let opts = LaunchOptions::default();
269        assert!(opts.headless.is_none());
270        assert!(opts.args.is_none());
271    }
272
273    #[test]
274    fn test_launch_options_builder() {
275        let opts = LaunchOptions::default()
276            .headless(false)
277            .slow_mo(100.0)
278            .args(vec!["--no-sandbox".to_string()]);
279
280        assert_eq!(opts.headless, Some(false));
281        assert_eq!(opts.slow_mo, Some(100.0));
282        assert_eq!(opts.args, Some(vec!["--no-sandbox".to_string()]));
283    }
284
285    #[test]
286    fn test_launch_options_normalize_env() {
287        let opts = LaunchOptions::default().env(HashMap::from([
288            ("FOO".to_string(), "bar".to_string()),
289            ("BAZ".to_string(), "qux".to_string()),
290        ]));
291
292        let normalized = opts.normalize();
293
294        // Verify env is converted to array format
295        assert!(normalized["env"].is_array());
296        let env_array = normalized["env"].as_array().unwrap();
297        assert_eq!(env_array.len(), 2);
298
299        // Check that both env vars are present (order may vary)
300        let names: Vec<_> = env_array
301            .iter()
302            .map(|v| v["name"].as_str().unwrap())
303            .collect();
304        assert!(names.contains(&"FOO"));
305        assert!(names.contains(&"BAZ"));
306    }
307
308    #[test]
309    fn test_launch_options_normalize_ignore_default_args_bool() {
310        let opts = LaunchOptions::default().ignore_default_args(IgnoreDefaultArgs::Bool(true));
311
312        let normalized = opts.normalize();
313
314        // Verify bool is converted to ignoreAllDefaultArgs
315        assert!(normalized["ignoreAllDefaultArgs"].as_bool().unwrap());
316        assert!(normalized.get("ignoreDefaultArgs").is_none());
317    }
318
319    #[test]
320    fn test_launch_options_normalize_ignore_default_args_array() {
321        let opts = LaunchOptions::default()
322            .ignore_default_args(IgnoreDefaultArgs::Array(vec!["--foo".to_string()]));
323
324        let normalized = opts.normalize();
325
326        // Verify array is preserved
327        assert!(normalized["ignoreDefaultArgs"].is_array());
328        assert_eq!(
329            normalized["ignoreDefaultArgs"][0].as_str().unwrap(),
330            "--foo"
331        );
332    }
333
334    #[test]
335    fn test_proxy_settings() {
336        let proxy = ProxySettings {
337            server: "http://proxy:8080".to_string(),
338            bypass: Some("localhost,127.0.0.1".to_string()),
339            username: Some("user".to_string()),
340            password: Some("pass".to_string()),
341        };
342
343        let opts = LaunchOptions::default().proxy(proxy);
344        assert!(opts.proxy.is_some());
345    }
346
347    #[test]
348    fn test_builder_pattern_chaining() {
349        let opts = LaunchOptions::new()
350            .headless(true)
351            .slow_mo(50.0)
352            .timeout(60000.0)
353            .args(vec![
354                "--no-sandbox".to_string(),
355                "--disable-gpu".to_string(),
356            ])
357            .channel("chrome".to_string());
358
359        assert_eq!(opts.headless, Some(true));
360        assert_eq!(opts.slow_mo, Some(50.0));
361        assert_eq!(opts.timeout, Some(60000.0));
362        assert_eq!(opts.args.as_ref().unwrap().len(), 2);
363        assert_eq!(opts.channel, Some("chrome".to_string()));
364    }
365}