Records initial golden set + wires verifyRoborazziDebug into CI. Diffs
uploaded as artifact on failure. ScreenshotTests @Ignore removed.
Root cause of the prior RoboMonitoringInstrumentation:102 failure:
createComposeRule() launches ActivityScenarioRule<ComponentActivity>
which fires a MAIN/LAUNCHER intent, but the merged unit-test manifest
declares androidx.activity.ComponentActivity without a LAUNCHER filter,
so Robolectric's PM returns "Unable to resolve activity for Intent".
Fix: switch to the standalone captureRoboImage(path) { composable }
helper from roborazzi-compose, which registers
RoborazziTransparentActivity with Robolectric's shadow PackageManager
at runtime and bypasses ActivityScenario entirely.
Also pin roborazzi outputDir to src/androidUnitTest/roborazzi so
goldens live in git (not build/) and survive gradle clean.
36 goldens, 540KB total.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4.8 KiB
Roborazzi screenshot regression tests (P8)
Roborazzi is a screenshot-diff testing tool purpose-built for Jetpack / Compose Multiplatform. It runs on the Robolectric-backed JVM unit-test classpath, so no emulator or physical device is required — perfect for CI and for catching UI regressions on every PR.
Why screenshot tests?
Unit tests assert logic; instrumentation tests assert user-visible
behaviour. Neither reliably catches design regressions: a colour drift
in Theme.kt, a typography scale change, an accidental padding edit.
Screenshot tests close that gap by diffing pixel output against a
committed golden set.
What we cover
The initial matrix (see composeApp/src/androidUnitTest/.../ScreenshotTests.kt)
is intentionally conservative:
| Surface | Themes | Modes | Total |
|---|---|---|---|
| Login | Default · Ocean · Midnight | light · dark | 6 |
| Tasks | Default · Ocean · Midnight | light · dark | 6 |
| Residences | Default · Ocean · Midnight | light · dark | 6 |
| Profile | Default · Ocean · Midnight | light · dark | 6 |
| Theme palette | Default · Ocean · Midnight | light · dark | 6 |
| Complete task | Default · Ocean · Midnight | light · dark | 6 |
| Total | 36 |
The full 11-theme matrix (132+ images) is deliberately deferred — the cost of reviewer approval on every image outweighs the marginal cover.
Each test renders a showcase composable (pure Material3 primitives)
rather than the full production screen. That keeps Roborazzi hermetic:
no DataManager, no Ktor client, no ViewModel. A regression in
Theme.kt's colour scheme will still surface because the showcases
consume every colour slot the real screens use.
Commands
# Record a fresh golden set (do this on first setup and after intentional UI changes)
./gradlew :composeApp:recordRoborazziDebug
# Verify current UI matches the golden set (fails the build on drift)
./gradlew :composeApp:verifyRoborazziDebug
# Generate side-by-side diff images (useful for review)
./gradlew :composeApp:compareRoborazziDebug
Committed goldens live at
composeApp/src/androidUnitTest/roborazzi/ — pinned there via the
roborazzi { outputDir = ... } block in composeApp/build.gradle.kts
so they survive gradle clean. Diffs and intermediate artefacts land
under composeApp/build/outputs/roborazzi/ and are uploaded as a CI
artifact on failure (android-ui-tests.yml → Upload screenshot diffs on failure).
Golden-image workflow
Roborazzi goldens are committed alongside the tests — see
composeApp/src/androidUnitTest/roborazzi/. The workflow is:
- Developer changes a composable (intentionally or otherwise).
- CI runs
verifyRoborazziDebugand fails on any drift; theroborazzi-diffsartifact is uploaded for review. - Developer inspects the diff locally via
compareRoborazziDebugor from the CI artifact. - If the drift is intentional, regenerate via
recordRoborazziDebugand commit the new PNGs inside the PR so the reviewer explicitly signs off on each image change. - If the drift is a regression, fix the composable and re-run.
Reviewer checklist: every committed .png under the roborazzi output
dir is an intentional design decision. Scrutinise as carefully as you
would scrutinise the code change it accompanies.
Adding a new screenshot test
@Test
fun mySurface_default_light() = runScreen(
name = "my_surface_default_light",
theme = AppThemes.Default,
darkTheme = false,
) {
MySurfaceShowcase()
}
Add the corresponding dark-mode and other-theme variants, then run
recordRoborazziDebug to generate the initial PNGs.
Known limitations
- Roborazzi requires
@GraphicsMode(Mode.NATIVE)— the Robolectric version in this repo (4.14.1) supports it. - The test runner uses a fixed device qualifier (
w360dp-h800dp-mdpi). If you change this, every golden must be regenerated. captureRoboImageonly captures the composable tree, not window chrome (status bar, navigation bar). That's intentional — chrome is owned by the OS, not our design system.- We use the standalone
captureRoboImage(filePath) { composable }helper fromroborazzi-composeinstead of thecreateComposeRule() + RoborazziRuleapproach. The helper registersRoborazziTransparentActivitywith Robolectric's shadow PackageManager on its own, avoiding the "Unable to resolve activity for Intent MAIN/LAUNCHER cmp=.../ComponentActivity" failure you hit ifcreateComposeRuletries to launchandroidx.activity.ComponentActivitythroughActivityScenarioon the unit-test classpath (where the manifest declaresComponentActivitywithout a MAIN/LAUNCHER intent filter).
References
- Upstream: https://github.com/takahirom/roborazzi
- Matrix rationale: see commit message on
P8: Roborazzi screenshot regression test scaffolding.