use std::{future::Future, io, net::SocketAddr, ops::Deref, panic};
use axum::Router;
use futures::FutureExt;
use thirtyfour::error::{WebDriverError, WebDriverResult};
use tokio::{net::TcpListener, sync::oneshot, task::JoinHandle};
pub struct App {
addr: SocketAddr,
shutdown_tx: Option<oneshot::Sender<()>>,
task: Option<JoinHandle<io::Result<()>>>,
driver: Option<thirtyfour::WebDriver>,
}
pub const WEBDRIVER_URL_ENV: &str = "WEBDRIVER_URL";
pub const CHROME_BIN_ENV: &str = "CHROME_BIN";
fn chromium_binary() -> Option<String> {
if let Ok(path) = std::env::var(CHROME_BIN_ENV) {
return Some(path);
}
std::env::var_os("PATH").and_then(|paths| {
std::env::split_paths(&paths)
.map(|p| p.join("chromium"))
.find(|p| p.is_file())
.map(|p| p.to_string_lossy().into_owned())
})
}
async fn chrome() -> WebDriverResult<thirtyfour::WebDriver> {
use thirtyfour::prelude::*;
const DEFAULT_WEBDRIVER_URL: &str = "http://127.0.0.1:9515";
let url = std::env::var(WEBDRIVER_URL_ENV).unwrap_or_else(|_| DEFAULT_WEBDRIVER_URL.to_owned());
let mut caps = DesiredCapabilities::chrome();
if let Some(path) = chromium_binary() {
caps.set_binary(&path)?;
}
caps.set_headless()?;
caps.set_no_sandbox()?;
caps.set_disable_gpu()?;
caps.set_disable_dev_shm_usage()?;
WebDriver::new(url, caps).await
}
impl App {
pub async fn new(app: Router) -> WebDriverResult<Self> {
let listener = TcpListener::bind(("127.0.0.1", 0)).await?;
let addr = listener.local_addr()?;
let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>();
let task = tokio::spawn(async move {
axum::serve(listener, app)
.with_graceful_shutdown(async {
let _ = shutdown_rx.await;
})
.await
});
let driver = match chrome().await {
Ok(driver) => driver,
Err(err) => {
let _ = shutdown_tx.send(());
return Err(err);
}
};
Ok(Self {
addr,
shutdown_tx: Some(shutdown_tx),
task: Some(task),
driver: Some(driver),
})
}
pub async fn run<F, Fut, T>(self, f: F) -> WebDriverResult<T>
where
F: FnOnce(Session) -> Fut,
Fut: Future<Output = WebDriverResult<T>>,
{
let session = Session {
addr: self.addr,
driver: self.driver.as_ref().expect("browser to be present").clone(),
};
let future = match panic::catch_unwind(panic::AssertUnwindSafe(|| f(session))) {
Ok(future) => future,
Err(payload) => {
if let Err(err) = self.shutdown().await {
eprintln!("failed to shut down test app after panic: {err}");
}
panic::resume_unwind(payload);
}
};
let result = panic::AssertUnwindSafe(future).catch_unwind().await;
match result {
Ok(result) => match (result, self.shutdown().await) {
(Ok(value), Ok(())) => Ok(value),
(Err(err), Ok(())) => Err(err),
(Ok(_), Err(err)) => Err(err),
(Err(err), Err(shutdown_err)) => {
eprintln!("failed to shut down test app after test error: {shutdown_err}");
Err(err)
}
},
Err(payload) => {
if let Err(err) = self.shutdown().await {
eprintln!("failed to shut down test app after panic: {err}");
}
panic::resume_unwind(payload);
}
}
}
fn remember_error(first_error: &mut Option<WebDriverError>, err: WebDriverError) {
if first_error.is_none() {
*first_error = Some(err);
}
}
async fn shutdown(mut self) -> WebDriverResult<()> {
let mut first_error = None;
if let Some(driver) = self.driver.take()
&& let Err(err) = driver.quit().await
{
Self::remember_error(&mut first_error, err);
}
if let Some(tx) = self.shutdown_tx.take() {
let _ = tx.send(());
}
if let Some(task) = self.task.take() {
match task.await {
Ok(Ok(())) => {}
Ok(Err(err)) => {
Self::remember_error(&mut first_error, WebDriverError::IoError(err))
}
Err(err) => Self::remember_error(
&mut first_error,
WebDriverError::IoError(io::Error::other(format!(
"test server task failed: {err}"
))),
),
}
}
if let Some(err) = first_error {
Err(err)
} else {
Ok(())
}
}
}
pub struct Session {
addr: SocketAddr,
driver: thirtyfour::WebDriver,
}
impl Session {
pub fn url(&self, path: impl AsRef<str>) -> String {
let path = path.as_ref();
let separator = if path.starts_with('/') { "" } else { "/" };
format!("http://{}{}{}", self.addr, separator, path)
}
}
impl Deref for App {
type Target = thirtyfour::WebDriver;
fn deref(&self) -> &Self::Target {
self.driver.as_ref().expect("browser to be present")
}
}
impl Deref for Session {
type Target = thirtyfour::WebDriver;
fn deref(&self) -> &Self::Target {
&self.driver
}
}