1use crate::protocol::proxy::ProxySettings;
7use serde::{Deserialize, Serialize};
8use serde_json::{Value, json};
9use std::collections::HashMap;
10
11#[derive(Debug, Clone, Default, Serialize, Deserialize)]
15#[serde(rename_all = "camelCase")]
16#[non_exhaustive]
17pub struct LaunchOptions {
18 #[serde(skip_serializing_if = "Option::is_none")]
20 pub args: Option<Vec<String>>,
21
22 #[serde(skip_serializing_if = "Option::is_none")]
26 pub artifacts_dir: Option<String>,
27
28 #[serde(skip_serializing_if = "Option::is_none")]
30 pub channel: Option<String>,
31
32 #[serde(skip_serializing_if = "Option::is_none")]
34 pub chromium_sandbox: Option<bool>,
35
36 #[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 #[serde(skip_serializing_if = "Option::is_none")]
52 pub downloads_path: Option<String>,
53
54 #[serde(skip_serializing_if = "Option::is_none")]
56 pub env: Option<HashMap<String, String>>,
57
58 #[serde(skip_serializing_if = "Option::is_none")]
60 pub executable_path: Option<String>,
61
62 #[serde(skip_serializing_if = "Option::is_none")]
64 pub firefox_user_prefs: Option<HashMap<String, Value>>,
65
66 #[serde(rename = "handleSIGHUP", skip_serializing_if = "Option::is_none")]
68 pub handle_sighup: Option<bool>,
69
70 #[serde(rename = "handleSIGINT", skip_serializing_if = "Option::is_none")]
72 pub handle_sigint: Option<bool>,
73
74 #[serde(rename = "handleSIGTERM", skip_serializing_if = "Option::is_none")]
76 pub handle_sigterm: Option<bool>,
77
78 #[serde(skip_serializing_if = "Option::is_none")]
80 pub headless: Option<bool>,
81
82 #[serde(skip_serializing_if = "Option::is_none")]
84 pub ignore_default_args: Option<IgnoreDefaultArgs>,
85
86 #[serde(skip_serializing_if = "Option::is_none")]
88 pub proxy: Option<ProxySettings>,
89
90 #[serde(skip_serializing_if = "Option::is_none")]
92 pub slow_mo: Option<f64>,
93
94 #[serde(skip_serializing_if = "Option::is_none")]
97 pub timeout: Option<f64>,
98
99 #[serde(skip_serializing_if = "Option::is_none")]
101 pub traces_dir: Option<String>,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
106#[serde(untagged)]
107#[non_exhaustive]
108pub enum IgnoreDefaultArgs {
109 Bool(bool),
111 Array(Vec<String>),
113}
114
115impl LaunchOptions {
116 pub fn new() -> Self {
118 Self::default()
119 }
120
121 pub fn args(mut self, args: Vec<String>) -> Self {
123 self.args = Some(args);
124 self
125 }
126
127 pub fn artifacts_dir(mut self, path: String) -> Self {
129 self.artifacts_dir = Some(path);
130 self
131 }
132
133 pub fn channel(mut self, channel: String) -> Self {
135 self.channel = Some(channel);
136 self
137 }
138
139 pub fn chromium_sandbox(mut self, enabled: bool) -> Self {
141 self.chromium_sandbox = Some(enabled);
142 self
143 }
144
145 #[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 pub fn downloads_path(mut self, path: String) -> Self {
160 self.downloads_path = Some(path);
161 self
162 }
163
164 pub fn env(mut self, env: HashMap<String, String>) -> Self {
166 self.env = Some(env);
167 self
168 }
169
170 pub fn executable_path(mut self, path: String) -> Self {
172 self.executable_path = Some(path);
173 self
174 }
175
176 pub fn firefox_user_prefs(mut self, prefs: HashMap<String, Value>) -> Self {
178 self.firefox_user_prefs = Some(prefs);
179 self
180 }
181
182 pub fn handle_sighup(mut self, enabled: bool) -> Self {
184 self.handle_sighup = Some(enabled);
185 self
186 }
187
188 pub fn handle_sigint(mut self, enabled: bool) -> Self {
190 self.handle_sigint = Some(enabled);
191 self
192 }
193
194 pub fn handle_sigterm(mut self, enabled: bool) -> Self {
196 self.handle_sigterm = Some(enabled);
197 self
198 }
199
200 pub fn headless(mut self, enabled: bool) -> Self {
202 self.headless = Some(enabled);
203 self
204 }
205
206 pub fn ignore_default_args(mut self, args: IgnoreDefaultArgs) -> Self {
208 self.ignore_default_args = Some(args);
209 self
210 }
211
212 pub fn proxy(mut self, proxy: ProxySettings) -> Self {
214 self.proxy = Some(proxy);
215 self
216 }
217
218 pub fn slow_mo(mut self, ms: f64) -> Self {
220 self.slow_mo = Some(ms);
221 self
222 }
223
224 pub fn timeout(mut self, ms: f64) -> Self {
226 self.timeout = Some(ms);
227 self
228 }
229
230 pub fn traces_dir(mut self, path: String) -> Self {
232 self.traces_dir = Some(path);
233 self
234 }
235
236 pub(crate) fn normalize(self) -> Value {
247 let mut value =
248 serde_json::to_value(&self).expect("serialization of LaunchOptions cannot fail");
249
250 if value.get("timeout").is_none() {
252 value["timeout"] = json!(crate::DEFAULT_LAUNCH_TIMEOUT_MS);
253 }
254
255 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 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 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 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 assert!(normalized["env"].is_array());
390 let env_array = normalized["env"].as_array().unwrap();
391 assert_eq!(env_array.len(), 2);
392
393 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 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 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}