Repository: takahirom/roborazzi
Pull request #438 (2024-07-17 02:18:48)

Rename ComposePreviewRobolectricTest to ComposePreviewTester to support custom Preview annotation tests in Experimental Compose Preview Support


Patch:
diff --git a/README.md b/README.md
index 6b5bf8f3..c04b5786 100644
--- a/README.md
+++ b/README.md
@@ -981,7 +981,7 @@ roborazzi {
     // 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.RobolectricPreviewTest].
-    customTestQualifiedClassName = "com.example.MyCustomRobolectricPreviewTest"
+    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(
diff --git a/docs/topics/preview_support.md b/docs/topics/preview_support.md
index 27961631..23188a7d 100644
--- a/docs/topics/preview_support.md
+++ b/docs/topics/preview_support.md
@@ -31,7 +31,7 @@ roborazzi {
     // 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.RobolectricPreviewTest].
-    customTestQualifiedClassName = "com.example.MyCustomRobolectricPreviewTest"
+    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(
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 3392aaf2..7e4e6c79 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
@@ -432,7 +432,7 @@ abstract class RoborazziPlugin : Plugin<Project> {
         val variantSlug = variant.name.capitalizeUS()
         val testVariantSlug = unitTest.name.capitalizeUS()
         val testTaskName = "test$testVariantSlug"
-        generateRobolectricPreviewTestsIfNeeded(
+        generateComposePreviewRobolectricTestsIfNeeded(
           project = project,
           variant = variant,
           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 b104f1bb..33732e0e 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
@@ -18,16 +18,28 @@ fun ComposablePreview<AndroidPreviewInfo>.captureRoboImage(
 }
 
 @ExperimentalRoborazziApi
-interface RobolectricPreviewTest {
-  fun previews(vararg packages: String): List<ComposablePreview<AndroidPreviewInfo>>
+interface ComposePreviewTester<T : Any> {
+  /**
+   * 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>>
 
+  /**
+   * Performs a test on a single composable preview.
+   * Note: This method will not be called as the same instance of previews() method.
+   *
+   * @param preview The ComposablePreview object to be tested.
+   */
   fun test(
-    preview: ComposablePreview<AndroidPreviewInfo>,
+    preview: ComposablePreview<T>,
   )
 }
 
 @InternalRoborazziApi
-class DefaultRobolectricPreviewTest : RobolectricPreviewTest {
+class AndroidComposePreviewTester : ComposePreviewTester<AndroidPreviewInfo> {
   override fun previews(vararg packages: String): List<ComposablePreview<AndroidPreviewInfo>> {
     return AndroidComposablePreviewScanner()
       .scanPackageTrees(*packages)
@@ -41,7 +53,10 @@ class DefaultRobolectricPreviewTest : RobolectricPreviewTest {
       } else {
         ""
       }
-    val name = roborazziDefaultNamingStrategy().generateOutputName(preview.declaringClass, createScreenshotIdFor(preview))
+    val name = roborazziDefaultNamingStrategy().generateOutputName(
+      preview.declaringClass,
+      createScreenshotIdFor(preview)
+    )
     val filePath = "$pathPrefix$name.png"
     preview.captureRoboImage(filePath)
   }
@@ -53,16 +68,17 @@ class DefaultRobolectricPreviewTest : RobolectricPreviewTest {
 }
 
 @InternalRoborazziApi
-fun getRobolectricPreviewTest(customTestQualifiedClassName: String): RobolectricPreviewTest {
-  val customTestClass = try {
-    Class.forName(customTestQualifiedClassName)
+fun getComposePreviewTester(testerQualifiedClassName: String): ComposePreviewTester<Any> {
+  val customTesterClass = try {
+    Class.forName(testerQualifiedClassName)
   } catch (e: ClassNotFoundException) {
-    throw IllegalArgumentException("The class $customTestQualifiedClassName not found")
+    throw IllegalArgumentException("The class $testerQualifiedClassName not found")
   }
-  if (!RobolectricPreviewTest::class.java.isAssignableFrom(customTestClass)) {
-    throw IllegalArgumentException("The class $customTestQualifiedClassName must implement RobolectricPreviewCapturer")
+  if (!ComposePreviewTester::class.java.isAssignableFrom(customTesterClass)) {
+    throw IllegalArgumentException("The class $testerQualifiedClassName must implement RobolectricPreviewCapturer")
   }
-  val robolectricPreviewTest =
-    customTestClass.getDeclaredConstructor().newInstance() as RobolectricPreviewTest
-  return robolectricPreviewTest
+  @Suppress("UNCHECKED_CAST")
+  val composePreviewTester =
+    customTesterClass.getDeclaredConstructor().newInstance() as ComposePreviewTester<Any>
+  return composePreviewTester
 }


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 d9d70b7b..663e7005 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
@@ -44,6 +44,14 @@ 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 androidBlock = """
           android {
             namespace = "com.github.takahirom.preview.tests"
@@ -94,6 +102,11 @@ class PreviewModule(
               androidTarget()
               
               sourceSets {
+                  val commonMain by getting {
+                      dependencies {
+                          api(compose.components.uiToolingPreview)
+                      }
+                  }
                   val androidMain by getting {
                       dependencies {
                           implementation(compose.material3)
@@ -123,12 +136,7 @@ class PreviewModule(
 
           $androidBlock
 
-          roborazzi {
-              generateComposePreviewRobolectricTests {
-                  enable = true
-                  packages = listOf("com.github.takahirom.preview.tests")
-              }
-          }
+          $roborazziExtension
 
           repositories {
               mavenCentral()
@@ -144,12 +152,7 @@ class PreviewModule(
     id("io.github.takahirom.roborazzi")
   }
 
-  roborazzi {
-    generateComposePreviewRobolectricTests {
-      enable = true
-      packages = listOf("com.github.takahirom.preview.tests")
-    }
-  }
+  $roborazziExtension
 
   repositories {
     mavenCentral()
diff --git a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/GenerateRobolectricComposePreviewTestsExtension.kt b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/GenerateComposePreviewRobolectricTestsExtension.kt
similarity index 87%
rename from include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/GenerateRobolectricComposePreviewTestsExtension.kt
rename to include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/GenerateComposePreviewRobolectricTestsExtension.kt
index 83657b50..e544be0b 100644
--- a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/GenerateRobolectricComposePreviewTestsExtension.kt
+++ b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/GenerateComposePreviewRobolectricTestsExtension.kt
@@ -31,10 +31,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.RobolectricPreviewTest].
+   * The fully qualified class name of the custom test class that implements [com.github.takahirom.roborazzi.ComposePreviewTester].
    */
-  val customTestQualifiedClassName: Property<String> = objects.property(String::class.java)
-    .convention("com.github.takahirom.roborazzi.DefaultRobolectricPreviewTest")
+  val testerQualifiedClassName: Property<String> = objects.property(String::class.java)
+    .convention("com.github.takahirom.roborazzi.AndroidComposePreviewTester")
 
   /**
    * [robolectricConfig] will be passed to the Robolectric's @Config annotation in the generated test class.
@@ -50,7 +50,7 @@ open class GenerateComposePreviewRobolectricTestsExtension @Inject constructor(o
       )
 }
 
-fun generateRobolectricPreviewTestsIfNeeded(
+fun generateComposePreviewRobolectricTestsIfNeeded(
   project: Project,
   variant: Variant,
   extension: GenerateComposePreviewRobolectricTestsExtension,
@@ -60,11 +60,11 @@ fun generateRobolectricPreviewTestsIfNeeded(
     return
   }
   val logger = project.logger
-  setupGeneratePreviewTestsTask(
+  setupGenerateComposePreviewRobolectricTestsTask(
     project = project,
     variant = variant,
     scanPackages = extension.packages,
-    customTestQualifiedClassName = extension.customTestQualifiedClassName,
+    testerQualifiedClassName = extension.testerQualifiedClassName,
     robolectricConfig = extension.robolectricConfig
   )
   project.afterEvaluate {
@@ -76,11 +76,11 @@ fun generateRobolectricPreviewTestsIfNeeded(
   }
 }
 
-private fun setupGeneratePreviewTestsTask(
+private fun setupGenerateComposePreviewRobolectricTestsTask(
   project: Project,
   variant: Variant,
   scanPackages: ListProperty<String>,
-  customTestQualifiedClassName: Property<String>,
+  testerQualifiedClassName: Property<String>,
   robolectricConfig: MapProperty<String, String>,
 ) {
   check(scanPackages.get().orEmpty().isNotEmpty()) {
@@ -89,25 +89,25 @@ private fun setupGeneratePreviewTestsTask(
   }
 
   val generateTestsTask = project.tasks.register(
-    "generate${variant.name.capitalize()}PreviewScreenshotTests",
-    GeneratePreviewScreenshotTestsTask::class.java
+    "generate${variant.name.capitalize()}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]PreviewScreenshotTests.
+    // 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.customTestQualifiedClassName.set(customTestQualifiedClassName)
+    it.testerQualifiedClassName.set(testerQualifiedClassName)
     it.robolectricConfig.set(robolectricConfig)
   }
   // We need to use sources.java here; otherwise, the generate task will not be executed.
   // https://stackoverflow.com/a/76870110/4339442
   variant.unitTest?.sources?.java?.addGeneratedSourceDirectory(
     generateTestsTask,
-    GeneratePreviewScreenshotTestsTask::outputDir
+    GenerateComposePreviewRobolectricTestsTask::outputDir
   )
 }
 
-abstract class GeneratePreviewScreenshotTestsTask : DefaultTask() {
+abstract class GenerateComposePreviewRobolectricTestsTask : DefaultTask() {
   @get:OutputDirectory
   abstract val outputDir: DirectoryProperty
 
@@ -115,7 +115,7 @@ abstract class GeneratePreviewScreenshotTestsTask : DefaultTask() {
   var scanPackageTrees: ListProperty<String> = project.objects.listProperty(String::class.java)
 
   @get:Input
-  abstract val customTestQualifiedClassName: Property<String>
+  abstract val testerQualifiedClassName: Property<String>
 
   @get:Input
   abstract val robolectricConfig: MapProperty<String, String>
@@ -136,7 +136,7 @@ abstract class GeneratePreviewScreenshotTestsTask : DefaultTask() {
       "@Config(" + robolectricConfig.get().entries.joinToString(", ") { (key, value) ->
         "$key = $value"
       } + ")"
-    val customTestQualifiedClassNameString = customTestQualifiedClassName.get()
+    val testerQualifiedClassNameString = testerQualifiedClassName.get()
     File(directory, "$className.kt").writeText(
       """
             package $packageName
@@ -156,19 +156,19 @@ abstract class GeneratePreviewScreenshotTestsTask : DefaultTask() {
             @OptIn(InternalRoborazziApi::class)
             @GraphicsMode(GraphicsMode.Mode.NATIVE)
             class $className(
-                private val preview: ComposablePreview<AndroidPreviewInfo>,
+                private val preview: ComposablePreview<Any>,
             ) {
 
                 companion object {
                     // lazy for performance
-                    val previews: List<ComposablePreview<AndroidPreviewInfo>> by lazy {
-                        getRobolectricPreviewTest("$customTestQualifiedClassNameString").previews(
+                    val previews: List<ComposablePreview<Any>> by lazy {
+                        getComposePreviewTester("$testerQualifiedClassNameString").previews(
                             $packagesExpr
                         )
                     }
                     @JvmStatic
                     @ParameterizedRobolectricTestRunner.Parameters(name = "{0}")
-                    fun values(): List<ComposablePreview<AndroidPreviewInfo>> =
+                    fun values(): List<ComposablePreview<Any>> =
                         previews
                 }
                 
@@ -177,7 +177,7 @@ abstract class GeneratePreviewScreenshotTestsTask : DefaultTask() {
                 $robolectricConfigString
                 @Test
                 fun test() {
-                    getRobolectricPreviewTest("$customTestQualifiedClassNameString").test(preview)
+                    getComposePreviewTester("$testerQualifiedClassNameString").test(preview)
                 }
 
             }
@@ -289,7 +289,7 @@ private fun verifyLibraryDependencies(
     val dependencies = this
     val libNameArray = libraryName.split(":")
     if (!dependencies.contains(libNameArray[0] to libNameArray[1])) {
-      val configurationNames = "'testImplementation'"
+      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(