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