Files
eepp/projects/scripts/test_atspi.py
T
Martín Lucas Golini a0de38a758 ui: improve native accessibility performance and event handling
- lazily activate AT-SPI work only when a real accessibility client connects
  - eliminate unused accessibility overhead during widget construction and startup
  - wake the SDL event loop for D-Bus traffic without blocking the UI thread
  - implement AT-SPI cache, structural events, and text/editable-text interfaces
  - improve UI Automation events, properties, runtime IDs, and window procedure safety
  - centralize widget semantics in the accessibility resolver
  - preserve accessible identity across model and widget lifecycle changes
  - optimize widget class storage with SmallVector
  - expand accessibility unit tests and add deterministic AT-SPI integration coverage
2026-09-06 18:10:40 -03:00

127 lines
3.9 KiB
Python

#!/usr/bin/env python3
import argparse
import subprocess
import time
import gi
gi.require_version("Atspi", "2.0")
from gi.repository import Atspi
def wait_for_application(name, timeout):
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
desktop = Atspi.get_desktop(0)
for index in range(desktop.get_child_count()):
application = desktop.get_child_at_index(index)
if application and application.get_name() == name:
return application
time.sleep(0.1)
raise RuntimeError(f'accessibility application "{name}" was not found')
def descendants(root):
result = []
pending = [root]
while pending:
node = pending.pop()
result.append(node)
for index in range(node.get_child_count() - 1, -1, -1):
child = node.get_child_at_index(index)
if child:
pending.append(child)
return result
def find_named(nodes, name):
for node in nodes:
if node.get_name() == name:
return node
raise RuntimeError(f'accessibility node "{name}" was not found')
def action_index(node, name):
for index in range(node.get_n_actions()):
if Atspi.Action.get_localized_name(node, index) == name:
return index
raise RuntimeError(f'action "{name}" was not exposed by "{node.get_name()}"')
def check(condition, message):
if not condition:
raise RuntimeError(message)
def validate(application):
nodes = descendants(application)
check(len(nodes) >= 20, f"expected a complete accessibility tree, got {len(nodes)} nodes")
project_name = find_named(nodes, "Project name")
check(project_name.get_role_name() == "entry", "Project name must be an entry")
check(project_name.get_character_count() == 4, "Project name character count must equal 4")
check(Atspi.Text.get_text(project_name, 0, -1) == "eepp", "Project name text query failed")
check(
Atspi.EditableText.set_text_contents(project_name, "eepp2"),
"Project name text replacement failed",
)
check(
Atspi.Text.get_text(project_name, 0, -1) == "eepp2",
"Project name replacement was not visible",
)
checkbox = find_named(nodes, "Enable autosave")
check(checkbox.get_role_name() == "check box", "autosave must be a check box")
check(checkbox.do_action(action_index(checkbox, "toggle")), "checkbox toggle failed")
time.sleep(0.05)
check(
checkbox.get_state_set().contains(Atspi.StateType.CHECKED),
"checkbox did not expose its checked state",
)
radio = find_named(nodes, "Light Theme")
check(radio.get_role_name() == "radio button", "theme must be a radio button")
check(radio.do_action(action_index(radio, "select")), "radio selection failed")
time.sleep(0.05)
states = radio.get_state_set()
check(states.contains(Atspi.StateType.CHECKED), "radio did not expose checked state")
check(states.contains(Atspi.StateType.SELECTED), "radio did not expose selected state")
spin = find_named(nodes, "Retry count")
check(spin.get_role_name() == "spin button", "Retry count must be a spin button")
check(spin.get_current_value() == 3, "Retry count must initially equal 3")
check(spin.get_minimum_value() == 0, "Retry count minimum must equal 0")
check(spin.get_maximum_value() == 10, "Retry count maximum must equal 10")
find_named(nodes, "Colors")
find_named(nodes, "Projects")
print(f"AT-SPI validation passed: {len(nodes)} nodes")
def main():
parser = argparse.ArgumentParser(description="Validate the eepp accessibility example via AT-SPI")
parser.add_argument("--application", default="eepp - Accessibility")
parser.add_argument("--executable", default="bin/eepp-ui-accessibility")
parser.add_argument("--no-launch", action="store_true")
parser.add_argument("--timeout", type=float, default=5.0)
args = parser.parse_args()
process = None
try:
if not args.no_launch:
process = subprocess.Popen([args.executable])
validate(wait_for_application(args.application, args.timeout))
finally:
if process:
process.terminate()
try:
process.wait(timeout=1)
except subprocess.TimeoutExpired:
process.kill()
process.wait()
if __name__ == "__main__":
main()