Skip to content

Conversation

takahirom
Copy link
Owner

@takahirom takahirom commented Aug 13, 2025

Summary

Fixes a bug where Preview annotation size attributes (heightDp/widthDp) incorrectly persist between captures when using ComposablePreviewScanner.

Problem

  • When one preview has heightDp=600, subsequent previews without explicit size specifications also get rendered at 600dp height instead of default size
  • Root cause: RuntimeEnvironment.setQualifiers() modifies Robolectric's global display configuration which persists between captures

Solution

Save and restore Robolectric qualifiers around each capture operation:

  • Save current qualifiers before applying configuration
  • Restore original qualifiers in finally block after capture
  • Keep RoborazziComposeOptions immutable (no mutable fields)

Test

Added PreviewSizeRetentionBugTest that reproduces the bug and verifies the fix works by checking actual image dimensions.

Fixes #719

Summary by CodeRabbit

  • Bug Fixes

    • Prevents preview/screenshot configuration from leaking between captures, improving reliability and consistency across tests and runs.
  • Tests

    • Added test suites that reproduce and validate preview size retention issues, ensuring the fix is covered.
  • Chores

    • Increased CI screenshot comparison job timeout to reduce flaky failures on slower runs.

@takahirom takahirom changed the title Add failing test for preview size retention bug Fix preview size retention bug in ComposablePreviewScanner Aug 13, 2025
@takahirom takahirom changed the title Fix preview size retention bug in ComposablePreviewScanner Fix preview size retention bug Aug 13, 2025
- Use RuntimeEnvironment.getQualifiers() to save current state
- Restore original qualifiers after capture completes
- Fixes issue where Preview annotation size persists between captures
Base automatically changed from tm/gradle-update-v2 to main August 14, 2025 00:23
Copy link

coderabbitai bot commented Aug 14, 2025

Walkthrough

Increased GitHub Actions compare-screenshot-test job timeout. Updated RoborazziCompose captureRoboImage(File, ...) to save and restore Robolectric runtime qualifiers around capture. Added a new Robolectric test class to reproduce preview size retention behavior across sequential captures.

Changes

Cohort / File(s) Change Summary
CI workflow timeout
.github/workflows/CompareScreenshot.yml
Bumped compare-screenshot-test timeout-minutes from 20 to 30.
Qualifier preservation in capture
roborazzi-compose/src/main/java/.../RoborazziCompose.kt
Wrapped captureRoboImage(File, ...) with try/finally to save RuntimeEnvironment qualifiers before, and restore after, preventing qualifier leakage across tests. No API changes.
Repro tests for preview sizing
sample-android/src/test/java/.../PreviewSizeRetentionBugTest.kt
Added tests that sequentially capture previews with/without explicit sizes to verify size isolation; includes helper to read PNG dimensions and cleanup logic.

Sequence Diagram(s)

sequenceDiagram
  participant Test as Test (Robolectric)
  participant RC as captureRoboImage(File,...)
  participant RE as RuntimeEnvironment
  participant Cap as Image Capture

  Test->>RC: captureRoboImage(file, options)
  RC->>RE: getQualifiers()
  RC->>RC: save savedQualifiers
  RC->>Cap: perform capture (may change qualifiers)
  Cap-->>RC: result
  RC->>RE: setQualifiers(savedQualifiers) (finally)
  RC-->>Test: return
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Assessment against linked issues

