use std::path::PathBuf;
use tracing::{debug, info, warn};
use super::detection::{
detect_all_browsers, get_browser_version, validate_browser, verify_executable,
};
use super::installer::BrowserInstaller;
use super::types::*;
use crate::error::{AuditError, Result};
#[derive(Default)]
pub struct BrowserResolveOptions {
pub browser_path: Option<String>,
pub browser_preference: Option<String>,
pub strict: bool,
}
pub fn resolve_browser(opts: &BrowserResolveOptions) -> Result<ResolvedBrowser> {
let mode = if opts.strict {
BrowserMode::Strict
} else {
BrowserMode::Standard
};
if let Some(ref path_str) = opts.browser_path {
let path = PathBuf::from(path_str);
let browser = validate_browser(&path, BrowserKind::Custom, BrowserSource::CliFlag)?;
info!(
"Using specified browser: {} v{}",
browser.kind.display_name(),
browser.version.as_deref().unwrap_or("unknown")
);
return Ok(ResolvedBrowser {
browser,
mode,
all_candidates: vec![],
});
}
if let Ok(path_str) = std::env::var("AUDITMYSITE_BROWSER") {
let path = PathBuf::from(&path_str);
if path.exists() {
let browser = validate_browser(&path, BrowserKind::Custom, BrowserSource::EnvVar)?;
info!("Using browser from AUDITMYSITE_BROWSER: {}", path.display());
return Ok(ResolvedBrowser {
browser,
mode,
all_candidates: vec![],
});
}
warn!(
"AUDITMYSITE_BROWSER points to non-existent path: {}",
path_str
);
}
if let Ok(path_str) = std::env::var("CHROME_PATH") {
let path = PathBuf::from(&path_str);
if path.exists() {
warn!("CHROME_PATH is deprecated, use AUDITMYSITE_BROWSER or --browser-path instead");
let browser = validate_browser(&path, BrowserKind::Chrome, BrowserSource::EnvVar)?;
return Ok(ResolvedBrowser {
browser,
mode,
all_candidates: vec![],
});
}
}
let filter_kind: Option<BrowserKind> = opts.browser_preference.as_deref().and_then(|s| match s
.to_lowercase()
.as_str()
{
"chrome" => Some(BrowserKind::Chrome),
"edge" => Some(BrowserKind::Edge),
"chromium" => Some(BrowserKind::Chromium),
"auto" | "" => None,
other => {
warn!("Unknown browser kind '{}', using auto-detection", other);
None
}
});
let all_candidates = detect_all_browsers();
debug!("Found {} browser candidates", all_candidates.len());
let best = if let Some(kind) = filter_kind {
all_candidates.iter().find(|b| b.kind == kind)
} else {
all_candidates.first()
};
if let Some(browser) = best {
info!(
"Selected browser: {} v{} ({})",
browser.kind.display_name(),
browser.version.as_deref().unwrap_or("unknown"),
browser.path.display()
);
return Ok(ResolvedBrowser {
browser: browser.clone(),
mode,
all_candidates,
});
}
if mode != BrowserMode::Strict {
if let Some(managed) = check_managed_install() {
info!("Using managed browser: {}", managed.path.display());
return Ok(ResolvedBrowser {
browser: managed,
mode,
all_candidates,
});
}
}
Err(AuditError::ChromeNotFound)
}
pub fn stalls_on_keyboard_journeys(browser: &DetectedBrowser) -> bool {
let is_shell = browser.kind == BrowserKind::HeadlessShell
|| browser
.path
.file_name()
.is_some_and(|name| name.to_string_lossy().starts_with("chrome-headless-shell"));
cfg!(target_os = "macos") && !is_shell
}
pub fn parallel_journeys_stall(browser_path: Option<&str>) -> bool {
static STALLS: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*STALLS.get_or_init(|| {
let opts = BrowserResolveOptions {
browser_path: browser_path.map(str::to_string),
..Default::default()
};
resolve_browser(&opts)
.map(|resolved| stalls_on_keyboard_journeys(&resolved.browser))
.unwrap_or(false)
})
}
fn check_managed_headless_shell() -> Option<DetectedBrowser> {
let dir = dirs::home_dir()?
.join(".auditmysite")
.join("browsers")
.join("headless-shell");
let path = BrowserInstaller::binary_path(&dir, InstallTarget::HeadlessShell);
if !path.exists() || verify_executable(&path).is_err() {
return None;
}
Some(DetectedBrowser {
kind: BrowserKind::HeadlessShell,
path,
version: read_version_file(&dir),
source: BrowserSource::ManagedInstall,
})
}
fn check_managed_install() -> Option<DetectedBrowser> {
let base = dirs::home_dir()?.join(".auditmysite").join("browsers");
let cft_path = managed_binary_path(&base, "chrome-for-testing");
if cft_path.exists() && verify_executable(&cft_path).is_ok() {
return Some(DetectedBrowser {
kind: BrowserKind::ChromeForTesting,
path: cft_path,
version: read_version_file(&base.join("chrome-for-testing")),
source: BrowserSource::ManagedInstall,
});
}
if let Some(shell) = check_managed_headless_shell() {
return Some(shell);
}
let legacy_path = legacy_chromium_path();
if let Some(ref path) = legacy_path {
if path.exists() && verify_executable(path).is_ok() {
let version = get_browser_version(path);
return Some(DetectedBrowser {
kind: BrowserKind::ChromeForTesting,
path: path.clone(),
version,
source: BrowserSource::ManagedInstall,
});
}
}
None
}
fn managed_binary_path(base: &std::path::Path, name: &str) -> PathBuf {
let dir = base.join(name);
if cfg!(target_os = "macos") {
if cfg!(target_arch = "aarch64") {
dir.join("chrome-mac-arm64")
.join("Google Chrome for Testing.app")
.join("Contents")
.join("MacOS")
.join("Google Chrome for Testing")
} else {
dir.join("chrome-mac-x64")
.join("Google Chrome for Testing.app")
.join("Contents")
.join("MacOS")
.join("Google Chrome for Testing")
}
} else if cfg!(target_os = "linux") {
dir.join("chrome-linux64").join("chrome")
} else {
dir.join("chrome-win64").join("chrome.exe")
}
}
fn legacy_chromium_path() -> Option<PathBuf> {
let cache_dir = dirs::home_dir()?.join(".auditmysite").join("chromium");
Some(if cfg!(target_os = "macos") {
cache_dir
.join("chrome-mac")
.join("Chromium.app")
.join("Contents")
.join("MacOS")
.join("Chromium")
} else if cfg!(target_os = "linux") {
cache_dir.join("chrome-linux").join("chrome")
} else {
cache_dir.join("chrome-win").join("chrome.exe")
})
}
fn read_version_file(dir: &std::path::Path) -> Option<String> {
std::fs::read_to_string(dir.join("version.txt"))
.ok()
.map(|s| s.trim().to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resolve_with_invalid_explicit_path() {
let opts = BrowserResolveOptions {
browser_path: Some("/nonexistent/browser".to_string()),
..Default::default()
};
assert!(resolve_browser(&opts).is_err());
}
#[test]
fn test_managed_binary_path_format() {
let base = PathBuf::from("/home/user/.auditmysite/browsers");
let path = managed_binary_path(&base, "chrome-for-testing");
assert!(path.to_string_lossy().contains("chrome-for-testing"));
}
fn browser(kind: BrowserKind, path: &str) -> DetectedBrowser {
DetectedBrowser {
kind,
path: PathBuf::from(path),
version: None,
source: BrowserSource::SystemPath,
}
}
#[test]
fn headless_shell_never_counts_as_stalling() {
let managed = browser(BrowserKind::HeadlessShell, "/x/chrome-headless-shell");
let explicit = browser(BrowserKind::Custom, "/cache/chrome-headless-shell");
assert!(!stalls_on_keyboard_journeys(&managed));
assert!(!stalls_on_keyboard_journeys(&explicit));
}
#[test]
fn full_chrome_stalls_only_on_macos() {
let chrome = browser(
BrowserKind::Chrome,
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
);
assert_eq!(
stalls_on_keyboard_journeys(&chrome),
cfg!(target_os = "macos")
);
}
}