diff --git a/.github/workflows/android-ui-tests.yml b/.github/workflows/android-ui-tests.yml index 0c484ae..93fb8c4 100644 --- a/.github/workflows/android-ui-tests.yml +++ b/.github/workflows/android-ui-tests.yml @@ -18,6 +18,14 @@ jobs: run: ./scripts/verify_test_tag_parity.sh - name: Run unit tests run: ./gradlew :composeApp:testDebugUnitTest + - name: Verify screenshot regressions + run: ./gradlew :composeApp:verifyRoborazziDebug + - name: Upload screenshot diffs on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: roborazzi-diffs + path: composeApp/build/outputs/roborazzi/ - name: Run instrumented tests (managed device) run: ./gradlew :composeApp:pixel7Api34DebugAndroidTest env: diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index e6bf697..88d1189 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -222,3 +222,12 @@ compose.desktop { } } } + +// Roborazzi screenshot-regression plugin (P8). Pin the golden-image +// output directory inside the test source set so goldens live in git +// alongside the tests themselves. Anything under build/ is gitignored +// and gets blown away by `gradle clean` — not where committed goldens +// belong. +roborazzi { + outputDir.set(layout.projectDirectory.dir("src/androidUnitTest/roborazzi")) +} diff --git a/composeApp/src/androidUnitTest/kotlin/com/tt/honeyDue/screenshot/ScreenshotTests.kt b/composeApp/src/androidUnitTest/kotlin/com/tt/honeyDue/screenshot/ScreenshotTests.kt index 0e85fd0..6095241 100644 --- a/composeApp/src/androidUnitTest/kotlin/com/tt/honeyDue/screenshot/ScreenshotTests.kt +++ b/composeApp/src/androidUnitTest/kotlin/com/tt/honeyDue/screenshot/ScreenshotTests.kt @@ -1,7 +1,6 @@ @file:OptIn(androidx.compose.material3.ExperimentalMaterial3Api::class) package com.tt.honeyDue.screenshot -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -30,14 +29,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.onRoot -import com.github.takahirom.roborazzi.RoborazziRule +import com.github.takahirom.roborazzi.RoborazziOptions import com.github.takahirom.roborazzi.captureRoboImage import com.tt.honeyDue.ui.theme.AppThemes import com.tt.honeyDue.ui.theme.HoneyDueTheme import com.tt.honeyDue.ui.theme.ThemeColors -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @@ -45,7 +41,7 @@ import org.robolectric.annotation.Config import org.robolectric.annotation.GraphicsMode /** - * Roborazzi-driven screenshot regression tests (P8 scaffolding). + * Roborazzi-driven screenshot regression tests (P8). * * Runs entirely on the Robolectric unit-test classpath — no emulator * required. The goal is to catch accidental UI drift (colour, spacing, @@ -56,40 +52,27 @@ import org.robolectric.annotation.GraphicsMode * (light / dark) = 36 images. This is a conservative baseline; the full * 11-theme matrix would produce 132+ images and is deferred. * + * Implementation notes: + * - We use the top-level `captureRoboImage(path) { composable }` form + * from roborazzi-compose. That helper registers + * `RoborazziTransparentActivity` at runtime via Robolectric's shadow + * PackageManager, so we don't need `createComposeRule()` / + * `ActivityScenarioRule` and therefore avoid the + * "Unable to resolve activity for Intent MAIN/LAUNCHER cmp=.../ComponentActivity" + * failure that bit the initial scaffolding (RoboMonitoringInstrumentation:102). + * - Goldens land under `composeApp/build/outputs/roborazzi/`, which the + * Roborazzi Gradle plugin picks up for record / verify / compare. + * * Workflow: * - Initial record: `./gradlew :composeApp:recordRoborazziDebug` * - Verify in CI: `./gradlew :composeApp:verifyRoborazziDebug` * - View diffs: `./gradlew :composeApp:compareRoborazziDebug` - * - * We intentionally build *theme showcase* surfaces locally rather than - * invoking the full production screens (LoginScreen, TasksScreen, etc.) - * because those screens depend on DataManager/network state that can't - * be safely initialized from a Robolectric test. The showcases render - * the same material3 primitives the screens are composed from, so a - * colour/typography regression in Theme.kt will still be caught. */ -// TEMPORARILY DISABLED: Roborazzi runtime pipeline needs additional setup -// before screenshot tests can run green in CI. Enable via `@Ignore` removal -// once `recordRoborazziDebug` successfully generates the initial golden -// image set and CI is configured to run `verifyRoborazziDebug`. -@org.junit.Ignore("Roborazzi pipeline pending — see docs/screenshot-tests.md") @RunWith(RobolectricTestRunner::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) @Config(qualifiers = "w360dp-h800dp-mdpi") class ScreenshotTests { - @get:Rule - val composeRule = createComposeRule() - - @get:Rule - val roborazziRule = RoborazziRule( - composeRule = composeRule, - captureRoot = composeRule.onRoot(), - options = RoborazziRule.Options( - outputDirectoryPath = "build/outputs/roborazzi", - ), - ) - // ---------- Login screen showcase ---------- @Test @@ -286,12 +269,14 @@ class ScreenshotTests { darkTheme: Boolean, content: @Composable () -> Unit, ) { - composeRule.setContent { + captureRoboImage( + filePath = "build/outputs/roborazzi/$name.png", + roborazziOptions = RoborazziOptions(), + ) { HoneyDueTheme(darkTheme = darkTheme, themeColors = theme) { content() } } - composeRule.onRoot().captureRoboImage("build/outputs/roborazzi/$name.png") } } diff --git a/composeApp/src/androidUnitTest/roborazzi/complete_task_default_dark.png b/composeApp/src/androidUnitTest/roborazzi/complete_task_default_dark.png new file mode 100644 index 0000000..7d9e776 Binary files /dev/null and b/composeApp/src/androidUnitTest/roborazzi/complete_task_default_dark.png differ diff --git a/composeApp/src/androidUnitTest/roborazzi/complete_task_default_light.png b/composeApp/src/androidUnitTest/roborazzi/complete_task_default_light.png new file mode 100644 index 0000000..8b96843 Binary files /dev/null and b/composeApp/src/androidUnitTest/roborazzi/complete_task_default_light.png differ diff --git a/composeApp/src/androidUnitTest/roborazzi/complete_task_midnight_dark.png b/composeApp/src/androidUnitTest/roborazzi/complete_task_midnight_dark.png new file mode 100644 index 0000000..a34afee Binary files /dev/null and b/composeApp/src/androidUnitTest/roborazzi/complete_task_midnight_dark.png differ diff --git a/composeApp/src/androidUnitTest/roborazzi/complete_task_midnight_light.png b/composeApp/src/androidUnitTest/roborazzi/complete_task_midnight_light.png new file mode 100644 index 0000000..aac6e30 Binary files /dev/null and b/composeApp/src/androidUnitTest/roborazzi/complete_task_midnight_light.png differ diff --git a/composeApp/src/androidUnitTest/roborazzi/complete_task_ocean_dark.png b/composeApp/src/androidUnitTest/roborazzi/complete_task_ocean_dark.png new file mode 100644 index 0000000..7b9cf1e Binary files /dev/null and b/composeApp/src/androidUnitTest/roborazzi/complete_task_ocean_dark.png differ diff --git a/composeApp/src/androidUnitTest/roborazzi/complete_task_ocean_light.png b/composeApp/src/androidUnitTest/roborazzi/complete_task_ocean_light.png new file mode 100644 index 0000000..7436805 Binary files /dev/null and b/composeApp/src/androidUnitTest/roborazzi/complete_task_ocean_light.png differ diff --git a/composeApp/src/androidUnitTest/roborazzi/login_default_dark.png b/composeApp/src/androidUnitTest/roborazzi/login_default_dark.png new file mode 100644 index 0000000..b8209fc Binary files /dev/null and b/composeApp/src/androidUnitTest/roborazzi/login_default_dark.png differ diff --git a/composeApp/src/androidUnitTest/roborazzi/login_default_light.png b/composeApp/src/androidUnitTest/roborazzi/login_default_light.png new file mode 100644 index 0000000..319d635 Binary files /dev/null and b/composeApp/src/androidUnitTest/roborazzi/login_default_light.png differ diff --git a/composeApp/src/androidUnitTest/roborazzi/login_midnight_dark.png b/composeApp/src/androidUnitTest/roborazzi/login_midnight_dark.png new file mode 100644 index 0000000..c37e633 Binary files /dev/null and b/composeApp/src/androidUnitTest/roborazzi/login_midnight_dark.png differ diff --git a/composeApp/src/androidUnitTest/roborazzi/login_midnight_light.png b/composeApp/src/androidUnitTest/roborazzi/login_midnight_light.png new file mode 100644 index 0000000..221bb92 Binary files /dev/null and b/composeApp/src/androidUnitTest/roborazzi/login_midnight_light.png differ diff --git a/composeApp/src/androidUnitTest/roborazzi/login_ocean_dark.png b/composeApp/src/androidUnitTest/roborazzi/login_ocean_dark.png new file mode 100644 index 0000000..63fd0f8 Binary files /dev/null and b/composeApp/src/androidUnitTest/roborazzi/login_ocean_dark.png differ diff --git a/composeApp/src/androidUnitTest/roborazzi/login_ocean_light.png b/composeApp/src/androidUnitTest/roborazzi/login_ocean_light.png new file mode 100644 index 0000000..4b0e0a4 Binary files /dev/null and b/composeApp/src/androidUnitTest/roborazzi/login_ocean_light.png differ diff --git a/composeApp/src/androidUnitTest/roborazzi/profile_default_dark.png b/composeApp/src/androidUnitTest/roborazzi/profile_default_dark.png new file mode 100644 index 0000000..f8c8465 Binary files /dev/null and b/composeApp/src/androidUnitTest/roborazzi/profile_default_dark.png differ diff --git a/composeApp/src/androidUnitTest/roborazzi/profile_default_light.png b/composeApp/src/androidUnitTest/roborazzi/profile_default_light.png new file mode 100644 index 0000000..ab303e8 Binary files /dev/null and b/composeApp/src/androidUnitTest/roborazzi/profile_default_light.png differ diff --git a/composeApp/src/androidUnitTest/roborazzi/profile_midnight_dark.png b/composeApp/src/androidUnitTest/roborazzi/profile_midnight_dark.png new file mode 100644 index 0000000..b8b11ed Binary files /dev/null and b/composeApp/src/androidUnitTest/roborazzi/profile_midnight_dark.png differ diff --git a/composeApp/src/androidUnitTest/roborazzi/profile_midnight_light.png b/composeApp/src/androidUnitTest/roborazzi/profile_midnight_light.png new file mode 100644 index 0000000..a82229e Binary files /dev/null and b/composeApp/src/androidUnitTest/roborazzi/profile_midnight_light.png differ diff --git a/composeApp/src/androidUnitTest/roborazzi/profile_ocean_dark.png b/composeApp/src/androidUnitTest/roborazzi/profile_ocean_dark.png new file mode 100644 index 0000000..14651e5 Binary files /dev/null and b/composeApp/src/androidUnitTest/roborazzi/profile_ocean_dark.png differ diff --git a/composeApp/src/androidUnitTest/roborazzi/profile_ocean_light.png b/composeApp/src/androidUnitTest/roborazzi/profile_ocean_light.png new file mode 100644 index 0000000..dbad688 Binary files /dev/null and b/composeApp/src/androidUnitTest/roborazzi/profile_ocean_light.png differ diff --git a/composeApp/src/androidUnitTest/roborazzi/residences_default_dark.png b/composeApp/src/androidUnitTest/roborazzi/residences_default_dark.png new file mode 100644 index 0000000..6eee2d2 Binary files /dev/null and b/composeApp/src/androidUnitTest/roborazzi/residences_default_dark.png differ diff --git a/composeApp/src/androidUnitTest/roborazzi/residences_default_light.png b/composeApp/src/androidUnitTest/roborazzi/residences_default_light.png new file mode 100644 index 0000000..e6bf1f0 Binary files /dev/null and b/composeApp/src/androidUnitTest/roborazzi/residences_default_light.png differ diff --git a/composeApp/src/androidUnitTest/roborazzi/residences_midnight_dark.png b/composeApp/src/androidUnitTest/roborazzi/residences_midnight_dark.png new file mode 100644 index 0000000..2c5fafc Binary files /dev/null and b/composeApp/src/androidUnitTest/roborazzi/residences_midnight_dark.png differ diff --git a/composeApp/src/androidUnitTest/roborazzi/residences_midnight_light.png b/composeApp/src/androidUnitTest/roborazzi/residences_midnight_light.png new file mode 100644 index 0000000..3f6ab52 Binary files /dev/null and b/composeApp/src/androidUnitTest/roborazzi/residences_midnight_light.png differ diff --git a/composeApp/src/androidUnitTest/roborazzi/residences_ocean_dark.png b/composeApp/src/androidUnitTest/roborazzi/residences_ocean_dark.png new file mode 100644 index 0000000..89376cf Binary files /dev/null and b/composeApp/src/androidUnitTest/roborazzi/residences_ocean_dark.png differ diff --git a/composeApp/src/androidUnitTest/roborazzi/residences_ocean_light.png b/composeApp/src/androidUnitTest/roborazzi/residences_ocean_light.png new file mode 100644 index 0000000..0d8c463 Binary files /dev/null and b/composeApp/src/androidUnitTest/roborazzi/residences_ocean_light.png differ diff --git a/composeApp/src/androidUnitTest/roborazzi/tasks_default_dark.png b/composeApp/src/androidUnitTest/roborazzi/tasks_default_dark.png new file mode 100644 index 0000000..116394b Binary files /dev/null and b/composeApp/src/androidUnitTest/roborazzi/tasks_default_dark.png differ diff --git a/composeApp/src/androidUnitTest/roborazzi/tasks_default_light.png b/composeApp/src/androidUnitTest/roborazzi/tasks_default_light.png new file mode 100644 index 0000000..3d8eed2 Binary files /dev/null and b/composeApp/src/androidUnitTest/roborazzi/tasks_default_light.png differ diff --git a/composeApp/src/androidUnitTest/roborazzi/tasks_midnight_dark.png b/composeApp/src/androidUnitTest/roborazzi/tasks_midnight_dark.png new file mode 100644 index 0000000..505597e Binary files /dev/null and b/composeApp/src/androidUnitTest/roborazzi/tasks_midnight_dark.png differ diff --git a/composeApp/src/androidUnitTest/roborazzi/tasks_midnight_light.png b/composeApp/src/androidUnitTest/roborazzi/tasks_midnight_light.png new file mode 100644 index 0000000..dc16bc3 Binary files /dev/null and b/composeApp/src/androidUnitTest/roborazzi/tasks_midnight_light.png differ diff --git a/composeApp/src/androidUnitTest/roborazzi/tasks_ocean_dark.png b/composeApp/src/androidUnitTest/roborazzi/tasks_ocean_dark.png new file mode 100644 index 0000000..eed1a0b Binary files /dev/null and b/composeApp/src/androidUnitTest/roborazzi/tasks_ocean_dark.png differ diff --git a/composeApp/src/androidUnitTest/roborazzi/tasks_ocean_light.png b/composeApp/src/androidUnitTest/roborazzi/tasks_ocean_light.png new file mode 100644 index 0000000..9c5c580 Binary files /dev/null and b/composeApp/src/androidUnitTest/roborazzi/tasks_ocean_light.png differ diff --git a/composeApp/src/androidUnitTest/roborazzi/themes_default_dark.png b/composeApp/src/androidUnitTest/roborazzi/themes_default_dark.png new file mode 100644 index 0000000..eb8d942 Binary files /dev/null and b/composeApp/src/androidUnitTest/roborazzi/themes_default_dark.png differ diff --git a/composeApp/src/androidUnitTest/roborazzi/themes_default_light.png b/composeApp/src/androidUnitTest/roborazzi/themes_default_light.png new file mode 100644 index 0000000..78aa25c Binary files /dev/null and b/composeApp/src/androidUnitTest/roborazzi/themes_default_light.png differ diff --git a/composeApp/src/androidUnitTest/roborazzi/themes_midnight_dark.png b/composeApp/src/androidUnitTest/roborazzi/themes_midnight_dark.png new file mode 100644 index 0000000..a696934 Binary files /dev/null and b/composeApp/src/androidUnitTest/roborazzi/themes_midnight_dark.png differ diff --git a/composeApp/src/androidUnitTest/roborazzi/themes_midnight_light.png b/composeApp/src/androidUnitTest/roborazzi/themes_midnight_light.png new file mode 100644 index 0000000..2342db0 Binary files /dev/null and b/composeApp/src/androidUnitTest/roborazzi/themes_midnight_light.png differ diff --git a/composeApp/src/androidUnitTest/roborazzi/themes_ocean_dark.png b/composeApp/src/androidUnitTest/roborazzi/themes_ocean_dark.png new file mode 100644 index 0000000..4d17d08 Binary files /dev/null and b/composeApp/src/androidUnitTest/roborazzi/themes_ocean_dark.png differ diff --git a/composeApp/src/androidUnitTest/roborazzi/themes_ocean_light.png b/composeApp/src/androidUnitTest/roborazzi/themes_ocean_light.png new file mode 100644 index 0000000..016ae40 Binary files /dev/null and b/composeApp/src/androidUnitTest/roborazzi/themes_ocean_light.png differ diff --git a/docs/screenshot-tests.md b/docs/screenshot-tests.md index 1376b87..377b480 100644 --- a/docs/screenshot-tests.md +++ b/docs/screenshot-tests.md @@ -50,19 +50,27 @@ consume every colour slot the real screens use. ./gradlew :composeApp:compareRoborazziDebug ``` -Output lands under `composeApp/build/outputs/roborazzi/`. +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 *not* auto-committed. The workflow is: +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. +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 sign-offs on each image change. + 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 @@ -94,6 +102,16 @@ Add the corresponding dark-mode and other-theme variants, then run - `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