at-spi-linux
Authors Linux desktop UI tests via AT-SPI - the DBus-based Assistive Technology Service Provider Interface implemented by `at-spi2-core` (registry daemon + `libatspi` C library + ATK GTK bridge). Covers enabling toolkit accessibility (`gsettings set org.gnome.desktop.interface toolkit-accessibility true`), driving GTK + Qt apps through Python `dogtail` (object-oriented + procedural APIs), inspecting the tree with Accerciser, scripting via `pyatspi`, and CI integration on headless Linux runners with `Xvfb` + `dbus-launch`. Use for Linux-side desktop tests of GTK applications, Qt apps with QAccessible enabled, and Electron apps on Linux.
at-spi-linux
Overview
AT-SPI (Assistive Technology Service Provider Interface) is the Linux desktop accessibility stack - a DBus-based protocol that exposes an application's UI tree to assistive technologies (screen readers, magnifiers) and to test clients. Per the at-spi2-core README:
"AT-SPI2-Core is the core of an accessibility stack for free software systems."
It provides (atspi2coreraw) "DBus interface definitions for the Assistive Technology Service Provider Interface" plus four runtime pieces:
| Component (atspi2coreraw) | Role |
|---|---|
registryd | "Daemon managing accessible applications and enabling communication between assistive technologies and applications." |
atspi | "C language binding for DBus accessibility interfaces." |
atk | "GObject-based API for implementing accessible applications and GUI toolkits." |
atk-adaptor | "Translates ATK API calls to the atspi API layer." |
GTK applications publish accessibility via ATK; Qt applications publish via Qt's own QAccessible infrastructure, which exposes an AT-SPI surface on Linux. Either path lets a test client walk the tree.
Strategic frame: desktop-test-strategy-reference places AT-SPI alongside Windows UIA and macOS XCTest as the three OS- native accessibility-tree backends.
When to use
Step 1 - Enable toolkit accessibility
Per the dogtail README:
gsettings set org.gnome.desktop.interface toolkit-accessibility trueWithout this setting, GTK applications publish nothing to the AT-SPI bus and tree-walking clients see an empty desktop.
For Qt apps, the equivalent is exporting QT_ACCESSIBILITY=1 in the environment that launches the Qt binary - without it Qt's QAccessible infrastructure stays inactive and the AT-SPI tree contains no Qt children.
For Electron / Chromium, set --force-renderer-accessibility on the binary launch (or --enable-blink-features=AccessibilityAriaVirtualContent for newer Chromium accessibility surfaces).
Step 2 - Install dogtail
Per dogtailraw:
# From PyPI
sudo python3 -m pip install dogtail
# From source
git clone https://gitlab.com/dogtail/dogtail.git
cd dogtail
python3 -m build
sudo pip3 install dist/dogtail-2.*-py3-none-any.whlThe dogtail README notes that "for Wayland support specifically: Install gnome-ponytail-daemon":
dnf install -y gnome-ponytail-daemon python3-gnome-ponytail-daemonThis bridges synthetic input events on Wayland sessions where direct X-style event injection isn't available.
Step 3 - Inspect the tree (Accerciser)
Before writing tests, walk the live tree with Accerciser (the GNOME accessibility inspector). It's the AT-SPI analogue of Inspect.exe (Windows) and Accessibility Inspector (macOS, per desktop-test-strategy-reference).
# Most distros:
sudo apt install accerciser # Debian / Ubuntu
sudo dnf install accerciser # Fedora
accerciserAccerciser walks the same registryd-published tree the test client sees, and lets the author copy out the exact role + name + description triple for each widget - which is what dogtail queries against.
Step 4 - Author a dogtail test (procedural API)
Per dogtailraw, dogtail "uses Accessibility (AT-SPI) technologies to interact with desktop applications". The procedural API is the closest mirror to the underlying AT-SPI tree:
#!/usr/bin/env python3
from dogtail.procedural import run, focus, click, type, keyCombo
from dogtail.utils import screenshot
# Launch the app — dogtail starts it and attaches to the AT-SPI tree
run('gnome-calculator')
# Resolve via the accessibility tree (focus + click are role-based)
focus.application('gnome-calculator')
focus.frame('Calculator')
click('7') # button named "7"
click('+')
click('3')
click('=')
# Assert via the result widget
focus.text(roleName='editbox')
assert focus.widget.text == '10', f'Expected 10, got {focus.widget.text!r}'
screenshot('calc-success.png')The role-based primitives (focus.application, focus.frame, focus.text, click, type) map onto AT-SPI roles published by the application - same primitives Orca screen reader uses.
Step 5 - Author a dogtail test (object-oriented API)
For larger suites where a Page-Object-style structure is appropriate, the object-oriented tree API exposes the registry as a navigable graph:
from dogtail.tree import root
calc = root.application('gnome-calculator')
frame = calc.child(roleName='frame')
frame.button('7').click()
frame.button('+').click()
frame.button('3').click()
frame.button('=').click()
result = frame.child(roleName='editbox')
assert result.text == '10'root is the AT-SPI desktop entry point - dogtail's wrapper over the libatspi get_desktop() function described in the libatspi reference:
"init() … connects to the accessibility registry and initializes the SPI."
"get_desktop() and get_desktop_list() to access the accessibility tree once connected."
Step 6 - Direct pyatspi for fine-grained control
For tests that need to listen for events on the AT-SPI bus rather than poll the tree, drop down to pyatspi:
import pyatspi
def on_state_change(event):
if event.type == 'object:state-changed:focused' and event.detail1:
print(f'Focus moved to: {event.source.name} ({event.source.getRoleName()})')
pyatspi.Registry.registerEventListener(
on_state_change,
'object:state-changed:focused',
)
pyatspi.Registry.start() # blocks; Ctrl-C to exitThis uses the AT-SPI event-listener pattern documented in atspi2docs:
"AtspiEventListener operates through a callback mechanism. The library defines a generic interface implemented by objects for the receipt of event notifications."
Step 7 - Run
# Standalone — assumes accessibility is enabled + an X / Wayland session
python3 tests/test_calculator.py
# Under pytest with JUnit output
pytest tests/ --junitxml=reports/atspi-junit.xmlStep 8 - Parsing results
JUnit XML from pytest feeds junit-xml-analysis for the cross-runner aggregation pipeline.
For dogtail-specific diagnostics, every failing run captures a screenshot (see Step 4 - screenshot() call) plus a dogtail session log under ~/.dogtail/logs/.
Step 9 - CI integration
The Linux runner needs a display server + session DBus bus before AT-SPI clients can connect:
# .github/workflows/atspi.yml
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Install AT-SPI + dogtail + app + Accerciser deps
run: |
sudo apt-get update
sudo apt-get install -y \
at-spi2-core \
python3-dogtail python3-pyatspi \
xvfb dbus-x11 gnome-calculator
- name: Enable accessibility for GTK
run: |
gsettings set org.gnome.desktop.interface toolkit-accessibility true \
|| true # the schema needs a session bus; the env block below ensures it
- name: Run tests under Xvfb + dbus-launch
env:
QT_ACCESSIBILITY: '1'
run: |
xvfb-run --auto-servernum --server-args='-screen 0 1280x1024x24' \
dbus-launch --exit-with-session \
pytest tests/ --junitxml=reports/atspi-junit.xml
- uses: actions/upload-artifact@v4
if: always()
with:
name: junit
path: reports/xvfb-run provides the X display, dbus-launch --exit-with-session spawns the session DBus bus (at-spi2-registryd requires the session bus per atspi2coreraw - without it the registry daemon refuses to start). This matches the CI guidance in the desktop-test-strategy-reference anti-patterns table.
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
Running dogtail tests without toolkit-accessibility=true | GTK apps publish nothing; tree is empty | gsettings set org.gnome.desktop.interface toolkit-accessibility true (dogtailraw) |
Driving Qt apps with QT_ACCESSIBILITY unset | QAccessible stays inactive; AT-SPI tree has no Qt children | Export QT_ACCESSIBILITY=1 in the launch env (Step 1) |
CI runner without dbus-launch | registryd refuses to start; sessions hang on attach | xvfb-run … dbus-launch … pytest … (Step 9) |
| Locating by visible label only | Localisation breaks the locator | Combine roleName= + accessible name; set Atk.set_accessible_name(...) from app code |
time.sleep(2) between actions | Flaky; brittle | Use dogtail's doDelay config (config.searchBackoffDuration) and tree polling helpers (dogtailraw) |
Wayland session without gnome-ponytail-daemon | Synthetic input events get dropped | Install daemon per dogtailraw |
| Mixing X test runner with Wayland app under test | Event injection mismatch | One session type per CI job; matrix-build over Xorg + Wayland separately |
| Test relies on Accerciser running concurrently | Two clients on the same registry race on tree refresh | Use Accerciser interactively for authoring; remove from CI runs |