Skip to content

feat(kmp): Roborazzi screenshot testing for Compose @Preview (:ui)#199

Open
caiqueslp wants to merge 22 commits into
mainfrom
worktree-lazy-roaming-scroll
Open

feat(kmp): Roborazzi screenshot testing for Compose @Preview (:ui)#199
caiqueslp wants to merge 22 commits into
mainfrom
worktree-lazy-roaming-scroll

Conversation

@caiqueslp

@caiqueslp caiqueslp commented May 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds Roborazzi screenshot testing for Compose @Previews in the :ui module — every preview is captured in light and dark, and CI records & auto-commits the golden images.

caiqueslp and others added 14 commits May 25, 2026 00:35
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>
Copilot AI review requested due to automatic review settings May 25, 2026 11:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-screenshot convention plugin to wire Roborazzi preview-test generation, dependencies, Robolectric config, and in-tree golden output.
  • Adds LemonadeComposePreviewTester to capture LTR previews in both light and dark themes with deterministic golden naming.
  • Refactors multiple @PreviewParameter providers to avoid cartesian explosion and reduces screenshot count; adds CI workflow + docs + .gitattributes for 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 thread .github/workflows/kmp_ci.yml Outdated
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))
}
caiqueslp and others added 3 commits May 25, 2026 14:24
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>
@caiqueslp caiqueslp marked this pull request as ready for review May 26, 2026 23:52
@caiqueslp caiqueslp requested a review from a team as a code owner May 26, 2026 23:52
caiqueslp and others added 5 commits May 27, 2026 01:53
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants