Arsitektur AT-SPI pada Stack Linux Desktop

Assistive Technology Service Provider Interface (AT-SPI) adalah standar D-Bus di lingkungan GNOME dan Linux desktop yang mengekspos representasi semantik elemen GUI ke teknologi asistif seperti pembaca layar (Orca). Aplikasi GTK menerjemahkan widget grafis menjadi simpul-simpul dalam accessibility tree (accessible tree). Setiap simpul mengekspos properti standar: nama, deskripsi, status (misalnya focused, checked), antarmuka aksi (AccessibleAction), dan peran (AccessibleRole).

Pengujian regresi fungsional berbasis koordinat piksel atau OCR rentan rusak terhadap perubahan tema sistem dan resolusi layar. Melalui AT-SPI via D-Bus, pengujian dijalankan langsung terhadap struktur pohon objek aksesibilitas aplikasi. Ini memungkinkan verifikasi semantik elemen secara deterministik tanpa interaksi perangkat keras fisik.

Konfigurasi Lingkungan Headless untuk CI/CD

AT-SPI membutuhkan daemon sesi D-Bus dan server grafis aktif agar dapat menjalin komunikasi antar-proses (IPC). Di lingkungan automasi headless atau pipeline CI/CD, kebutuhan ini dipenuhi dengan menjalankan virtual framebuffer (Xvfb) dan menginisialisasi bus AT-SPI secara terisolasi.

sudo apt-get update && sudo apt-get install -y \
    xvfb \
    at-spi2-core \
    dbus-x11 \
    python3-gi \
    gir1.2-atspi-2.0 \
    python3-pytest

Sebelum meluncurkan aplikasi target dan skrip uji, variabel lingkungan sesi D-Bus dan AT-SPI harus aktif di terminal:

# Inisialisasi virtual display
export DISPLAY=:99
Xvfb :99 -screen 0 1024x768x24 &

# Aktifkan daemon AT-SPI jika belum berjalan otomatis
export GTK_A11Y=1
export NO_AT_BRIDGE=0
eval $(dbus-launch --sh-syntax)
/usr/libexec/at-spi-bus-launcher --launch-immediately &
/usr/libexec/at-spi2-registryd &

Implementasi Test Harness Menggunakan Python

Pustaka introspeksi GObject (gi.repository.Atspi) mengekspos C API asli AT-SPI ke Python. Alternatif tingkat tinggi seperti pustaka dogtail membungkus panggilan ini, tetapi menggunakan Atspi langsung lewat PyGObject meminimalkan dependensi eksternal dan memberi kendali penuh atas navigasi pohon.

Berikut adalah modul test harness untuk mencari aplikasi dan memvalidasi properti semantik widget secara rekursif:

import time
import gi

gi.require_version("Atspi", "2.0")
from gi.repository import Atspi


class A11yHarness:
    @staticmethod
    def wait_for_app(app_name: str, timeout: float = 10.0):
        start = time.time()
        while time.time() - start < timeout:
            desktop = Atspi.get_desktop(0)
            for i in range(desktop.get_child_count()):
                app = desktop.get_child_at_index(i)
                if app and app.get_name() == app_name:
                    return app
            time.sleep(0.5)
        raise TimeoutError(f"Aplikasi '{app_name}' tidak ditemukan pada AT-SPI bus.")

    @classmethod
    def find_node(cls, root, role_name: str = None, name: str = None):
        if not root:
            return None

        current_role = root.get_role_name()
        current_name = root.get_name()

        match_role = (role_name is None) or (current_role == role_name)
        match_name = (name is None) or (current_name == name)

        if match_role and match_name:
            return root

        for i in range(root.get_child_count()):
            child = root.get_child_at_index(i)
            found = cls.find_node(child, role_name=role_name, name=name)
            if found:
                return found
        return None

Verifikasi Semantik Tree dan Role Widget dengan Pytest

Skrip uji berikut memverifikasi bahwa aplikasi target menyediakan role yang benar untuk kontrol formulir, label terkait tidak kosong, dan aksi klik dapat dipicu melalui antarmuka aksesibilitas.