Objective Addressed Explanation
Prevent preview size leakage across captures due to Preview heightDp/widthDp (#719)
Ensure default-sized previews are unaffected by prior fixed-size previews in loops/scanners (#719)
Provide regression tests reproducing the sizing issue (#719)

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
Increased job timeout for compare-screenshot-test (.github/workflows/CompareScreenshot.yml) CI configuration change not required to address preview size leakage; unrelated to functional fix in codebase.

Poem

I nibble bytes and hop through scenes,
Saving “w” and “h” like leafy greens.
Snap! Then back the fields restore—
No more sizes leaking o’er.
With longer builds and tests to run,
My burrowed fixes are done—fun! 🐇🌈

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dc0bf84 and 839a9b8.

📒 Files selected for processing (1)
  • sample-android/src/test/java/com/github/takahirom/roborazzi/sample/boxed/PreviewSizeRetentionBugTest.kt (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • sample-android/src/test/java/com/github/takahirom/roborazzi/sample/boxed/PreviewSizeRetentionBugTest.kt
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/preview-size-retention-bug

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@takahirom
Copy link
Owner Author

@coderabbitai review

Copy link

coderabbitai bot commented Aug 14, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (2)
roborazzi-compose/src/main/java/com/github/takahirom/roborazzi/RoborazziCompose.kt (1)

65-83: Guard against parallel test interference when mutating global qualifiers.

RuntimeEnvironment.setQualifiers affects global state. If Robolectric tests run in parallel forks/threads, interleaving could cause flaky dimensions. Consider serializing the critical section that mutates qualifiers.

Minimal approach:

  • Add a process-wide lock.
  • Wrap the save/launch/capture/restore sequence in synchronized.

Add this at top-level (outside any function):

private val qualifiersLock = Any()

Then wrap the refactored body:

synchronized(qualifiersLock) {
  // save -> launch -> capture -> afterCapture -> restore
}

This is defensive but prevents rare race conditions when multiple captures with different qualifiers run concurrently.

sample-android/src/test/java/com/github/takahirom/roborazzi/sample/boxed/PreviewSizeRetentionBugTest.kt (1)

35-36: Update test docstring to reflect current intent (regression, not expected failure).

Now that the fix is in place, this test validates the regression and should pass. Consider renaming the method or updating the comment to avoid confusion.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 54a1a1f and 18e6bfc.

📒 Files selected for processing (4)
  • .github/workflows/CompareScreenshot.yml (1 hunks)
  • .github/workflows/StoreScreenshot.yml (1 hunks)
  • roborazzi-compose/src/main/java/com/github/takahirom/roborazzi/RoborazziCompose.kt (3 hunks)
  • sample-android/src/test/java/com/github/takahirom/roborazzi/sample/boxed/PreviewSizeRetentionBugTest.kt (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
sample-android/src/test/java/com/github/takahirom/roborazzi/sample/boxed/PreviewSizeRetentionBugTest.kt (4)
include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/RoborazziOptions.kt (1)
  • File (225-247)
sample-android/src/test/java/com/github/takahirom/roborazzi/sample/boxed/boxedEnvironment.kt (1)
  • boxedEnvironment (9-21)
include-build/roborazzi-core/src/commonMain/kotlin/com/github/takahirom/roborazzi/RoborazziProperties.kt (1)
  • roborazziSystemPropertyOutputDirectory (6-9)
roborazzi-compose/src/main/java/com/github/takahirom/roborazzi/RoborazziComposeOptions.kt (1)
  • size (166-172)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: ollama-test
🔇 Additional comments (3)
.github/workflows/StoreScreenshot.yml (1)

14-14: Timeout increase looks good and aligns with the compare workflow.

Raising to 30 minutes is reasonable for heavier screenshot tasks. Keep both workflows in sync (as done here) to avoid false waits and flaky pipeline behavior.

.github/workflows/CompareScreenshot.yml (1)

11-11: Timeout increase acknowledged.

Matches the StoreScreenshot workflow. This should reduce CI flakiness on longer Roborazzi runs.

roborazzi-compose/src/main/java/com/github/takahirom/roborazzi/RoborazziCompose.kt (1)

65-83: Not required — createScenario does not mutate Robolectric qualifiers

I searched the codebase: RuntimeEnvironment.setQualifiers is only called from RoborazziComposeOptions.ConfigBuilder.applyToRobolectric() (invoked by configured()), not from createScenario or any activity-scenario creator implementations. Therefore the current save/restore already captures qualifier changes made by configured(), and moving save/restore to wrap launchRoborazziActivity is unnecessary.

Key locations:

  • roborazzi-compose/src/main/java/com/github/takahirom/roborazzi/RoborazziComposeOptions.kt — ConfigBuilder.applyToRobolectric() calls setQualifiers(...)
  • roborazzi-compose/src/main/java/com/github/takahirom/roborazzi/RoborazziCompose.kt — RuntimeEnvironment.getQualifiers() (saved) and RuntimeEnvironment.setQualifiers(...) (restore) (current placement)
  • ActivityScenario creators: RoborazziComposeActivityThemeOption (RoborazziComposeOptions.kt) and RoborazziPreviewScannerSupport.createScenario — neither call setQualifiers

Resulting action: ignore the proposed refactor — the original concern is not supported by the codebase.

Likely an incorrect or invalid review comment.

- Replace Kotlin assert with JUnit Assert for reliability
- Add null check for BitmapFactory.decodeFile()
- Add try-finally blocks for proper cleanup
@takahirom
Copy link
Owner Author

@coderabbitai review

Copy link

coderabbitai bot commented Aug 14, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
sample-android/src/test/java/com/github/takahirom/roborazzi/sample/boxed/PreviewSizeRetentionBugTest.kt (1)

103-111: Nice: migrated to JUnit assertions and ensured cleanup with try/finally.

  • Replacing Kotlin assert with JUnit assertions makes the tests reliable regardless of JVM -ea flags.
  • try/finally cleanup guarantees artifact deletion even when assertions fail.

Also applies to: 133-136, 168-171, 231-238, 172-175, 239-242

🧹 Nitpick comments (3)
sample-android/src/test/java/com/github/takahirom/roborazzi/sample/boxed/PreviewSizeRetentionBugTest.kt (3)

64-67: Optional: avoid decoding full bitmap; read dimensions only (memory-friendly).

To reduce memory overhead and avoid retaining a Bitmap, decode bounds only.

-        val bitmap = BitmapFactory.decodeFile(filePath)
-            ?: throw AssertionError("Failed to decode bitmap: $filePath")
-        return bitmap.width to bitmap.height
+        val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
+        BitmapFactory.decodeFile(filePath, options)
+        val width = options.outWidth
+        val height = options.outHeight
+        if (width <= 0 || height <= 0) {
+            throw AssertionError("Failed to decode image dimensions: $filePath")
+        }
+        return width to height

70-70: Nit: rename tests to reflect current intent (regression tests that pass post-fix).

Names like “shouldFailBeforeFix” can confuse future readers when the test passes in main. Consider framing as a regression test.

-    fun testPreviewSizeRetentionBug_shouldFailBeforeFix() {
+    fun regression_previewSizeDoesNotLeakBetweenCaptures() {
-    fun testMultiplePreviewsWithSameCode_simulatesComposablePreviewScanner() {
+    fun regression_sizeIsolation_simulatesComposablePreviewScanner() {

Also applies to: 180-180


224-238: Stronger invariant: compare “default” sizes to a baseline default capture.

Asserting “not equal” verifies the leak is gone, but a more robust check is:

  • Capture a baseline “default” (no size) before any sized preview,
  • After each sized capture, capture again with default and assert it equals the baseline size.

This guards against coincidental equality/inequality and asserts the intended behavior precisely.

If you’d like, I can draft a variant that records a baseline default size and asserts equality against it for previews 2 and 4.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 18e6bfc and dc0bf84.

📒 Files selected for processing (1)
  • sample-android/src/test/java/com/github/takahirom/roborazzi/sample/boxed/PreviewSizeRetentionBugTest.kt (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
sample-android/src/test/java/com/github/takahirom/roborazzi/sample/boxed/PreviewSizeRetentionBugTest.kt (4)
include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/RoborazziOptions.kt (1)
  • File (225-247)
sample-android/src/test/java/com/github/takahirom/roborazzi/sample/boxed/boxedEnvironment.kt (1)
  • boxedEnvironment (9-21)
include-build/roborazzi-core/src/commonMain/kotlin/com/github/takahirom/roborazzi/RoborazziProperties.kt (1)
  • roborazziSystemPropertyOutputDirectory (6-9)
roborazzi-compose/src/main/java/com/github/takahirom/roborazzi/RoborazziComposeOptions.kt (1)
  • size (166-172)
🔇 Additional comments (1)
sample-android/src/test/java/com/github/takahirom/roborazzi/sample/boxed/PreviewSizeRetentionBugTest.kt (1)

59-67: Good fix: handle decodeFile null to avoid NPEs.

The explicit null check with a clear AssertionError makes failures actionable and prevents NPEs when decoding bitmaps.

@takahirom takahirom marked this pull request as ready for review August 14, 2025 03:24
@takahirom takahirom merged commit 2042b0d into main Aug 14, 2025
8 checks passed
@takahirom takahirom deleted the fix/preview-size-retention-bug branch August 14, 2025 03:32
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.

Incorrect preview snapshot sizes
1 participant