Repository: takahirom/roborazzi (kotlin)

Issue:
[Feature Request] Private Preview Annotation Support
I would like to request support for private previews.
I suggest adding an option to use `includePrivatePreviews()` when working with ComposablePreviewScanner.

Reference Links:
- [ComposablePreviewScanner's Documentation](https://github.com/sergio-sastre/ComposablePreviewScanner/blob/4d899cfe6e5243bb03f8b1381719581423de6e40/README.md?plain=1#L86)
- [Roborazzi Code](https://github.com/takahirom/roborazzi/blob/b10e3e96f8f2fbdc47c3ce632e42192aa7561fed/roborazzi-compose-preview-scanner-support/src/main/java/com/github/takahirom/roborazzi/RoborazziPreviewScannerSupport.kt#L45)

Pull request:
close #441
close #446

Patch:
diff --git a/README.md b/README.md
index 0745d91f8..712a53a09 100644
--- a/README.md
+++ b/README.md
@@ -951,7 +951,7 @@ If you are having trouble debugging your test, try Dump mode as follows.
 # Experimental Compose Preview Support
 
 Roborazzi provides support for generating screenshot tests and easy setup for Jetpack Compose Preview.
-This support uses [ComposePreviewScanner](https://github.com/sergio-sastre/ComposablePreviewScanner) to scan the Composable Previews in your project.
+This support uses [ComposablePreviewScanner](https://github.com/sergio-sastre/ComposablePreviewScanner) to scan the Composable Previews in your project.
 
 ## Generate Compose Preview screenshot tests
 
@@ -980,27 +980,29 @@ roborazzi {
     enable = true
     // The package names to scan for Composable Previews.
     packages = listOf("com.example")
-    // The fully qualified class name of the custom test class that implements [com.github.takahirom.roborazzi.ComposePreviewTester].
-    testerQualifiedClassName = "com.example.MyCustomComposePreviewTester"
     // robolectricConfig will be passed to Robolectric's @Config annotation in the generated test class.
     // See https://robolectric.org/configuring/ for more information.
     robolectricConfig = mapOf(
       "sdk" to "[32]",
       "qualifiers" to "RobolectricDeviceQualifiers.Pixel5",
     )
+    // If true, the private previews will be included in the test.
+    includePrivatePreviews = true
+    // The fully qualified class name of the custom test class that implements [com.github.takahirom.roborazzi.ComposePreviewTester].
+    testerQualifiedClassName = "com.example.MyCustomComposePreviewTester"
   }
 }
 ```
 
 ## Manually adding Compose Preview screenshot tests
 
-Roborazzi provides a helper function for ComposePreviewScanner.
+Roborazzi provides a helper function for ComposablePreviewScanner.
 You can add the following dependency to your project to use the helper function:
 
 `testImplementation("io.github.takahirom.roborazzi:roborazzi-compose-preview-scanner-support:[version]")`
 
 Then you can use the `ComposablePreview<AndroidPreviewInfo>.captureRoboImage()` function to capture the Composable Preview using the settings in Preview annotations.
-To obtain the `ComposablePreview` object, please refer to [ComposePreviewScanner](https://github.com/sergio-sastre/ComposablePreviewScanner).
+To obtain the `ComposablePreview` object, please refer to [ComposablePreviewScanner](https://github.com/sergio-sastre/ComposablePreviewScanner).
 
 ```kotlin
 fun ComposablePreview<AndroidPreviewInfo>.captureRoboImage(
@@ -1021,11 +1023,11 @@ We are looking forward to your contributions to support more annotation options.
 <!-- Generated by docs/topics/idea_plugin.md. Do not edit this file. -->
 # Roborazzi IntelliJ IDEA and Android Studio Plugin
 
-https://plugins.jetbrains.com/plugin/24561-roborazzi
+[https://plugins.jetbrains.com/plugin/24561-roborazzi](https://plugins.jetbrains.com/plugin/24561-roborazzi)
 
 Demo of Roborazzi IntelliJ IDEA and Android Studio Plugin.
 
-https://github.com/takahirom/roborazzi/assets/1386930/bd3e8106-1971-4ec4-b764-3f1831999228
+[https://github.com/takahirom/roborazzi/assets/1386930/bd3e8106-1971-4ec4-b764-3f1831999228](https://github.com/takahirom/roborazzi/assets/1386930/bd3e8106-1971-4ec4-b764-3f1831999228)
 
 The Roborazzi plugin enhances your development workflow by automatically displaying screenshots related to Roborazzi's screenshot tests within IntelliJ IDEA. Easily view and verify visual outputs directly in your IDE, streamlining your testing and development process.
 </div>
diff --git a/docs/topics/preview_support.md b/docs/topics/preview_support.md
index 14d33b4c4..775f4c673 100644
--- a/docs/topics/preview_support.md
+++ b/docs/topics/preview_support.md
@@ -1,7 +1,7 @@
 # Experimental Compose Preview Support
 
 Roborazzi provides support for generating screenshot tests and easy setup for Jetpack Compose Preview.
-This support uses [ComposePreviewScanner](https://github.com/sergio-sastre/ComposablePreviewScanner) to scan the Composable Previews in your project.
+This support uses [ComposablePreviewScanner](https://github.com/sergio-sastre/ComposablePreviewScanner) to scan the Composable Previews in your project.
 
 ## Generate Compose Preview screenshot tests
 
@@ -30,27 +30,29 @@ roborazzi {
     enable = true
     // The package names to scan for Composable Previews.
     packages = listOf("com.example")
-    // The fully qualified class name of the custom test class that implements [com.github.takahirom.roborazzi.ComposePreviewTester].
-    testerQualifiedClassName = "com.example.MyCustomComposePreviewTester"
     // robolectricConfig will be passed to Robolectric's @Config annotation in the generated test class.
     // See https://robolectric.org/configuring/ for more information.
     robolectricConfig = mapOf(
       "sdk" to "[32]",
       "qualifiers" to "RobolectricDeviceQualifiers.Pixel5",
     )
+    // If true, the private previews will be included in the test.
+    includePrivatePreviews = true
+    // The fully qualified class name of the custom test class that implements [com.github.takahirom.roborazzi.ComposePreviewTester].
+    testerQualifiedClassName = "com.example.MyCustomComposePreviewTester"
   }
 }
 ```
 
 ## Manually adding Compose Preview screenshot tests
 
-Roborazzi provides a helper function for ComposePreviewScanner.
+Roborazzi provides a helper function for ComposablePreviewScanner.
 You can add the following dependency to your project to use the helper function:
 
 `testImplementation("io.github.takahirom.roborazzi:roborazzi-compose-preview-scanner-support:[version]")`
 
 Then you can use the `ComposablePreview<AndroidPreviewInfo>.captureRoboImage()` function to capture the Composable Preview using the settings in Preview annotations.
-To obtain the `ComposablePreview` object, please refer to [ComposePreviewScanner](https://github.com/sergio-sastre/ComposablePreviewScanner).
+To obtain the `ComposablePreview` object, please refer to [ComposablePreviewScanner](https://github.com/sergio-sastre/ComposablePreviewScanner).
 
 ```kotlin
 fun ComposablePreview<AndroidPreviewInfo>.captureRoboImage(
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index ed9ba4091..adfd872ce 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -43,7 +43,7 @@ webjar-material-design-icons = "4.0.0"
 webjar-materialize = "1.0.0"
 webjars-locator-lite = "0.0.4"
 
-composable-preview-scanner = "0.1.2"
+composable-preview-scanner = "0.1.3"
 
 [libraries]
 roborazzi = { module = "io.github.takahirom.roborazzi:roborazzi", version.ref = "roborazzi-for-replacing-by-include-build" }
diff --git a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/RoborazziPlugin.kt b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/RoborazziPlugin.kt
index 7e4e6c795..a33fa24b2 100644
--- a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/RoborazziPlugin.kt
+++ b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/RoborazziPlugin.kt
@@ -450,7 +450,7 @@ abstract class RoborazziPlugin : Plugin<Project> {
     project.pluginManager.withPlugin("com.android.application") {
       project.extensions.getByType(ApplicationAndroidComponentsExtension::class.java)
         .configureComponents()
-      verifyGenerateRobolectricPreviewTests(
+      verifyGenerateComposePreviewRobolectricTests(
         project = project,
         androidExtension = project.extensions.getByType(TestedExtension::class.java),
         extension = extension.generateComposePreviewRobolectricTests
@@ -459,7 +459,7 @@ abstract class RoborazziPlugin : Plugin<Project> {
     project.pluginManager.withPlugin("com.android.library") {
       project.extensions.getByType(LibraryAndroidComponentsExtension::class.java)
         .configureComponents()
-      verifyGenerateRobolectricPreviewTests(
+      verifyGenerateComposePreviewRobolectricTests(
         project = project,
         androidExtension = project.extensions.getByType(TestedExtension::class.java),
         extension = extension.generateComposePreviewRobolectricTests
diff --git a/roborazzi-compose-preview-scanner-support/src/main/java/com/github/takahirom/roborazzi/RoborazziPreviewScannerSupport.kt b/roborazzi-compose-preview-scanner-support/src/main/java/com/github/takahirom/roborazzi/RoborazziPreviewScannerSupport.kt
index 33732e0e3..7cdf7cc0a 100644
--- a/roborazzi-compose-preview-scanner-support/src/main/java/com/github/takahirom/roborazzi/RoborazziPreviewScannerSupport.kt
+++ b/roborazzi-compose-preview-scanner-support/src/main/java/com/github/takahirom/roborazzi/RoborazziPreviewScannerSupport.kt
@@ -1,5 +1,7 @@
 package com.github.takahirom.roborazzi
 
+import org.junit.rules.TestRule
+import org.junit.rules.TestWatcher
 import sergio.sastre.composable.preview.scanner.android.AndroidComposablePreviewScanner
 import sergio.sastre.composable.preview.scanner.android.AndroidPreviewInfo
 import sergio.sastre.composable.preview.scanner.android.screenshotid.AndroidPreviewScreenshotIdBuilder
@@ -16,16 +18,53 @@ fun ComposablePreview<AndroidPreviewInfo>.captureRoboImage(
     composablePreview()
   }
 }
-
+/**
+ * ComposePreviewTester is an interface that allows you to define a custom test for a Composable preview.
+ * The class that implements this interface should have a parameterless constructor.
+ * You can set the custom tester class name in the roborazzi.generateComposePreviewRobolectricTests.testerQualifiedClassName property.
+ * A new instance of the tester class is created for each test execution and preview generation.
+ */
 @ExperimentalRoborazziApi
 interface ComposePreviewTester<T : Any> {
+  data class Options(
+    val testLifecycleOptions: TestLifecycleOptions = JUnit4TestLifecycleOptions(),
+    val scanOptions: ScanOptions = ScanOptions(emptyList()),
+  ) {
+    interface TestLifecycleOptions
+    data class JUnit4TestLifecycleOptions(
+      /**
+       * The TestRule factory to be used for the generated tests.
+       * You can use this to add custom behavior to the generated tests.
+       */
+      // Used from generated tests
+      @Suppress("unused") val testRuleFactory: () -> TestRule = { object : TestWatcher() {} },
+    ) : TestLifecycleOptions
+
+    data class ScanOptions(
+      /**
+       * The packages to scan for composable previews.
+       */
+      val packages: List<String>,
+      /**
+       * Whether to include private previews in the scan.
+       */
+      val includePrivatePreviews: Boolean = false,
+    )
+  }
+
+  /**
+   * Retrieves the options for the ComposePreviewTester.
+   *
+   * @return The Options object.
+   */
+  fun options(): Options = defaultOptionsFromPlugin
+
   /**
    * Retrieves a list of composable previews from the specified packages.
    *
-   * @param packages Vararg parameter representing the package names to scan for previews.
    * @return A list of ComposablePreview objects of type T.
    */
-  fun previews(vararg packages: String): List<ComposablePreview<T>>
+  fun previews(): List<ComposablePreview<T>>
 
   /**
    * Performs a test on a single composable preview.
@@ -33,16 +72,28 @@ interface ComposePreviewTester<T : Any> {
    *
    * @param preview The ComposablePreview object to be tested.
    */
-  fun test(
-    preview: ComposablePreview<T>,
-  )
+  fun test(preview: ComposablePreview<T>)
+
+  companion object {
+    // Should be replaced with the actual default options from the plugin.
+    @InternalRoborazziApi
+    var defaultOptionsFromPlugin = Options()
+  }
 }
 
-@InternalRoborazziApi
+@ExperimentalRoborazziApi
 class AndroidComposePreviewTester : ComposePreviewTester<AndroidPreviewInfo> {
-  override fun previews(vararg packages: String): List<ComposablePreview<AndroidPreviewInfo>> {
+  override fun previews(): List<ComposablePreview<AndroidPreviewInfo>> {
+    val options = options()
     return AndroidComposablePreviewScanner()
-      .scanPackageTrees(*packages)
+      .scanPackageTrees(*options.scanOptions.packages.toTypedArray())
+      .let {
+        if (options.scanOptions.includePrivatePreviews) {
+          it.includePrivatePreviews()
+        } else {
+          it
+        }
+      }
       .getPreviews()
   }
 


Test patch:
diff --git a/include-build/roborazzi-gradle-plugin/src/integrationTest/java/io/github/takahirom/roborazzi/PreviewGenerateTest.kt b/include-build/roborazzi-gradle-plugin/src/integrationTest/java/io/github/takahirom/roborazzi/PreviewGenerateTest.kt
index 663e70055..640fa8c2f 100644
--- a/include-build/roborazzi-gradle-plugin/src/integrationTest/java/io/github/takahirom/roborazzi/PreviewGenerateTest.kt
+++ b/include-build/roborazzi-gradle-plugin/src/integrationTest/java/io/github/takahirom/roborazzi/PreviewGenerateTest.kt
@@ -17,6 +17,7 @@ class GeneratePreviewTestTest {
       checkHasImages()
     }
   }
+
   @Test
   fun whenKmpModuleAndRecordRunImagesShouldBeRecorded() {
     RoborazziGradleRootProject(testProjectDir).previewModule.apply {
@@ -26,6 +27,40 @@ class GeneratePreviewTestTest {
       checkHasImages()
     }
   }
+
+  @Test
+  fun whenDisablePreviewAndRecordRunImagesShouldNotBeRecorded() {
+    RoborazziGradleRootProject(testProjectDir).previewModule.apply {
+      buildGradle.enable = false
+
+      record()
+
+      checkNoImages()
+    }
+  }
+
+  @Test
+  fun whenCustomTesterAndRecordRunImagesShouldBeRecordedAndCanSeeJUnitLog() {
+    RoborazziGradleRootProject(testProjectDir).previewModule.apply {
+      buildGradle.useCustomTester = true
+
+      record {
+        itShouldHaveJUnitRuleLog()
+      }
+
+      checkHasImages()
+    }
+  }
+
+  @Test
+  fun whenIncludePrivatePreviewsAndRecordRunImagesShouldBeRecorded() {
+    RoborazziGradleRootProject(testProjectDir).previewModule.apply {
+      buildGradle.isIncludePrivatePreviews = true
+      record()
+
+      checkHasPrivatePreviewImages()
+    }
+  }
 }
 
 class PreviewModule(
@@ -35,6 +70,7 @@ class PreviewModule(
   companion object {
     val moduleName = "sample-generate-preview-tests"
   }
+
   val buildGradle = BuildGradle(testProjectDir)
 
   class BuildGradle(private val projectFolder: TemporaryFolder) {
@@ -44,14 +80,8 @@ class PreviewModule(
       val file =
         projectFolder.root.resolve(PATH)
       file.parentFile.mkdirs()
-      val roborazziExtension = """
-          roborazzi {
-            generateComposePreviewRobolectricTests {
-              enable = true
-              packages = listOf("com.github.takahirom.preview.tests")
-            }
-          }
-      """.trimIndent()
+
+      val roborazziExtension = createRoborazziExtension()
       val androidBlock = """
           android {
             namespace = "com.github.takahirom.preview.tests"
@@ -89,7 +119,7 @@ class PreviewModule(
           }
 
       """.trimIndent()
-      val buildGradleText = if(isKmp)
+      val buildGradleText = if (isKmp)
         """
           plugins {
               kotlin("multiplatform")
@@ -122,6 +152,12 @@ class PreviewModule(
                           implementation(libs.junit)
                           implementation(libs.robolectric)
                           implementation(libs.composable.preview.scanner)
+                          implementation(libs.androidx.compose.ui.test.junit4)
+                      }
+                  }
+                  val androidDebug by creating {
+                      dependencies {
+                          implementation(libs.androidx.compose.ui.test.manifest)
                       }
                   }
                   
@@ -144,7 +180,7 @@ class PreviewModule(
               maven { url = uri("https://jitpack.io") }
           }
         """.trimIndent()
-        else """
+      else """
   plugins {
     id("com.android.application")
   //  id("com.android.library")
@@ -171,6 +207,8 @@ class PreviewModule(
     testImplementation("io.github.takahirom.roborazzi:roborazzi-compose-preview-scanner-support:0.1.0")
     testImplementation(libs.junit)
     testImplementation(libs.robolectric)
+    testImplementation(libs.androidx.compose.ui.test.junit4)
+    debugImplementation(libs.androidx.compose.ui.test.manifest)
     testImplementation(libs.composable.preview.scanner)
     androidTestImplementation(libs.androidx.test.ext.junit)
     androidTestImplementation(libs.androidx.test.espresso.core)
@@ -180,10 +218,44 @@ class PreviewModule(
         buildGradleText.trimIndent()
       )
     }
+
+    var enable = true
+    var isIncludePrivatePreviews = false
+    var useCustomTester = false
+
+    private fun createRoborazziExtension(): String {
+      val includePrivatePreviewsExpr = if (isIncludePrivatePreviews) {
+        """includePrivatePreviews = $isIncludePrivatePreviews"""
+      } else {
+        ""
+      }
+      val customTesterExpr = if (useCustomTester) {
+        """testerQualifiedClassName = "com.github.takahirom.sample.CustomPreviewTester""""
+      } else {
+        ""
+      }
+      val roborazziExtension = """
+              roborazzi {
+                generateComposePreviewRobolectricTests {
+                  enable = $enable
+                  packages = listOf("com.github.takahirom.preview.tests")
+                  $includePrivatePreviewsExpr
+                  $customTesterExpr
+                }
+              }
+          """.trimIndent()
+      return roborazziExtension
+    }
+  }
+
+  fun record(checks: BuildResult.() -> Unit = {}) {
+    val result = runTask("recordRoborazziDebug")
+    result.checks()
   }
 
-  fun record() {
-    runTask("recordRoborazziDebug")
+  fun BuildResult.itShouldHaveJUnitRuleLog() {
+    assert(output.contains("JUnit4TestLifecycleOptions starting"))
+    assert(output.contains("JUnit4TestLifecycleOptions finished"))
   }
 
   private fun runTask(
@@ -205,4 +277,24 @@ class PreviewModule(
     println("images:" + images?.toList())
     assert(images?.isNotEmpty() == true)
   }
+
+  fun checkNoImages() {
+    val images = testProjectDir.root.resolve("$moduleName/build/outputs/roborazzi/").listFiles()
+    println("images:" + images?.toList())
+    checkResultCount(
+      testProjectDir.root.resolve("$moduleName/build/test-results/roborazzi/results-summary.json")
+      // All zero
+    )
+
+    assert(images?.isEmpty() == true)
+  }
+
+  fun checkHasPrivatePreviewImages() {
+    val privateImages =
+      testProjectDir.root.resolve("$moduleName/build/outputs/roborazzi/").listFiles()
+        .orEmpty()
+        .filter { it.name.contains("PreviewWithPrivate") }
+    println("images:" + privateImages.toList())
+    assert(privateImages.isNotEmpty() == true)
+  }
 }
\ No newline at end of file
diff --git a/include-build/roborazzi-gradle-plugin/src/integrationTest/java/io/github/takahirom/roborazzi/RoborazziGradleProject.kt b/include-build/roborazzi-gradle-plugin/src/integrationTest/java/io/github/takahirom/roborazzi/RoborazziGradleProject.kt
index 9e1bbcfb2..b135a3b33 100644
--- a/include-build/roborazzi-gradle-plugin/src/integrationTest/java/io/github/takahirom/roborazzi/RoborazziGradleProject.kt
+++ b/include-build/roborazzi-gradle-plugin/src/integrationTest/java/io/github/takahirom/roborazzi/RoborazziGradleProject.kt
@@ -337,19 +337,7 @@ dependencies {
   ) {
     val recordedFile =
       testProjectDir.root.resolve("app/$buildDirName/test-results/roborazzi/results-summary.json")
-    val results = CaptureResults.fromJsonFile(recordedFile.absolutePath)
-    assert(results.resultSummary.recorded == recorded) {
-      "Expected count: $recorded, actual count: ${results.resultSummary.recorded} summary:${results.resultSummary}"
-    }
-    assert(results.resultSummary.added == added) {
-      "Expected count: $added, actual count: ${results.resultSummary.added} summary:${results.resultSummary}"
-    }
-    assert(results.resultSummary.changed == changed) {
-      "Expected count: $changed, actual count: ${results.resultSummary.changed} summary:${results.resultSummary}"
-    }
-    assert(results.resultSummary.unchanged == unchanged) {
-      "Expected count: $unchanged, actual count: ${results.resultSummary.unchanged} summary:${results.resultSummary}"
-    }
+    checkResultCount(recordedFile, recorded, added, changed, unchanged)
   }
 
   fun checkRecordedFileExists(path: String) {
@@ -646,3 +634,25 @@ class RoborazziTest {
 
   val buildGradle = BuildGradle(testProjectDir)
 }
+
+fun checkResultCount(
+  recordedFile: File,
+  recorded: Int = 0,
+  added: Int = 0,
+  changed: Int = 0,
+  unchanged: Int = 0
+) {
+  val results = CaptureResults.fromJsonFile(recordedFile.absolutePath)
+  assert(results.resultSummary.recorded == recorded) {
+    "Expected count: $recorded, actual count: ${results.resultSummary.recorded} summary:${results.resultSummary}"
+  }
+  assert(results.resultSummary.added == added) {
+    "Expected count: $added, actual count: ${results.resultSummary.added} summary:${results.resultSummary}"
+  }
+  assert(results.resultSummary.changed == changed) {
+    "Expected count: $changed, actual count: ${results.resultSummary.changed} summary:${results.resultSummary}"
+  }
+  assert(results.resultSummary.unchanged == unchanged) {
+    "Expected count: $unchanged, actual count: ${results.resultSummary.unchanged} summary:${results.resultSummary}"
+  }
+}
diff --git a/include-build/roborazzi-gradle-plugin/src/integrationTest/projects/sample-generate-preview-tests/build.gradle.kts b/include-build/roborazzi-gradle-plugin/src/integrationTest/projects/sample-generate-preview-tests/build.gradle.kts
index 5d56f7012..026f3fc9d 100644
--- a/include-build/roborazzi-gradle-plugin/src/integrationTest/projects/sample-generate-preview-tests/build.gradle.kts
+++ b/include-build/roborazzi-gradle-plugin/src/integrationTest/projects/sample-generate-preview-tests/build.gradle.kts
@@ -1,3 +1,7 @@
+/**
+ * This file is overwritten by the IntegrationTest.
+ */
+
 plugins {
   id("com.android.application")
 //  id("com.android.library")
@@ -64,6 +68,7 @@ dependencies {
   testImplementation(libs.junit)
   testImplementation(libs.robolectric)
   testImplementation(libs.composable.preview.scanner)
+  testImplementation(libs.androidx.compose.ui.test.junit4)
   androidTestImplementation(libs.androidx.test.ext.junit)
   androidTestImplementation(libs.androidx.test.espresso.core)
 }
\ No newline at end of file
diff --git a/include-build/roborazzi-gradle-plugin/src/integrationTest/projects/sample-generate-preview-tests/src/main/java/com/github/takahirom/preview/tests/Previews.kt b/include-build/roborazzi-gradle-plugin/src/integrationTest/projects/sample-generate-preview-tests/src/main/java/com/github/takahirom/preview/tests/Previews.kt
index 2d9971a9f..7fcef71eb 100644
--- a/include-build/roborazzi-gradle-plugin/src/integrationTest/projects/sample-generate-preview-tests/src/main/java/com/github/takahirom/preview/tests/Previews.kt
+++ b/include-build/roborazzi-gradle-plugin/src/integrationTest/projects/sample-generate-preview-tests/src/main/java/com/github/takahirom/preview/tests/Previews.kt
@@ -53,6 +53,23 @@ fun PreviewDarkMode() {
   }
 }
 
+@Preview
+@Composable
+private fun PreviewWithPrivate() {
+  val isSystemInDarkTheme = isSystemInDarkTheme()
+  MaterialTheme {
+    Card(
+      Modifier
+        .width(180.dp)
+    ) {
+      Text(
+        modifier = Modifier.padding(8.dp),
+        text = "This is private preview"
+      )
+    }
+  }
+}
+
 @Preview(
   name = "Preview Name",
   // These properties are not supported by Roborazzi yet.
diff --git a/include-build/roborazzi-gradle-plugin/src/integrationTest/projects/sample-generate-preview-tests/src/test/java/com/github/takahirom/sample/CustomPreviewTester.kt b/include-build/roborazzi-gradle-plugin/src/integrationTest/projects/sample-generate-preview-tests/src/test/java/com/github/takahirom/sample/CustomPreviewTester.kt
new file mode 100644
index 000000000..3f3e6b76a
--- /dev/null
+++ b/include-build/roborazzi-gradle-plugin/src/integrationTest/projects/sample-generate-preview-tests/src/test/java/com/github/takahirom/sample/CustomPreviewTester.kt
@@ -0,0 +1,39 @@
+package com.github.takahirom.sample
+
+import org.junit.Assert.*
+import com.github.takahirom.roborazzi.*
+import org.junit.rules.RuleChain
+import org.junit.rules.TestWatcher
+import sergio.sastre.composable.preview.scanner.android.AndroidPreviewInfo
+import sergio.sastre.composable.preview.scanner.core.preview.ComposablePreview
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onRoot
+
+class CustomPreviewTester : ComposePreviewTester<AndroidPreviewInfo> by AndroidComposePreviewTester() {
+  val composeTestRule = createComposeRule()
+  override fun options(): ComposePreviewTester.Options = super.options().copy(
+    testLifecycleOptions = ComposePreviewTester.Options.JUnit4TestLifecycleOptions(
+      testRuleFactory = {
+        RuleChain.outerRule(
+          object : TestWatcher() {
+            override fun starting(description: org.junit.runner.Description?) {
+              println("JUnit4TestLifecycleOptions starting")
+            }
+
+            override fun finished(description: org.junit.runner.Description?) {
+              println("JUnit4TestLifecycleOptions finished")
+            }
+          }
+        )
+          .around(composeTestRule)
+      }
+    )
+  )
+
+  override fun test(preview: ComposablePreview<AndroidPreviewInfo>) {
+    composeTestRule.setContent {
+      preview()
+    }
+    composeTestRule.onRoot().captureRoboImage("${roborazziSystemPropertyOutputDirectory()}/${preview.methodName}.png")
+  }
+}
\ No newline at end of file
diff --git a/include-build/roborazzi-gradle-plugin/src/integrationTest/projects/sample-generate-preview-tests/src/test/java/com/github/takahirom/sample/ExampleUnitTest.kt b/include-build/roborazzi-gradle-plugin/src/integrationTest/projects/sample-generate-preview-tests/src/test/java/com/github/takahirom/sample/ExampleUnitTest.kt
deleted file mode 100644
index 6289244c0..000000000
--- a/include-build/roborazzi-gradle-plugin/src/integrationTest/projects/sample-generate-preview-tests/src/test/java/com/github/takahirom/sample/ExampleUnitTest.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.github.takahirom.sample
-
-import org.junit.Test
-
-import org.junit.Assert.*
-
-/**
- * Example local unit test, which will execute on the development machine (host).
- *
- * See [testing documentation](http://d.android.com/tools/testing).
- */
-class ExampleUnitTest {
-    @Test
-    fun addition_isCorrect() {
-        assertEquals(4, 2 + 2)
-    }
-}
\ No newline at end of file
diff --git a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/GenerateComposePreviewRobolectricTestsExtension.kt b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/GenerateComposePreviewRobolectricTestsExtension.kt
index e544be0b1..c4617e9e0 100644
--- a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/GenerateComposePreviewRobolectricTestsExtension.kt
+++ b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/GenerateComposePreviewRobolectricTestsExtension.kt
@@ -19,6 +19,7 @@ import org.gradle.api.tasks.testing.Test
 import org.gradle.invocation.DefaultGradle
 import java.io.File
 import java.net.URLEncoder
+import java.util.Locale
 import javax.inject.Inject
 
 open class GenerateComposePreviewRobolectricTestsExtension @Inject constructor(objects: ObjectFactory) {
@@ -31,10 +32,10 @@ open class GenerateComposePreviewRobolectricTestsExtension @Inject constructor(o
   val packages: ListProperty<String> = objects.listProperty(String::class.java)
 
   /**
-   * The fully qualified class name of the custom test class that implements [com.github.takahirom.roborazzi.ComposePreviewTester].
+   * If true, the private previews will be included in the test.
    */
-  val testerQualifiedClassName: Property<String> = objects.property(String::class.java)
-    .convention("com.github.takahirom.roborazzi.AndroidComposePreviewTester")
+  val includePrivatePreviews: Property<Boolean> = objects.property(Boolean::class.java)
+    .convention(false)
 
   /**
    * [robolectricConfig] will be passed to the Robolectric's @Config annotation in the generated test class.
@@ -48,6 +49,14 @@ open class GenerateComposePreviewRobolectricTestsExtension @Inject constructor(o
           "qualifiers" to "RobolectricDeviceQualifiers.Pixel4a",
         )
       )
+
+  /**
+   * The fully qualified class name of the custom test class that implements [com.github.takahirom.roborazzi.ComposePreviewTester].
+   * This is advanced usage. You can implement your own test class that implements [com.github.takahirom.roborazzi.ComposePreviewTester].
+   */
+  val testerQualifiedClassName: Property<String> = objects.property(String::class.java)
+    .convention("com.github.takahirom.roborazzi.AndroidComposePreviewTester")
+
 }
 
 fun generateComposePreviewRobolectricTestsIfNeeded(
@@ -63,9 +72,10 @@ fun generateComposePreviewRobolectricTestsIfNeeded(
   setupGenerateComposePreviewRobolectricTestsTask(
     project = project,
     variant = variant,
-    scanPackages = extension.packages,
+    extension = extension,
     testerQualifiedClassName = extension.testerQualifiedClassName,
-    robolectricConfig = extension.robolectricConfig
+    robolectricConfig = extension.robolectricConfig,
+    testTaskProvider = testTaskProvider
   )
   project.afterEvaluate {
     // We use afterEvaluate only for verify
@@ -79,23 +89,25 @@ fun generateComposePreviewRobolectricTestsIfNeeded(
 private fun setupGenerateComposePreviewRobolectricTestsTask(
   project: Project,
   variant: Variant,
-  scanPackages: ListProperty<String>,
+  extension: GenerateComposePreviewRobolectricTestsExtension,
   testerQualifiedClassName: Property<String>,
   robolectricConfig: MapProperty<String, String>,
+  testTaskProvider: TaskCollection<Test>
 ) {
-  check(scanPackages.get().orEmpty().isNotEmpty()) {
-    "Please set roborazzi.generateRobolectricPreviewTests.packages in the generatePreviewTests extension or set roborazzi.generateRobolectricPreviewTests.enable = false." +
+  check(extension.packages.get().orEmpty().isNotEmpty()) {
+    "Please set roborazzi.generateComposePreviewRobolectricTests.packages in the generatePreviewTests extension or set roborazzi.generateComposePreviewRobolectricTests.enable = false." +
       "See https://github.com/sergio-sastre/ComposablePreviewScanner?tab=readme-ov-file#how-to-use for more information."
   }
 
   val generateTestsTask = project.tasks.register(
-    "generate${variant.name.capitalize()}ComposePreviewRobolectricTests",
+    "generate${variant.name.capitalize(Locale.ROOT)}ComposePreviewRobolectricTests",
     GenerateComposePreviewRobolectricTestsTask::class.java
   ) {
     // It seems that this directory path is overridden by addGeneratedSourceDirectory.
     // The generated tests will be located in build/JAVA/generate[VariantName]ComposePreviewRobolectricTests.
     it.outputDir.set(project.layout.buildDirectory.dir("generated/roborazzi/preview-screenshot"))
-    it.scanPackageTrees.set(scanPackages)
+    it.scanPackageTrees.set(extension.packages)
+    it.includePrivatePreviews.set(extension.includePrivatePreviews)
     it.testerQualifiedClassName.set(testerQualifiedClassName)
     it.robolectricConfig.set(robolectricConfig)
   }
@@ -105,6 +117,10 @@ private fun setupGenerateComposePreviewRobolectricTestsTask(
     generateTestsTask,
     GenerateComposePreviewRobolectricTestsTask::outputDir
   )
+  // It seems that the addGeneratedSourceDirectory does not affect the inputs.dir and does not invalidate the task.
+  testTaskProvider.configureEach {
+    it.inputs.dir(generateTestsTask.flatMap { it.outputDir })
+  }
 }
 
 abstract class GenerateComposePreviewRobolectricTestsTask : DefaultTask() {
@@ -114,6 +130,9 @@ abstract class GenerateComposePreviewRobolectricTestsTask : DefaultTask() {
   @get:Input
   var scanPackageTrees: ListProperty<String> = project.objects.listProperty(String::class.java)
 
+  @get:Input
+  abstract val includePrivatePreviews: Property<Boolean>
+
   @get:Input
   abstract val testerQualifiedClassName: Property<String>
 
@@ -126,6 +145,7 @@ abstract class GenerateComposePreviewRobolectricTestsTask : DefaultTask() {
     testDir.mkdirs()
 
     val packagesExpr = scanPackageTrees.get().joinToString(", ") { "\"$it\"" }
+    val includePrivatePreviewsExpr = includePrivatePreviews.get()
 
     val generatedClassFQDN = "com.github.takahirom.roborazzi.RoborazziPreviewParameterizedTests"
     val packageName = generatedClassFQDN.substringBeforeLast(".")
@@ -140,8 +160,11 @@ abstract class GenerateComposePreviewRobolectricTestsTask : DefaultTask() {
     File(directory, "$className.kt").writeText(
       """
             package $packageName
+            import org.junit.Rule
             import org.junit.Test
             import org.junit.runner.RunWith
+            import org.junit.rules.TestWatcher
+            import org.junit.rules.RuleChain
             import org.robolectric.ParameterizedRobolectricTestRunner
             import org.robolectric.annotation.Config
             import org.robolectric.annotation.GraphicsMode
@@ -153,40 +176,56 @@ abstract class GenerateComposePreviewRobolectricTestsTask : DefaultTask() {
 
 
             @RunWith(ParameterizedRobolectricTestRunner::class)
-            @OptIn(InternalRoborazziApi::class)
+            @OptIn(InternalRoborazziApi::class, ExperimentalRoborazziApi::class)
             @GraphicsMode(GraphicsMode.Mode.NATIVE)
             class $className(
                 private val preview: ComposablePreview<Any>,
             ) {
-
-                companion object {
-                    // lazy for performance
-                    val previews: List<ComposablePreview<Any>> by lazy {
-                        getComposePreviewTester("$testerQualifiedClassNameString").previews(
-                            $packagesExpr
-                        )
-                    }
-                    @JvmStatic
-                    @ParameterizedRobolectricTestRunner.Parameters(name = "{0}")
-                    fun values(): List<ComposablePreview<Any>> =
-                        previews
-                }
-                
+                private val tester = getComposePreviewTester("$testerQualifiedClassNameString")
+                private val testLifecycleOptions = tester.options().testLifecycleOptions
+                @get:Rule
+                val rule = RuleChain.outerRule(
+                  if(testLifecycleOptions is ComposePreviewTester.Options.JUnit4TestLifecycleOptions) {
+                    (testLifecycleOptions as ComposePreviewTester.Options.JUnit4TestLifecycleOptions).testRuleFactory()
+                  } else {
+                    object : TestWatcher() {}
+                  }
+                )
                 
                 @GraphicsMode(GraphicsMode.Mode.NATIVE)
                 $robolectricConfigString
                 @Test
                 fun test() {
-                    getComposePreviewTester("$testerQualifiedClassNameString").test(preview)
+                    tester.test(preview)
                 }
-
+                
+                companion object {
+                    // lazy for performance
+                    val previews: List<ComposablePreview<Any>> by lazy {
+                        setupDefaultOptions()
+                        val tester = getComposePreviewTester("$testerQualifiedClassNameString")
+                        tester.previews()
+                    }
+                    @JvmStatic
+                    @ParameterizedRobolectricTestRunner.Parameters(name = "{0}")
+                    fun values(): List<ComposablePreview<Any>> = previews 
+                    
+                    fun setupDefaultOptions() {
+                        ComposePreviewTester.defaultOptionsFromPlugin = ComposePreviewTester.Options(
+                            scanOptions = ComposePreviewTester.Options.ScanOptions(
+                              packages = listOf($packagesExpr),
+                              includePrivatePreviews = $includePrivatePreviewsExpr, 
+                            )
+                        )
+                    }
+                } 
             }
         """.trimIndent()
     )
   }
 }
 
-fun verifyGenerateRobolectricPreviewTests(
+fun verifyGenerateComposePreviewRobolectricTests(
   project: Project,
   androidExtension: TestedExtension,
   extension: GenerateComposePreviewRobolectricTestsExtension
@@ -289,7 +328,8 @@ private fun verifyLibraryDependencies(
     val dependencies = this
     val libNameArray = libraryName.split(":")
     if (!dependencies.contains(libNameArray[0] to libNameArray[1])) {
-      val configurationNames = "'testImplementation'(For Android Project) or 'kotlin.sourceSets.androidUnitTest.dependencies.implementation'(For KMP)"
+      val configurationNames =
+        "'testImplementation'(For Android Project) or 'kotlin.sourceSets.androidUnitTest.dependencies.implementation'(For KMP)"
       error(
         "Roborazzi: Please add the following $configurationNames dependency to the 'dependencies' block in the 'build.gradle' file: '$libraryName' for the $configurationNames configuration.\n" +
           "For your convenience, visit https://www.google.com/search?q=" + URLEncoder.encode(