feat(kmp): Roborazzi screenshot testing for Compose @Preview (:ui)#199
Open
caiqueslp wants to merge 22 commits into
Open
feat(kmp): Roborazzi screenshot testing for Compose @Preview (:ui)#199caiqueslp wants to merge 22 commits into
caiqueslp wants to merge 22 commits into
Conversation
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…iewTester Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… + docs Address code-review follow-ups on the preview tester: - Reinstate the conditional manualAdvance step that AndroidComposePreviewTester.test applies when a preview's @RoboComposePreviewOptions declares a manual clock, so the fork is behaviour-complete (latent today; no :ui preview uses it). - Document that the LemonadeDarkTheme branch is provisioned but dormant (no preview declares a night uiMode; dark coverage is author-opt-in). - Soften absolute "verbatim/exactly" wording in KDoc; add a TODO to drop the test() override once Roborazzi exposes a content-wrapping (Capturer) hook. - Extract the Robolectric SDK literal into a ROBOLECTRIC_SDK const and add an upgrade-checklist comment on the roborazzi version-catalog entry. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per product decision, LemonadeComposePreviewTester now:
- Drops RTL goldens: testParameters() filters out the Arabic-locale ("ar")
variation contributed by @LemonadePreview, keeping LTR ("en") and bare
empty-locale previews. @LemonadePreview is untouched so the IDE still shows
both directions.
- Captures every kept preview twice - once in LemonadeLightTheme and once in
LemonadeDarkTheme - into deterministic golden paths suffixed _Light / _Dark
before the file extension. The uiMode-based single-theme selection is gone.
Net per preview: 2 goldens (LTR light + LTR dark) instead of 2 (LTR + RTL,
light only).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… variants Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Capturing a preview twice (light + dark) in one test() NPE'd: binding the options to the shared single-use composeTestRule meant the first captureRoboImage closed that ActivityScenario, so the second capture hit "Activity has been destroyed already" in RoborazziComposeBackgroundOption.configureWithActivityScenario (triggered by showBackground = true). Fix: build the render options WITHOUT the shared composeTestRule. With no activity-scenario-creator option present, RoborazziComposeOptions.createScenario falls back to launching (and closing) a fresh ActivityScenario per captureRoboImage call, so the two theme captures are independent. Preview render options (device/locale/fontScale/background) and the _Light/_Dark golden naming are unchanged. Trade-off: manualAdvance needs the shared rule and is dropped here; no :ui preview declares @RoboComposePreviewOptions, so it's latent and documented. Verified: recordRoborazziDebug + verifyRoborazziDebug both BUILD SUCCESSFUL; 227 LTR previews -> 227 _Light + 227 _Dark goldens, 0 RTL. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Set roborazzi.outputDir to the module-relative `screenshots/` folder so goldens land in `kmp/ui/screenshots/` (CI-committable) instead of the gitignored `build/outputs/roborazzi`. The Roborazzi 1.46.1 plugin propagates the extension's outputDir to the test JVM as the `roborazzi.output.dir` system property (project-relative path), which LemonadeComposePreviewTester reads via roborazziSystemPropertyOutputDirectory() when reconstructing golden paths under the default RelativePathFromCurrentDirectory record strategy - so record writes in-tree and verify reads from the same dir. Verified: recordRoborazziDebug writes 454 goldens (227 _Light + 227 _Dark) to kmp/ui/screenshots/ with none under build/; verifyRoborazziDebug BUILD SUCCESSFUL reading from there. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Goldens under any screenshots/ dir (e.g. kmp/ui/screenshots/) are images; mark them binary so git never line-diffs or merges them. GitHub still renders a visual image diff in PR views. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… Linux) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Roborazzi Gradle plugin is applied by id inside LemonadeScreenshotPlugin and pulled onto the build-logic classpath via the gradle-roborazzi library coordinate; the [plugins] alias was never referenced. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Adds end-to-end Compose @Preview screenshot testing for the KMP :ui module using Roborazzi + Robolectric, including CI automation and a custom preview tester that standardizes theming/output paths.
Changes:
- Introduces a
lemonade-screenshotconvention plugin to wire Roborazzi preview-test generation, dependencies, Robolectric config, and in-tree golden output. - Adds
LemonadeComposePreviewTesterto capture LTR previews in both light and dark themes with deterministic golden naming. - Refactors multiple
@PreviewParameterproviders to avoid cartesian explosion and reduces screenshot count; adds CI workflow + docs +.gitattributesfor PNG goldens.
Reviewed changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| kmp/ui/src/mobileMain/kotlin/com/teya/lemonade/TopBar.mobile.kt | Refactors preview provider to representative variants to reduce screenshot permutations. |
| kmp/ui/src/commonMain/kotlin/com/teya/lemonade/TextField.kt | Refactors preview provider to representative variants (one-axis-at-a-time). |
| kmp/ui/src/commonMain/kotlin/com/teya/lemonade/SymbolContainer.kt | Refactors preview provider to representative variants and de-duplicates. |
| kmp/ui/src/commonMain/kotlin/com/teya/lemonade/Switch.kt | Refactors preview provider to representative variants and de-duplicates. |
| kmp/ui/src/commonMain/kotlin/com/teya/lemonade/SelectListItem.kt | Refactors preview provider to representative variants and de-duplicates. |
| kmp/ui/src/commonMain/kotlin/com/teya/lemonade/SelectField.kt | Refactors preview provider to representative variants and de-duplicates. |
| kmp/ui/src/commonMain/kotlin/com/teya/lemonade/RadioButton.kt | Refactors preview provider to representative variants and de-duplicates. |
| kmp/ui/src/commonMain/kotlin/com/teya/lemonade/Notice.kt | Refactors preview provider to representative variants and de-duplicates. |
| kmp/ui/src/commonMain/kotlin/com/teya/lemonade/ListItem.kt | Refactors preview provider to representative variants and de-duplicates. |
| kmp/ui/src/commonMain/kotlin/com/teya/lemonade/IconButton.kt | Refactors preview provider to representative variants and de-duplicates. |
| kmp/ui/src/commonMain/kotlin/com/teya/lemonade/HistoryTimeline.kt | Refactors preview provider to representative variants and de-duplicates. |
| kmp/ui/src/commonMain/kotlin/com/teya/lemonade/CountryFlag.kt | Refactors preview provider to representative variants and de-duplicates. |
| kmp/ui/src/commonMain/kotlin/com/teya/lemonade/ContentListItem.kt | Refactors preview provider to representative variants and de-duplicates. |
| kmp/ui/src/commonMain/kotlin/com/teya/lemonade/Chip.kt | Refactors preview provider to representative variants and de-duplicates. |
| kmp/ui/src/commonMain/kotlin/com/teya/lemonade/Card.kt | Refactors preview provider to representative variants (currently missing de-duplication). |
| kmp/ui/src/commonMain/kotlin/com/teya/lemonade/Button.kt | Refactors preview provider to representative variants and de-duplicates. |
| kmp/ui/src/commonMain/kotlin/com/teya/lemonade/BrandLogo.kt | Refactors preview provider to representative variants and de-duplicates. |
| kmp/ui/src/androidUnitTest/kotlin/com/teya/lemonade/screenshot/LemonadeComposePreviewTester.kt | Adds custom Roborazzi preview tester to enforce LTR-only + light/dark themed captures with deterministic naming. |
| kmp/ui/build.gradle.kts | Applies the new lemonade-screenshot convention plugin to :ui. |
| kmp/README.md | Documents the screenshot testing workflow and local commands. |
| kmp/gradle/libs.versions.toml | Adds/pins Roborazzi/Robolectric/preview-scanner/test deps in the version catalog. |
| kmp/build-logic/src/main/kotlin/LemonadeScreenshotPlugin.kt | Implements the convention plugin to configure Roborazzi generation, dependencies, output dir, and source registration. |
| kmp/build-logic/build.gradle.kts | Adds the Roborazzi Gradle plugin to build-logic and registers lemonade-screenshot. |
| .github/workflows/kmp_ci.yml | Adds an “Android Screenshots” job (verify/record/auto-commit) and gates it via ci-status. |
| .gitattributes | Marks **/screenshots/**/*.png as binary to prevent line diffs/merges. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+239
to
243
| return buildList { | ||
| add(element = base) | ||
| LemonadeCardPadding.entries.forEach { contentPadding -> | ||
| LemonadeCardBackground.entries.forEach { background -> | ||
| listOf(true, false).forEach { withHeader -> | ||
| add( | ||
| CardPreviewData( | ||
| background = background, | ||
| contentPadding = contentPadding, | ||
| header = CardHeaderConfig( | ||
| title = "Card heading", | ||
| trailingSlot = { | ||
| LemonadeUi.Tag("Tag label", voice = TagVoice.Neutral) | ||
| }, | ||
| ).takeIf { withHeader }, | ||
| ), | ||
| ) | ||
| } | ||
| } | ||
| add(element = base.copy(contentPadding = contentPadding)) | ||
| } |
Adopt teya's canonical screenshot approach (PR #1077): authors stack Light/Dark via @Preview(uiMode=…) and the tester captures one image per preview, wrapping by uiMode. Replaces the prior RTL/LTR + double-capture model. - @LemonadePreview: RTL/LTR (locale ar/en, showBackground) -> stacked Light/Dark by uiMode (UI_MODE_NIGHT_NO/YES, inlined since commonMain can't reference Android Configuration). uiMode confirmed available on the CMP common @Preview annotation. - Tester: single captureRoboImage, theme chosen from previewInfo.uiMode; restored composeTestRule binding + conditional manualAdvance (safe again without double-capture). Dropped the Theme enum, _Light/_Dark suffixing and RTL filter. Paints LemonadeTheme.colors.background.bgDefault behind each preview so dark content isn't invisible on a transparent surface (previews no longer set showBackground). Needs compose-foundation on the test source set. - Plugin outputDir -> src/androidMain/screenshots (teya convention). Goldens now named …Kt.<Fn>.Light_DAY.png / .Dark_NIGHT.png. Verified: record + verify BUILD SUCCESSFUL, 454 goldens (227 Light_DAY + 227 Dark_NIGHT). No full-screen/Scaffold previews exist, so no per-preview backdrop was needed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… commit) Port the screenshot CI flow from teya-business-app#1077: - verifyAndRecordRoborazziDebug only rewrites pixel-changed goldens - second verify pass distinguishes pixel diffs from preview crashes - GPG-signed bot commit pushed via PAT to re-trigger CI - diffs uploaded as an artifact; fork PRs / main pushes fail with guidance Keeps ubuntu-latest (Linux determinism); no GPG cleanup (ephemeral runner). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reflects the teya-aligned approach: @LemonadePreview stacks Light/Dark via uiMode (tester wraps the theme — no manual LemonadeTheme), goldens at src/androidMain/screenshots, and the CI verifyAndRecord + signed bot commit flow. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The provider refactor shifted source positions in Card.kt, so the Compose compiler regenerated a ComposableSingletons$CardKt synthetic lambda key. BCV tracks these accessors, so apiDump + commit is required (no real public API change — only a generated lambda symbol was renamed). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
import-ownertrust and git user.signingkey require the 40-char fingerprint; the 16-char sec key id was rejected as "invalid fingerprint", failing the screenshot bot's GPG setup step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
So the Linux-rendered goldens can be retrieved and committed to the branch directly, since the bot's auto-commit push is blocked until the automation PAT is SSO-authorized for the org. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
454 goldens (227 Light + 227 Dark) recorded by the Android Screenshots CI job on ubuntu-latest. Committed directly because the auto-commit bot's push is blocked until SCREENSHOT_AUTOMATION_TOKEN is SSO-authorized for the org; CI now verifies against these. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds Roborazzi screenshot testing for Compose
@Previews in the:uimodule — every preview is captured in light and dark, and CI records & auto-commits the golden images.