Composable Preview Scanner

repository·master·Indexed 43 days ago

https://github.com/sergio-sastre/composablepreviewscanner

A Compose Multiplatform friendly tool for discovering Jetpack Compose @Preview composables at build time or runtime. It facilitates automated screenshot testing by integrating with libraries such as Paparazzi, Roborazzi, Shot, Dropshots, and Android-Testify. It supports Android and Glance previews, including support for @PreviewWrapper and various preview annotations.

Tokens
12.3K
Snippets
32
Records
44
Agent score
56%

What's inside Composable Preview Scanner

  1. Overview of Composable Preview Scanner

    master

    Composable Preview Scanner is a Compose Multiplatform friendly library designed to auto-generate screenshot tests from Composable Previews (such as Android and Glance). It is library-agnostic and works with both JVM-based screenshot testing libraries (e.g., Paparazzi, Roborazzi) and Instrumentation-based libraries (e.g., Shot, Dropshots, Android-Testify).

    Key features include:

    • Support for @PreviewWrapper (since 0.9.0+).
    • Support for @PreviewParameters, @PreviewScreenSizes, @PreviewFontScales, @PreviewLightDark, and @PreviewDynamicColors.
    • Ability to scan @Previews in any source set (main, screenshotTest, androidTest).
    • Support for @Previews inside public or private classes.
    • Support for both Android and Compose Multiplatform (Desktop/JVM and common source sets).
  2. Scan previews from a file to speed up tests

    master

    To avoid the overhead of scanning previews during every test run, you can reuse previously scanned data by providing a file containing the ScanResult.

    • For JVM-based libraries (Roborazzi & Paparazzi): Use .scanFile(jsonFile: File).
    • For Instrumentation-based libraries: Use .scanFile(targetInputStream: InputStream, customPreviewsInfoInputStream: InputStream).

    To generate these files, use ScanResultDumper to dump the results to a file or assets.

    class SaveScanResultInFiles {
        @Test
        fun `task -- save scan result in file`() {
            val scanResultFileName = "scan_result.json"
    
            ScanResultDumper()
                .setTargetSourceSet(Classpath(SourceSet.ANDROID_TEST))
                .scanPackageTrees("my.package")
                // For unit tests (Roborazzi/Paparazzi)
                .dumpScanResultToFile(scanResultFileName)
                // For instrumentation tests
                .dumpScanResultToFileInAssets(
                   flavourName = "myFlavour",
                   fileName = scanResultFileName
                )
        }
    }
  3. Enable parallel execution for Paparazzi tests

    master

    Paparazzi runs parameterized tests sequentially within a single class. To achieve parallelism, this plugin can split previews into multiple test classes (shards), which Gradle can then run across multiple worker processes.

    To enable this, you must set both generatedTestClassCount in the plugin configuration and maxParallelForks in the standard Gradle Test task type.

    Note: generatedTestClassCount defaults to the value of maxParallelForks. The plugin does not automatically modify your Gradle settings; you must configure maxParallelForks explicitly.

    // In your module's build.gradle.kts
    composablePreviewPaparazzi {
        // 1. Generate 4 test classes
        generatedTestClassCount = 4 
    }
    
    tasks.withType<Test> {
        // 2. Allow Gradle to run up to 4 test classes at the same time
        maxParallelForks = 4 
    }
  4. How ComposablePreviewScanner works

    master

    The library is built on top of ClassGraph, a high-performance parallelized classpath scanner.

    Because ClassGraph scans at the bytecode level, ComposablePreviewScanner can detect annotations even if they have AnnotationRetention.BINARY or AnnotationRetention.RUNTIME. This allows it to successfully find Android, Glance, and Wear Compose @Preview annotations which often use BINARY retention.

  5. Use CommonComposablePreviewScanner for Compose Multiplatform Previews

    master

    If you are using Compose Multiplatform @Previews defined in common and want to run screenshot tests in a jvm-target module (like Android or Desktop), you can use the CommonComposablePreviewScanner via the :common module.

    Note: This approach is deprecated in favor of using Android @Preview annotations.

    // Add :common dependency
    // Maven Central
    testImplementation("io.github.sergio-sastre.ComposablePreviewScanner:common:<version>")
    
    // JitPack
    testImplementation("com.github.sergio-sastre.ComposablePreviewScanner:common:<version>")
  6. Compose Multiplatform Support

    master

    Composable Preview Scanner supports scanning and running previews across Android, Desktop, and common source sets. While it can find previews in common packages, screenshot tests must be executed on a supported platform target (Android or Desktop) using a compatible screenshot library.

    Platform Support Matrix:

    • Android: Supported by Paparazzi, Roborazzi, and instrumentation libraries (Dropshots, Android-Testify, etc.).
    • Desktop: Supported by Roborazzi (native plugin support since version 1.70.0).
    • Common: No library runs tests directly in common; use an Android or Desktop target to run tests found in common packages.

    To scan previews across both platform-specific and shared packages, use scanPackageTrees with a list of package names.

    AndroidComposablePreviewScanner()
        .scanPackageTrees(
            include = listOf(
                "your.package.android_or_desktop", // platform-specific
                "your.package.common"              // commonMain
            )
        )
        .getPreviews()
  7. Scan previews in non-main Source Sets (screenshotTest, androidTest)

    master

    To scan previews located in source sets other than main, use .setTargetSourceSet(classpath: Classpath). The library provides helper constants for common source sets:

    • screenshotTest: Classpath(SourceSet.SCREENSHOT_TEST)
    • androidTest: Classpath(SourceSet.ANDROID_TEST)

    Requirements for non-main Source Sets

    1. Ensure compiled classes exist

    You must run the compilation task for the target source set before scanning. For example, for a debug variant in a module named mymodule:

    • ScreenshotTest: :mymodule:compileDebugScreenshotTestKotlin
    • AndroidTest: :mymodule:compileDebugAndroidTestKotlin

    You can automate this in Gradle:

    tasks.withType<Test> {
       dependsOn("compileDebugScreenshotTestKotlin")
    }

    2. Ensure dependencies are available

    If your previews are in screenshotTest and not in main, ensure the ui-tooling-preview dependency is also added to your testImplementation (for Roborazzi/Paparazzi) or androidTestImplementation.

    3. Include classes in the APK (Instrumentation Tests only)

    For instrumentation-based tests, you must ensure the target source set classes are included in the installed .apk. You can add the source directory to the androidTest source set in your build.gradle:

    val includeScreenshotTests = project.hasProperty("includeSourceSetScreenshotTest")
    if (includeScreenshotTests) {
        sourceSets {
           getByName("androidTest") {
              java.srcDir("src/screenshotTest/java")
              res.srcDir("src/screenshotTest/res")
           }
        }
    }

    Then run your tests with the property: ./gradlew :tests:screenshotRecord -PincludeSourceSetScreenshotTest.

  8. Integrate with Paparazzi for JVM Screenshot Testing

    master

    You can use composable-preview-scanner to drive Paparazzi screenshot tests by scanning for @Preview annotations and mapping their properties to Paparazzi's DeviceConfig. This allows you to run parameterized tests that cover multiple device configurations, UI modes, and locales automatically.

    To implement this:

    1. Define a custom annotation to pass configuration (like maxPercentDifference) to the scanner.
    2. Use AndroidComposablePreviewScanner to find previews in your package trees.
    3. Map AndroidPreviewInfo to Paparazzi's DeviceConfig and SessionParams.
    4. Use a Parameterized test class where the @Parameterized.Parameters are provided by the scanner's results.
    5. Use AndroidPreviewScreenshotIdBuilder to generate stable, meaningful screenshot filenames.

    Executable examples are available in the Android-screenshot-testing-playground repository.

    @RunWith(Parameterized::class)
    class PreviewTestParameterTests(
        val preview: ComposablePreview<AndroidPreviewInfo>,
    ) {
    
       companion object {
          private val cachedPreviews: List<ComposablePreview<AndroidPreviewInfo>> by lazy {
             AndroidComposablePreviewScanner()
                .scanPackageTrees("your.package", "your.package2")
                .includeAnnotationInfoForAllOf(PaparazziConfig::class.java)
                .getPreviews()
          }
    
          @JvmStatic
          @Parameterized.Parameters
          fun values(): List<ComposablePreview<AndroidPreviewInfo>> = cachedPreviews
       }
    
        @get:Rule
        val paparazzi: Paparazzi = PaparazziPreviewRule.createFor(preview)
    
        @Test
        fun snapshot() {
           val screenshotId = AndroidPreviewScreenshotIdBuilder(preview).build()
           paparazzi.snapshot(name = screenshotId) { 
              // ... render preview ...
           }
        }
    }
  9. Install the Composable Preview Paparazzi Plugin

    master

    To use this plugin, first include the plugin folder in your project's settings.gradle.kts file using includeBuild. Then, apply the plugin to the specific module where you want to generate Paparazzi tests from @Preview composables. Note that you must also apply the app.cash.paparazzi plugin.

    // settings.gradle.kts
    includeBuild("paparazzi-plugin")
    
    // module/build.gradle.kts
    plugins {
        id("app.cash.paparazzi")
        id("io.github.sergio-sastre.composable-preview-scanner.paparazzi-plugin")
    }
  10. Workaround for Desktop Previews using JvmAnnotationScanner

    master

    Compose-Desktop previews use AnnotationRetention.SOURCE, making them invisible to ClassGraph. To work around this, you can define a custom annotation (without AnnotationRetention.SOURCE) and use JvmAnnotationScanner to find it.

    This requires the :jvm module from ComposablePreviewScanner 0.2.0+.

    // 1. Add :jvm dependency
    // Maven Central
    testImplementation("io.github.sergio-sastre.ComposablePreviewScanner:jvm:<version>")
    
    // JitPack
    testImplementation("com.github.sergio-sastre.ComposablePreviewScanner:jvm:<version>")
    
    // 2. Define a custom annotation (DO NOT use AnnotationRetention.SOURCE)
    package my.package.path
    @Target(AnnotationTarget.FUNCTION)
    annotation class DesktopScreenshot
    
    // 3. Annotate your composables
    @DesktopScreenshot
    @Preview
    @Composable
    fun MyDesktopComposable() { ... }
    
    // 4. Use JvmAnnotationScanner in a TestParameterValuesProvider
    class DesktopPreviewProvider : TestParameterValuesProvider() {
      override fun provideValues(context: Context?): List<ComposablePreview<JvmAnnotationInfo>> =
        JvmAnnotationScanner("my.package.path.DesktopScreenshot")
          .enableScanningLogs()
          .scanPackageTrees("previews")
          .getPreviews()
    }
  11. Install Composable Preview Scanner via JitPack

    master

    To use the library via JitPack, first add the JitPack repository to your root build.gradle file, then use the com.github prefix in your dependencies.

    • Use the :android artifact for androidx.compose.ui.tooling.preview.Preview.
    • Use the :glance artifact for androidx.glance.preview.Preview (supported since 0.7.0+ in Android target).
    // In root build.gradle
    allprojects {
       repositories {
          maven { url = uri('https://jitpack.io') }
       }
    }
    
    // In module build.gradle
    dependencies {
        // android previews (androidx.compose.ui.tooling.preview.Preview)
        // supported in jvm targets e.g. android & desktop
        testImplementation("com.github.sergio-sastre.ComposablePreviewScanner:android:<version>")
    
        // glance previews (androidx.glance.preview.Preview)
        // supported since 0.7.0+ in android target
        testImplementation("com.github.sergio-sastre.ComposablePreviewScanner:glance:<version>")
    }
  12. Add required dependencies for generated tests

    master

    Because the plugin generates test files that import specific libraries, you must add the following dependencies to your module's dependencies block to ensure the generated code compiles and runs:

    dependencies {
      testImplementation("io.github.sergio-sastre.ComposablePreviewScanner:android:0.9.3")
      testImplementation("junit:junit:4.13.2")
    }