#[cfg(test)]
mod tests {
use crate::integ as h;
use std::time::{Duration, Instant};
use xa11y::*;
const TEST_APP_NAMES: [&str; 2] = ["xa11y-test-app", "xa11y Test App"];
fn current_app() -> Result<App> {
App::find(Duration::from_secs(2), |d| {
d.name
.as_deref()
.is_some_and(|n| TEST_APP_NAMES.contains(&n))
})
}
fn app_windows(pid: Option<u32>) -> Result<Vec<(String, bool)>> {
#[cfg(target_os = "windows")]
{
Ok(App::list()?
.into_iter()
.filter(|a| a.pid == pid)
.map(|a| {
let el = a.as_element();
(a.name.clone(), el.states.active)
})
.collect())
}
#[cfg(not(target_os = "windows"))]
{
let _ = pid;
let app = current_app()?;
Ok(app
.locator("window")
.elements()?
.into_iter()
.map(|w| (w.name.clone().unwrap_or_default(), w.states.active))
.collect())
}
}
fn wait_until<T>(timeout: Duration, what: &str, mut f: impl FnMut() -> Option<T>) -> T {
let deadline = Instant::now() + timeout;
loop {
if let Some(v) = f() {
return v;
}
if Instant::now() >= deadline {
panic!("timed out after {timeout:?} waiting for {what}");
}
std::thread::sleep(Duration::from_millis(50));
}
}
fn press_close_dialog() -> Result<()> {
#[cfg(target_os = "windows")]
let roots: Vec<App> = {
let app = current_app()?;
App::list()?
.into_iter()
.filter(|a| a.pid == app.pid)
.collect()
};
#[cfg(not(target_os = "windows"))]
let roots: Vec<App> = vec![current_app()?];
for root in &roots {
let buttons = root.locator(r#"[name*="Close Dialog"]"#).elements()?;
if let Some(btn) = buttons.first() {
return btn.provider().perform_action(btn, "press");
}
}
Ok(())
}
struct DialogGuard {
pid: Option<u32>,
}
impl DialogGuard {
fn open() -> Self {
let app = h::app_root();
let pid = app.pid;
let open_btn = h::named(&app, "Open Dialog");
h::try_act(&open_btn, "press").expect("press 'Open Dialog'");
wait_until(
Duration::from_secs(5),
"the dialog window to appear",
|| {
let windows = app_windows(pid).expect("enumerate windows");
(windows.len() >= 2).then_some(())
},
);
DialogGuard { pid }
}
}
impl Drop for DialogGuard {
fn drop(&mut self) {
let _ = press_close_dialog();
let deadline = Instant::now() + Duration::from_secs(3);
while Instant::now() < deadline {
if app_windows(self.pid).map(|w| w.len()).unwrap_or(1) <= 1 {
break;
}
std::thread::sleep(Duration::from_millis(50));
}
}
}
#[test]
#[ignore]
fn second_window_appears() {
let app = h::app_root();
let pid = app.pid;
let _guard = DialogGuard::open();
let names = wait_until(Duration::from_secs(5), "two named windows", || {
let windows = app_windows(pid).expect("enumerate windows");
let names: Vec<String> = windows.into_iter().map(|(n, _)| n).collect();
(names.len() >= 2).then_some(names)
});
assert!(
names.iter().any(|n| n.contains("xa11y Test App")),
"main window title missing from {names:?}"
);
assert!(
names.iter().any(|n| n.contains("xa11y Test Dialog")),
"dialog window title missing from {names:?}"
);
}
#[test]
#[ignore]
fn active_follows_window_focus() {
let app = h::app_root();
let pid = app.pid;
{
let _guard = DialogGuard::open();
let active = wait_until(
Duration::from_secs(5),
"exactly one active window (the dialog)",
|| sole_active_window(pid),
);
assert!(
active.contains("xa11y Test Dialog"),
"the active window should be the dialog, got {active:?}"
);
}
let active = wait_until(
Duration::from_secs(5),
"the main window to become active again",
|| sole_active_window(pid),
);
assert!(
active.contains("xa11y Test App"),
"the main window should be active after closing the dialog, got {active:?}"
);
}
fn sole_active_window(pid: Option<u32>) -> Option<String> {
let windows = app_windows(pid).expect("enumerate windows");
let mut active = windows.into_iter().filter(|(_, a)| *a).map(|(n, _)| n);
let first = active.next()?;
match active.next() {
Some(_) => None, None => Some(first),
}
}
#[test]
#[ignore]
fn foreground_scenario_two_windows() {
let app = h::app_root();
let pid = app.pid;
let _guard = DialogGuard::open();
let foreground = App::foreground(Duration::from_secs(2))
.expect("App::foreground must resolve with the dialog open");
assert_eq!(
foreground.pid, pid,
"the foreground app must be the test app, got {:?}",
foreground.name
);
#[cfg(target_os = "windows")]
{
let mine: Vec<App> = App::list()
.expect("App::list must succeed")
.into_iter()
.filter(|a| a.pid == pid)
.collect();
assert_eq!(
mine.len(),
2,
"both top-level windows of the process must appear in App::list(), got {:?}",
mine.iter().map(|a| &a.name).collect::<Vec<_>>()
);
let foreground_count = mine.iter().filter(|a| a.is_foreground()).count();
assert_eq!(
foreground_count, 1,
"exactly one window may report is_foreground(), got {foreground_count}"
);
}
#[cfg(not(target_os = "windows"))]
{
let mine: Vec<App> = App::list()
.expect("App::list must succeed")
.into_iter()
.filter(|a| a.pid == pid)
.collect();
assert_eq!(
mine.len(),
1,
"the process should be a single App::list() entry on macOS/Linux, got {:?}",
mine.iter().map(|a| &a.name).collect::<Vec<_>>()
);
assert!(
mine[0].is_foreground(),
"the test-app entry must report is_foreground() while it holds the foreground"
);
}
}
}