Skip to content

Desktop Testing

xa11y’s Locator pattern makes it natural to write integration tests for desktop apps — find elements, interact with them, wait for changes, and assert on the result. If you’ve used Playwright for web testing, the workflow is familiar.

Waits

Desktop UIs update asynchronously. After pressing a button, the result might not appear for a few frames. Locator wait_*() methods poll until a condition is met or a timeout expires:

  • wait_visible() / wait_hidden() — element appears or disappears
  • wait_enabled() / wait_disabled() — element becomes interactive or not
  • wait_attached() / wait_detached() — element exists in the tree or not
  • wait_focused() / wait_unfocused() — element gains or loses focus
  • wait_until(predicate) — arbitrary condition on the resolved element

Always use waits instead of sleeping — they’re faster (resolve as soon as the condition is met) and more reliable (won’t flake on slow machines).

Example: testing a calculator

This test opens Calculator, performs 7 × 8, and asserts the result is 56.

use xa11y::*;
use std::time::Duration;
#[test]
fn calculator_multiplication() -> Result<()> {
let calc = App::by_name("Calculator")?;
let timeout = Duration::from_secs(5);
// Wait for the app to be ready
calc.locator("window").wait_visible(timeout)?;
// Press 7 × 8 =
calc.locator("button[name='7']").press()?;
calc.locator("button[name='×']").press()?;
calc.locator("button[name='8']").press()?;
calc.locator("button[name='=']").press()?;
// Wait for the result to update, then assert
let result = calc.locator("static_text[name='Result']");
result.wait_until(
|element| element.and_then(|e| e.value.as_deref()) == Some("56"),
timeout,
)?;
assert_eq!(result.element()?.value.as_deref(), Some("56"));
Ok(())
}

Debugging with the CLI

The xa11y CLI is helpful when writing and debugging tests. Use it to explore the accessibility tree of your app and find the right selectors before putting them in test code:

Terminal window
# See what's in the tree
xa11y tree --app MyApp
# Test a selector interactively
xa11y find "button[name='Submit']" --app MyApp
# Try an action manually
xa11y action press "button[name='Submit']" --app MyApp
# Watch events while interacting with the app
xa11y events --app MyApp

See the Overview for the full CLI reference.

Tips

  • Be specific with selectors. button[name='7'] is better than button — it won’t break when the UI adds more buttons.
  • Use locators, not elements, for interactions. Locators re-resolve their selector on every operation, making them resilient to UI changes.
  • Prefer wait_until for complex assertions. Instead of sleeping then asserting, combine the wait and the condition — the test passes as soon as the UI is ready.
  • Keep timeouts generous in CI. CI machines are slower. Use 5–10 seconds for waits even if your local machine resolves in milliseconds.