#![cfg(feature = "manager-tests")]
use std::future::Future;
use std::time::Duration;
use thirtyfour::manager::WebDriverManager;
use thirtyfour::prelude::*;
use thirtyfour::{ChromeCapabilities, EdgeCapabilities, FirefoxCapabilities};
const TEST_TIMEOUT: Duration = Duration::from_secs(180);
async fn with_timeout<F, T>(f: F) -> WebDriverResult<T>
where
F: Future<Output = WebDriverResult<T>>,
{
tokio::time::timeout(TEST_TIMEOUT, f).await.unwrap_or_else(|_| {
Err(WebDriverError::FatalError(format!("test exceeded {}s budget", TEST_TIMEOUT.as_secs())))
})
}
fn skip_navigation() -> bool {
cfg!(target_os = "windows")
}
fn chrome_caps() -> ChromeCapabilities {
let mut caps = DesiredCapabilities::chrome();
caps.set_headless().unwrap();
caps.set_no_sandbox().unwrap();
caps.set_disable_gpu().unwrap();
caps.set_disable_dev_shm_usage().unwrap();
caps.add_arg("--no-sandbox").unwrap();
caps
}
fn firefox_caps() -> FirefoxCapabilities {
let mut caps = DesiredCapabilities::firefox();
caps.set_headless().unwrap();
caps
}
fn edge_caps() -> EdgeCapabilities {
let mut caps = DesiredCapabilities::edge();
caps.add_arg("--headless=new").unwrap();
caps.add_arg("--no-sandbox").unwrap();
caps.add_arg("--disable-gpu").unwrap();
caps.add_arg("--disable-dev-shm-usage").unwrap();
caps
}
#[tokio::test(flavor = "multi_thread")]
async fn managed_chrome_smoke() -> WebDriverResult<()> {
with_timeout(async {
let driver = WebDriver::managed(chrome_caps()).await?;
if !skip_navigation() {
driver.goto("about:blank").await?;
}
driver.quit().await?;
Ok(())
})
.await
}
#[tokio::test(flavor = "multi_thread")]
async fn managed_firefox_smoke() -> WebDriverResult<()> {
with_timeout(async {
let driver = WebDriver::managed(firefox_caps()).await?;
driver.goto("about:blank").await?;
driver.quit().await?;
Ok(())
})
.await
}
#[tokio::test(flavor = "multi_thread")]
async fn managed_edge_smoke() -> WebDriverResult<()> {
with_timeout(async {
let driver = WebDriver::managed(edge_caps()).await?;
driver.quit().await?;
Ok(())
})
.await
}
#[cfg(target_os = "macos")]
#[tokio::test(flavor = "multi_thread")]
async fn managed_safari_smoke() -> WebDriverResult<()> {
with_timeout(async {
let mut caps = DesiredCapabilities::safari();
let _ = &mut caps; let driver = WebDriver::managed(caps).await?;
driver.goto("about:blank").await?;
driver.quit().await?;
Ok(())
})
.await
}
#[tokio::test(flavor = "multi_thread")]
async fn managed_chrome_options_and_dedup() -> WebDriverResult<()> {
with_timeout(async {
let cache = tempfile::tempdir().expect("tempdir");
let mgr = WebDriverManager::builder()
.match_local() .cache_dir(cache.path().to_path_buf())
.ready_timeout(Duration::from_secs(60))
.build();
let d1 = mgr.launch(chrome_caps()).await?;
let d2 = mgr.launch(chrome_caps()).await?;
assert_eq!(
d1.server_url(),
d2.server_url(),
"two managed sessions for the same browser must share a chromedriver process"
);
if !skip_navigation() {
d1.goto("about:blank").await?;
d2.goto("about:blank").await?;
}
d1.quit().await?;
d2.quit().await?;
Ok(())
})
.await
}
async fn driver_is_listening(server_url: &url::Url) -> bool {
let host = server_url.host_str().unwrap_or("127.0.0.1");
let port = server_url.port_or_known_default().unwrap_or(0);
tokio::time::timeout(Duration::from_secs(1), tokio::net::TcpStream::connect((host, port)))
.await
.map(|r| r.is_ok())
.unwrap_or(false)
}
#[tokio::test(flavor = "multi_thread")]
async fn dropping_managed_driver_kills_subprocess_multi_thread() -> WebDriverResult<()> {
drop_kills_subprocess_inner().await
}
#[tokio::test(flavor = "current_thread")]
async fn dropping_managed_driver_kills_subprocess_current_thread() -> WebDriverResult<()> {
drop_kills_subprocess_inner().await
}
async fn drop_kills_subprocess_inner() -> WebDriverResult<()> {
with_timeout(async {
let server_url = {
let driver = WebDriver::managed(chrome_caps()).await?;
let url = driver.server_url().clone();
assert!(driver_is_listening(&url).await, "driver should be listening while alive");
if !skip_navigation() {
driver.goto("about:blank").await?;
}
url
};
tokio::time::sleep(Duration::from_millis(500)).await;
assert!(
!driver_is_listening(&server_url).await,
"chromedriver at {server_url} should be gone after WebDriver was dropped without quit"
);
Ok(())
})
.await
}
#[tokio::test(flavor = "multi_thread")]
async fn panic_unwind_kills_managed_driver_subprocess() -> WebDriverResult<()> {
use std::sync::{Arc, Mutex};
with_timeout(async {
let captured: Arc<Mutex<Option<url::Url>>> = Arc::new(Mutex::new(None));
let captured_clone = Arc::clone(&captured);
let join = std::thread::spawn(move || {
thirtyfour::support::block_on(async move {
let driver = WebDriver::managed(chrome_caps()).await.expect("managed launch");
*captured_clone.lock().unwrap() = Some(driver.server_url().clone());
if !skip_navigation() {
driver.goto("about:blank").await.expect("goto");
}
panic!("simulated user panic with WebDriver still in scope");
});
})
.join();
assert!(join.is_err(), "thread should have panicked");
let url = captured
.lock()
.unwrap()
.take()
.expect("server URL should have been captured before panic");
tokio::time::sleep(Duration::from_millis(500)).await;
assert!(
!driver_is_listening(&url).await,
"chromedriver at {url} should be gone after panic unwind dropped the WebDriver"
);
Ok(())
})
.await
}
#[tokio::test(flavor = "multi_thread")]
async fn concurrent_drops_kill_all_subprocesses() -> WebDriverResult<()> {
const N: usize = 3;
with_timeout(async {
let drivers: Vec<WebDriver> = futures_util::future::try_join_all(
(0..N).map(|_| async { WebDriver::managed(chrome_caps()).await }),
)
.await?;
let urls: Vec<url::Url> = drivers.iter().map(|d| d.server_url().clone()).collect();
for url in &urls {
assert!(driver_is_listening(url).await, "driver at {url} should be listening");
}
let drop_threads: Vec<_> =
drivers.into_iter().map(|d| std::thread::spawn(move || drop(d))).collect();
for t in drop_threads {
t.join().expect("drop thread panicked");
}
tokio::time::sleep(Duration::from_millis(500)).await;
for url in &urls {
assert!(
!driver_is_listening(url).await,
"chromedriver at {url} should be gone after concurrent drop"
);
}
Ok(())
})
.await
}
#[tokio::test(flavor = "multi_thread")]
async fn concurrent_sessions_against_shared_manager() -> WebDriverResult<()> {
use std::sync::Arc;
use thirtyfour::manager::WebDriverManager;
const ROUNDS: usize = 3;
const PARALLEL: usize = 3;
with_timeout(async {
let mgr: Arc<WebDriverManager> = WebDriverManager::builder().build();
for _round in 0..ROUNDS {
let sessions: Vec<WebDriver> =
futures_util::future::try_join_all((0..PARALLEL).map(|_| {
let mgr = Arc::clone(&mgr);
async move { mgr.launch(chrome_caps()).await }
}))
.await?;
assert_eq!(sessions.len(), PARALLEL, "all sessions should have launched");
let first_url = sessions[0].server_url().clone();
for s in &sessions {
assert_eq!(
s.server_url(),
&first_url,
"shared manager must dedup all sessions onto one chromedriver"
);
}
let drop_threads: Vec<_> =
sessions.into_iter().map(|d| std::thread::spawn(move || drop(d))).collect();
for t in drop_threads {
t.join().expect("drop thread panicked");
}
}
Ok(())
})
.await
}
#[tokio::test(flavor = "multi_thread")]
async fn interleaved_create_and_drop() -> WebDriverResult<()> {
use std::sync::Arc;
use thirtyfour::manager::WebDriverManager;
const ROUNDS: usize = 5;
with_timeout(async {
let mgr: Arc<WebDriverManager> = WebDriverManager::builder().build();
let mut prev: Option<WebDriver> = None;
for _round in 0..ROUNDS {
let next = mgr.launch(chrome_caps()).await?;
if let Some(old) = prev.take() {
let t = std::thread::spawn(move || drop(old));
t.join().expect("drop thread panicked");
}
prev = Some(next);
}
if let Some(last) = prev.take() {
last.quit().await?;
}
Ok(())
})
.await
}