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 disappearswait_enabled()/wait_disabled()— element becomes interactive or notwait_attached()/wait_detached()— element exists in the tree or notwait_focused()/wait_unfocused()— element gains or loses focuswait_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(())}import xa11yimport subprocessimport pytest
@pytest.fixturedef calculator(): """Launch Calculator and return an App handle.""" proc = subprocess.Popen(["open", "-a", "Calculator"]) yield xa11y.App.by_name("Calculator") proc.terminate()
def test_calculator_multiplication(calculator): calc = calculator
# Wait for the app to be ready calc.locator("window").wait_visible(timeout=5.0)
# 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 result = calc.locator("static_text[name='Result']") result.wait_until(lambda element: element and element.value == "56", timeout=5.0) assert result.element().value == "56"import { App } from '@crowecawcaw/xa11y';import { test } from 'node:test';import assert from 'node:assert/strict';
test('calculator multiplication', async () => { const calc = await App.byName('Calculator');
// Wait for the app to be ready await calc.locator('window').waitVisible(5);
// Press 7 × 8 = await calc.locator("button[name='7']").press(); await calc.locator("button[name='×']").press(); await calc.locator("button[name='8']").press(); await calc.locator("button[name='=']").press();
// Wait for the result to update, then assert const result = calc.locator("static_text[name='Result']"); await result.waitUntil((el) => el?.value === '56', { timeout: 5000 }); assert.equal((await result.element()).value, '56');});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:
# See what's in the treexa11y tree --app MyApp
# Test a selector interactivelyxa11y find "button[name='Submit']" --app MyApp
# Try an action manuallyxa11y action press "button[name='Submit']" --app MyApp
# Watch events while interacting with the appxa11y events --app MyAppSee the Overview for the full CLI reference.
Tips
- Be specific with selectors.
button[name='7']is better thanbutton— 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_untilfor 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.