playwright_core/
driver.rs

1// Playwright driver management
2//
3// Handles locating and managing the Playwright Node.js driver.
4// Follows the same architecture as playwright-python, playwright-java, and playwright-dotnet.
5
6use crate::{Error, Result};
7use std::path::{Path, PathBuf};
8use std::process::Command;
9
10/// Get the path to the Playwright driver executable
11///
12/// This function attempts to locate the Playwright driver in the following order:
13/// 1. Bundled driver downloaded by build.rs (PRIMARY - matches official bindings)
14/// 2. PLAYWRIGHT_DRIVER_PATH environment variable (user override)
15/// 3. PLAYWRIGHT_NODE_EXE and PLAYWRIGHT_CLI_JS environment variables (user override)
16/// 4. Global npm installation (`npm root -g`) (development fallback)
17/// 5. Local npm installation (`npm root`) (development fallback)
18///
19/// Returns a tuple of (node_executable_path, cli_js_path).
20///
21/// # Errors
22///
23/// Returns `Error::ServerNotFound` if the driver cannot be located in any of the search paths.
24///
25/// # Example
26///
27/// ```ignore
28/// use playwright_core::driver::get_driver_executable;
29///
30/// let (node_exe, cli_js) = get_driver_executable()?;
31/// println!("Node: {}", node_exe.display());
32/// println!("CLI:  {}", cli_js.display());
33/// # Ok::<(), playwright_core::Error>(())
34/// ```
35pub fn get_driver_executable() -> Result<(PathBuf, PathBuf)> {
36    // 1. Try bundled driver from build.rs (PRIMARY PATH - matches official bindings)
37    if let Some(result) = try_bundled_driver()? {
38        return Ok(result);
39    }
40
41    // 2. Try PLAYWRIGHT_DRIVER_PATH environment variable
42    if let Some(result) = try_driver_path_env()? {
43        return Ok(result);
44    }
45
46    // 3. Try PLAYWRIGHT_NODE_EXE and PLAYWRIGHT_CLI_JS environment variables
47    if let Some(result) = try_node_cli_env()? {
48        return Ok(result);
49    }
50
51    // 4. Try npm global installation (development fallback)
52    if let Some(result) = try_npm_global()? {
53        return Ok(result);
54    }
55
56    // 5. Try npm local installation (development fallback)
57    if let Some(result) = try_npm_local()? {
58        return Ok(result);
59    }
60
61    Err(Error::ServerNotFound)
62}
63
64/// Try to find bundled driver from build.rs
65///
66/// This is the PRIMARY path and matches how playwright-python, playwright-java,
67/// and playwright-dotnet distribute their drivers.
68fn try_bundled_driver() -> Result<Option<(PathBuf, PathBuf)>> {
69    // Check if build.rs set the environment variables (compile-time)
70    if let (Some(node_exe), Some(cli_js)) = (
71        option_env!("PLAYWRIGHT_NODE_EXE"),
72        option_env!("PLAYWRIGHT_CLI_JS"),
73    ) {
74        let node_path = PathBuf::from(node_exe);
75        let cli_path = PathBuf::from(cli_js);
76
77        if node_path.exists() && cli_path.exists() {
78            return Ok(Some((node_path, cli_path)));
79        }
80    }
81
82    // Fallback: Check PLAYWRIGHT_DRIVER_DIR and construct paths (compile-time)
83    if let Some(driver_dir) = option_env!("PLAYWRIGHT_DRIVER_DIR") {
84        let driver_path = PathBuf::from(driver_dir);
85        let node_exe = if cfg!(windows) {
86            driver_path.join("node.exe")
87        } else {
88            driver_path.join("node")
89        };
90        let cli_js = driver_path.join("package").join("cli.js");
91
92        if node_exe.exists() && cli_js.exists() {
93            return Ok(Some((node_exe, cli_js)));
94        }
95    }
96
97    Ok(None)
98}
99
100/// Try to find driver from PLAYWRIGHT_DRIVER_PATH environment variable
101///
102/// User can set PLAYWRIGHT_DRIVER_PATH to a directory containing:
103/// - node (or node.exe on Windows)
104/// - package/cli.js
105fn try_driver_path_env() -> Result<Option<(PathBuf, PathBuf)>> {
106    if let Ok(driver_path) = std::env::var("PLAYWRIGHT_DRIVER_PATH") {
107        let driver_dir = PathBuf::from(driver_path);
108        let node_exe = if cfg!(windows) {
109            driver_dir.join("node.exe")
110        } else {
111            driver_dir.join("node")
112        };
113        let cli_js = driver_dir.join("package").join("cli.js");
114
115        if node_exe.exists() && cli_js.exists() {
116            return Ok(Some((node_exe, cli_js)));
117        }
118    }
119
120    Ok(None)
121}
122
123/// Try to find driver from PLAYWRIGHT_NODE_EXE and PLAYWRIGHT_CLI_JS environment variables
124///
125/// User can set both variables to explicitly specify paths.
126fn try_node_cli_env() -> Result<Option<(PathBuf, PathBuf)>> {
127    if let (Ok(node_exe), Ok(cli_js)) = (
128        std::env::var("PLAYWRIGHT_NODE_EXE"),
129        std::env::var("PLAYWRIGHT_CLI_JS"),
130    ) {
131        let node_path = PathBuf::from(node_exe);
132        let cli_path = PathBuf::from(cli_js);
133
134        if node_path.exists() && cli_path.exists() {
135            return Ok(Some((node_path, cli_path)));
136        }
137    }
138
139    Ok(None)
140}
141
142/// Try to find driver in npm global installation (development fallback)
143fn try_npm_global() -> Result<Option<(PathBuf, PathBuf)>> {
144    let output = Command::new("npm").args(["root", "-g"]).output();
145
146    if let Ok(output) = output {
147        if output.status.success() {
148            let npm_root = String::from_utf8_lossy(&output.stdout).trim().to_string();
149            let node_modules = PathBuf::from(npm_root);
150            if node_modules.exists() {
151                if let Ok(paths) = find_playwright_in_node_modules(&node_modules) {
152                    return Ok(Some(paths));
153                }
154            }
155        }
156    }
157
158    Ok(None)
159}
160
161/// Try to find driver in npm local installation (development fallback)
162fn try_npm_local() -> Result<Option<(PathBuf, PathBuf)>> {
163    let output = Command::new("npm").args(["root"]).output();
164
165    if let Ok(output) = output {
166        if output.status.success() {
167            let npm_root = String::from_utf8_lossy(&output.stdout).trim().to_string();
168            let node_modules = PathBuf::from(npm_root);
169            if node_modules.exists() {
170                if let Ok(paths) = find_playwright_in_node_modules(&node_modules) {
171                    return Ok(Some(paths));
172                }
173            }
174        }
175    }
176
177    Ok(None)
178}
179
180/// Find Playwright CLI in node_modules directory
181///
182/// Returns (node_executable, cli_js_path)
183fn find_playwright_in_node_modules(node_modules: &Path) -> Result<(PathBuf, PathBuf)> {
184    // Look for playwright or @playwright/test package
185    let playwright_dirs = [
186        node_modules.join("playwright"),
187        node_modules.join("@playwright").join("test"),
188    ];
189
190    for playwright_dir in &playwright_dirs {
191        if !playwright_dir.exists() {
192            continue;
193        }
194
195        // Find cli.js in the package
196        let cli_js = playwright_dir.join("cli.js");
197        if !cli_js.exists() {
198            continue;
199        }
200
201        // Find node executable from PATH
202        if let Ok(node_exe) = find_node_executable() {
203            return Ok((node_exe, cli_js));
204        }
205    }
206
207    Err(Error::ServerNotFound)
208}
209
210/// Find the node executable in PATH or common locations
211fn find_node_executable() -> Result<PathBuf> {
212    // Try which/where command first
213    #[cfg(not(windows))]
214    let which_cmd = "which";
215    #[cfg(windows)]
216    let which_cmd = "where";
217
218    if let Ok(output) = Command::new(which_cmd).arg("node").output() {
219        if output.status.success() {
220            let node_path = String::from_utf8_lossy(&output.stdout).trim().to_string();
221            if !node_path.is_empty() {
222                let path = PathBuf::from(node_path.lines().next().unwrap_or(&node_path));
223                if path.exists() {
224                    return Ok(path);
225                }
226            }
227        }
228    }
229
230    // Try common locations
231    #[cfg(not(windows))]
232    let common_locations = [
233        "/usr/local/bin/node",
234        "/usr/bin/node",
235        "/opt/homebrew/bin/node",
236        "/opt/local/bin/node",
237    ];
238
239    #[cfg(windows)]
240    let common_locations = [
241        "C:\\Program Files\\nodejs\\node.exe",
242        "C:\\Program Files (x86)\\nodejs\\node.exe",
243    ];
244
245    for location in &common_locations {
246        let path = PathBuf::from(location);
247        if path.exists() {
248            return Ok(path);
249        }
250    }
251
252    Err(Error::LaunchFailed(
253        "Node.js executable not found. Please install Node.js or set PLAYWRIGHT_NODE_EXE."
254            .to_string(),
255    ))
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261
262    #[test]
263    fn test_find_node_executable() {
264        // This should succeed on any system with Node.js installed
265        let result = find_node_executable();
266        match result {
267            Ok(node_path) => {
268                println!("Found node at: {:?}", node_path);
269                assert!(node_path.exists());
270            }
271            Err(e) => {
272                println!(
273                    "Node.js not found (expected if Node.js not installed): {:?}",
274                    e
275                );
276                // Don't fail the test if Node.js is not installed
277            }
278        }
279    }
280
281    #[test]
282    fn test_get_driver_executable() {
283        // This test will pass if any driver source is available
284        let result = get_driver_executable();
285        match result {
286            Ok((node, cli)) => {
287                println!("Found Playwright driver:");
288                println!("  Node: {:?}", node);
289                println!("  CLI:  {:?}", cli);
290                assert!(node.exists());
291                assert!(cli.exists());
292            }
293            Err(Error::ServerNotFound) => {
294                println!("Playwright driver not found (expected in some environments)");
295                println!(
296                    "This is OK - driver will be bundled at build time or can be installed via npm"
297                );
298            }
299            Err(e) => panic!("Unexpected error: {:?}", e),
300        }
301    }
302
303    #[test]
304    fn test_bundled_driver_detection() {
305        // Test that we can detect bundled driver if build.rs set env vars
306        let result = try_bundled_driver();
307        match result {
308            Ok(Some((node, cli))) => {
309                println!("Found bundled driver:");
310                println!("  Node: {:?}", node);
311                println!("  CLI:  {:?}", cli);
312                assert!(node.exists());
313                assert!(cli.exists());
314            }
315            Ok(None) => {
316                println!("No bundled driver (expected during development)");
317            }
318            Err(e) => panic!("Unexpected error: {:?}", e),
319        }
320    }
321}