Rivet keeps UI tests native for the same reason it keeps production UI native: the platform accessibility and interaction stacks are part of the product. The reference Taskboard is exercised through UI Automation on Windows, AXUIElement on macOS, and AT-SPI on Linux. No shared widget DSL or synthetic Rivet view tree sits between the test and WinUI 3, SwiftUI/AppKit, or GTK4.
The Native UI End-to-End workflow packages the application before testing it
and requires each platform driver to prove all of the following:
- a real native window can be discovered and activated;
- stable semantic names/identifiers and roles are exposed;
- invoking
new-taskcrosses the generated client and RVT1 boundary and the resulting State reaches the native list; - invoking
generate-demomakes anoperation-progressEvent visible through the native accessibility surface; - the final 1,000-row State reaches the UI; and
- native window close or
SIGTERMcompletes the orderly backend shutdown.
Every run uploads the bounded interaction trace and accessibility snapshots.
It also captures a screen image when the runner supports the platform capture
API, plus application/compositor logs on Linux. The evidence is uploaded with
if: always() so a failed assertion remains diagnosable.
Product applications own their views, so they also own the identifiers and scenario vocabulary. Keep those identifiers in native source:
- WinUI:
AutomationProperties.AutomationIdandAutomationProperties.Name; - SwiftUI/AppKit:
.accessibilityIdentifier, labels, and native roles; and - GTK4:
GtkAccessibleproperties and meaningful widget roles.
Copy the matching driver under rivet/tests/native-ui/, replace the reference
identifiers and actions with one product-critical scenario, and add the driver
to a packaging job. Declare that driver as a native-ui suite in the
product's rivet-acceptance.rktd so missing platform coverage fails before the
job starts; see product acceptance evidence. A useful
minimum scenario has one RPC-driven mutation, one
Event, a stable accessibility-tree assertion, and orderly close. Test-only
protocol messages and a cross-platform selector language are deliberately not
extension points.
The macOS driver receives the application's process id (--pid) and an
evidence output directory (--output); the suite command owns launching and
reaping the application around it. A proven shape for that runner:
raco rivet packageproduces the development distributable; locate the newest bundle underdist/and its executable withplutil -extract CFBundleExecutable rawon the bundle'sInfo.plist. By defaultpackagealso launches the artifact once as a launch smoke — pass--skip-launch-smokewhen the suite itself owns launching, so the run stays hermetic.- Launch the executable in the background with a disposable data root in the
environment (the application's own override hook — never the developer's
real profile), and take
$!as the pid for the driver. - Poll
kill -0for an orderly exit after the driver finishes, then run a second launch against the same data root to assert persisted effects. - Compile the Swift driver with
swiftc(or precompile it in the suite directory); the driver requires the process to hold Accessibility trust, so CI runners must grant it to the test host.
Keep the scenario's keyboard path real: focus (asserting
kAXFocusedAttribute), typeText (CGEvent unicode-string events), and
postKey (Return is keycode 36, Escape is 53) are part of the reference
driver for exactly this purpose — a text field driven only through
AXUIElementSetValue skips the event path the user actually exercises. Fixed
sleeps are not a substitute for the bounded waitFor probes.
Keep snapshots small: cap recursion depth, record semantic fields rather than platform object dumps, and never include user data or secret values. Failure artifacts should be retained only as long as the repository's normal CI logs.