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