# 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

```bash
# 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:

1. Developer changes a composable (intentionally or otherwise).
2. CI runs `verifyRoborazziDebug` and fails on any drift; the
   `roborazzi-diffs` artifact is uploaded for review.
3. Developer inspects the diff locally via `compareRoborazziDebug` or
   from the CI artifact.
4. If the drift is intentional, regenerate via
   `recordRoborazziDebug` and commit the new PNGs inside the PR so the
   reviewer explicitly signs off on each image change.
5. 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

```kotlin
@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.
- `captureRoboImage` only 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 from `roborazzi-compose` instead of the
  `createComposeRule() + RoborazziRule` approach. The helper registers
  `RoborazziTransparentActivity` with Robolectric's shadow PackageManager
  on its own, avoiding the
  "Unable to resolve activity for Intent MAIN/LAUNCHER cmp=.../ComponentActivity"
  failure you hit if `createComposeRule` tries to launch
  `androidx.activity.ComponentActivity` through `ActivityScenario` on the
  unit-test classpath (where the manifest declares `ComponentActivity`
  without a MAIN/LAUNCHER intent filter).

## References

- Upstream: https://github.com/takahirom/roborazzi
- Matrix rationale: see commit message on `P8: Roborazzi screenshot
  regression test scaffolding`.