import pytest
from gi.repository import Atspi
from harness import A11yHarness


@pytest.fixture(scope="session")
def target_app():
    return A11yHarness.wait_for_app("gnome-calculator", timeout=5.0)


def test_button_semantic_properties(target_app):
    # Cari tombol spesifik dalam hierarki
    button = A11yHarness.find_node(
        target_app, role_name="push button", name="Clear entry"
    )
    assert button is not None, "Tombol 'Clear entry' tidak ditemukan dalam accessibility tree"

    # Verifikasi role
    assert button.get_role() == Atspi.Role.PUSH_BUTTON

    # Verifikasi status widget
    states = button.get_state_set().get_states()
    assert Atspi.StateType.VISIBLE in states
    assert Atspi.StateType.SENSITIVE in states
    assert Atspi.StateType.DEFUNCT not in states


def test_trigger_accessible_action(target_app):
    button = A11yHarness.find_node(target_app, role_name="push button", name="1")
    assert button is not None

    # Mengakses antarmuka AccessibleAction
    action_count = button.get_n_actions()
    assert action_count > 0, "Elemen interaktif tidak memiliki antarmuka AccessibleAction"

    action_name = button.get_action_name(0)
    assert action_name in ["click", "press", "activate"]

    # Eksekusi aksi tanpa pointer mouse
    success = button.do_action(0)
    assert success is True, "Gagal memicu aksi aksesibilitas pada target"

Mitigasi Flaky Test Akibat Async Event Bus

Komunikasi AT-SPI bersifat asinkron via D-Bus. State perubahan widget (seperti perpindahan fokus, validasi form, atau perubahan teks) tidak instan. Menggunakan sleep() statis adalah penyebab utama flaky tests di CI/CD.

Gunakan Polling Bersyarat

Gantikan jeda waktu tetap dengan polling deterministik berbasis predicate function:

def wait_until(predicate, timeout: float = 3.0, interval: float = 0.1):
    start = time.time()
    while time.time() - start < timeout:
        result = predicate()
        if result:
            return result
        time.sleep(interval)
    raise TimeoutError("Kondisi aksesibilitas tidak terpenuhi sebelum timeout")


# Contoh penggunaan: Menunggu status focused diperbarui
def test_input_focus_state(target_app):
    text_entry = A11yHarness.find_node(target_app, role_name="text")
    assert text_entry is not None

    # Minta fokus via AT-SPI Component interface
    component = text_entry.get_component()
    component.grab_focus()

    # Hindari sleep(1), gunakan predicate polling
    def is_focused():
        states = text_entry.get_state_set().get_states()
        return Atspi.StateType.FOCUSED in states

    assert wait_until(is_focused, timeout=2.0) is True

Manfaatkan AT-SPI Event Listeners

Untuk verifikasi state reaktif yang kompleks, gunakan Atspi.EventListener guna mendengarkan signal langsung dari D-Bus:

received_events = []


def on_state_changed(event):
    if event.type == "object:state-changed:focused":
        received_events.append(event)


listener = Atspi.EventListener.new(on_state_changed)
listener.register("object:state-changed:focused")

# Operasi GUI pemicu...

Debugging dan Inspeksi Simpul A11y

Saat pengujian gagal menemukan simpul atau atribut salah dibaca, beberapa langkah debugging sistemik dapat digunakan:

  • Accerciser: Gunakan tool interaktif accerciser pada sesi lokal untuk melihat langsung hierarki pohon objek AT-SPI dan memvalidasi jalur XPath dari widget yang bersangkutan.
  • Monitor Bus D-Bus: Jalankan perintah CLI untuk memantau event aksesibilitas secara langsung:
    dbus-monitor --session "interface='org.a11y.atspi.Event.Object'"
  • Logging Dump: Saat test gagal pada runner CI, buat fungsi traversal untuk mencetak pohon objek saat kegagalan terjadi guna memudahkan audit struktur widget aplikasi yang sedang diuji.