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")]
16#[non_exhaustive]
17pub struct LaunchOptions {
18    /// Additional arguments to pass to browser instance
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub args: Option<Vec<String>>,
21
22    /// Directory to write artifacts (traces, video, downloads) to.
23    /// Surfaces Playwright's `artifactsDir` option; if unset, Playwright
24    /// chooses a temp directory.
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub artifacts_dir: Option<String>,
27
28    /// Browser distribution channel (e.g., "chrome", "msedge")
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub channel: Option<String>,
31
32    /// Enable Chromium sandboxing (default: false on Linux)
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub chromium_sandbox: Option<bool>,
35
36    /// Auto-open DevTools.
37    ///
38    /// Playwright 1.58 removed this option. The driver drops undeclared
39    /// parameters without erroring, so setting it has no effect and no longer
40    /// reaches the wire. Nor does it imply a headed browser any more. For
41    /// Chromium, pass `--auto-open-devtools-for-tabs` in `args` and set
42    /// `headless(false)`.
43    #[deprecated(
44        since = "0.20.0",
45        note = "removed from the protocol in Playwright 1.58; setting it has no effect. For Chromium, pass `--auto-open-devtools-for-tabs` in `args` instead"
46    )]
47    #[serde(skip_serializing)]
48    pub devtools: Option<bool>,
49
50    /// Directory to save downloads
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub downloads_path: Option<String>,
53
54    /// Environment variables for browser process
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub env: Option<HashMap<String, String>>,
57
58    /// Path to custom browser executable
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub executable_path: Option<String>,
61
62    /// Firefox user preferences (Firefox only)
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub firefox_user_prefs: Option<HashMap<String, Value>>,
65
66    /// Close browser on SIGHUP (default: true)
67    #[serde(rename = "handleSIGHUP", skip_serializing_if = "Option::is_none")]
68    pub handle_sighup: Option<bool>,
69
70    /// Close browser on SIGINT/Ctrl-C (default: true)
71    #[serde(rename = "handleSIGINT", skip_serializing_if = "Option::is_none")]
72    pub handle_sigint: Option<bool>,
73
74    /// Close browser on SIGTERM (default: true)
75    #[serde(rename = "handleSIGTERM", skip_serializing_if = "Option::is_none")]
76    pub handle_sigterm: Option<bool>,
77
78    /// Run in headless mode (default: true)
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub headless: Option<bool>,
81
82    /// Filter or disable default browser arguments
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub ignore_default_args: Option<IgnoreDefaultArgs>,
85
86    /// Network proxy settings
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub proxy: Option<ProxySettings>,
89
90    /// Slow down operations by N milliseconds
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub slow_mo: Option<f64>,
93
94    /// Timeout for browser launch in milliseconds (default:
95    /// [`DEFAULT_LAUNCH_TIMEOUT_MS`](crate::DEFAULT_LAUNCH_TIMEOUT_MS))
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub timeout: Option<f64>,
98
99    /// Directory to save traces
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub traces_dir: Option<String>,
102}
103
104/// Filter or disable default browser arguments
105#[derive(Debug, Clone, Serialize, Deserialize)]
106#[serde(untagged)]
107#[non_exhaustive]
108pub enum IgnoreDefaultArgs {
109    /// Ignore all default arguments
110    Bool(bool),
111    /// Filter specific default arguments
112    Array(Vec<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 the directory to write artifacts (traces, video, downloads) to
128    pub fn artifacts_dir(mut self, path: String) -> Self {
129        self.artifacts_dir = Some(path);
130        self
131    }
132
133    /// Set browser distribution channel
134    pub fn channel(mut self, channel: String) -> Self {
135        self.channel = Some(channel);
136        self
137    }
138
139    /// Enable or disable Chromium sandboxing
140    pub fn chromium_sandbox(mut self, enabled: bool) -> Self {
141        self.chromium_sandbox = Some(enabled);
142        self
143    }
144
145    /// Auto-open DevTools. Playwright 1.58 removed the option, so this has no effect.
146    #[deprecated(
147        since = "0.20.0",
148        note = "removed from the protocol in Playwright 1.58; setting it has no effect. For Chromium, pass `--auto-open-devtools-for-tabs` in `args` instead"
149    )]
150    pub fn devtools(mut self, enabled: bool) -> Self {
151        #[allow(deprecated)]
152        {
153            self.devtools = Some(enabled);
154        }
155        self
156    }
157
158    /// Set directory to save downloads
159    pub fn downloads_path(mut self, path: String) -> Self {
160        self.downloads_path = Some(path);
161        self
162    }
163
164    /// Set environment variables for browser process
165    pub fn env(mut self, env: HashMap<String, String>) -> Self {
166        self.env = Some(env);
167        self
168    }
169
170    /// Set path to custom browser executable
171    pub fn executable_path(mut self, path: String) -> Self {
172        self.executable_path = Some(path);
173        self
174    }
175
176    /// Set Firefox user preferences (Firefox only)
177    pub fn firefox_user_prefs(mut self, prefs: HashMap<String, Value>) -> Self {
178        self.firefox_user_prefs = Some(prefs);
179        self
180    }
181
182    /// Set whether to close browser on SIGHUP
183    pub fn handle_sighup(mut self, enabled: bool) -> Self {
184        self.handle_sighup = Some(enabled);
185        self
186    }
187
188    /// Set whether to close browser on SIGINT/Ctrl-C
189    pub fn handle_sigint(mut self, enabled: bool) -> Self {
190        self.handle_sigint = Some(enabled);
191        self
192    }
193
194    /// Set whether to close browser on SIGTERM
195    pub fn handle_sigterm(mut self, enabled: bool) -> Self {
196        self.handle_sigterm = Some(enabled);
197        self
198    }
199
200    /// Run in headless mode
201    pub fn headless(mut self, enabled: bool) -> Self {
202        self.headless = Some(enabled);
203        self
204    }
205
206    /// Filter or disable default browser arguments
207    pub fn ignore_default_args(mut self, args: IgnoreDefaultArgs) -> Self {
208        self.ignore_default_args = Some(args);
209        self
210    }
211
212    /// Set network proxy settings
213    pub fn proxy(mut self, proxy: ProxySettings) -> Self {
214        self.proxy = Some(proxy);
215        self
216    }
217
218    /// Slow down operations by N milliseconds
219    pub fn slow_mo(mut self, ms: f64) -> Self {
220        self.slow_mo = Some(ms);
221        self
222    }
223
224    /// Set timeout for browser launch in milliseconds
225    pub fn timeout(mut self, ms: f64) -> Self {
226        self.timeout = Some(ms);
227        self
228    }
229
230    /// Set directory to save traces
231    pub fn traces_dir(mut self, path: String) -> Self {
232        self.traces_dir = Some(path);
233        self
234    }
235
236    /// Normalize options for protocol transmission
237    ///
238    /// This performs transformations required by the Playwright protocol:
239    /// 1. Set a default timeout if none is given. `Connection::send_message`
240    ///    moves it into the message metadata, and without one the server
241    ///    gives the launch no deadline.
242    /// 2. Convert env HashMap to array of {name, value} objects
243    /// 3. Convert bool ignoreDefaultArgs to ignoreAllDefaultArgs
244    ///
245    /// This matches the behavior of playwright-python's parameter normalization.
246    pub(crate) fn normalize(self) -> Value {
247        let mut value =
248            serde_json::to_value(&self).expect("serialization of LaunchOptions cannot fail");
249
250        // A launch with no timeout has no deadline on the server.
251        if value.get("timeout").is_none() {
252            value["timeout"] = json!(crate::DEFAULT_LAUNCH_TIMEOUT_MS);
253        }
254
255        // Convert env HashMap to array of {name, value} objects
256        if let Some(env_map) = value.get_mut("env")
257            && let Some(map) = env_map.as_object()
258        {
259            let env_array: Vec<_> = map
260                .iter()
261                .map(|(k, v)| json!({"name": k, "value": v}))
262                .collect();
263            *env_map = json!(env_array);
264        }
265
266        // Convert bool ignoreDefaultArgs to ignoreAllDefaultArgs
267        if let Some(ignore) = value.get("ignoreDefaultArgs")
268            && let Some(b) = ignore.as_bool()
269        {
270            if b {
271                value["ignoreAllDefaultArgs"] = json!(true);
272            }
273            value
274                .as_object_mut()
275                .expect("params is a JSON object")
276                .remove("ignoreDefaultArgs");
277        }
278
279        value
280    }
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286
287    #[test]
288    fn test_launch_options_default() {
289        let opts = LaunchOptions::default();
290        assert!(opts.headless.is_none());
291        assert!(opts.args.is_none());
292    }
293
294    #[test]
295    fn every_key_sent_is_a_parameter_the_driver_declares() {
296        use crate::protocol_spec::{BROWSER_TYPE, command_parameters, keys};
297
298        // A literal with no `..Default::default()`, so a new field fails to
299        // compile here until it is added and checked.
300        let options = LaunchOptions {
301            args: Some(vec![]),
302            artifacts_dir: Some(String::new()),
303            channel: Some(String::new()),
304            chromium_sandbox: Some(false),
305            #[allow(deprecated)]
306            devtools: Some(false),
307            downloads_path: Some(String::new()),
308            env: Some(HashMap::new()),
309            executable_path: Some(String::new()),
310            firefox_user_prefs: Some(HashMap::new()),
311            handle_sighup: Some(false),
312            handle_sigint: Some(false),
313            handle_sigterm: Some(false),
314            headless: Some(true),
315            ignore_default_args: Some(IgnoreDefaultArgs::Array(vec![])),
316            proxy: Some(ProxySettings::new("http://proxy")),
317            slow_mo: Some(0.0),
318            timeout: Some(0.0),
319            traces_dir: Some(String::new()),
320        };
321
322        let sent = keys(&options.normalize());
323        let declared = command_parameters(BROWSER_TYPE, "launch");
324        let undeclared: Vec<&str> = sent
325            .iter()
326            .filter(|key| !declared.contains(*key))
327            .map(String::as_str)
328            .collect();
329        // timeout moves into the message metadata, where the server takes a
330        // call's deadline from.
331        assert_eq!(undeclared, ["timeout"]);
332    }
333
334    #[test]
335    fn a_launch_without_a_timeout_gets_three_minutes() {
336        assert_eq!(
337            LaunchOptions::default().normalize()["timeout"],
338            json!(180000.0)
339        );
340        assert_eq!(
341            LaunchOptions::default().timeout(5000.0).normalize()["timeout"],
342            json!(5000.0)
343        );
344    }
345
346    #[test]
347    fn devtools_still_deserializes_but_is_never_sent() {
348        let options: LaunchOptions = serde_json::from_value(json!({"devtools": true})).unwrap();
349        #[allow(deprecated)]
350        let devtools = options.devtools;
351        assert_eq!(devtools, Some(true));
352        assert!(options.normalize().get("devtools").is_none());
353    }
354
355    #[test]
356    fn signal_options_use_the_driver_spelling() {
357        let value = LaunchOptions::default()
358            .handle_sighup(false)
359            .handle_sigint(true)
360            .handle_sigterm(false)
361            .normalize();
362        assert_eq!(value["handleSIGHUP"], json!(false));
363        assert_eq!(value["handleSIGINT"], json!(true));
364        assert_eq!(value["handleSIGTERM"], json!(false));
365    }
366
367    #[test]
368    fn test_launch_options_builder() {
369        let opts = LaunchOptions::default()
370            .headless(false)
371            .slow_mo(100.0)
372            .args(vec!["--no-sandbox".to_string()]);
373
374        assert_eq!(opts.headless, Some(false));
375        assert_eq!(opts.slow_mo, Some(100.0));
376        assert_eq!(opts.args, Some(vec!["--no-sandbox".to_string()]));
377    }
378
379    #[test]
380    fn test_launch_options_normalize_env() {
381        let opts = LaunchOptions::default().env(HashMap::from([
382            ("FOO".to_string(), "bar".to_string()),
383            ("BAZ".to_string(), "qux".to_string()),
384        ]));
385
386        let normalized = opts.normalize();
387
388        // Verify env is converted to array format
389        assert!(normalized["env"].is_array());
390        let env_array = normalized["env"].as_array().unwrap();
391        assert_eq!(env_array.len(), 2);
392
393        // Check that both env vars are present (order may vary)
394        let names: Vec<_> = env_array
395            .iter()
396            .map(|v| v["name"].as_str().unwrap())
397            .collect();
398        assert!(names.contains(&"FOO"));
399        assert!(names.contains(&"BAZ"));
400    }
401
402    #[test]
403    fn test_launch_options_normalize_ignore_default_args_bool() {
404        let opts = LaunchOptions::default().ignore_default_args(IgnoreDefaultArgs::Bool(true));
405
406        let normalized = opts.normalize();
407
408        // Verify bool is converted to ignoreAllDefaultArgs
409        assert!(normalized["ignoreAllDefaultArgs"].as_bool().unwrap());
410        assert!(normalized.get("ignoreDefaultArgs").is_none());
411    }
412
413    #[test]
414    fn test_launch_options_normalize_ignore_default_args_array() {
415        let opts = LaunchOptions::default()
416            .ignore_default_args(IgnoreDefaultArgs::Array(vec!["--foo".to_string()]));
417
418        let normalized = opts.normalize();
419
420        // Verify array is preserved
421        assert!(normalized["ignoreDefaultArgs"].is_array());
422        assert_eq!(
423            normalized["ignoreDefaultArgs"][0].as_str().unwrap(),
424            "--foo"
425        );
426    }
427
428    #[test]
429    fn test_proxy_settings() {
430        let proxy = ProxySettings {
431            server: "http://proxy:8080".to_string(),
432            bypass: Some("localhost,127.0.0.1".to_string()),
433            username: Some("user".to_string()),
434            password: Some("pass".to_string()),
435        };
436
437        let opts = LaunchOptions::default().proxy(proxy);
438        assert!(opts.proxy.is_some());
439    }
440
441    #[test]
442    fn test_builder_pattern_chaining() {
443        let opts = LaunchOptions::new()
444            .headless(true)
445            .slow_mo(50.0)
446            .timeout(60000.0)
447            .args(vec![
448                "--no-sandbox".to_string(),
449                "--disable-gpu".to_string(),
450            ])
451            .channel("chrome".to_string());
452
453        assert_eq!(opts.headless, Some(true));
454        assert_eq!(opts.slow_mo, Some(50.0));
455        assert_eq!(opts.timeout, Some(60000.0));
456        assert_eq!(opts.args.as_ref().unwrap().len(), 2);
457        assert_eq!(opts.channel, Some("chrome".to_string()));
458    }
459}