use std::sync::{Mutex, OnceLock};
use std::time::Duration;
use crate::error::{Error, Result};
pub const DEFAULT_TIMEOUT_ENV_VAR: &str = "XA11Y_DEFAULT_TIMEOUT";
const BUILTIN_DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);
static PROGRAMMATIC_DEFAULT: Mutex<Option<Duration>> = Mutex::new(None);
static ENV_DEFAULT: OnceLock<std::result::Result<Option<Duration>, String>> = OnceLock::new();
fn parse_timeout_secs(raw: &str) -> std::result::Result<Duration, String> {
let secs: f64 = raw
.trim()
.parse()
.map_err(|_| format!("expected a number of seconds, got {raw:?}"))?;
if !secs.is_finite() || secs < 0.0 {
return Err(format!(
"expected a finite, non-negative number of seconds, got {raw:?}"
));
}
Ok(Duration::from_secs_f64(secs))
}
fn env_default() -> std::result::Result<Option<Duration>, String> {
ENV_DEFAULT
.get_or_init(|| match std::env::var(DEFAULT_TIMEOUT_ENV_VAR) {
Ok(raw) => parse_timeout_secs(&raw)
.map(Some)
.map_err(|msg| format!("{DEFAULT_TIMEOUT_ENV_VAR}: {msg}")),
Err(std::env::VarError::NotPresent) => Ok(None),
Err(std::env::VarError::NotUnicode(_)) => Err(format!(
"{DEFAULT_TIMEOUT_ENV_VAR}: value is not valid Unicode"
)),
})
.clone()
}
pub fn set_default_timeout(timeout: Duration) {
*PROGRAMMATIC_DEFAULT
.lock()
.unwrap_or_else(|e| e.into_inner()) = Some(timeout);
}
pub fn default_timeout() -> Result<Duration> {
if let Some(t) = *PROGRAMMATIC_DEFAULT
.lock()
.unwrap_or_else(|e| e.into_inner())
{
return Ok(t);
}
match env_default() {
Ok(Some(t)) => Ok(t),
Ok(None) => Ok(BUILTIN_DEFAULT_TIMEOUT),
Err(message) => Err(Error::InvalidConfig { message }),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use crate::locator::Locator;
use crate::mock::build_provider;
use crate::provider::Provider;
#[test]
fn parse_accepts_integers_floats_and_zero() {
assert_eq!(parse_timeout_secs("30").unwrap(), Duration::from_secs(30));
assert_eq!(
parse_timeout_secs("2.5").unwrap(),
Duration::from_secs_f64(2.5)
);
assert_eq!(parse_timeout_secs("0").unwrap(), Duration::ZERO);
assert_eq!(
parse_timeout_secs(" 10 ").unwrap(),
Duration::from_secs(10),
"surrounding whitespace must be tolerated"
);
}
#[test]
fn parse_rejects_non_numeric_negative_and_non_finite() {
for bad in ["abc", "", "-1", "inf", "NaN", "5s", "1,5"] {
let err = parse_timeout_secs(bad)
.expect_err(&format!("{bad:?} must be rejected as a timeout"));
assert!(
err.contains("seconds"),
"error must explain the expected unit: {err}"
);
}
}
#[test]
fn global_default_precedence_and_locator_integration() {
if std::env::var_os(DEFAULT_TIMEOUT_ENV_VAR).is_none() {
assert_eq!(default_timeout().unwrap(), BUILTIN_DEFAULT_TIMEOUT);
}
let provider: Arc<dyn Provider> = build_provider();
let missing = Locator::new(provider.clone(), None, r#"button[name="DoesNotExist"]"#);
set_default_timeout(Duration::from_millis(150));
assert_eq!(default_timeout().unwrap(), Duration::from_millis(150));
let start = std::time::Instant::now();
let err = missing
.press()
.expect_err("press on a never-matching selector must time out");
assert!(matches!(err, Error::Timeout { .. }), "got {err:?}");
assert!(
start.elapsed() < Duration::from_secs(2),
"auto-wait must use the 150ms global default, not the 5s built-in; took {:?}",
start.elapsed()
);
set_default_timeout(Duration::from_secs(30));
let start = std::time::Instant::now();
let err = missing
.clone()
.with_timeout(Duration::from_millis(150))
.press()
.expect_err("press on a never-matching selector must time out");
assert!(matches!(err, Error::Timeout { .. }), "got {err:?}");
assert!(
start.elapsed() < Duration::from_secs(2),
"explicit with_timeout must beat the 30s global default; took {:?}",
start.elapsed()
);
set_default_timeout(BUILTIN_DEFAULT_TIMEOUT);
}
}