1#![allow(missing_docs)]
4use std::io::Write;
17use std::path::{Path, PathBuf};
18
19#[derive(Debug, Clone)]
21pub struct LaunchOptions {
22 pub headless: bool,
23 pub executable_path: Option<String>,
24 pub proxy: Option<String>,
25 pub proxy_bypass: Option<String>,
26 pub proxy_username: Option<String>,
27 pub proxy_password: Option<String>,
28 pub profile: Option<String>,
29 pub args: Vec<String>,
30 pub allow_file_access: bool,
31 pub extensions: Option<Vec<String>>,
32 pub storage_state: Option<String>,
33 pub user_agent: Option<String>,
34 pub ignore_https_errors: bool,
35 pub color_scheme: Option<String>,
36 pub download_path: Option<String>,
37 pub hide_scrollbars: bool,
39 pub viewport_size: Option<(u32, u32)>,
41 pub use_real_keychain: bool,
43 pub webgpu: bool,
45 pub no_xvfb: bool,
47 pub restrict_webrtc: bool,
49}
50
51impl Default for LaunchOptions {
52 fn default() -> Self {
53 Self {
54 headless: true,
55 executable_path: None,
56 proxy: None,
57 proxy_bypass: None,
58 proxy_username: None,
59 proxy_password: None,
60 profile: None,
61 args: Vec::new(),
62 allow_file_access: false,
63 extensions: None,
64 storage_state: None,
65 user_agent: None,
66 ignore_https_errors: false,
67 color_scheme: None,
68 download_path: None,
69 hide_scrollbars: true,
70 viewport_size: None,
71 use_real_keychain: false,
72 webgpu: false,
73 no_xvfb: false,
74 restrict_webrtc: false,
75 }
76 }
77}
78
79pub(crate) struct ChromeArgs {
81 pub args: Vec<String>,
82 pub user_data_dir: PathBuf,
83 pub temp_user_data_dir: Option<PathBuf>,
84}
85
86#[cfg_attr(not(test), allow(dead_code))]
90pub(crate) fn materialize_temp_user_data_dir_sync(args: &ChromeArgs) -> Result<(), String> {
91 if let Some(ref dir) = args.temp_user_data_dir {
92 std::fs::create_dir_all(dir)
93 .map_err(|e| format!("Failed to create temp profile dir: {}", e))?;
94 }
95 Ok(())
96}
97
98pub(crate) fn build_chrome_args(options: &LaunchOptions) -> Result<ChromeArgs, String> {
103 let mut enable_features: Vec<String> = vec![
105 "NetworkService".to_string(),
106 "NetworkServiceInProcess".to_string(),
107 ];
108 if options.webgpu && cfg!(target_os = "linux") {
109 enable_features.push("Vulkan".to_string());
110 }
111
112 let mut user_args: Vec<String> = Vec::new();
113 for arg in &options.args {
114 if let Some(values) = arg.strip_prefix("--enable-features=") {
115 for feature in values.split(',').map(str::trim).filter(|f| !f.is_empty()) {
116 if !enable_features.iter().any(|f| f == feature) {
117 enable_features.push(feature.to_string());
118 }
119 }
120 } else {
121 user_args.push(arg.clone());
122 }
123 }
124
125 let mut disable_features: Vec<String> = vec!["Translate".to_string()];
127 let has_extensions = options
128 .extensions
129 .as_ref()
130 .is_some_and(|exts| !exts.is_empty());
131 if has_extensions {
132 disable_features.push("DisableLoadExtensionCommandLineSwitch".to_string());
134 }
135
136 let mut args = vec![
137 "--remote-debugging-port=0".to_string(),
138 "--no-first-run".to_string(),
139 "--no-default-browser-check".to_string(),
140 "--disable-background-networking".to_string(),
141 "--disable-backgrounding-occluded-windows".to_string(),
142 "--disable-component-update".to_string(),
143 "--disable-default-apps".to_string(),
144 "--disable-hang-monitor".to_string(),
145 "--disable-popup-blocking".to_string(),
146 "--disable-prompt-on-repost".to_string(),
147 "--disable-sync".to_string(),
148 format!("--disable-features={}", disable_features.join(",")),
149 format!("--enable-features={}", enable_features.join(",")),
150 "--disable-metrics".to_string(),
153 "--disable-metrics-reporting".to_string(),
154 "--disable-breakpad".to_string(),
155 "--disable-crash-reporter".to_string(),
156 "--disable-domain-reliability".to_string(),
157 "--disable-client-side-phishing-detection".to_string(),
158 "--disable-component-extensions-with-background-pages".to_string(),
159 ];
160
161 if options.webgpu {
162 args.push("--enable-unsafe-webgpu".to_string());
163 if cfg!(target_os = "linux") {
164 args.push("--use-angle=vulkan".to_string());
165 args.push("--use-vulkan=swiftshader".to_string());
166 args.push("--use-webgpu-adapter=swiftshader".to_string());
167 args.push("--disable-vulkan-surface".to_string());
168 }
169 }
170
171 if !options.use_real_keychain {
172 args.push("--password-store=basic".to_string());
173 args.push("--use-mock-keychain".to_string());
174 }
175
176 if options.headless && !has_extensions {
177 args.push("--headless=new".to_string());
178 if options.hide_scrollbars {
179 args.push("--hide-scrollbars".to_string());
180 }
181 args.push("--enable-unsafe-swiftshader".to_string());
182 }
183
184 if let Some(ref proxy) = options.proxy {
185 args.push(format!("--proxy-server={}", proxy));
186 }
187
188 if let Some(ref bypass) = options.proxy_bypass {
189 args.push(format!("--proxy-bypass-list={}", bypass));
190 }
191
192 let (user_data_dir, temp_user_data_dir) = if let Some(ref profile) = options.profile {
193 let expanded = expand_tilde(profile);
194 let dir = PathBuf::from(&expanded);
195 args.push(format!("--user-data-dir={}", expanded));
196 (dir, None)
197 } else {
198 let dir = std::env::temp_dir().join(format!(
202 "browser-automation-cli-chrome-{}",
203 uuid::Uuid::new_v4()
204 ));
205 args.push(format!("--user-data-dir={}", dir.display()));
206 (dir.clone(), Some(dir))
207 };
208
209 if options.ignore_https_errors {
210 args.push("--ignore-certificate-errors".to_string());
211 }
212
213 if options.allow_file_access {
214 args.push("--allow-file-access-from-files".to_string());
215 args.push("--allow-file-access".to_string());
216 }
217
218 if let Some(ref exts) = options.extensions {
219 if !exts.is_empty() {
220 let ext_list = exts.join(",");
221 args.push(format!("--load-extension={}", ext_list));
222 args.push(format!("--disable-extensions-except={}", ext_list));
223 }
224 }
225
226 let has_window_size = options
227 .args
228 .iter()
229 .any(|a| a.starts_with("--start-maximized") || a.starts_with("--window-size="));
230
231 if !has_window_size && options.headless && !has_extensions {
232 let (w, h) = options.viewport_size.unwrap_or((1280, 720));
233 args.push(format!("--window-size={},{}", w, h));
234 }
235
236 args.extend(user_args);
237
238 if options.restrict_webrtc {
239 args.retain(|arg| !arg.starts_with("--force-webrtc-ip-handling-policy="));
240 args.push("--force-webrtc-ip-handling-policy=disable_non_proxied_udp".to_string());
241 }
242
243 if should_disable_sandbox(&args) {
244 args.push("--no-sandbox".to_string());
245 }
246
247 if should_disable_dev_shm(&args) {
248 args.push("--disable-dev-shm-usage".to_string());
249 }
250
251 Ok(ChromeArgs {
252 args,
253 user_data_dir,
254 temp_user_data_dir,
255 })
256}
257
258pub fn find_chrome() -> Option<PathBuf> {
270 if let Some(p) = crate::xdg::chrome_path_from_config() {
271 let path = PathBuf::from(p);
272 if crate::platform::is_executable_file(&path) {
273 crate::platform::warn_if_sandboxed_browser(&path);
274 return Some(path);
275 }
276 }
277 if let Some(p) = crate::install::find_installed_chrome() {
278 if crate::platform::is_executable_file(&p) {
279 crate::platform::warn_if_sandboxed_browser(&p);
280 return Some(p);
281 }
282 }
283
284 let cache_dir = crate::install::get_browsers_dir();
285 if cache_dir.exists() {
286 let _ = writeln!(
287 std::io::stderr(),
288 "Warning: Chrome cache directory exists ({}) but no Chrome binary found inside. \
289 Falling back to system Chrome (product browsers cache empty).",
290 cache_dir.display()
291 );
292 }
293
294 if let Some(p) = find_chrome_on_path() {
295 crate::platform::warn_if_sandboxed_browser(&p);
296 return Some(p);
297 }
298
299 if let Some(p) = find_chrome_known_paths() {
300 crate::platform::warn_if_sandboxed_browser(&p);
301 return Some(p);
302 }
303
304 if let Some(p) = find_puppeteer_chrome() {
305 if crate::platform::is_executable_file(&p) {
306 return Some(p);
307 }
308 }
309 if let Some(p) = find_playwright_chromium() {
310 if crate::platform::is_executable_file(&p) {
311 return Some(p);
312 }
313 }
314
315 None
316}
317
318fn find_chrome_on_path() -> Option<PathBuf> {
320 const NAMES: &[&str] = &[
321 "google-chrome",
322 "google-chrome-stable",
323 "google-chrome-beta",
324 "google-chrome-unstable",
325 "chromium",
326 "chromium-browser",
327 "microsoft-edge",
328 "microsoft-edge-stable",
329 "msedge",
330 "brave-browser",
331 "brave-browser-stable",
332 "brave",
333 ];
334 for name in NAMES {
335 if let Some(p) = crate::platform::which_bin(name) {
336 return Some(p);
337 }
338 }
339 None
340}
341
342fn find_chrome_known_paths() -> Option<PathBuf> {
344 #[cfg(target_os = "macos")]
345 {
346 let mut candidates: Vec<PathBuf> = vec![
347 PathBuf::from("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"),
348 PathBuf::from(
349 "/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta",
350 ),
351 PathBuf::from(
352 "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",
353 ),
354 PathBuf::from("/Applications/Chromium.app/Contents/MacOS/Chromium"),
355 PathBuf::from("/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"),
356 PathBuf::from("/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"),
357 ];
358 if let Some(home) = dirs::home_dir() {
359 candidates.push(
360 home.join("Applications/Google Chrome.app/Contents/MacOS/Google Chrome"),
361 );
362 candidates.push(
363 home.join("Applications/Chromium.app/Contents/MacOS/Chromium"),
364 );
365 candidates.push(
366 home.join("Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"),
367 );
368 }
369 return crate::platform::first_existing_executable(candidates.iter().map(|p| p.as_path()));
370 }
371
372 #[cfg(target_os = "linux")]
373 {
374 let mut candidates: Vec<PathBuf> = vec![
375 PathBuf::from("/usr/bin/google-chrome"),
376 PathBuf::from("/usr/bin/google-chrome-stable"),
377 PathBuf::from("/usr/bin/google-chrome-beta"),
378 PathBuf::from("/usr/bin/google-chrome-unstable"),
379 PathBuf::from("/usr/bin/chromium"),
380 PathBuf::from("/usr/bin/chromium-browser"),
381 PathBuf::from("/usr/bin/microsoft-edge"),
382 PathBuf::from("/usr/bin/microsoft-edge-stable"),
383 PathBuf::from("/usr/bin/brave-browser"),
384 PathBuf::from("/opt/google/chrome/chrome"),
385 PathBuf::from("/opt/google/chrome/google-chrome"),
386 PathBuf::from("/opt/microsoft/msedge/msedge"),
387 PathBuf::from("/snap/bin/chromium"),
388 PathBuf::from("/snap/bin/chromium-browser"),
389 PathBuf::from("/var/lib/flatpak/exports/bin/com.google.Chrome"),
390 PathBuf::from("/var/lib/flatpak/exports/bin/org.chromium.Chromium"),
391 PathBuf::from("/var/lib/flatpak/exports/bin/com.brave.Browser"),
392 PathBuf::from("/var/lib/flatpak/exports/bin/com.microsoft.Edge"),
393 ];
394 if let Some(home) = dirs::home_dir() {
395 candidates.push(home.join(".local/share/flatpak/exports/bin/com.google.Chrome"));
396 candidates.push(home.join(".local/share/flatpak/exports/bin/org.chromium.Chromium"));
397 candidates.push(home.join(".local/share/flatpak/exports/bin/com.brave.Browser"));
398 }
399 return crate::platform::first_existing_executable(candidates.iter().map(|p| p.as_path()));
400 }
401
402 #[cfg(target_os = "windows")]
403 {
404 let mut candidates: Vec<PathBuf> = Vec::with_capacity(24);
405 let rels = [
407 r"Google\Chrome\Application\chrome.exe",
408 r"Google\Chrome Beta\Application\chrome.exe",
409 r"Google\Chrome SxS\Application\chrome.exe", r"Microsoft\Edge\Application\msedge.exe",
411 r"Microsoft\Edge Beta\Application\msedge.exe",
412 r"BraveSoftware\Brave-Browser\Application\brave.exe",
413 ];
414 for key in ["ProgramFiles", "ProgramFiles(x86)", "LOCALAPPDATA"] {
415 if let Some(base) = std::env::var_os(key) {
416 let base = PathBuf::from(base);
417 for rel in &rels {
418 candidates.push(base.join(rel));
419 }
420 }
421 }
422 candidates.push(PathBuf::from(
424 r"C:\Program Files\Google\Chrome\Application\chrome.exe",
425 ));
426 candidates.push(PathBuf::from(
427 r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
428 ));
429 candidates.push(PathBuf::from(
430 r"C:\Program Files\Microsoft\Edge\Application\msedge.exe",
431 ));
432 return crate::platform::first_existing_executable(candidates.iter().map(|p| p.as_path()));
433 }
434
435 #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
436 {
437 None
438 }
439}
440
441fn should_disable_sandbox(existing_args: &[String]) -> bool {
442 if existing_args.iter().any(|a| a == "--no-sandbox") {
443 return false;
444 }
445 #[cfg(unix)]
447 {
448 if unsafe { libc::geteuid() } == 0 {
454 return true;
455 }
456 }
457 crate::platform::HostEnvironment::detect().container
459}
460
461fn should_disable_dev_shm(existing_args: &[String]) -> bool {
462 if existing_args.iter().any(|a| a == "--disable-dev-shm-usage") {
463 return false;
464 }
465 #[cfg(unix)]
466 {
467 if unsafe { libc::geteuid() } == 0 {
473 return true;
474 }
475 }
476 crate::platform::HostEnvironment::detect().container
477}
478
479fn find_puppeteer_chrome() -> Option<PathBuf> {
481 let mut search_dirs = Vec::new();
482 if let Ok(bd) = crate::xdg::browsers_dir() {
483 search_dirs.push(bd);
484 }
485 if let Some(home) = dirs::home_dir() {
486 search_dirs.push(home.join(".cache/puppeteer/chrome"));
488 search_dirs.push(home.join(".cache/ms-playwright"));
489 }
490 for dir in &search_dirs {
491 if !dir.is_dir() {
492 continue;
493 }
494 if let Ok(entries) = std::fs::read_dir(dir) {
495 let mut matches: Vec<PathBuf> = entries
496 .filter_map(|e| e.ok())
497 .filter(|e| e.path().is_dir())
498 .filter_map(|e| {
499 let path = e.path();
500 let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
501 if name.starts_with("chromium-") {
502 let linux = path.join("chrome-linux64/chrome");
503 if linux.exists() {
504 return Some(linux);
505 }
506 let win = path.join("chrome-win64/chrome.exe");
507 if win.exists() {
508 return Some(win);
509 }
510 }
511 let candidate = build_puppeteer_binary_path(&path);
512 if candidate.exists() {
513 Some(candidate)
514 } else {
515 None
516 }
517 })
518 .collect();
519 matches.sort();
520 matches.reverse();
521 if let Some(p) = matches.into_iter().next() {
522 return Some(p);
523 }
524 }
525 }
526 None
527}
528
529#[cfg(target_os = "linux")]
530fn build_puppeteer_binary_path(version_dir: &Path) -> PathBuf {
531 version_dir.join("chrome-linux64/chrome")
532}
533
534#[cfg(target_os = "macos")]
535fn build_puppeteer_binary_path(version_dir: &Path) -> PathBuf {
536 let arm = version_dir.join(
537 "chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing",
538 );
539 if arm.exists() {
540 return arm;
541 }
542 version_dir.join(
543 "chrome-mac-x64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing",
544 )
545}
546
547#[cfg(target_os = "windows")]
548fn build_puppeteer_binary_path(version_dir: &Path) -> PathBuf {
549 version_dir.join(r"chrome-win64\chrome.exe")
550}
551
552#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
553fn build_puppeteer_binary_path(version_dir: &Path) -> PathBuf {
554 version_dir.join("chrome")
555}
556
557fn find_playwright_chromium() -> Option<PathBuf> {
558 let mut search_dirs = Vec::new();
560 if let Some(home) = dirs::home_dir() {
561 search_dirs.push(home.join(".cache/ms-playwright"));
562 }
563 if let Ok(bd) = crate::xdg::browsers_dir() {
564 search_dirs.push(bd);
565 }
566 for dir in &search_dirs {
567 if !dir.is_dir() {
568 continue;
569 }
570 if let Ok(entries) = std::fs::read_dir(dir) {
571 let mut matches: Vec<PathBuf> = entries
572 .filter_map(|e| e.ok())
573 .filter(|e| {
574 e.file_name()
575 .to_str()
576 .map(|n| n.starts_with("chromium-"))
577 .unwrap_or(false)
578 })
579 .filter_map(|e| {
580 let candidate = build_playwright_binary_path(&e.path());
581 if candidate.exists() {
582 Some(candidate)
583 } else {
584 None
585 }
586 })
587 .collect();
588 matches.sort();
589 matches.reverse();
590 if let Some(p) = matches.into_iter().next() {
591 return Some(p);
592 }
593 }
594 }
595 None
596}
597
598#[cfg(target_os = "linux")]
599fn build_playwright_binary_path(chromium_dir: &Path) -> PathBuf {
600 let standard = chromium_dir.join("chrome-linux/chrome");
601 if standard.exists() {
602 return standard;
603 }
604 chromium_dir.join("chrome-linux64/chrome")
605}
606
607#[cfg(target_os = "macos")]
608fn build_playwright_binary_path(chromium_dir: &Path) -> PathBuf {
609 chromium_dir.join("chrome-mac/Chromium.app/Contents/MacOS/Chromium")
610}
611
612#[cfg(target_os = "windows")]
613fn build_playwright_binary_path(chromium_dir: &Path) -> PathBuf {
614 chromium_dir.join("chrome-win/chrome.exe")
615}
616
617#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
618fn build_playwright_binary_path(chromium_dir: &Path) -> PathBuf {
619 chromium_dir.join("chrome")
620}
621
622fn expand_tilde(path: &str) -> String {
623 if let Some(rest) = path.strip_prefix('~') {
624 if let Some(home) = dirs::home_dir() {
625 return home
626 .join(rest.strip_prefix('/').unwrap_or(rest))
627 .to_string_lossy()
628 .to_string();
629 }
630 }
631 path.to_string()
632}
633
634#[cfg(test)]
635mod tests {
636 use super::*;
637 use crate::test_utils::EnvGuard;
638
639 #[test]
640 fn test_find_chrome_returns_some_on_host() {
641 let _ = find_chrome();
643 }
644
645 #[test]
646 fn test_expand_tilde() {
647 if dirs::home_dir().is_some() {
648 let expanded = expand_tilde("~/foo");
649 assert!(!expanded.starts_with('~'));
650 assert!(expanded.ends_with("foo") || expanded.ends_with("foo/"));
651 }
652 }
653
654 #[test]
655 fn test_expand_tilde_no_tilde() {
656 assert_eq!(expand_tilde("/tmp/x"), "/tmp/x");
657 }
658
659 #[test]
660 fn test_should_disable_sandbox_skips_if_already_set() {
661 assert!(!should_disable_sandbox(&["--no-sandbox".to_string()]));
662 }
663
664 #[test]
665 fn test_find_playwright_chromium_nonexistent() {
666 let g = EnvGuard::new(&["PLAYWRIGHT_BROWSERS_PATH"]);
667 g.set("PLAYWRIGHT_BROWSERS_PATH", "/nonexistent-playwright-path");
668 let result = find_playwright_chromium();
669 assert!(result.is_none());
670 }
671
672 #[test]
673 fn test_build_args_headless_includes_headless_flag() {
674 let opts = LaunchOptions {
675 headless: true,
676 ..Default::default()
677 };
678 let result = build_chrome_args(&opts).unwrap();
679 materialize_temp_user_data_dir_sync(&result).unwrap();
680 assert!(result.args.iter().any(|a| a == "--headless=new"));
681 assert!(result.args.iter().any(|a| a == "--hide-scrollbars"));
682 assert!(result
683 .args
684 .iter()
685 .any(|a| a == "--enable-unsafe-swiftshader"));
686 assert!(result.args.iter().any(|a| a == "--window-size=1280,720"));
687 assert!(result.temp_user_data_dir.is_some());
688 let dir = result.temp_user_data_dir.unwrap();
689 assert!(dir.exists());
690 let _ = std::fs::remove_dir_all(&dir);
691 }
692
693 #[test]
694 fn test_build_args_headed_no_headless_flag() {
695 let opts = LaunchOptions {
696 headless: false,
697 ..Default::default()
698 };
699 let result = build_chrome_args(&opts).unwrap();
700 materialize_temp_user_data_dir_sync(&result).unwrap();
701 assert!(!result.args.iter().any(|a| a.contains("--headless")));
702 assert!(!result.args.iter().any(|a| a == "--hide-scrollbars"));
703 assert!(result.temp_user_data_dir.is_some());
704 if let Some(ref dir) = result.temp_user_data_dir {
705 let _ = std::fs::remove_dir_all(dir);
706 }
707 }
708
709 #[test]
710 fn test_build_args_temp_user_data_dir_created() {
711 let opts = LaunchOptions::default();
712 let result = build_chrome_args(&opts).unwrap();
713 materialize_temp_user_data_dir_sync(&result).unwrap();
714 let dir = result.temp_user_data_dir.as_ref().unwrap();
715 assert!(dir.exists());
716 assert!(result
717 .args
718 .iter()
719 .any(|a| a.starts_with("--user-data-dir=")));
720 let _ = std::fs::remove_dir_all(dir);
721 }
722
723 #[test]
724 fn test_build_args_profile_no_temp_dir() {
725 let opts = LaunchOptions {
726 profile: Some("/tmp/my-profile".to_string()),
727 ..Default::default()
728 };
729 let result = build_chrome_args(&opts).unwrap();
730 materialize_temp_user_data_dir_sync(&result).unwrap();
731 assert!(result.temp_user_data_dir.is_none());
732 assert!(result
733 .args
734 .iter()
735 .any(|a| a == "--user-data-dir=/tmp/my-profile"));
736 }
737
738 #[test]
739 fn test_build_args_custom_window_size_not_overridden() {
740 let opts = LaunchOptions {
741 headless: true,
742 args: vec!["--window-size=1920,1080".to_string()],
743 ..Default::default()
744 };
745 let result = build_chrome_args(&opts).unwrap();
746 materialize_temp_user_data_dir_sync(&result).unwrap();
747 assert!(!result.args.iter().any(|a| a == "--window-size=1280,720"));
748 assert!(result.args.iter().any(|a| a == "--window-size=1920,1080"));
749 if let Some(ref dir) = result.temp_user_data_dir {
750 let _ = std::fs::remove_dir_all(dir);
751 }
752 }
753
754 #[test]
755 fn test_build_args_hide_scrollbars_false_suppresses_default_hide_scrollbars() {
756 let opts = LaunchOptions {
757 headless: true,
758 hide_scrollbars: false,
759 ..Default::default()
760 };
761 let result = build_chrome_args(&opts).unwrap();
762 materialize_temp_user_data_dir_sync(&result).unwrap();
763 assert!(!result.args.iter().any(|a| a == "--hide-scrollbars"));
764 if let Some(ref dir) = result.temp_user_data_dir {
765 let _ = std::fs::remove_dir_all(dir);
766 }
767 }
768
769 #[test]
770 fn test_build_args_start_maximized_suppresses_default_window_size() {
771 let opts = LaunchOptions {
772 headless: true,
773 args: vec!["--start-maximized".to_string()],
774 ..Default::default()
775 };
776 let result = build_chrome_args(&opts).unwrap();
777 materialize_temp_user_data_dir_sync(&result).unwrap();
778 assert!(!result.args.iter().any(|a| a == "--window-size=1280,720"));
779 assert!(result.args.iter().any(|a| a == "--start-maximized"));
780 if let Some(ref dir) = result.temp_user_data_dir {
781 let _ = std::fs::remove_dir_all(dir);
782 }
783 }
784
785 #[test]
786 fn test_build_args_disables_translate() {
787 let opts = LaunchOptions::default();
788 let result = build_chrome_args(&opts).unwrap();
789 materialize_temp_user_data_dir_sync(&result).unwrap();
790 assert!(result
791 .args
792 .iter()
793 .any(|a| a.contains("--disable-features") && a.contains("Translate")));
794 if let Some(ref dir) = result.temp_user_data_dir {
795 let _ = std::fs::remove_dir_all(dir);
796 }
797 }
798
799 #[test]
800 fn test_build_args_webgpu_default_off() {
801 let opts = LaunchOptions::default();
802 let result = build_chrome_args(&opts).unwrap();
803 materialize_temp_user_data_dir_sync(&result).unwrap();
804 assert!(!result.args.iter().any(|a| a == "--enable-unsafe-webgpu"));
805 if let Some(ref dir) = result.temp_user_data_dir {
806 let _ = std::fs::remove_dir_all(dir);
807 }
808 }
809
810 #[test]
811 fn test_build_args_restrict_webrtc_enforces_safe_policy() {
812 let opts = LaunchOptions {
813 restrict_webrtc: true,
814 args: vec!["--force-webrtc-ip-handling-policy=default".to_string()],
815 ..Default::default()
816 };
817 let result = build_chrome_args(&opts).unwrap();
818 materialize_temp_user_data_dir_sync(&result).unwrap();
819 let policies: Vec<&String> = result
820 .args
821 .iter()
822 .filter(|arg| arg.starts_with("--force-webrtc-ip-handling-policy="))
823 .collect();
824 assert_eq!(
825 policies,
826 vec![&"--force-webrtc-ip-handling-policy=disable_non_proxied_udp".to_string()]
827 );
828 if let Some(ref dir) = result.temp_user_data_dir {
829 let _ = std::fs::remove_dir_all(dir);
830 }
831 }
832
833 #[test]
834 fn test_build_args_webgpu_includes_webgpu_flags() {
835 let opts = LaunchOptions {
836 webgpu: true,
837 ..Default::default()
838 };
839 let result = build_chrome_args(&opts).unwrap();
840 materialize_temp_user_data_dir_sync(&result).unwrap();
841 assert!(result.args.iter().any(|a| a == "--enable-unsafe-webgpu"));
842 if cfg!(target_os = "linux") {
843 assert!(result.args.iter().any(|a| a == "--use-angle=vulkan"));
844 assert!(result.args.iter().any(|a| a == "--use-vulkan=swiftshader"));
845 }
846 if let Some(ref dir) = result.temp_user_data_dir {
847 let _ = std::fs::remove_dir_all(dir);
848 }
849 }
850
851 #[test]
852 fn test_build_args_merges_user_enable_features() {
853 let opts = LaunchOptions {
854 webgpu: true,
855 args: vec![
856 "--enable-features=Foo,Bar".to_string(),
857 "--some-other-flag".to_string(),
858 "--enable-features=NetworkService".to_string(),
859 ],
860 ..Default::default()
861 };
862 let result = build_chrome_args(&opts).unwrap();
863 materialize_temp_user_data_dir_sync(&result).unwrap();
864 let flags: Vec<&String> = result
865 .args
866 .iter()
867 .filter(|a| a.starts_with("--enable-features="))
868 .collect();
869 assert_eq!(flags.len(), 1);
870 let features: Vec<&str> = flags[0]
871 .strip_prefix("--enable-features=")
872 .unwrap()
873 .split(',')
874 .collect();
875 assert!(features.contains(&"NetworkService"));
876 assert!(features.contains(&"Foo"));
877 assert!(features.contains(&"Bar"));
878 assert!(result.args.iter().any(|a| a == "--some-other-flag"));
879 if let Some(ref dir) = result.temp_user_data_dir {
880 let _ = std::fs::remove_dir_all(dir);
881 }
882 }
883
884 #[test]
885 fn test_build_args_single_enable_features_flag() {
886 let opts = LaunchOptions {
887 webgpu: true,
888 ..Default::default()
889 };
890 let result = build_chrome_args(&opts).unwrap();
891 materialize_temp_user_data_dir_sync(&result).unwrap();
892 let count = result
893 .args
894 .iter()
895 .filter(|a| a.starts_with("--enable-features="))
896 .count();
897 assert_eq!(count, 1);
898 if let Some(ref dir) = result.temp_user_data_dir {
899 let _ = std::fs::remove_dir_all(dir);
900 }
901 }
902
903 #[test]
904 fn test_build_args_headless_with_extensions_skips_headless_flag() {
905 let opts = LaunchOptions {
906 headless: true,
907 extensions: Some(vec!["/tmp/ext".to_string()]),
908 ..Default::default()
909 };
910 let result = build_chrome_args(&opts).unwrap();
911 materialize_temp_user_data_dir_sync(&result).unwrap();
912 assert!(!result.args.iter().any(|a| a.contains("--headless")));
913 assert!(result
914 .args
915 .iter()
916 .any(|a| a.starts_with("--load-extension=")));
917 assert!(result.args.iter().any(|a| {
918 a.starts_with("--disable-features=")
919 && a.contains("DisableLoadExtensionCommandLineSwitch")
920 }));
921 if let Some(ref dir) = result.temp_user_data_dir {
922 let _ = std::fs::remove_dir_all(dir);
923 }
924 }
925
926 #[test]
927 fn test_build_args_ignore_https_errors_includes_flag() {
928 let opts = LaunchOptions {
929 ignore_https_errors: true,
930 ..Default::default()
931 };
932 let result = build_chrome_args(&opts).unwrap();
933 materialize_temp_user_data_dir_sync(&result).unwrap();
934 assert!(result
935 .args
936 .iter()
937 .any(|a| a == "--ignore-certificate-errors"));
938 if let Some(ref dir) = result.temp_user_data_dir {
939 let _ = std::fs::remove_dir_all(dir);
940 }
941 }
942
943 #[test]
944 fn test_build_args_ignore_https_errors_default_no_flag() {
945 let opts = LaunchOptions::default();
946 let result = build_chrome_args(&opts).unwrap();
947 materialize_temp_user_data_dir_sync(&result).unwrap();
948 assert!(!result
949 .args
950 .iter()
951 .any(|a| a == "--ignore-certificate-errors"));
952 if let Some(ref dir) = result.temp_user_data_dir {
953 let _ = std::fs::remove_dir_all(dir);
954 }
955 }
956}