1use crate::error::{BrowserError, Result};
2use crate::policy::{AdmissionHook, EventSink, NetworkPolicy};
3use std::path::{Path, PathBuf};
4use std::time::Duration;
5
6#[derive(Clone, Debug, Eq, PartialEq)]
8pub enum ProfileSpec {
9 Temporary,
11 Named { root: PathBuf, name: String },
14 Directory(PathBuf),
17}
18
19#[derive(Clone)]
20pub struct LaunchOptions {
21 pub headless: bool,
22 pub mute_audio: bool,
24 pub profile: ProfileSpec,
25 pub chrome_path: Option<PathBuf>,
28 pub allow_system_chrome: bool,
32 pub use_system_chrome_only: bool,
35 pub viewport: (u32, u32),
36 pub launch_timeout: Duration,
37 pub extra_args: Vec<String>,
39 pub download_dir: Option<PathBuf>,
42 pub upload_root: Option<PathBuf>,
44 pub admission: Option<AdmissionHook>,
47 pub network: NetworkPolicy,
50 pub on_event: Option<EventSink>,
52 #[cfg(windows)]
55 pub windows_job: Option<std::sync::Arc<std::os::windows::io::OwnedHandle>>,
56}
57
58impl std::fmt::Debug for LaunchOptions {
59 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60 f.debug_struct("LaunchOptions")
61 .field("headless", &self.headless)
62 .field("profile", &self.profile)
63 .field("network", &self.network)
64 .field("admission", &self.admission.is_some())
65 .finish_non_exhaustive()
66 }
67}
68
69impl Default for LaunchOptions {
70 fn default() -> Self {
71 Self {
72 headless: true,
73 mute_audio: true,
74 profile: ProfileSpec::Temporary,
75 chrome_path: None,
76 allow_system_chrome: false,
77 use_system_chrome_only: false,
78 viewport: (1280, 800),
79 launch_timeout: Duration::from_secs(60),
80 extra_args: Vec::new(),
81 download_dir: None,
82 upload_root: None,
83 admission: None,
84 network: NetworkPolicy::default(),
85 on_event: None,
86 #[cfg(windows)]
87 windows_job: None,
88 }
89 }
90}
91
92impl LaunchOptions {
93 pub fn system_chrome(mut self) -> Self {
95 self.allow_system_chrome = true;
96 self.use_system_chrome_only = false;
97 self
98 }
99 pub fn system_chrome_only(mut self) -> Self {
103 self.use_system_chrome_only = true;
104 self.allow_system_chrome = false;
105 self
106 }
107 pub fn headed(mut self) -> Self {
108 self.headless = false;
109 self
110 }
111 pub fn admission(mut self, hook: AdmissionHook) -> Self {
112 self.admission = Some(hook);
113 self
114 }
115 pub fn network(mut self, policy: NetworkPolicy) -> Self {
116 self.network = policy;
117 self
118 }
119 pub fn on_event(
120 mut self,
121 f: impl Fn(&crate::policy::BrowserEvent) + Send + Sync + 'static,
122 ) -> Self {
123 self.on_event = Some(std::sync::Arc::new(f));
124 self
125 }
126 #[cfg(windows)]
132 pub fn windows_job(
133 mut self,
134 job: std::os::windows::io::BorrowedHandle<'_>,
135 ) -> std::io::Result<Self> {
136 self.windows_job = Some(std::sync::Arc::new(job.try_clone_to_owned()?));
137 Ok(self)
138 }
139 pub fn named_profile(mut self, root: impl Into<PathBuf>, name: impl Into<String>) -> Self {
140 self.profile = ProfileSpec::Named {
141 root: root.into(),
142 name: name.into(),
143 };
144 self
145 }
146 pub fn profile_dir(mut self, path: impl Into<PathBuf>) -> Self {
150 self.profile = ProfileSpec::Directory(path.into());
151 self
152 }
153}
154
155pub(crate) fn validate_profile_dir(path: &Path) -> Result<()> {
156 if !path.is_absolute() {
157 return Err(BrowserError::Profile(format!(
158 "profile directory must be absolute: {}",
159 path.display()
160 )));
161 }
162 let metadata = std::fs::metadata(path).map_err(|e| {
163 BrowserError::Profile(format!(
164 "profile directory must exist and be accessible: {} ({e})",
165 path.display()
166 ))
167 })?;
168 if !metadata.is_dir() {
169 return Err(BrowserError::Profile(format!(
170 "profile path is not a directory: {}",
171 path.display()
172 )));
173 }
174 Ok(())
175}
176
177pub(crate) fn validate_profile_name(name: &str) -> Result<()> {
178 let ok = !name.is_empty()
179 && name.len() <= 64
180 && !name.starts_with('.')
181 && name
182 .chars()
183 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.'));
184 if ok {
185 Ok(())
186 } else {
187 Err(BrowserError::Profile(format!(
188 "invalid profile name '{name}'"
189 )))
190 }
191}
192
193pub const CHROME_FOR_TESTING_ENV: &str = "RIGHTKIT_CHROME_FOR_TESTING";
195
196pub const CHROME_FOR_TESTING_ROOT_ENV: &str = "RIGHTKIT_CHROME_FOR_TESTING_ROOT";
198
199pub const CHROME_FOR_TESTING_INSTALL_HINT: &str = "Install Chrome for Testing (any one of):\n \
201pnpm dlx @puppeteer/browsers install chrome@stable --path ~/.cache/puppeteer\n \
202pnpm exec playwright install chromium\n\
203or point RIGHTKIT_CHROME_FOR_TESTING at its executable. Regular Google Chrome is used only \
204with LaunchOptions::system_chrome(): on macOS each killed instance leaks a code-sign clone.";
205
206const MAC_CFT_APP: &str = "Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing";
207
208fn home_dir() -> Option<PathBuf> {
209 std::env::var_os("HOME")
210 .or_else(|| std::env::var_os("USERPROFILE"))
211 .map(PathBuf::from)
212}
213
214fn version_key(name: &str) -> Vec<u64> {
215 name.split(|c: char| !c.is_ascii_digit())
216 .filter(|p| !p.is_empty())
217 .filter_map(|p| p.parse().ok())
218 .collect()
219}
220
221fn versioned_children(root: &Path, prefixes: &[&str]) -> Vec<PathBuf> {
223 let mut names: Vec<String> = std::fs::read_dir(root)
224 .map(|rd| {
225 rd.filter_map(|e| e.ok())
226 .map(|e| e.file_name().to_string_lossy().into_owned())
227 .filter(|n| prefixes.iter().any(|p| n.starts_with(p)))
228 .collect()
229 })
230 .unwrap_or_default();
231 names.sort_by(|a, b| version_key(b).cmp(&version_key(a)).then_with(|| b.cmp(a)));
232 names.into_iter().map(|n| root.join(n)).collect()
233}
234
235fn managed_chrome_for_testing_root(
236 home: &Path,
237 env_path: &impl Fn(&str) -> Option<PathBuf>,
238) -> PathBuf {
239 if let Some(root) = env_path(CHROME_FOR_TESTING_ROOT_ENV) {
240 return root;
241 }
242 let app_data = if cfg!(target_os = "macos") {
243 home.join("Library/Application Support")
244 } else if cfg!(target_os = "windows") {
245 env_path("LOCALAPPDATA").unwrap_or_else(|| home.join("AppData").join("Local"))
246 } else {
247 env_path("XDG_DATA_HOME").unwrap_or_else(|| home.join(".local/share"))
248 };
249 app_data.join("Orthic Labs/shared/chrome-for-testing")
250}
251
252fn managed_chrome_for_testing_candidates(root: &Path) -> Vec<PathBuf> {
253 versioned_children(root, &[""])
255 .into_iter()
256 .filter(|dir| {
257 dir.is_dir()
258 && dir
259 .file_name()
260 .and_then(|n| n.to_str())
261 .is_some_and(|name| {
262 name.contains('.')
263 && name.split('.').all(|part| {
264 !part.is_empty() && part.bytes().all(|b| b.is_ascii_digit())
265 })
266 })
267 })
268 .map(|dir| {
269 if cfg!(target_os = "macos") {
270 let sub = if cfg!(target_arch = "aarch64") {
271 "chrome-mac-arm64"
272 } else {
273 "chrome-mac-x64"
274 };
275 dir.join(sub).join(MAC_CFT_APP)
276 } else if cfg!(target_os = "windows") {
277 dir.join("chrome-win64/chrome.exe")
278 } else {
279 dir.join("chrome-linux64/chrome")
280 }
281 })
282 .collect()
283}
284
285pub fn chrome_for_testing_candidates() -> Vec<PathBuf> {
290 let home = home_dir().unwrap_or_default();
291 let env_path = |k: &str| {
292 std::env::var_os(k)
293 .filter(|v| !v.is_empty())
294 .map(PathBuf::from)
295 };
296 let puppeteer = env_path("PUPPETEER_CACHE_DIR")
297 .unwrap_or_else(|| home.join(".cache").join("puppeteer"))
298 .join("chrome");
299 let managed_root = managed_chrome_for_testing_root(&home, &env_path);
300 let mut out = managed_chrome_for_testing_candidates(&managed_root);
301 if cfg!(target_os = "macos") {
302 out.push(Path::new("/Applications").join(MAC_CFT_APP));
303 out.push(home.join("Applications").join(MAC_CFT_APP));
304 let subs: [&str; 2] = if cfg!(target_arch = "aarch64") {
305 ["chrome-mac-arm64", "chrome-mac-x64"]
306 } else {
307 ["chrome-mac-x64", "chrome-mac-arm64"]
308 };
309 let playwright = env_path("PLAYWRIGHT_BROWSERS_PATH")
310 .unwrap_or_else(|| home.join("Library/Caches/ms-playwright"));
311 for dir in versioned_children(&puppeteer, &["mac_arm-", "mac-"])
312 .into_iter()
313 .chain(versioned_children(&playwright, &["chromium-"]))
314 {
315 for sub in subs {
316 out.push(dir.join(sub).join(MAC_CFT_APP));
317 }
318 }
319 } else if cfg!(target_os = "windows") {
320 let local = env_path("LOCALAPPDATA").unwrap_or_else(|| home.join(r"AppData\Local"));
321 for dir in versioned_children(&puppeteer, &["win64-", "win32-"]) {
322 out.push(dir.join("chrome-win64").join("chrome.exe"));
323 out.push(dir.join("chrome-win32").join("chrome.exe"));
324 }
325 let playwright =
326 env_path("PLAYWRIGHT_BROWSERS_PATH").unwrap_or_else(|| local.join("ms-playwright"));
327 for dir in versioned_children(&playwright, &["chromium-"]) {
328 out.push(dir.join("chrome-win64").join("chrome.exe"));
329 }
330 } else {
331 for dir in versioned_children(&puppeteer, &["linux-", "linux_arm-"]) {
332 out.push(dir.join("chrome-linux64").join("chrome"));
333 out.push(dir.join("chrome-linux-arm64").join("chrome"));
334 }
335 let playwright = env_path("PLAYWRIGHT_BROWSERS_PATH")
336 .unwrap_or_else(|| home.join(".cache").join("ms-playwright"));
337 for dir in versioned_children(&playwright, &["chromium-"]) {
338 out.push(dir.join("chrome-linux64").join("chrome"));
339 }
340 }
341 out
342}
343
344pub fn chrome_for_testing_path(explicit: Option<&Path>) -> Result<PathBuf> {
348 let missing = |what: String| {
349 BrowserError::ChromeForTestingNotFound(format!("{what}\n{CHROME_FOR_TESTING_INSTALL_HINT}"))
350 };
351 if let Some(p) = explicit {
352 return if p.exists() {
353 Ok(p.to_path_buf())
354 } else {
355 Err(missing(format!(
356 "explicit Chrome for Testing executable does not exist: {}",
357 p.display()
358 )))
359 };
360 }
361 if let Some(p) = std::env::var_os(CHROME_FOR_TESTING_ENV).filter(|v| !v.is_empty()) {
362 let p = PathBuf::from(p);
363 return if p.exists() {
364 Ok(p)
365 } else {
366 Err(missing(format!(
367 "{CHROME_FOR_TESTING_ENV} points at a missing file: {}",
368 p.display()
369 )))
370 };
371 }
372 let candidates = chrome_for_testing_candidates();
373 if let Some(p) = candidates.iter().find(|p| p.exists()) {
374 return Ok(p.clone());
375 }
376 let searched: Vec<String> = candidates
377 .iter()
378 .map(|p| format!(" {}", p.display()))
379 .collect();
380 Err(missing(format!(
381 "Chrome for Testing is not installed; searched:\n{}",
382 if searched.is_empty() {
383 " (nothing)".to_string()
384 } else {
385 searched.join("\n")
386 }
387 )))
388}
389
390pub(crate) fn resolve_executable(opts: &LaunchOptions) -> Result<PathBuf> {
393 resolve_executable_with(opts, || chrome_for_testing_path(None), find_chrome)
394}
395
396fn resolve_executable_with(
397 opts: &LaunchOptions,
398 cft: impl FnOnce() -> Result<PathBuf>,
399 system: impl FnOnce() -> Option<PathBuf>,
400) -> Result<PathBuf> {
401 if opts.use_system_chrome_only {
402 return system().ok_or(BrowserError::ChromeNotFound);
403 }
404 if let Some(p) = &opts.chrome_path {
405 return Ok(p.clone());
406 }
407 match cft() {
408 Ok(p) => Ok(p),
409 Err(e) if opts.allow_system_chrome => system().ok_or(e),
410 Err(e) => Err(e),
411 }
412}
413
414pub fn find_chrome() -> Option<PathBuf> {
418 if let Ok(p) = std::env::var("CHROME") {
419 let p = PathBuf::from(p);
420 if p.exists() {
421 return Some(p);
422 }
423 }
424 let candidates: &[&str] = if cfg!(target_os = "windows") {
425 &[
426 r"C:\Program Files\Google\Chrome\Application\chrome.exe",
427 r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
428 r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe",
429 r"C:\Program Files\Microsoft\Edge\Application\msedge.exe",
430 ]
431 } else if cfg!(target_os = "macos") {
432 &[
433 "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
434 "/Applications/Chromium.app/Contents/MacOS/Chromium",
435 "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
436 ]
437 } else {
438 &[
439 "/usr/bin/google-chrome",
440 "/usr/bin/chromium",
441 "/usr/bin/chromium-browser",
442 "/usr/bin/microsoft-edge",
443 ]
444 };
445 candidates
446 .iter()
447 .map(PathBuf::from)
448 .find(|p| Path::new(p).exists())
449}
450
451pub(crate) fn validate_extra_args(args: &[String]) -> Result<()> {
454 const FORBIDDEN: &[&str] = &[
455 "user-data-dir",
456 "profile-directory",
457 "remote-debugging-port",
458 "remote-debugging-address",
459 "remote-debugging-pipe",
460 "remote-allow-origins",
461 "disable-web-security",
462 "incognito-bypass",
463 ];
464 for a in args {
465 let name = a.trim_start_matches('-').split('=').next().unwrap_or("");
466 if FORBIDDEN.contains(&name) {
467 return Err(BrowserError::Invalid(format!(
468 "chrome flag '--{name}' is managed by the session"
469 )));
470 }
471 }
472 Ok(())
473}
474
475pub(crate) fn reject_real_browser_profile(root: &Path) -> Result<()> {
477 let mut real: Vec<PathBuf> = Vec::new();
478 if let Some(home) = std::env::var_os("HOME")
479 .or_else(|| std::env::var_os("USERPROFILE"))
480 .map(PathBuf::from)
481 {
482 for rel in [
483 "Library/Application Support/Google/Chrome",
484 "Library/Application Support/Chromium",
485 "Library/Application Support/Microsoft Edge",
486 ".config/google-chrome",
487 ".config/chromium",
488 ".config/microsoft-edge",
489 ] {
490 real.push(home.join(rel));
491 }
492 }
493 if let Some(local) = std::env::var_os("LOCALAPPDATA").map(PathBuf::from) {
494 for rel in [
495 r"Google\Chrome\User Data",
496 r"Chromium\User Data",
497 r"Microsoft\Edge\User Data",
498 ] {
499 real.push(local.join(rel));
500 }
501 }
502 std::fs::create_dir_all(root)?;
503 let root = root.canonicalize()?;
504 for r in real {
505 if let Ok(r) = r.canonicalize() {
506 if root.starts_with(&r) {
507 return Err(BrowserError::Profile(
508 "refusing to use the user's real browser profile".into(),
509 ));
510 }
511 }
512 }
513 Ok(())
514}
515
516#[cfg(test)]
517mod tests {
518 use super::*;
519 use std::ffi::OsString;
520 use std::sync::{Mutex, MutexGuard};
521
522 static ENV_LOCK: Mutex<()> = Mutex::new(());
524
525 struct EnvGuard {
526 saved: Vec<(&'static str, Option<OsString>)>,
527 _lock: MutexGuard<'static, ()>,
528 }
529
530 impl EnvGuard {
531 fn new(values: &[(&'static str, Option<&Path>)]) -> Self {
532 let lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
533 let saved = values
534 .iter()
535 .map(|(key, _)| (*key, std::env::var_os(key)))
536 .collect();
537 for (key, value) in values {
538 match value {
539 Some(value) => std::env::set_var(key, value),
540 None => std::env::remove_var(key),
541 }
542 }
543 Self { saved, _lock: lock }
544 }
545 }
546
547 impl Drop for EnvGuard {
548 fn drop(&mut self) {
549 for (key, value) in &self.saved {
550 match value {
551 Some(value) => std::env::set_var(key, value),
552 None => std::env::remove_var(key),
553 }
554 }
555 }
556 }
557
558 fn write_executable(path: &Path) {
559 std::fs::create_dir_all(path.parent().unwrap()).unwrap();
560 std::fs::write(path, "fixture, not a browser").unwrap();
561 }
562
563 fn managed_executable(root: &Path, version: &str) -> PathBuf {
564 let relative = if cfg!(target_os = "macos") {
565 if cfg!(target_arch = "aarch64") {
566 "chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing"
567 } else {
568 "chrome-mac-x64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing"
569 }
570 } else if cfg!(target_os = "windows") {
571 "chrome-win64/chrome.exe"
572 } else {
573 "chrome-linux64/chrome"
574 };
575 root.join(version).join(relative)
576 }
577
578 #[test]
579 fn system_chrome_only_ignores_explicit_env_and_managed_cft() {
580 let temp = tempfile::tempdir().unwrap();
581 let root = temp.path().join("managed CfT");
582 let managed = managed_executable(&root, "130.10.0.0");
583 let explicit = temp.path().join("explicit CfT");
584 let system = temp.path().join("system Chrome");
585 for path in [&managed, &explicit, &system] {
586 write_executable(path);
587 }
588 let _env = EnvGuard::new(&[
589 (CHROME_FOR_TESTING_ROOT_ENV, Some(&root)),
590 (CHROME_FOR_TESTING_ENV, Some(&managed)),
591 ("CHROME", Some(&system)),
592 ]);
593 assert_eq!(chrome_for_testing_path(None).unwrap(), managed);
594 let opts = LaunchOptions {
595 chrome_path: Some(explicit),
596 ..LaunchOptions::default().system_chrome_only()
597 };
598 assert_eq!(resolve_executable(&opts).unwrap(), system);
599 assert_eq!(find_chrome().unwrap(), system);
600 assert_eq!(
602 resolve_executable_with(
603 &opts,
604 || panic!("CfT discovery called"),
605 || Some(system.clone()),
606 )
607 .unwrap(),
608 system
609 );
610 }
611
612 #[test]
613 fn system_chrome_only_errors_when_system_browser_is_missing() {
614 let opts = LaunchOptions::default().system_chrome_only();
615 let err =
616 resolve_executable_with(&opts, || panic!("CfT discovery called"), || None).unwrap_err();
617 assert!(matches!(&err, BrowserError::ChromeNotFound));
618 assert_eq!(
619 err.to_string(),
620 "no Chrome, Chromium, or Edge executable found"
621 );
622 }
623
624 #[test]
625 fn defaults_and_system_chrome_preserve_cft_preference_and_fallback() {
626 let temp = tempfile::tempdir().unwrap();
627 let cft = temp.path().join("CfT");
628 let system = temp.path().join("system Chrome");
629 write_executable(&cft);
630 write_executable(&system);
631 let defaults = LaunchOptions::default();
632 assert!(defaults.headless && defaults.mute_audio);
633 assert!(!defaults.allow_system_chrome && !defaults.use_system_chrome_only);
634 for opts in [defaults.clone(), defaults.clone().system_chrome()] {
635 assert_eq!(
636 resolve_executable_with(
637 &opts,
638 || Ok(cft.clone()),
639 || panic!("system lookup called"),
640 )
641 .unwrap(),
642 cft
643 );
644 }
645 assert!(matches!(
646 resolve_executable_with(
647 &defaults,
648 || Err(BrowserError::ChromeForTestingNotFound("missing".into())),
649 || panic!("default must not fall back"),
650 ),
651 Err(BrowserError::ChromeForTestingNotFound(_))
652 ));
653 let fallback = defaults.system_chrome_only().system_chrome();
654 assert!(!fallback.use_system_chrome_only);
655 assert_eq!(
656 resolve_executable_with(
657 &fallback,
658 || Err(BrowserError::ChromeForTestingNotFound("missing".into())),
659 || Some(system.clone()),
660 )
661 .unwrap(),
662 system
663 );
664 }
665
666 #[test]
667 fn managed_root_uses_platform_data_directory_and_env_override() {
668 let temp = tempfile::tempdir().unwrap();
669 let home = temp.path();
670 let suffix = Path::new("Orthic Labs/shared/chrome-for-testing");
671 let default_data = if cfg!(target_os = "macos") {
672 home.join("Library/Application Support")
673 } else if cfg!(target_os = "windows") {
674 home.join("AppData/Local")
675 } else {
676 home.join(".local/share")
677 };
678 assert_eq!(
679 managed_chrome_for_testing_root(home, &|_| None),
680 default_data.join(suffix)
681 );
682 let data = home.join("custom app data");
683 let with_data = |key: &str| {
684 if key == "LOCALAPPDATA" || key == "XDG_DATA_HOME" {
685 Some(data.clone())
686 } else {
687 None
688 }
689 };
690 let expected_data = if cfg!(target_os = "macos") {
691 default_data
692 } else {
693 data.clone()
694 };
695 assert_eq!(
696 managed_chrome_for_testing_root(home, &with_data),
697 expected_data.join(suffix)
698 );
699 let override_root = home.join("custom managed root");
700 assert_eq!(
701 managed_chrome_for_testing_root(home, &|key| {
702 if key == CHROME_FOR_TESTING_ROOT_ENV {
703 Some(override_root.clone())
704 } else {
705 None
706 }
707 }),
708 override_root
709 );
710 }
711
712 #[test]
713 fn managed_folder_wins_with_newest_numeric_version_first() {
714 let temp = tempfile::tempdir().unwrap();
715 let root = temp.path().join("managed Chrome for Testing");
716 let puppeteer = temp.path().join("puppeteer");
717 let older = managed_executable(&root, "129.99.0.0");
718 let recent = managed_executable(&root, "130.2.0.0");
719 let newest = managed_executable(&root, "130.10.0.0");
720 let legacy_dir = if cfg!(target_os = "macos") {
721 "mac-999.0.0.0"
722 } else if cfg!(target_os = "windows") {
723 "win64-999.0.0.0"
724 } else {
725 "linux-999.0.0.0"
726 };
727 let legacy = managed_executable(&puppeteer.join("chrome"), legacy_dir);
728 for path in [&older, &recent, &newest, &legacy] {
729 write_executable(path);
730 }
731 std::fs::create_dir(root.join("999.staging")).unwrap();
732 std::fs::write(root.join("999.0.0.0"), "not a version directory").unwrap();
733 let _env = EnvGuard::new(&[
734 (CHROME_FOR_TESTING_ROOT_ENV, Some(&root)),
735 (CHROME_FOR_TESTING_ENV, None),
736 ("PUPPETEER_CACHE_DIR", Some(&puppeteer)),
737 ]);
738 let candidates = chrome_for_testing_candidates();
739 assert_eq!(&candidates[..3], &[newest.clone(), recent.clone(), older]);
740 assert!(candidates.iter().position(|p| p == &legacy).unwrap() >= 3);
741 assert_eq!(chrome_for_testing_path(None).unwrap(), newest);
742 std::env::set_var(CHROME_FOR_TESTING_ENV, &legacy);
743 assert_eq!(chrome_for_testing_path(None).unwrap(), legacy);
744 std::env::remove_var(CHROME_FOR_TESTING_ENV);
745 std::fs::remove_file(&newest).unwrap();
746 assert_eq!(chrome_for_testing_path(None).unwrap(), recent);
747 assert_eq!(chrome_for_testing_path(Some(&legacy)).unwrap(), legacy);
749 }
750}