Part of #79.
Problem
The iOS app has no automated test of its own. The web conformance gate (bun run gate, 18 checks) validates the built dist/, which the app loads — but nothing exercises the native shell: that it launches, that the WebView reaches its content, that a deep link routes, or that the offline fallback actually engages when the server is gone. The offline path is the one most worth testing, because it only runs when something else has already broken.
What to do
Port the wildloop reference suite:
.maestro/flows/— it has01-navigation.yaml,02-deep-link.yaml,02-deep-link-ios.yaml,03-offline-bundle.yamlscripts/run-mobile-e2e.ts(509 lines) — boots a simulator, builds, installs, runs the flows, saves screenshots and JUnit reportsscripts/mobile-target.ts(57 lines) — shared target resolution
Then bun run test:e2e:ios, plus the preview variants that make it usable by hand:
preview:ios bundled, offline simulator build
preview:ios:local simulator against ./buddy dev on this Mac
preview:ios:production benchreview.org, for demos with no local serverFlows worth having for bench specifically:
- Cold start, bundled — launch with
MOBILE_E2E=1(sowebAssets: 'dist-offline'), assert the home page renders and the nav is present. This is the one that catches a broken offline bundle. - Navigation — home → judges index → a judge tab. Note the judge tabs are served by one shared shell (
judges/_shell/*) and resolve the id fromlocation.pathname, so this exercises that path natively. - Deep link —
benchreview://scheme and, once #universal-links lands, anapplinks:URL. - Offline fallback — start pointed at a server, kill it, assert the app serves bundled pages rather than an error, then assert it returns when the server answers.
Checks
bun run test:e2e:iosgreen locally- Wired into CI, with the Maestro + simulator prerequisites documented (wildloop has
docs/mobile-e2e.mdfor this) - Screenshots and JUnit output saved as artifacts
- The suite fails, not passes, when
dist-offline/is missing or empty
Done (1b96a6d4) — three flows green on an iPhone 17 Pro
✓ 01-cold-start.yaml
✓ 02-navigation.yaml
✓ 04-offline.yamlAdapted rather than ported wholesale: wildloop's runner is 509 lines covering Android, location seeding and signed-in journeys. bench declares deviceFamilies: ['iphone'] and has no Android config, so an Android arm would be dead code pretending to be coverage.
02-navigation goes through the hamburger, because below lg that is the only navigation an iPhone has (#90).
Three things writing it found
1. The offline bundle was a week stale. bun run build refreshes dist/ and does not refresh dist-offline/ — only build:ios chains the two — so the app shipped whatever the bundle last contained. That copy predated the judge directory's load-failure state, so the offline app claimed "No judges yet" during an outage: the exact false claim that state was written to prevent.
The runner now refuses a bundle that is missing, empty, missing a page the flows visit, or older than dist/. 04-offline additionally asserts that string is absent, so the regression cannot come back quietly.
2. The generated Xcode project cannot be built. It declares no PRODUCT_NAME, so Xcode resolves the product to a bare .app and the build dies with "Multiple commands produce …/.app" — the link step and the create-directory step both claiming the same empty name.
Framework, not app: project.yml and project.pbxproj are both written by ./buddy build:ios under storage/framework/, regenerating from that spec with XcodeGen reproduces it exactly, and a sibling project on a newer framework emits PRODUCT_NAME six times. Worked around with an override on the xcodebuild command line, which survives regeneration where editing the generated spec would not.
3. benchreview:// does not route — filed as #94. The scheme is registered and iOS hands the URL to the app; the shell does nothing with it. Same shape as #80: an entitlement with nothing on the other side, failing silently.
That flow is kept as pending-03-deep-link.yaml rather than deleted — it is the evidence — and the runner skips pending-* by default, so a red suite always means a regression rather than a known bug. 04-offline originally reached its second page through that link, which made the offline test fail for a reason unrelated to being offline; it navigates through the menu now.
Also worth knowing
Maestro needs a JDK, and Homebrew's openjdk is keg-only so /usr/libexec/java_home does not find it — it exited before running a single step. The runner resolves JAVA_HOME itself.
A run that executed no tests is treated as a failure. Maestro exits 0 when it matches no flows, and reading that as success is how a suite reports green while testing nothing at all.
Still open
- Wired into CI. It needs a macOS runner with Xcode and a simulator — a different and much slower job than the current Linux CI, and a cost decision rather than a technical one. The suite is runnable locally today;
docs/mobile-e2e.mdhas the prerequisites.