{"id":27304424,"url":"https://github.com/sergio-sastre/composablepreviewscanner","last_synced_at":"2026-05-02T12:06:13.350Z","repository":{"id":243549799,"uuid":"812732732","full_name":"sergio-sastre/ComposablePreviewScanner","owner":"sergio-sastre","description":"A library to help auto-generate screenshot tests from Composable Previews (e.g. Android, Glance \u0026 Compose Multiplatform) with any screenshot testing library: JVM-based (i.e. Paparazzi, Roborazzi) as well as Instrumentation-based (i.e. Shot, Dropshots, Android-Testify, etc.)","archived":false,"fork":false,"pushed_at":"2026-04-28T10:33:15.000Z","size":2065,"stargazers_count":326,"open_issues_count":12,"forks_count":11,"subscribers_count":2,"default_branch":"master","last_synced_at":"2026-04-28T12:19:44.396Z","etag":null,"topics":["android","compose-multiplatform","screenshot-testing"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sergio-sastre.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":["sergio-sastre"]}},"created_at":"2024-06-09T18:12:17.000Z","updated_at":"2026-04-25T16:36:47.000Z","dependencies_parsed_at":"2024-10-24T21:44:27.603Z","dependency_job_id":"0f2ab76b-91a9-4b85-85fa-9e57b2680ef7","html_url":"https://github.com/sergio-sastre/ComposablePreviewScanner","commit_stats":null,"previous_names":["sergio-sastre/composablepreviewscanner"],"tags_count":20,"template":false,"template_full_name":null,"purl":"pkg:github/sergio-sastre/ComposablePreviewScanner","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sergio-sastre%2FComposablePreviewScanner","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sergio-sastre%2FComposablePreviewScanner/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sergio-sastre%2FComposablePreviewScanner/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sergio-sastre%2FComposablePreviewScanner/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sergio-sastre","download_url":"https://codeload.github.com/sergio-sastre/ComposablePreviewScanner/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sergio-sastre%2FComposablePreviewScanner/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32533373,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-02T11:28:32.350Z","status":"ssl_error","status_checked_at":"2026-05-02T11:27:30.140Z","response_time":132,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","compose-multiplatform","screenshot-testing"],"created_at":"2025-04-12T03:25:06.140Z","updated_at":"2026-05-02T12:06:13.342Z","avatar_url":"https://github.com/sergio-sastre.png","language":"Kotlin","funding_links":["https://github.com/sponsors/sergio-sastre"],"categories":[],"sub_categories":[],"readme":"[![](https://jitpack.io/v/sergio-sastre/ComposablePreviewScanner.svg)](https://jitpack.io/#sergio-sastre/ComposablePreviewScanner) ![](https://jitpack.io/v/sergio-sastre/ComposablePreviewScanner/month.svg)\u003c/br\u003e\n[![](https://img.shields.io/maven-central/v/io.github.sergio-sastre.ComposablePreviewScanner/android)](https://central.sonatype.com/artifact/io.github.sergio-sastre.ComposablePreviewScanner/android) ![](https://img.shields.io/badge/downloads-unknown-yell)\u003c/br\u003e\n\n\u003ca href=\"https://androidweekly.net/issues/issue-628\"\u003e\n\u003cimg src=\"https://androidweekly.net/issues/issue-628/badge\"\u003e\n\u003c/a\u003e\u003c/br\u003e\n\u003ca href=\"https://jetc.dev/issues/221.html\"\u003e\u003cimg src=\"https://img.shields.io/badge/As_Seen_In-jetc.dev_Newsletter_Issue_%23221-blue?logo=Jetpack+Compose\u0026amp;logoColor=white\" alt=\"As Seen In - jetc.dev Newsletter Issue #221\"\u003e\u003c/a\u003e\n\n# \u003cp align=\"center\"\u003eComposable Preview Scanner\u003c/p\u003e\n\n\n\u003cp align=\"center\"\u003e\n\u003cimg width=\"400\" src=\"https://github.com/sergio-sastre/ComposablePreviewScanner/assets/6097181/63d9676d-22c4-4bd1-8680-3fcf1a72e001\"\u003e\n\u003c/p\u003e\n\nA library to help auto-generate screenshot tests from Composable Previews (e.g. **Android**, **Glance** \u0026 **Compose Multiplatform**) with any screenshot testing library:\nJVM-based (i.e. Paparazzi, Roborazzi) as well as Instrumentation-based (i.e. Shot, Dropshots, Android-Testify, etc.)\n\n![composable_preview_scanner_overview.png](composable_preview_scanner_overview.png)\n\u003e [!NOTE]\n\u003e 1. Support for Wear OS Tile `@Previews` is under evaluation\u003c/br\u003e\n\u003e 2. `common` and `desktop` previews are deprecated in favour of the `android` preview (`androidx.compose.ui.tooling.preview.Preview`), which can be used\n\u003e in `common` and JVM-based source sets like `desktop` since Compose Multiplatform 1.10.0-beta-02. This library has also deprecated its support. More info in [README_DEPRECATED.md](README_DEPRECATED.md)\u003c/br\u003e\n\n\n#### Provide anonymous feedback\nAlready using ComposablePreviewScanner?\u003c/br\u003e\nI'd love to hear your thoughts!\u003c/br\u003e\nHelp shape its future by taking [this quick survey](https://forms.gle/jcvggBxv14CLqjFo6)\n\n# Comparison with other solutions\n|                                                      | Composable Preview Scanner                                             | Showkase                                                        | Compose Preview Screenshot Testing            |\n|------------------------------------------------------|------------------------------------------------------------------------|-----------------------------------------------------------------|-----------------------------------------------|\n| Independent of AGP version                           | ✅                                                                      | ✅                                                               | ❌                                             |\n| Library-agnostic solution                            | ✅                                                                      | ✅                                                               | ❌\u003csup\u003e1\u003c/sup\u003e                                 |\n| Scans previews in different sources sets\u003csup\u003e2\u003c/sup\u003e | ✅ main\u003cbr/\u003e✅ screenshotTest\u003cbr/\u003e✅ androidTest                          | ✅ main\u003cbr/\u003e❌ screenshotTest\u003cbr/\u003e❌ androidTest                   | ❌ main\u003cbr/\u003e✅ screenshotTest\u003cbr/\u003e❌ androidTest |\n| Preview Infos available                              | ✅                                                                      | ❌\u003csup\u003e3\u003c/sup\u003e                                                   | ✅                                             |\n| Specific Config (e.g. for Libs) available            | ✅\u003csup\u003e4\u003c/sup\u003e                                                          | ❌                                                               | ⚠️\u003csup\u003e5\u003c/sup\u003e                                |\n| Supported Preview types                              | ✅ Android\u003c/br\u003e ✅ Glance\u003c/br\u003e ✅ Compose Multiplatform \u003csup\u003e6\u003c/sup\u003e\u003c/br\u003e | ✅ Android\u003c/br\u003e ❌Glance\u003c/br\u003e ❌Compose Multiplatform \u003csup\u003e7\u003c/sup\u003e | ✅ Android\u003c/br\u003e ❌ Glance\u003c/br\u003e ❌ Compose Multiplatform               |\n\n\u003csup\u003e1\u003c/sup\u003e Compose Preview Screenshot Testing is a standalone solution based on LayoutLib, whereas ComposablePreviewScanner and Showkase provide Composables' infos so you can run screenshot tests with your favourite screenshot testing library.\u003c/br\u003e\u003c/br\u003e\n\u003csup\u003e2\u003c/sup\u003e From version 0.5.0, ComposablePreviewScanner can scan previews in any source set. Compose Preview Screenshot Testing requires to put the previews in a brand-new \"screenshotTest\" source.\u003c/br\u003e\u003c/br\u003e\n\u003csup\u003e3\u003c/sup\u003e Showkase components only hold information about the Composable, but not about the Preview Info (i.e. ApiLevel, Locale, UiMode, FontScale...).\u003c/br\u003e\u003c/br\u003e\n\u003csup\u003e4\u003c/sup\u003e ComposablePreviewScanner supports adding extra lib-config (e.g. Paparazzi's Rendering Mode or Roborazzi's compare options) in the form of annotations that are additionally added to the preview. You can check how in the examples below in [Jvm Screenshot Tests](#jvm-screenshot-tests) and [Instrumentation Screenshot Tests](#instrumentation-screenshot-tests) respectively.\u003c/br\u003e\u003c/br\u003e\n\u003csup\u003e5\u003c/sup\u003e Compose Preview Screenshot Testing supports *only general tolerance* via gradle plugin from version [0.0.1-alpha06](https://developer.android.com/studio/preview/compose-screenshot-testing#001-alpha06)\u003c/br\u003e\u003c/br\u003e\n\u003csup\u003e6\u003c/sup\u003e Desktop Previews (which are deprecated since Compose Multiplatform 1.10.0-beta02) are supported with a workaround. See [README_DEPRECATED.md](README_DEPRECATED.md)\u003c/br\u003e\u003c/br\u003e\n\u003csup\u003e7\u003c/sup\u003e [Showkase: Compose Multiplatform Support](https://github.com/airbnb/Showkase/issues/364)\n\u003c/br\u003e\u003c/br\u003e\u003c/br\u003e\nComposablePreviewScanner also works with:\n- **NEW*** `@PreviewWrapper` (since 0.9.0+) automatically. No changes required in the Screenshot Testing library using ComposablePreviewScanner.\n- `@PreviewParameters` (for Compose Multiplatform since 0.6.0+)\n- Multi-Previews, including  `@PreviewScreenSizes`, `@PreviewFontScales`, `@PreviewLightDark`, and `@PreviewDynamicColors` as well as custom multi-previews.\n- private `@Previews` (from version 0.1.3+)\n- `@Previews` inside public classes\u003csup\u003e1\u003c/sup\u003e (from version 0.3.0+), not nested classes though\n- `@Previews` located in any source set, like \"main\", \"screenshotTest\" and \"androidTest\" (from version 0.5.0+)\n- `@Previews` with default-parameters (from version 0.5.1+)\n\n\u003csup\u003e1\u003c/sup\u003e The [Compose Preview Screenshot Testing tool](https://developer.android.com/studio/preview/compose-screenshot-testing) from Google requires you to put your `@Previews` inside a class.\n\n# How to set up\n\u003e [!WARNING]  \n\u003e Beware the prefixes:\u003c/br\u003e\n\u003e *Maven Central* -\u003e **io.github**\u003c/br\u003e\n\u003e *JitPack* -\u003e **com.github**\u003c/br\u003e\n\n## Maven Central (since 0.3.2)\n```kotlin\ndependencies {\n    // android previews (androidx.compose.ui.tooling.preview.Preview)\n    // supported in jvm targets e.g. android \u0026 desktop\n    testImplementation(\"io.github.sergio-sastre.ComposablePreviewScanner:android:\u003cversion\u003e\")\n\n    // glance previews (androidx.glance.preview.Preview)\n    // supported since 0.7.0+ in android target\n    testImplementation(\"io.github.sergio-sastre.ComposablePreviewScanner:glance:\u003cversion\u003e\")\n    \n    // common previews (org.jetbrains.compose.ui.tooling.preview.Preview) (deprecated)\n    // supported in jvm targets e.g. android \u0026 desktop\n    testImplementation(\"io.github.sergio-sastre.ComposablePreviewScanner:common:\u003cversion\u003e\")\n\n    // desktop previews (androidx.compose.desktop.ui.tooling.preview.Preview) via custom annotation (deprecated)\n    // supported in jvm targets e.g. android \u0026 desktop\n    testImplementation(\"io.github.sergio-sastre.ComposablePreviewScanner:jvm:\u003cversion\u003e\")\n}\n```\n\n## JitPack\nAdd JitPack to your root build.gradle file:\n```kotlin\nallprojects {\n   repositories {\n      maven { url = uri('https://jitpack.io') }\n   }\n}\n```\n\n```kotlin\ndependencies {\n    // android previews (androidx.compose.ui.tooling.preview.Preview)\n    // supported in jvm targets e.g. android \u0026 desktop\n    testImplementation(\"com.github.sergio-sastre.ComposablePreviewScanner:android:\u003cversion\u003e\")\n\n    // glance previews (androidx.glance.preview.Preview)\n    // supported since 0.7.0+ in android target\n    testImplementation(\"com.github.sergio-sastre.ComposablePreviewScanner:glance:\u003cversion\u003e\")\n\n    // common previews (org.jetbrains.compose.ui.tooling.preview.Preview) (deprecated)\n    // supported in jvm targets e.g. android \u0026 desktop\n    testImplementation(\"com.github.sergio-sastre.ComposablePreviewScanner:common:\u003cversion\u003e\")\n\n    // desktop previews (androidx.compose.desktop.ui.tooling.preview.Preview) via custom annotation (deprecated)\n    // supported in jvm targets e.g. android \u0026 desktop\n    testImplementation(\"com.github.sergio-sastre.ComposablePreviewScanner:jvm:\u003cversion\u003e\")\n}\n```\n\n# How to use\n### Examples with Screenshot Testing Libraries (Android target)\n1. [JVM Screenshot Tests](#jvm-screenshot-tests)\u003c/br\u003e\n   1.1  [Paparazzi](#paparazzi)\u003c/br\u003e\n   1.2  [Roborazzi](#roborazzi)\u003c/br\u003e\n2. [Instrumentation Screenshot Tests](#instrumentation-screenshot-tests)\n\nIf you encounter any issues when executing the screenshot tests, take a look at the [Troubleshooting](#troubleshooting) section.\n\n\u003e [!NOTE]\n\u003e [Roborazzi](https://github.com/takahirom/roborazzi) has integrated ComposablePreviewScanner in its plugin since [version 1.22](https://github.com/takahirom/roborazzi/releases/tag/1.22.0)\n\n### Related\n1. [Glance Previews Support](#glance-previews-support)\n2. [Compose Multiplatform Support](#compose-multiplatform-support)\n\n## API   \n`AndroidComposablePreviewScanner`, `GlanceComposablePreviewScanner`, `CommonComposablePreviewScanner` (deprecated), and `JvmAnnotationScanner` (deprecated) have the same API.\nThe API is pretty simple:\n\n```kotlin\nAndroidComposablePreviewScanner()\n    // Optional to log scanning info like scanning time or amount of previews found\n    .enableScanningLogs()\n    // Optional to scan previews in compiled classes of other source sets, like \"screenshotTest\" or \"androidTest\"\n    // If omitted, it scans previews in 'main' at build time\n    .setTargetSourceSet(\n       Classpath(SourceSet.SCREENSHOT_TEST) // scan previews under \"screenshotTest\"\n    )\n    // Required: define where to scan for Previews.\n    // See 'Scanning Source Options (packages, files, inputStreams)'\n    .scanPackageTrees(\n        include = listOf(\"your.package\", \"your.package2\"),\n        exclude = listOf(\"your.package.subpackage1\", \"your.package2.subpackage1\")\n    )\n    // Optional to filter out scanned previews with any of the given annotations\n    // Warning: this and its 'include' counterpart are mutually exclusive by API design\n    .excludeIfAnnotatedWithAnyOf(\n        ExcludeForScreenshot::class.java, \n        ExcludeForScreenshot2::class.java\n    )\n    // Optional to filter in only scanned previews with any of the given annotations\n    // Warning: this and its 'exclude' counterpart are mutually exclusive by API design\n    .includeIfAnnotatedWithAnyOf(\n        IncludeForScreenshot::class.java,\n        IncludeForScreenshot2::class.java\n    )\n    // Optional to include configuration info of the screenshot testing library in use\n    // See 'How to use -\u003e Libraries' above for further info\n    .includeAnnotationInfoForAllOf(\n        ScreenshotConfig::class.java,\n        ScreenshotConfig2::class.java\n    )\n    // Optional to also provide private Previews\n    .includePrivatePreviews()\n    // Optional to filter by any previewInfo: name, group, apiLevel, locale, uiMode, fontScale...\n    .filterPreviews {\n        previewInfo -\u003e  previewInfo.apiLevel == 30 \n    }\n    // ---\n    .getPreviews()\n```\n\n## Scanning\n\n### Scanning Source Sets (screenshotTest, androidTest, main)\nBy default, ComposablePreviewScanner scans `@Preview`s in the `main` Source Set at build time.\nHowever, one can scan previews in other Source Sets different from `main` by using `.setTargetSourceSet(classpath:Classpath)`,\nwhere `classpath` is the local path to the compiled classes of that Source Set.\u003c/br\u003e\nComposablePreviewScanner provides some default values to facilitate this:\n```kotlin\n// Previews under \"screenshotTest\"\nClasspath(SourceSet.SCREENSHOT_TEST)\n\n// Previews under \"androidTest\"\nClasspath(SourceSet.ANDROID_TEST)\n```\n\n#### Ensure compiled classes exist\nYou have to make sure the corresponding compiled classes for that Source Set exist and are up to date.\nThe simplest way is to execute the corresponding compile task before running your tests or dumping the scan result to a file, namely `\u003cmodule\u003e:compile\u003cVariant\u003e\u003cSourceset\u003eKotlin`, for instance\n1. ScreenshotTest \n   - Debug -\u003e   `:mymodule:compileDebugScreenshotTestKotlin`\n   - Release -\u003e `:mymodule:compileReleaseScreenshotTestKotlin`\n2. AndroidTest\n   - Debug -\u003e   `:mymodule:compileDebugAndroidTestKotlin`\n   - Release -\u003e `:mymodule:compileReleaseAndroidTestKotlin`\n\nTo ensure you don't forget it, you can configure gradle accordingly, so those tasks are always executed previously.\nFor instance, if you're using Roborazzi or Paparazzi and want to scan previews in the `screenshotTest` Source Set for the `debug` variant\n```kotlin\n// Create Compiled Classes always before unit tests, including Roborazzi/Paparazzi tests\ntasks.withType\u003cTest\u003e {\n   dependsOn(\"compileDebugScreenshotTestKotlin\")\n}\n```\n\n#### Ensure Source Set dependencies available in tests\nLast but not least, make sure all the code inside the previews of the target Source Set is also\navailable in `test` (for Roborazzi and Paparazzi) or `android test` (for any instrumentation-based library).\u003c/br\u003e\nSo, let's say that you only have `@Preview`s in `screenshotTest`, and not in `main`. Therefore you've only added that dependency to `screenshotTest`:\n```kotlin\nscreenshotTestImplementation(\"androidx.compose.ui:ui-tooling-preview:\u003cversion\u003e\")\n```\nIf you're running Roborazzi or Paparazzi screenshot tests, you'll need to add that dependency to 'test' build Type\n```kotlin\nscreenshotTestImplementation(\"androidx.compose.ui:ui-tooling-preview:\u003cversion\u003e\")\ntestImplementation(\"androidx.compose.ui:ui-tooling-preview:\u003cversion\u003e\")\n```\n\n\u003e [!WARNING]\n\u003e For instrumentation tests and Source Sets different from `main` or `androidTest`, like `screenshotTest`, you'll also need to ensure that the classes of those source sets\n\u003e are also included in the .apk installed on the device or emulator, or it will throw ClassNotFoundErrors.\n\u003e The easiest way to achieve this is to add the following code snippet to your gradle file:\n\u003e ```kotlin\n\u003e val includeScreenshotTests = project.hasProperty(\"includeSourceSetScreenshotTest\")\n\u003e if (includeScreenshotTests) {\n\u003e     sourceSets {\n\u003e        getByName(\"androidTest\") {\n\u003e           java.srcDir(\"src/screenshotTest/java\") //or kotlin\n\u003e           res.srcDir(\"src/screenshotTest/res\")\n\u003e        }\n\u003e     }\n\u003e}\n\u003e ```\n\u003e And pass that gradle property when executing the screenshot tests via command-line, e.g.:\n\u003e `./gradlew :tests:screenshotRecord -PincludeSourceSetScreenshotTest`\n\u003e \n\u003e This is NOT necessary for JVM-based screenshot testing libraries like Roborazzi and Paparazzi\n\n### Scanning Source Options (packages, files, inputStreams)\nApart from `scanPackageTrees(include:List\u003cString\u003e, exclude:List\u003cString\u003e)`, there are 2 more options to scan previews:\n1. All Packages: `scanAllPackages()`. This might require a huge amount of memory since it would scan not only in a set of packages, but in all packages used in your app/module (i.e. also in its transitive dependencies). This is in 99% of the cases unnecessary, and scanning the main package trees of your module should be sufficient. \n2. From a file containing the ScanResult. This speeds up your screenshot tests, since it avoids the time-consuming process of scanning each time by reusing previously scanned data: \u003c/br\u003e\n   2.1. `scanFile(jsonFile: File)`. Use this for JVM-based screenshot testing libraries (i.e. Roborazzi \u0026 Paparazzi).\u003c/br\u003e\n   2.2. `scanFile(targetInputStream: InputStream, customPreviewsInfoInputStream: InputStream)`. This is meant for Instrumentation-based screenshot testing libraries.\u003c/br\u003e\u003c/br\u003e\nYou can create a unit test for that:\n```kotlin\nclass SaveScanResultInFiles {\n    @Test\n    fun `task -- save scan result in file`() {\n        val scanResultFileName = \"scan_result.json\"\n\n        ScanResultDumper()\n            .setTargetSourceSet(Classpath(SourceSet.ANDROID_TEST)) // optional\n            .scanPackageTrees(\"my.package\")\n            // for unit tests\n            .dumpScanResultToFile(scanResultFileName)\n            // for instrumentation tests\n            .dumpScanResultToFileInAssets(\n               flavourName = \"myFlavour\",\n               fileName = scanResultFileName\n            )\n    }\n}\n```\n\n## JVM Screenshot Tests\n\n### Paparazzi\nYou can find [executable examples here](https://github.com/sergio-sastre/Android-screenshot-testing-playground/tree/master/lazycolumnscreen-previews/paparazzi/src)\n\n\u003e [!NOTE]\n\u003e You can also find a paparazzi-plugin in this repo that generates all this boilerplate code for you!\n\u003e Take a look at [its README.md](paparazzi-plugin/README.md)\n\nLet's say we want to enable some custom Paparazzi config for some Previews, for instance a maxPercentDifference value\n\n1. Define your own annotation for the Lib config.\n```kotlin\nannotation class PaparazziConfig(val maxPercentDifference: Double)\n```\n\n2. Annotate the corresponding Previews accordingly (you do not need to annotate all):\n```kotlin\n@PaparazziConfig(maxPercentDifference = 0.1)\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nfun MyComposable(){\n   // Composable code here\n}\n```\n3. Create custom record and verify `SnapshotHandler`s for better control over the screenshot file names.\u003c/br\u003e By default, Paparazzi prefixes all generated screenshot files using its internal `SnapshotHandler`. While this works for most cases, it causes issues in parameterized tests: the default `SnapshotHandler` includes the test [index] in the filename. If the order of your previews changes, filenames no longer match, which can break snapshot verification.\u003c/br\u003e\u003c/br\u003e\n   To solve this, we can create custom `SnapshotHandler`s that use a fixed prefix, like \"Paparazzi_Preview_Test\", instead of a test-index-dependent name. This ensures filenames remain stable regardless of test order.\n```kotlin\n// Define the prefix = \u003cpackageName\u003e_\u003cclassName\u003e_\u003cmethodName\u003e\nprivate val paparazziTestName =\n   TestName(packageName = \"Paparazzi\", className = \"Preview\", methodName = \"Test\")\n\nprivate class PreviewSnapshotVerifier(\n   maxPercentDifference: Double\n): SnapshotHandler {\n   private val snapshotHandler = SnapshotVerifier(\n      maxPercentDifference = maxPercentDifference\n   )\n   override fun newFrameHandler(\n      snapshot: Snapshot,\n      frameCount: Int,\n      fps: Int\n   ): SnapshotHandler.FrameHandler {\n      val newSnapshot = Snapshot(\n         name = snapshot.name,\n         testName = paparazziTestName,\n         timestamp = snapshot.timestamp,\n         tags = snapshot.tags,\n         file = snapshot.file,\n      )\n      return snapshotHandler.newFrameHandler(\n         snapshot = newSnapshot,\n         frameCount = frameCount,\n         fps = fps\n      )\n   }\n\n   override fun close() {\n      snapshotHandler.close()\n   }\n}\n\nprivate class PreviewHtmlReportWriter: SnapshotHandler {\n   private val snapshotHandler = HtmlReportWriter()\n   override fun newFrameHandler(\n      snapshot: Snapshot,\n      frameCount: Int,\n      fps: Int\n   ): SnapshotHandler.FrameHandler {\n      val newSnapshot = Snapshot(\n         name = snapshot.name,\n         testName = paparazziTestName,\n         timestamp = snapshot.timestamp,\n         tags = snapshot.tags,\n         file = snapshot.file,\n      )\n      return snapshotHandler.newFrameHandler(\n         snapshot = newSnapshot,\n         frameCount = frameCount,\n         fps = fps\n      )\n   }\n\n   override fun close() {\n      snapshotHandler.close()\n   }\n}\n```\n\nIn the next step, we’ll show how to pass these custom SnapshotHandlers to the Paparazzi TestRule to take full control of screenshot filenames.\n\n4. Map the PreviewInfo and PaparazziConfig values.\n```kotlin\nclass Dimensions(\n   val screenWidthInPx: Int,\n   val screenHeightInPx: Int\n)\n\nobject ScreenDimensions {\n   fun dimensions(\n      parsedDevice: Device,\n      widthDp: Int,\n      heightDp: Int\n   ): Dimensions {\n      val conversionFactor = parsedDevice.densityDpi / 160f\n      val previewWidthInPx = ceil(widthDp * conversionFactor).toInt()\n      val previewHeightInPx = ceil(heightDp * conversionFactor).toInt()\n      return Dimensions(\n         screenHeightInPx = when (heightDp \u003e 0) {\n            true -\u003e previewHeightInPx\n            false -\u003e parsedDevice.dimensions.height.toInt()\n         },\n         screenWidthInPx = when (widthDp \u003e 0) {\n            true -\u003e previewWidthInPx\n            false -\u003e parsedDevice.dimensions.width.toInt()\n         }\n      )\n   }\n}\n\nobject DeviceConfigBuilder {\n   fun build(preview: AndroidPreviewInfo): DeviceConfig {\n      val parsedDevice =\n         DevicePreviewInfoParser.parse(preview.device)?.inPx() ?: return DeviceConfig()\n\n      val dimensions = ScreenDimensions.dimensions(\n         parsedDevice = parsedDevice,\n         widthDp = preview.widthDp,\n         heightDp = preview.heightDp\n      )\n\n      return DeviceConfig(\n         screenHeight = dimensions.screenHeightInPx,\n         screenWidth = dimensions.screenWidthInPx,\n         density = Density(parsedDevice.densityDpi),\n         xdpi = parsedDevice.densityDpi, // not 100% precise\n         ydpi = parsedDevice.densityDpi, // not 100% precise\n         size = ScreenSize.valueOf(parsedDevice.screenSize.name),\n         ratio = ScreenRatio.valueOf(parsedDevice.screenRatio.name),\n         screenRound = ScreenRound.valueOf(parsedDevice.shape.name),\n         orientation = ScreenOrientation.valueOf(parsedDevice.orientation.name),\n         locale = preview.locale.ifBlank { \"en\" },\n          fontScale = preview.fontScale,\n         nightMode = when (preview.uiMode and UI_MODE_NIGHT_MASK == UI_MODE_NIGHT_YES) {\n            true -\u003e NightMode.NIGHT\n            false -\u003e NightMode.NOTNIGHT\n         }\n      )\n   }\n}\n\nobject PaparazziPreviewRule {\n    const val UNDEFINED_API_LEVEL = -1\n    const val MAX_API_LEVEL = 36\n    \n    fun createFor(preview: ComposablePreview\u003cAndroidPreviewInfo\u003e): Paparazzi {\n       val previewInfo = preview.previewInfo\n       val previewApiLevel = when(previewInfo.apiLevel == UNDEFINED_API_LEVEL) {\n          true -\u003e MAX_API_LEVEL\n          false -\u003e previewInfo.apiLevel\n       }\n       // other library configurations...\n       val tolerance = preview.getAnnotation\u003cPaparazziConfig\u003e()?.maxPercentDifference ?: 0.0\n       return Paparazzi(\n            environment = detectEnvironment().copy(compileSdkVersion = previewApiLevel),\n            deviceConfig = DeviceConfigBuilder.build(previewInfo),\n            supportsRtl = true,\n            showSystemUi = previewInfo.showSystemUi,\n            renderingMode = when {\n                previewInfo.showSystemUi -\u003e SessionParams.RenderingMode.NORMAL\n                previewInfo.widthDp \u003e 0 \u0026\u0026 previewInfo.heightDp \u003e 0 -\u003e SessionParams.RenderingMode.FULL_EXPAND\n                else -\u003e SessionParams.RenderingMode.SHRINK\n            },\n            snapshotHandler = when(System.getProperty(\"paparazzi.test.verify\")?.toBoolean() == true) {\n               true -\u003e PreviewSnapshotVerifier(tolerance)\n               false -\u003e PreviewHtmlReportWriter()\n            },\n            maxPercentDifference = tolerance\n        )\n    }\n}\n\n/**\n * A composable function that wraps content inside a Box with a specified size\n * This is used to simulate what previews render when showSystemUi is true:\n * - The Preview takes up the entire screen\n * - The Composable still keeps its original size,\n * - Background color of the Device is white,\n *   but the @Composable background color is the one defined in the Preview\n */\n@Composable\nfun SystemUiSize(\n   widthInDp: Int,\n   heightInDp: Int,\n   content: @Composable () -\u003e Unit\n) {\n   Box(Modifier\n      .size(\n         width = widthInDp.dp,\n         height = heightInDp.dp\n      )\n      .background(Color.White)\n   ) {\n      content()\n   }\n}\n\n// Additional to support @Preview's 'showBackground' and 'backgroundColor' properties\n@Composable\nfun PreviewBackground(\n    showBackground: Boolean,\n    backgroundColor: Long,\n    content: @Composable () -\u003e Unit\n) {\n    when (showBackground) {\n        false -\u003e content()\n        true -\u003e {\n            val color = when (backgroundColor != 0L) {\n                true -\u003e Color(backgroundColor)\n                false -\u003e Color.White\n            }\n            Box(Modifier.background(color)) {\n                content()\n            }\n        }\n    }\n}\n```\n\n5. Create the corresponding Parameterized Test:\n```kotlin\n@RunWith(Parameterized::class)\nclass PreviewTestParameterTests(\n    val preview: ComposablePreview\u003cAndroidPreviewInfo\u003e,\n) {\n\n   companion object {\n      // Optimization: This avoids scanning for every test\n      private val cachedPreviews: List\u003cComposablePreview\u003cAndroidPreviewInfo\u003e\u003e by lazy {\n         AndroidComposablePreviewScanner()\n            .scanPackageTrees(\"your.package\", \"your.package2\")\n            .includeAnnotationInfoForAllOf(PaparazziConfig::class.java)\n            .getPreviews()\n      }\n\n      @JvmStatic\n      @Parameterized.Parameters\n      fun values(): List\u003cComposablePreview\u003cAndroidPreviewInfo\u003e\u003e = cachedPreviews\n   }\n\n    @get:Rule\n    val paparazzi: Paparazzi = PaparazziPreviewRule.createFor(preview)\n\n    @Test\n    fun snapshot() {\n        val screenshotId = AndroidPreviewScreenshotIdBuilder(preview).build()\n       \n        paparazzi.snapshot(name = screenshotId) {\n            val previewInfo = preview.previewInfo\n            when (previewInfo.showSystemUi) {\n                false -\u003e PreviewBackground(\n                    showBackground = previewInfo.showBackground,\n                    backgroundColor = previewInfo.backgroundColor\n                ) {\n                    preview()\n                }\n\n                true -\u003e {\n                    val parsedDevice = (DevicePreviewInfoParser.parse(previewInfo.device) ?: DEFAULT).inDp()\n                    SystemUiSize(\n                        widthInDp = parsedDevice.dimensions.width.toInt(),\n                        heightInDp = parsedDevice.dimensions.height.toInt()\n                    ) {\n                        PreviewBackground(\n                            showBackground = true,\n                            backgroundColor = previewInfo.backgroundColor,\n                        ) {\n                            preview()\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n```\n\n6. Run these Paparazzi tests together with the existing ones by executing the corresponding command e.g. `./gradlew yourModule:recordPaparazziDebug`\n\n### Roborazzi\nYou can find [executable examples here](https://github.com/sergio-sastre/Android-screenshot-testing-playground/tree/master/lazycolumnscreen-previews/roborazzi/src)\n\nLet's say we want to enable some custom Roborazzi Config for some Previews, for instance a maxPercentDifferent value\n\n1. Define your own annotation for the Lib Config.\n```kotlin\nannotation class RoborazziConfig(val comparisonThreshold: Double)\n```\n\n2. Annotate the corresponding Previews accordingly:\n```kotlin\n@RoborazziConfig(comparisonThreshold = 0.1)\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nfun MyComposable(){\n    // Composable code here\n}\n```\n\n3. Map the PreviewInfo and RoborazziConfig values. For instance, you can use a custom class for that.\n```kotlin\nobject RoborazziOptionsMapper {\n    fun createFor(preview: ComposablePreview\u003cAndroidPreviewInfo\u003e): RoborazziOptions =\n       preview.getAnnotation\u003cRoborazziConfig\u003e()?.let { config -\u003e\n          RoborazziOptions(\n             compareOptions = CompareOptions(resultValidator = ThresholdValidator(config.comparisonThreshold))\n          )\n       } ?: RoborazziOptions()\n}\n\nobject RoborazziComposeOptionsMapper {\n    @OptIn(ExperimentalRoborazziApi::class)\n    fun createFor(preview: ComposablePreview\u003cAndroidPreviewInfo\u003e): RoborazziComposeOptions =\n        RoborazziComposeOptions {\n            val previewInfo = preview.previewInfo\n            previewDevice(previewInfo.device.ifBlank { Devices.NEXUS_5 } )\n            size(\n                widthDp = previewInfo.widthDp,\n                heightDp = previewInfo.heightDp\n            )\n            background(\n                showBackground = previewInfo.showBackground,\n                backgroundColor = previewInfo.backgroundColor\n            )\n            locale(previewInfo.locale)\n            uiMode(previewInfo.uiMode)\n            fontScale(previewInfo.fontScale)\n        }\n}\n```\nCheck the following link for a full list of [Robolectric device qualifiers](https://robolectric.org/device-configuration/) and this blog post on how to [set the cumulative Qualifiers dynamically](https://sergiosastre.hashnode.dev/efficient-testing-with-robolectric-roborazzi-across-many-ui-states-devices-and-configurations)\n\n4. Create the corresponding Parameterized Test:\n```kotlin\n@RunWith(ParameterizedRobolectricTestRunner::class)\nclass PreviewParameterizedTests(\n    private val preview: ComposablePreview\u003cAndroidPreviewInfo\u003e,\n) {\n\n    companion object {\n        // Optimization: This avoids scanning for every test\n        private val cachedPreviews: List\u003cComposablePreview\u003cAndroidPreviewInfo\u003e\u003e by lazy {\n            AndroidComposablePreviewScanner()\n                .scanPackageTrees(\"your.package\", \"your.package2\")\n                .filterPreviews { previewParams -\u003e previewParams.apiLevel == 30 }\n                .includeAnnotationInfoForAllOf(RoborazziConfig::class.java)\n                .getPreviews()\n        }\n\n        @JvmStatic\n        @ParameterizedRobolectricTestRunner.Parameters\n        fun values(): List\u003cComposablePreview\u003cAndroidPreviewInfo\u003e\u003e = cachedPreviews\n    }\n\n    // Recommended for more meaningful screenshot file names. See #Advanced Usage\n    fun screenshotNameFor(preview: ComposablePreview\u003cAndroidPreviewInfo\u003e): String =\n        \"$DEFAULT_ROBORAZZI_OUTPUT_DIR_PATH/${AndroidPreviewScreenshotIdBuilder(preview).build()}.png\"\n\n    @GraphicsMode(GraphicsMode.Mode.NATIVE)\n    @Config(sdk = [30]) // same as the previews we've filtered\n    @Test\n    fun snapshot() {\n        captureRoboImage(\n           filePath = screenshotNameFor(preview),\n           roborazziOptions = RoborazziOptionsMapper.createFor(preview),\n           roborazziComposeOptions = RoborazziComposeOptionsMapper.createFor(preview)\n        ) {\n            preview()\n        }\n    }\n}\n```\n\n5. Run these Roborazzi tests together with the existing ones by executing the corresponding command e.g. `./gradlew yourModule:recordRoborazziDebug`\n\n## Instrumentation Screenshot Tests\nYou can find executable examples that use ComposablePreviewScanner with the different instrumentation-based libraries in the corresponding links below:\n- [Dropshots](https://github.com/sergio-sastre/Android-screenshot-testing-playground/tree/master/recyclerviewscreen-previews/dropshots)\n- [Shot](https://github.com/sergio-sastre/Android-screenshot-testing-playground/tree/master/recyclerviewscreen-previews/shot)\n- [Android-Testify](https://github.com/sergio-sastre/Android-screenshot-testing-playground/tree/master/recyclerviewscreen-previews/android-testify)\u003c/br\u003e\n\nAndroid does not use the standard Java bytecode format and does not actually even have a runtime classpath.\nMoreover, the \"build\" folders, where the compiled classes are located, are not accessible from instrumentation tests.\nTherefore, the current way to support instrumentation tests, is by previously dumping the relevant classes into a file and moving it into a folder that can be accessed while running instrumentation tests.\n1. run the scan in a unit test \u0026 save it in a file accessible by instrumentation tests e.g. in assets\n```kotlin\nclass SaveScanResultInAssets {\n    @Test\n    fun `task -- save scan result in assets`() {\n        val scanResultFileName = \"scan_result.json\"\n\n        ScanResultDumper()\n            .scanPackageTrees(\"my.package\")\n            .dumpScanResultToFileInAssets(\n                fileName = scanResultFileName\n            )\n\n        assert(\n           assetsFilePath(\n               fileName = scanResultFileName\n           ).exists())\n    }\n}\n```\nEnsure that the .json with the scan result is up-to-date before executing the instrumentation screenshot tests. For instance, execute that test always before your instrumentation screenshot tests.\nIdeally, this scanning could be done via a Gradle Plugin in the future instead of by running it in a unit test.\n\n2. Now proceed to prepare your Composable Preview Tests with, for instance, Dropshots.\nLet's say we want to enable some custom Dropshots Config for some Previews, for instance a maxPercentDifferent value.\n   - Define your own annotation\n      ```kotlin\n      annotation class DropshotsConfig(val comparisonThreshold: Double)\n      ```\n   - Annotate the corresponding Previews accordingly:\n   ```kotlin\n   @DropshotsConfig(comparisonThreshold = 0.15)\n   @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n   @Composable\n   fun MyComposable() {\n      // Composable code here\n   }\n   ```\n   - Map the PreviewInfo and DropshotsConfig values. For instance, you can use a custom class for that. To map the Preview Info values, I recommend to use the ActivityScenarioForComposableRule of [AndroidUiTestingUtils](https://github.com/sergio-sastre/AndroidUiTestingUtils)\n   ```kotlin\n   object DropshotsPreviewRule {\n      fun createFor(preview: ComposablePreview\u003cAndroidPreviewInfo\u003e): Dropshots =\n         preview.getAnnotation\u003cDropshotsConfig\u003e()?.let { config -\u003e\n            Dropshots(\n               resultValidator = ThresholdValidator(config.comparisonThreshold))\n            )\n         } ?: Dropshots()\n   }\n\n   object ActivityScenarioForComposablePreviewRule {\n       fun createFor(preview: ComposablePreview\u003cAndroidPreviewInfo\u003e): ActivityScenarioForComposableRule {\n           val uiMode =\n               when (preview.previewInfo.uiMode and UI_MODE_NIGHT_MASK == UI_MODE_NIGHT_YES) {\n                   true -\u003e UiMode.NIGHT\n                   false -\u003e UiMode.DAY\n               }\n\n           val orientation =\n               when (DevicePreviewInfoParser.parse(preview.previewInfo.device)?.orientation == Orientation.LANDSCAPE) {\n                   true -\u003e ComposableConfigOrientation.LANDSCAPE\n                   false -\u003e ComposableConfigOrientation.PORTRAIT\n               }\n\n           val locale =\n               preview.previewInfo.locale.removePrefix(\"b+\").replace(\"+\", \"-\").ifBlank { \"en\" }\n\n           return ActivityScenarioForComposableRule(\n               backgroundColor = Color.TRANSPARENT,\n               config = ComposableConfigItem(\n                   uiMode = uiMode,\n                   fontSize = FontSizeScale.Value(preview.previewInfo.fontScale),\n                   orientation = orientation,\n                   locale = locale\n               )\n           )\n       }\n   }\n   ```\n- Create the corresponding Parameterized Test:\n```kotlin\n   @RunWith(ParameterizedTestRunner::class)\n   class PreviewParameterizedTests(\n      private val preview: ComposablePreview\u003cAndroidPreviewInfo\u003e,\n   ) {\n\n      companion object {\n        private val cachedPreviews: List\u003cComposablePreview\u003cAndroidPreviewInfo\u003e\u003e by lazy {\n            AndroidComposablePreviewScanner()\n                  .scanFile(getInstrumentation().context.assets.open(\"scan_result.json\"))\n                  .includeAnnotationInfoForAllOf(DropshotsConfig::class.java)\n                  .getPreviews()\n        }\n   \n         @JvmStatic\n         @ParameterizedTestRunner.Parameters\n         fun values(): List\u003cComposablePreview\u003cAndroidPreviewInfo\u003e\u003e = cachedPreviews\n      }\n   \n      @get:Rule\n      val dropshots: Dropshots = DropshotsPreviewRule.createFor(preview)\n   \n      @get:Rule\n      val activityScenarioForComposableRule: ActivityScenarioForComposableRule = \n            ActivityScenarioForComposablePreviewRule.createFor(preview)\n\n      @Test\n      fun snapshot() {\n        activityScenarioForComposableRule.onActivity {\n            it.setContent {\n                preview()\n            }\n        }\n\n        dropshots.assertSnapshot(\n            view = activityScenarioForComposableRule.activity.waitForComposeView()\n        )\n      }\n   }\n   ```\n   - Run these Dropshots tests together with the existing ones by executing the corresponding command e.g. `./gradlew yourModule:connectedAndroidTest -Pdropshots.record`\n\n\u003e [!WARNING]\n\u003e Beware that Locale Strings in Preview Infos, unlike AndroidUiTestingUtils, use The BCP-47 tag but with + instead of - as separators, and have the prefix b+. Therefore, the BCP-47 tag \"zh-Hans-CN\" would be written as \"b+zh+Hans+CN\" instead. \n\u003e So for this case, you'd have to convert locale \"b+zh+Hans+CN\" to \"zh-Hans-CN\" in order to use it with AndroidUiTestingUtils, for instance as showcased above: \u003c/br\u003e\n\u003e `val locale = preview.previewInfo.locale.removePrefix(\"b+\").replace(\"+\", \"-\").ifBlank { \"en\" }`\n\n## Advanced Usage\n### Screenshot File Names\nComposablePreviewScanner also provides classes to customize the name of the generated screenshots based on its Preview Info.\nThese are `AndroidPreviewScreenshotIdBuilder`, `GlancePreviewScreenshotIdBuilder` and `CommonPreviewScreenshotIdBuilder` respectively, and they both share the same API.\nBy default, these classes do not include the Preview Info in the screenshot file name if it is the same as its default value, but it can be configured to behave differently.\nThat means, for @Preview(showBackground = false), showBackground would not be included in the screenshot file name since it is the default.\n\n```kotlin \nAndroidPreviewScreenshotIdBuilder(preview)\n    .ignoreClassName()\n    .ignoreMethodName()\n    // use this if you have previews in the same file with the same method name but different signature\n    .doNotIgnoreMethodParametersType()\n    // use this if you prefer index instead of displayName when using PreviewParameters\n    .replaceDisplayNameWithIndex()\n    .ignoreIdFor(\"heightDp\")\n    .ignoreIdFor(\"widthDp\")\n    .overrideDefaultIdFor(\n       previewInfoName = \"showBackground\",\n       applyInfoValue = { info -\u003e\n           when (info.showBackground) {\n               true -\u003e \"WITH_BACKGROUND\"\n               false -\u003e \"WITHOUT_BACKGROUND\"\n           }\n       }\n    )\n    .build()\n```\n\nand then in your test\n```kotlin\n@Test\nfun snapshot() {\n    paparazzi.snapshot(\n        name = createScreenshotIdFor(preview)\n    ) {\n        preview()\n    }\n}\n```\n\nSo, for the following Preview\n```kotlin\nclass MyClass {\n    \n    @Preview(widthDp = 33, heightDp = 33, fontScale = 1.5f)\n    @Composable\n    fun MyComposable(){\n        // Composable code here\n    }\n}\n```\ncreateScreenshotIdFor(preview) will generate the following id: `\"MyClass.MyComposable.FONT_1_5f_WITHOUT_BACKGROUND\"`\n\n### Parsing Preview Device String (Android Previews)\nSince 0.4.0, ComposablePreviewScanner also provides `DevicePreviewInfoParser.parse(device: String)`\nwhich returns a `Device` object containing all the necessary information to support different devices in your Roborazzi \u0026 Paparazzi screenshot tests!\n\nIt can parse ALL possible combinations of \"device strings\" up to Android Studio Narwhal, namely:\n```kotlin\n// The over 80 devices supported either by id and/or name, for instance:\n@Preview(device = \"id:pixel_9_pro\")\n@Preview(device = \"name:Pixel 9 Pro\")\n@Preview(device = \"spec:parent=pixel_9_pro, orientation=landscape, navigation=buttons\")\n\n// And custom devices\n@Preview(device = \"spec:width = 411dp, height = 891dp, orientation = landscape, dpi = 420, isRound = false, chinSize = 0dp, cutout = corner\")\n@Preview(device = \"spec:id=reference_desktop,shape=Normal,width=1920,height=1080,unit=px,dpi=160\") // in pixels\n@Preview(device = \"spec:id=reference_desktop,shape=Normal,width=1920,height=1080,unit=dp,dpi=160\") // in dp\n```\nFor further info on how to use them, see [Roborazzi](#roborazzi) and [Paparazzi](#paparazzi) sections respectively.\n\n## How it works\nThis library is written on top of [ClassGraph](https://github.com/classgraph/classgraph), an uber-fast parallelized classpath scanner.\n\nClassGraph can scan everything that is available either at bytecode level or at runtime.\nThis is also the case of annotations without retention or with either `AnnotationRetention.BINARY` or `AnnotationRetention.RUNTIME`, like Android Composable Previews\n```kotlin\npackage androidx.compose.ui.tooling.preview\n\n@Retention(AnnotationRetention.BINARY)\nannotation class Preview(\n   // Preview code here ...\n)\n```\n\nHowever, those with `AnnotationRetention.SOURCE` are not visible to Classgraph. Such annotations are mainly used for IDE tooling, and that is the case for the Compose-Desktop Preview annotation.\n```kotlin\npackage androidx.compose.desktop.ui.tooling.preview\n\n@Retention(AnnotationRetention.SOURCE)\nannotation class Preview\n```\n\n## Glance Previews Support\nYou can find executable examples in this repo with different screenshot libraries:\n- [Roborazzi](tests/src/test/java/sergio/sastre/composable/preview/scanner/tests/roborazzi/RoborazziGlanceComposablePreviewInvokeTests.kt)\n- [Paparazzi](tests/src/test/java/sergio/sastre/composable/preview/scanner/tests/paparazzi/PaparazziGlanceComposablePreviewInvokeTests.kt)\n- [Android-Testify](tests/src/androidTest/java/sergio/sastre/composable/preview/scanner/screenshots/AndroidTestifyGlanceComposablePreviewScannerInstrumentationTest.kt) (Check the [instrumentation-screenshot-tests](#instrumentation-screenshot-tests) section before)\n\nTo write such screenshot tests you have to:\n\n1. Add `:glance` dependency for ComposablePreviewScanner e.g. `io.github.sergio-sastre.ComposablePreviewScanner:glance:\u003cversion\u003e`. This contains some utils to correctly set the size of the Composable as well as the size of the device. Take a look at the executable examples above to see how they are used.\n2. Ensure `targetSdk` is set to any value in the gradle file\u003csup\u003e1\u003c/sup\u003e. Otherwise you can see some discrepancies between the Preview and the generated screenshot file for Glance `@Preview`s without `widthDp`.\n3. Write the Parameterized screenshot test like in the examples above.\n\n\u003csup\u003e1\u003c/sup\u003e Unfortunately, Paparazzi is not able to always render screenshots accurately for Glance `@Preview`s without `widthDp`.\n\n## Compose Multiplatform Support\nStarting with Compose Multiplatform 1.10.0-beta02, Common and Desktop @Preview annotations are deprecated. Instead, Android `@Preview` can now be used across `common` and `desktop` platforms.\nComposablePreviewScanner 0.8.0+ fully supports this modern setup. You can use the `AndroidComposablePreviewScanner` to scan for `@Preview` annotations across all relevant source sets, including commonMain.\u003c/br\u003e\nFor example, to scan previews located in both a platform-specific (androidMain or desktopMain) and a shared (commonMain) source set, you would configure the scanner like this:\n\n```kotlin\nAndroidComposablePreviewScanner()\n    .scanPackageTrees(\n        \"package.tree.android.or.desktop\",\n        \"package.tree.common\"\n    )\n```\n\n\u003e [!NOTE]\n\u003e Screenshot tests must run on a platform supported by your screenshot library.\u003c/br\u003e\n\u003e • Android: Paparazzi and Roborazzi are supported.\u003c/br\u003e\n\u003e • Desktop: Roborazzi only.\u003c/br\u003e\n\u003e • Common: no library runs directly in common; run your tests from an Android or Desktop target instead. ComposablePreviewScanner can still find the Previews in common target packages.\u003c/br\u003e\n\nYou can find executable examples with Roborazzi here:\n- [Android @Previews in common](https://github.com/sergio-sastre/roborazzi/blob/droidcon/preview_tests/sample-generate-preview-common/src/androidUnitTest/kotlin/com/github/takahirom/preview/tests/AndroidPreviewTest.kt)\n- [Android @Previews in desktop](https://github.com/sergio-sastre/roborazzi/blob/droidcon/preview_tests/sample-generate-preview-desktop/src/desktopTest/kotlin/AndroidPreviewTest.kt)\n\nIf you are still using the deprecated Common or Desktop `@Preview` annotations, see [README_DEPRECATED.md](README_DEPRECATED.md) for guidance.\n\n# Resources \n## Tech talks\nIn these tech-talks have also been mentioned the benefits of using ComposablePreviewScanner:\n- DroidKaigi 2024 [in JA 🇯🇵 with EN 🇬🇧 slides]:\u003c/br\u003e\n  [Understand the mechanism! Let's do screenshots testing of Compose Previews with various variations](https://www.youtube.com/watch?app=desktop\u0026v=c4AxUXTQgw4) by [Sumio Toyama](https://x.com/sumio_tym)\u003c/br\u003e\n- Droidcon Lisbon 2024:\u003c/br\u003e\n  [Composable Preview Driven Development: TDD-fying your UI with ease!](https://www.google.com/url?sa=t\u0026source=web\u0026rct=j\u0026opi=89978449\u0026url=https://www.youtube.com/watch%3Fv%3DcDqdosrS83k\u0026ved=2ahUKEwjprPGKqaiPAxWxSvEDHdSRBKkQwqsBegQIFRAG\u0026usg=AOvVaw2ZfX6fYQbNI4Op6KN0d5i5) by Sergio Sastre\u003c/br\u003e\n- [“Fast Feedback loops \u0026 Composable Preview Scanner”](https://www.youtube.com/watch?v=SphQelcGdHk) with the Skool Android Community by Sergio Sastre\u003c/br\u003e\n- Droidcon Lisbon \u0026 Berlin 2025:\u003c/br\u003e\n  [Let's @Preview the future: Automating Screenshot Testing in Compose Multiplatform](https://www.youtube.com/watch?v=zYsNXrf2-Lo) by Sergio Sastre\n\n## Blog posts\n- [Automating screens verification with Roborazzi and GitHub Actions](https://medium.com/@matiasdelbel/automating-screens-verification-with-roborazzi-and-github-actions-473b3301a5c0) by Matías del Bel\n- [Implementing Screenshot Testing in the Unlimited Android App Was Tougher Than Expected](https://blog.kinto-technologies.com/posts/2024-12-13-Introducing-Screenshot-Testing-in-UnlimitedApp-en/) by KINTO Technologies\n\n# Testing\nThe core of ComposablePreviewScanner has been (and it's being) developed using Test-Driven Development (TDD).\u003c/br\u003e\nI strongly believe this approach is one of the key reasons the library has very few known bugs although it's widely used with over 150k monthly downloads.\n\nHowever, some tests have specific preconditions and may be skipped if those aren't met.\u003c/br\u003e\nFor example, when running tests to retrieve @Previews from a SourceSet other than main, such as screenshotTest or androidTest,\nthe corresponding compiled classes must be generated first via the corresponding Gradle task.\u003c/br\u003e\n\nMoreover, Paparazzi \u0026 Roborazzi tests also play a key role:\n1. Each of these libraries uses a different mechanism to download Android resources for running tests. ComposablePreviewScanner also loads certain classes by using ClassLoaders, and for those classes to be available it is necessary that Paparazzi and Roborazzi already downloaded them to [avoid issues like this one](https://github.com/sergio-sastre/ComposablePreviewScanner/issues/27). These tests help catch and avoid such errors.\n2. They help avoid errors in @Composable invocations. Since they can only occur within the context of a @Composable function and standard unit tests cannot access Android resources (e.g. Composable framework), it is hard to verify their correctness without UI tests. \n\nTo streamline this process and support my TDD workflow, I’ve created custom Gradle tasks that handle these prerequisites automatically,\nsaving time and reducing friction during development.\u003c/br\u003e\nThey can also help you in case you fork this library and make some code adjustments, to ensure everything still works as expected.\u003c/br\u003e\n\nThese custom gradle tasks are the following:\u003c/br\u003e\n1. API logic tests:`./gradlew :tests:testApi`\n2. SourceSet logic tests: `./gradlew :tests:testSourceSets`\n3. Paparazzi integration tests: `./gradlew :tests:paparazziPreviews` and `./gradlew :tests:paparazziPreviews -Pverify=true`\n4. Roborazzi integration tests: `./gradlew :tests:roborazziPreviews` and `./gradlew :tests:roborazziPreviews -Pverify=true`\n\nCustom gradle tasks for Android-testify integration tests (i.e. instrumentation screenshot testing libraries) coming soon\n\n# Troubleshooting\n\n## Slow JVM Screenshot tests\nJVM Screenshot tests can consume significant memory, and ComposablePreviewScanner may require even more RAM when scanning large sets of subpackages.\nTo improve performance, you can increase the RAM allocated to your unit tests by configuring your Gradle file as follows:\n```kotlin\ntestOptions.unitTests {\n    all { test -\u003e\n        // allocate 4GB RAM\n        test.jvmArgs(\"-Xmx4g\")\n    }\n}\n```\nThis adjustment helps reduce test execution time during large scans.\n\n## java.io.FileNotFoundException (File name too long)\n\n`java.io.FileNotFoundException: ... (File name too long)`\u003c/br\u003e\nThis is more likely to happen when using Paparazzi. By default, Paparazzi additionally prefixes the screenshot file named internally instead of just using the `name` we pass to its `snapshot()` method, and this results sometimes in the final screenshot file name being longer than allowed.\u003c/br\u003e\u003c/br\u003e\nThat is why it is recommended to [set a custom SnapshotHandler](#paparazzi) in the Paparazzi Test Rule.\n\nBut if you're still experiencing such issues, consider:\n1. Using `AndroidPreviewScreenshotIdBuilder` methods like `ignoreIdFor()` or `overrideDefaultIdFor()` to shorten the given name.\n2. Avoid `AndroidPreviewScreenshotIdBuilder` and use `paparazzi.snapshot {}` instead of `paparazzi.snapshot(name = screenshotId)`\n\n## java.lang.IllegalArgumentException: Generated method name contains invalid characters\n\nSome libraries restrict the characters allowed in filenames and may alter the provided screenshot name (e.g., Paparazzi 1.3.5+ like reported in this issue [here](https://github.com/cashapp/paparazzi/issues/1963)).\nThis is especially problematic when the `TestParameterInjector` test runner is used.\u003c/br\u003e\nTo avoid issues, `ComposablePreviewScanner`s ScreenshotIdBuilders should be used with the standard JUnit4 `Parameterized` test runner, and invalid characters should be encoded if needed:\u003c/br\u003e\n\nComposablePreviewScanner 0.8.0+\n```kotlin\nAndroidPreviewScreenshotIdBuilder(preview)\n    ...\n    .encodeUnsafeCharacters()\n    .build()\n```\n\nComposablePreviewScanner \u003c 0.8.0:\nYou have to encode them manually\n```kotlin\nAndroidPreviewScreenshotIdBuilder(preview)\n    ...\n    .build()\n    .replace(\"\u003c\", \"%3C\")\n    .replace(\"\u003e\", \"%3E\")\n    .replace(\"?\", \"%3F\")\n```\n\u003e [!NOTE]\n\u003e When using `TestParameterInjector` invalid characters may persist.\n\u003e Use the JUnit4 `Parameterized` test runner for valid results.\n\n## Cannot inline bytecode built with JVM target 17\n\n```text\nTask compileDebugUnitTestKotlin FAILED\ne: file:... Cannot inline bytecode built with JVM target 17 into bytecode that is being built with JVM target 11. Specify proper '-jvm-target' option.\n```\n\nYou need to upgrade the 'jvmTarget' in the gradle build file of the module where it is failing like this:\n```kotlin\nkotlin {\n    compilerOptions {\n        jvmTarget = JvmTarget.JVM_17 // or higher\n    }\n}\n```\n\n## GooglePlayServicesMissingManifestValueException\n\n```text\ncom.google.android.gms.common.GooglePlayServicesMissingManifestValueException: A required meta-data tag in your app's AndroidManifest.xml does not exist.  You must have the following declaration within the \u003capplication\u003e element:     \u003cmeta-data android:name=\"com.google.android.gms.version\" android:value=\"@integer/google_play_services_version\" /\u003e\n```\n\nIf you are using 'GoogleMap' composable, then you will need to wrap your composable preview content with `CompositionLocalProvider(LocalInspectionMode provides true)`\n\n```kotlin\n@Preview\n@Composable\ninternal fun MapScreenPreview() {\n    CompositionLocalProvider(LocalInspectionMode provides true) {\n        MapScreen()\n    }\n}\n```\n\n\u003c/br\u003e\u003c/br\u003e\n\u003ca href=\"https://www.flaticon.com/free-icons/magnify\" title=\"magnify icons\"\u003eComposable Preview Scanner logo modified from one by Freepik - Flaticon\u003c/a\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsergio-sastre%2Fcomposablepreviewscanner","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsergio-sastre%2Fcomposablepreviewscanner","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsergio-sastre%2Fcomposablepreviewscanner/lists"}