Skip to content

Your first xa11y script

By the end of this page you will have driven a real desktop application from a script. Along the way you will:

  • print an application’s accessibility tree and read it
  • act on an element through that tree
  • hold until the interface catches up
  • produce one of xa11y’s failure diagnoses on purpose, and read it back

It takes about ten minutes.

You will use your platform’s Calculator as the target, because every desktop has one and its tree is small enough to read in full. Nothing here is specific to Calculator. The last step points at how to repeat it against your own application.

Terminal window
open -a Calculator

Leave the window open. Every step below reads the live application, so closing it ends the lesson.

xa11y installs a command-line tool alongside the library. Ask it what is running:

Terminal window
xa11y apps
1234 Calculator focused
5678 Terminal
9012 Finder

Checkpoint. Calculator appears in that list with a process ID. If the list is empty or Calculator is missing, the permissions from Install xa11y have not taken effect. On macOS, restart your terminal. On Linux, confirm the AT-SPI bus is running.

This is the step that matters most, and the habit worth forming before you write any code. Print the tree:

Terminal window
xa11y tree --app Calculator
application "Calculator" [enabled visible] bounds=(0,0,400,600)
├── window "Calculator" [enabled visible] bounds=(0,0,400,600)
│ ├── group "Display" [enabled visible]
│ │ └── static_text "Result" value="0" [enabled visible]
│ └── group "Keypad" [enabled visible]
│ ├── button "7" [enabled visible focusable] actions=[press,focus]
│ ├── button "8" [enabled visible focusable] actions=[press,focus]
│ └── button "+" [enabled visible focusable] actions=[press,focus]
└── menu_bar [enabled visible]

Your output will differ from the tree above, and that is the lesson. An element’s accessibility name is what the application publishes for screen readers, and it is frequently something other than the glyph painted on the button. The multiplication key reports × on one platform and Multiply on another. The display element is static_text "Result" in the tree above and something else on your machine.

So read your own output and pick two things out of it:

  • The name of the button for the digit 7.
  • The role and name of the element showing the running total.

Write them down. The next steps use them.

Checkpoint. You have a tree with a window in it and buttons below that. A tree containing only application and a single window means the application is not publishing its contents. On Linux that happens with Electron and Chromium applications, which need a launch flag (see Platform Details).

Step 4: Test a selector before you write it into code

Section titled “Step 4: Test a selector before you write it into code”

xa11y finds elements with CSS-like selectors. Try one against the live application, substituting the button name you wrote down:

Terminal window
xa11y find "button[name='7']" --app Calculator
button "7" [enabled visible focusable] actions=[press,focus]
(1 match)

Checkpoint. Exactly one match. A selector that matches nothing is an error rather than an empty list:

error: no elements matched selector: button[name='7']

That means the name does not match what the application publishes, so go back to your tree output and copy the name character for character.

Now press it from the shell:

Terminal window
xa11y action press "button[name='7']" --app Calculator

Watch the Calculator window. The display changes to 7. You have driven a desktop application without writing a line of code.

Create first_script.py (or the equivalent for your language) and put this in it. Replace the three names below with the ones from your own tree, exactly as step 3 printed them:

Placeholder in the code Replace with
button[name='7'] the digit-7 button you wrote down in step 3
button[name='+'] the addition button from the same tree
static_text[name='Result'] the role and name of the element showing the running total

Names vary by platform. macOS Calculator publishes plain glyphs (7, +, =) and a display whose role is static_text; Windows Calculator publishes word names (Seven, Plus, Equals); GNOME Calculator publishes glyphs with its own display role. Copying from your own step-3 output is what makes this work whichever you are on.

import xa11y
calc = xa11y.App.by_name("Calculator")
# Press 7, then +, then 8, then =
calc.locator("button[name='7']").press()
calc.locator("button[name='+']").press()
calc.locator("button[name='8']").press()
calc.locator("button[name='=']").press()
display = calc.locator("static_text[name='Result']")
print(display.element().value)

Run it.

Checkpoint. The script prints 15, and you watched the Calculator window change while it ran. A TimeoutError here means one of the three names is still the placeholder rather than yours; the error names which selector failed and lists what it found instead, which step 7 covers.

App.by_name polled until the operating system reported Calculator to the accessibility API, rather than failing on the first attempt. Each locator(...) then re-resolved its selector at the moment it acted, so the script targeted whatever was on screen at that moment rather than a handle captured earlier.

Step 6: Wait for the result instead of assuming it

Section titled “Step 6: Wait for the result instead of assuming it”

Desktop interfaces update a few frames after the input that caused the change. The script above read the display immediately after pressing =, which works on a fast machine and flakes on a slow one.

Replace the last two lines with a wait that carries the assertion:

display = calc.locator("static_text[name='Result']")
display.wait_until(
lambda element: element is not None and element.value == "15",
timeout=5.0,
)
print("got 15")

Checkpoint. The script prints got 15, and it returns as soon as the display updates rather than after a fixed delay.

This is the shape every xa11y test takes: act, then wait for the condition you care about. A sleep long enough to be safe on a cold CI runner is a sleep long enough to slow every local run, so xa11y gives you no reason to write one.

Failures are where a library either helps you or wastes your afternoon, so make this one fail. Change a selector to a name that does not exist:

calc.locator("button[name='Multiplyy']").press()

Run it again. The error names the selector it was resolving, the condition it was waiting for, what it saw on the final attempt, and which nearby elements almost matched:

xa11y.TimeoutError: Timeout after 5.0s; waiting for: press target actionable
(visible && enabled); selector: button[name='Multiplyy']; last observed:
selector never matched; candidates: button "Multiply", button "Minus"

Checkpoint. Your error lists real buttons from your own Calculator under candidates. That list is the fix: the near-miss is the name you meant.

Every field in that message is also available as a structured attribute, so a test harness can act on it without parsing prose. That is covered in Errors and diagnosis.

  • Read the tree before writing selectors, because accessibility names are something other than the on-screen labels.
  • Try a selector with xa11y find before committing it to code.
  • Act through a Locator, which re-resolves its selector on every call.
  • Wait for a condition rather than sleeping.
  • Read the diagnosis attached to a failure instead of re-running under logging.
  • Write desktop tests turns this script into a test suite that launches the application under test and tears it down.
  • Find the right selector is the recipe for the step 3 and step 4 loop when the tree is large and unfamiliar.
  • Selectors is the full syntax: operators, combinators, nth, and alternation.
  • How xa11y works explains accessibility trees, the App / Element / Locator model, and where the data comes from.