use crate::{Error, Result};
use std::path::{Path, PathBuf};
use std::process::Command;
pub fn get_driver_executable() -> Result<(PathBuf, PathBuf)> {
if let Some(result) = try_bundled_driver()? {
return Ok(result);
}
if let Some(result) = try_driver_path_env()? {
return Ok(result);
}
if let Some(result) = try_node_cli_env()? {
return Ok(result);
}
if let Some(result) = try_npm_global()? {
return Ok(result);
}
if let Some(result) = try_npm_local()? {
return Ok(result);
}
Err(Error::ServerNotFound)
}
fn try_bundled_driver() -> Result<Option<(PathBuf, PathBuf)>> {
if let (Some(node_exe), Some(cli_js)) = (
option_env!("PLAYWRIGHT_NODE_EXE"),
option_env!("PLAYWRIGHT_CLI_JS"),
) {
let node_path = PathBuf::from(node_exe);
let cli_path = PathBuf::from(cli_js);
if node_path.exists() && cli_path.exists() {
return Ok(Some((node_path, cli_path)));
}
}
if let Some(driver_dir) = option_env!("PLAYWRIGHT_DRIVER_DIR") {
let driver_path = PathBuf::from(driver_dir);
let node_exe = if cfg!(windows) {
driver_path.join("node.exe")
} else {
driver_path.join("node")
};
let cli_js = driver_path.join("package").join("cli.js");
if node_exe.exists() && cli_js.exists() {
return Ok(Some((node_exe, cli_js)));
}
}
Ok(None)
}
fn try_driver_path_env() -> Result<Option<(PathBuf, PathBuf)>> {
if let Ok(driver_path) = std::env::var("PLAYWRIGHT_DRIVER_PATH") {
let driver_dir = PathBuf::from(driver_path);
let node_exe = if cfg!(windows) {
driver_dir.join("node.exe")
} else {
driver_dir.join("node")
};
let cli_js = driver_dir.join("package").join("cli.js");
if node_exe.exists() && cli_js.exists() {
return Ok(Some((node_exe, cli_js)));
}
}
Ok(None)
}
fn try_node_cli_env() -> Result<Option<(PathBuf, PathBuf)>> {
if let (Ok(node_exe), Ok(cli_js)) = (
std::env::var("PLAYWRIGHT_NODE_EXE"),
std::env::var("PLAYWRIGHT_CLI_JS"),
) {
let node_path = PathBuf::from(node_exe);
let cli_path = PathBuf::from(cli_js);
if node_path.exists() && cli_path.exists() {
return Ok(Some((node_path, cli_path)));
}
}
Ok(None)
}
fn try_npm_global() -> Result<Option<(PathBuf, PathBuf)>> {
let output = Command::new("npm").args(["root", "-g"]).output();
if let Ok(output) = output
&& output.status.success()
{
let npm_root = String::from_utf8_lossy(&output.stdout).trim().to_string();
let node_modules = PathBuf::from(npm_root);
if node_modules.exists()
&& let Ok(paths) = find_playwright_in_node_modules(&node_modules)
{
return Ok(Some(paths));
}
}
Ok(None)
}
fn try_npm_local() -> Result<Option<(PathBuf, PathBuf)>> {
let output = Command::new("npm").args(["root"]).output();
if let Ok(output) = output
&& output.status.success()
{
let npm_root = String::from_utf8_lossy(&output.stdout).trim().to_string();
let node_modules = PathBuf::from(npm_root);
if node_modules.exists()
&& let Ok(paths) = find_playwright_in_node_modules(&node_modules)
{
return Ok(Some(paths));
}
}
Ok(None)
}
fn find_playwright_in_node_modules(node_modules: &Path) -> Result<(PathBuf, PathBuf)> {
let playwright_dirs = [
node_modules.join("playwright"),
node_modules.join("@playwright").join("test"),
];
for playwright_dir in &playwright_dirs {
if !playwright_dir.exists() {
continue;
}
let cli_js = playwright_dir.join("cli.js");
if !cli_js.exists() {
continue;
}
if let Ok(node_exe) = find_node_executable() {
return Ok((node_exe, cli_js));
}
}
Err(Error::ServerNotFound)
}
fn find_node_executable() -> Result<PathBuf> {
#[cfg(not(windows))]
let which_cmd = "which";
#[cfg(windows)]
let which_cmd = "where";
if let Ok(output) = Command::new(which_cmd).arg("node").output()
&& output.status.success()
{
let node_path = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !node_path.is_empty() {
let path = PathBuf::from(node_path.lines().next().unwrap_or(&node_path));
if path.exists() {
return Ok(path);
}
}
}
#[cfg(not(windows))]
let common_locations = [
"/usr/local/bin/node",
"/usr/bin/node",
"/opt/homebrew/bin/node",
"/opt/local/bin/node",
];
#[cfg(windows)]
let common_locations = [
"C:\\Program Files\\nodejs\\node.exe",
"C:\\Program Files (x86)\\nodejs\\node.exe",
];
for location in &common_locations {
let path = PathBuf::from(location);
if path.exists() {
return Ok(path);
}
}
Err(Error::LaunchFailed(
"Node.js executable not found. Please install Node.js or set PLAYWRIGHT_NODE_EXE."
.to_string(),
))
}
pub async fn install_browsers(browsers: Option<&[&str]>) -> Result<()> {
install_browsers_impl(browsers, false).await
}
pub async fn install_browsers_with_deps(browsers: Option<&[&str]>) -> Result<()> {
install_browsers_impl(browsers, true).await
}
async fn install_browsers_impl(browsers: Option<&[&str]>, with_deps_forced: bool) -> Result<()> {
let (node_exe, cli_js) = get_driver_executable()?;
let mut cmd = tokio::process::Command::new(&node_exe);
cmd.arg(&cli_js).arg("install");
if let Some(browser_list) = browsers {
for browser in browser_list {
cmd.arg(browser);
}
}
if with_deps_forced || cfg!(target_os = "linux") {
cmd.arg("--with-deps");
}
let output = cmd.output().await.map_err(|e| {
Error::LaunchFailed(format!("Failed to spawn browser install process: {}", e))
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
return Err(Error::LaunchFailed(format!(
"Browser installation failed (exit code {:?}).\nstdout: {}\nstderr: {}",
output.status.code(),
stdout.trim(),
stderr.trim(),
)));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_find_node_executable() {
let result = find_node_executable();
match result {
Ok(node_path) => {
tracing::info!("Found node at: {:?}", node_path);
assert!(node_path.exists());
}
Err(e) => {
tracing::warn!(
"Node.js not found (expected if Node.js not installed): {:?}",
e
);
}
}
}
#[test]
fn test_get_driver_executable() {
let result = get_driver_executable();
match result {
Ok((node, cli)) => {
tracing::info!("Found Playwright driver:");
tracing::info!(" Node: {:?}", node);
tracing::info!(" CLI: {:?}", cli);
assert!(node.exists());
assert!(cli.exists());
}
Err(Error::ServerNotFound) => {
tracing::warn!("Playwright driver not found (expected in some environments)");
tracing::warn!(
"This is OK - driver will be bundled at build time or can be installed via npm"
);
}
Err(e) => panic!("Unexpected error: {:?}", e),
}
}
#[test]
fn test_bundled_driver_detection() {
let result = try_bundled_driver();
match result {
Ok(Some((node, cli))) => {
tracing::info!("Found bundled driver:");
tracing::info!(" Node: {:?}", node);
tracing::info!(" CLI: {:?}", cli);
assert!(node.exists());
assert!(cli.exists());
}
Ok(None) => {
tracing::info!("No bundled driver (expected during development)");
}
Err(e) => panic!("Unexpected error: {:?}", e),
}
}
}