Chapter 11 — Cross-Platform UI Automation
Where the previous chapter walked a scaffolding-stage package, this one walks a mature one.
codecraftai-uiaimplements UI automation across Linux (AT-SPI / X11 / Wayland), macOS (Accessibility / scripting bridges), and Windows (UIA — currently stubbed with NotImplementedError). The class hierarchy is small, the platform wrappers are substantial, and a recent 20-bug audit hardened the package.
The framework's UI automation package is the answer to a
specific kind of automation question: the system under test
is a graphical application. Volume 1 chapter 1 listed test
shapes the framework supports; UI tests are one of them, and
codecraftai-uia is the substrate.
This chapter walks the platform-neutral hierarchy, the per-platform wrappers, the factory dispatch model, and the recent audit history. Chapter 12 walks the declarative application mappings on top of this substrate.
The hierarchy
File: codecraftai-uia/source/packages/ccai/uia/platforms/base.py
# Lines 13–33
class UIObject(ABC):
"""
Base class for all UI objects.
"""
# Lines 35–72
class DisplayElement(UIObject):
"""
Foundation class for any displayed UI object.
"""
@property
@abstractmethod
def bounds(self) -> Tuple[int, int, int, int]: ...
@property
@abstractmethod
def position(self) -> Tuple[int, int]: ...
@property
@abstractmethod
def size(self) -> Tuple[int, int]: ...
@property
@abstractmethod
def is_visible(self) -> bool: ...
# Lines 74–107
class ScreenElement(DisplayElement):
"""
Foundation class for screen-level objects like windows and applications.
"""
def __init__(self, parent: Optional['UIElement'] = None):
self._parent = weakref.ref(parent) if parent is not None else None
# Includes bring_to_front() and send_to_back()
# Lines 109–196
class UIElement(DisplayElement):
"""
Abstract base class for UI elements (controls/widgets).
"""
# Includes find_element, find_elements, wait_for_element, wait_until_gone
# Lines 496–536
class UIWindow(ScreenElement):
"""
Abstract base class for windows.
"""
@property
@abstractmethod
def name(self) -> str: ...
@property
@abstractmethod
def title(self) -> str: ...
# Lines 560–601
class UIApplication(ScreenElement):
"""
Abstract base class for applications.
"""
def __init__(self, parent: Optional['UIElement'] = None):
super().__init__(parent)
self._declarative_model = None
Six classes. Each inherits from the one above:
UIObject— the abstract root. Has__repr__andexists.DisplayElement— anything visible. Addsbounds,position,size,is_visible.ScreenElement— top-level visible objects (windows, applications). Addsbring_to_front,send_to_back.UIElement— interactive controls (buttons, text fields). Addsfind_element,find_elements,wait_for_element,wait_until_gone.UIWindow— a window. Addsname,title.UIApplication— a running application. Adds_declarative_model(the linkage to chapter 12's declarative model).
The hierarchy is small enough to memorise. Six classes; two parallel branches (UIWindow and UIApplication both inherit from ScreenElement; UIElement is its own branch). A reader of any UI test code should be able to locate every class they encounter in this hierarchy.
The platform wrappers
Each platform has its own per-class wrapper that implements the abstract methods using the platform's native UI APIs.
macOS
File:
codecraftai-uia/source/packages/ccai/uia/platforms/macos/wrappers/uiaapplication.py:29
class MacApplication(WrappedObject, UIApplication):
"""
Wrapper for an application process. Use this object to attach to and
communicate with the application process; window objects are exposed
separately via window wrappers.
"""
APPLICATION_NAME = "NotSet"
BUNDLE_IDENTIFIER = "NotSet"
def __init__(self, controller: "MacOSAutomation", native_obj: Any):
super().__init__(controller, native_obj)
self._sb_process = None # AppleScript scripting-bridge process
self._ax_app = None # NSAccessibility app
MacApplication wraps two macOS-native interfaces:
_sb_process—appscript/ scripting-bridge for AppleScript-driven automation (file menus, scriptable applications)._ax_app— NSAccessibility'sAXUIElementfor the accessibility-tree-driven automation (buttons, text fields, menus that are AX-accessible).
The macOS layer has 51 source files and provides per-app
wrapper classes for 27 system applications (Calculator,
Calendar, Contacts, Excel, Mail, Safari, etc.). Each
per-app class sets APPLICATION_NAME and
BUNDLE_IDENTIFIER for the framework's lookup.
Linux
File:
codecraftai-uia/source/packages/ccai/uia/platforms/linux/wrappers/uiaapplication.py:17
class LinuxApplication(LinuxElement, UIApplication):
"""
Wrapper for Linux applications using Agent Architecture.
"""
def __init__(self, native_obj: Any, access_agent: AccessibilityAgent,
wm_agent: WindowManagerAgent, parent: Optional[UIElement] = None):
super().__init__(native_obj, access_agent, wm_agent, parent=parent)
LinuxApplication uses an agent architecture:
access_agent— the accessibility agent. The shipped implementation isPyAtspiAccessibilityAgentusing AT-SPI (pyatspi) over D-Bus.wm_agent— the window-manager agent. Two implementations:X11WindowManagerAgentfor X11,WaylandGnomeWindowManagerAgentfor GNOME on Wayland.
The agent split exists because Linux's UI stack is not unified: AT-SPI handles accessibility (which buttons / menus / text fields exist and what they say); the window manager handles window lifecycle (which window is on top, how to move a window, how to close one). The two systems talk different protocols. The agent split keeps each agent's responsibility clean.
Windows
File:
codecraftai-uia/source/packages/ccai/uia/platforms/windows/
Currently a stub. The factory's dispatch:
elif system == "Windows":
raise NotImplementedError("Windows automation is not yet implemented.")
Windows UI Automation (UIA) is the intended substrate when the implementation lands. The stub means projects that need Windows UI tests today cannot use the framework's UI automation; they fall back to other tools. The chapter is honest about this gap.
The factory
File: codecraftai-uia/source/packages/ccai/uia/factory.py
class AutomationFactory:
"""
Factory class for creating automation engines based on the current platform.
"""
@staticmethod
def get_automation_engine() -> AutomationEngine:
"""
Returns the automation engine for the current platform.
"""
system = platform.system()
if system == "Darwin":
from ccai.uia.platforms.macos.automation import MacOSAutomation
return MacOSAutomation()
elif system == "Windows":
raise NotImplementedError("Windows automation is not yet implemented.")
elif system == "Linux":
from ccai.uia.platforms.linux.automation import LinuxAutomation
return LinuxAutomation()
else:
raise NotImplementedError(f"Unsupported platform: {system}")
The factory dispatches on platform.system(). Three
branches: macOS, Linux, Windows (stubbed). Other platforms
raise.
A note on the design: this is not a factory protocol like
the Landscape's (chapter 7). It's a hardcoded
if/elif/else. Adding a new platform requires editing
the factory's source, not registering through a namespace
package.
This is a small wart on an otherwise mature package.
Future work might lift the factory to use the same protocol
pattern as LandscapingFactoryProtocol, but the present
implementation is what ships.
What test code sees
A test using UI automation:
from ccai.uia.factory import AutomationFactory
engine = AutomationFactory.get_automation_engine()
calc = engine.get_application_by_name("Calculator")
calc.bring_to_front()
main_window = calc.find_element(role="window", name="Calculator")
button_5 = main_window.find_element(role="button", name="Five")
button_5.click()
The flow:
AutomationFactoryreturns the platform-specific engine.engine.get_application_by_name(...)returns a platform-specificUIApplicationsubclass instance (MacApplicationon macOS,LinuxApplicationon Linux).calc.bring_to_front()calls into the platform-specific implementation.calc.find_element(...)returns a platform-specificUIElementsubclass instance..click()performs the click using the platform's native click machinery (mouse events for X11;AXPressfor macOS Accessibility; etc.).
The test code is platform-neutral in the sense that the same code works against macOS or Linux. The behaviour is platform-specific in that the underlying machinery differs.
In practice, projects often pair UI tests with declarative application mappings (chapter 12) for better readability and reuse.
The 20-bug audit
The package recently went through an audit pass that fixed 20 bugs across 17 actionable defects + 3 medium-severity follow-ups, plus a 31-file docstring typo cleanup.
Commits:
79172d0— "Fix audit-surfaced bugs across uia platform layers" (17 defects).2479151— 31-file docstring typo fix ("Represents and instance" → "Represents an instance").e5e4cad— three more medium-severity bugs.
Bug classes caught:
- Model layer:
DeclarativeApplication.attach()referenced an undefinedAPPLICATION_NAMEclass variable;ScreenElementhad no__init__, causingTypeErroronsuper().__init__(parent)fromUIApplication; four bareexcept:clauses replaced withexcept Exception:. - Linux platform: Broken import path
(
agents.wm.x11windowmanageragent→agents.x11.x11windowmanageragent);AccessibilityAgentmissing abstractget_value/set_valuemethods;WaylandGnomeWindowManagerAgent's_last_x/_last_yuninitialised when sync=False; non-compliant JSON-RPC (hardcodedreq_id=1, checking for non-existent "ok" field); debugprint()polluting stdout on every keystroke;MenuElement.close()sending invalid<Esc>instead of{ESC}. - macOS platform: Bare
except:and stdout pollution on optional HIServices import failure;SbWindow.groups()debug print loop on every call. - Test suite: Imports of nonexistent symbols; missing
Linux platform guards on Linux-specific tests; missing
defaults on
uia_controllerparameter.
Test status post-audit: 16/16 macOS tests passing (was 9/15 before the first pass).
The four bundle-identifier verifications
A subtle point: the audit verified — and chose not to change — four legacy Apple bundle identifiers that initially looked stale:
- Calendar:
com.apple.iCal(the legacy iCal name). - Contacts:
com.apple.AddressBook(the legacy AddressBook name). - Numbers:
com.apple.iWork.Numbers. - Pages:
com.apple.iWork.Pages.
Live macOS still uses these IDs; updating to a "Calendar" or "Contacts" name would have broken the lookup. The audit's discipline included verifying against live macOS before "fixing" what wasn't broken. The chapter calls this out because it's a useful reminder: not every plausible correction is correct.
What the next chapter does
Chapter 12 walks the declarative application mappings —
the project-side mechanism for naming UI elements
semantically. DeclarativeApplication, ElementHint, and
the per-app classes (Calculator, MicrosoftExcel,
LibreOfficeCalc) that ship in source. The chapter shows
how a project declares an application's UI structure once
and writes test code against the declared names rather
than against role / role / hint chains.