GUI TESTING
Two suites, for two different jobs.
just gui-test (primary) | just gui-test-onhardware (smoke) | |
|---|---|---|
| Backend | Broadway + Playwright | dogtail/behave + AT-SPI |
| Needs a GNOME session | no | yes |
Needs gnome-ponytail-daemon | no | yes |
| Runs on a build host | yes | no |
| Exercises real pkexec/polkit | no | yes |
| Runs the real Flatpak | no | yes |
Use the first for everyday work. Keep the second for verifying the things Broadway can never reach β a real elevation prompt, a real Flatpak sandbox.
Primary suite: Broadway + screenshots + action journalβ
Why this shapeβ
The dogtail suite was effectively unrunnable: it needs a live GNOME session and
gnome-ponytail-daemon, which isn't in the Dakota image. That blocker is why
GUI coverage stalled.
Broadway (gtk4-broadwayd) renders GTK4 into a browser over WebSockets, so the
app runs headless with no compositor at all. Ported from
tuna-os/gtk-office-suite's broadway-inspect skill β with one important
difference discovered here:
GTK4 rasterises text into textures. Broadway's DOM has real element nodes but zero text nodes (measured on this app: 133
<div>, 19<img>, 0 text). The office-suite approach of scraping text and clicking viaget_by_text()therefore cannot work.
What does work, and what the suite is built on:
- Rendering β pixel-accurate. Screenshots are excellent.
- Input β Broadway forwards mouse and keyboard to GTK.
AdwDialogand popovers render and respond correctly. - Backend intent β the JSONL action journal (
src/action_journal.rs).
AT-SPI was investigated as the selector layer and rejected: GTK4 under the
Broadway backend reports Unrecognized accessibility backend "atspi". Targets
are therefore coordinates in a fixed-size window (see WIDGETS in
tests/gui/harness.py), or keyboard traversal via activate_by_keyboard().
Each check is a tripleβ
with FinupdateApp() as app: # 1. launch, dry-run, isolated config
app.click("automatic_updates_switch") # drive
app.screenshot("uupd-timer-toggled") # 2. look
app.assert_action( # 3. assert the BACKEND intent
"set_uupd_timer",
would_run_contains=["pkexec", "systemctl", "--now", "uupd.timer"],
suppressed=True,
)
Step 3 is the one a screenshot can never make. It is the difference between
"the rebase dialog looks right" and "clicking Switch would really have run
bootc switch ghcr.io/ublue-os/bluefin:stable β and did not run it."
Runningβ
just gui-test-setup # once: playwright + chromium
just gui-test # everything
just gui-test "idle narrow" # named checks
just broadway 360x640 # launch and poke by hand at localhost:8085
Screenshots land in tests/gui/screenshots/<theme>/. They are artifacts for a
human to review, not golden-image comparisons β pixel-diffing GTK across
libadwaita releases is a maintenance sink, and the behavioural assertion lives
in the journal instead.
Safety and determinismβ
Nothing destructive runs. The app is launched --dry-run --no-dev-mode, so real
code paths execute but privileged() withholds every privileged command and
records it instead. In addition the launcher pins:
- an isolated
XDG_CONFIG_HOMEβ the suite never reads or writes your real~/.config/finupdate/settings.json; GTK_ENABLE_ANIMATIONS=0, so no capture lands mid-transition;- a fixed window geometry via
FINUPDATE_WINDOW_SIZE; - a fixed mock image via
FINUPDATE_IMAGE.
Gotchas worth knowingβ
- A blank screenshot usually means a stale instance.
pkill -x finupdatedoes not match processes started viatoolbox run, so old instances accumulate; a leftover one holds the D-Bus name and the Broadway surface and you get a white PNG rather than an error. The launcher now matches on the full command line and warns if anything survives. $HOMEis shared with the toolbox;/var/tmpis not. The journal lives at~/.finupdate-test-journal.jsonlfor exactly this reason β a/var/tmppath is written inside the container and is invisible to the harness.cargo test --libdoes not rebuild the binary. Runcargo build --binsbefore the GUI suite or you will test a stale app. (This produced a real false failure during development.)/tmpon the build host is a small tmpfs and is often full; Playwright's chromium then fails with a confusing profile error. HenceTMPDIR=/var/tmp/pw-tmp.
On-hardware smoke suiteβ
tests/smoke/features/ β dogtail + behave via qecore, unchanged. It is the only
thing that exercises the real Flatpak in a real session including polkit, so it
is kept, but demoted out of the normal loop.
Requirements: an active GNOME session, toolkit-accessibility enabled, and
gnome-ponytail-daemon (just install-ponytail builds it into ~/.local if
your image lacks it).
just gui-test-onhardware # all
just gui-test-onhardware smoke @launch
qecore leaves GNOME Shell in unsafe_mode; the recipe resets it on exit via
just _reset-unsafe-mode.
Adding a checkβ
- Add a
@check("name", "what it proves")totests/gui/test_features.py. - If you need a new click target, capture a screenshot, read the coordinate off
it, and add it to
WIDGETSinharness.pywith a comment naming the capture it came from. Preferactivate_by_keyboard()where the tab order is stable β a moved pixel target fails as a confusing screenshot diff rather than an error. - If the feature touches the host, assert the journal entry too. A check that only screenshots is half a check.
- Add the corresponding row to
docs/app-logic-map.mdΒ§4 in the same change, so the map stays load-bearing.