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