From 0d763a021c376532169e383abfbdda9980bf1390 Mon Sep 17 00:00:00 2001 From: takahirom Date: Wed, 26 Jun 2024 12:14:53 +0900 Subject: [PATCH 01/24] Implement autoPreviewScreenshots --- build.gradle | 2 +- .../takahirom/roborazzi/PreviewScreenshots.kt | 121 ++++++++++++++++++ .../takahirom/roborazzi/RoborazziPlugin.kt | 9 +- sample-auto-preview/.gitignore | 1 + sample-auto-preview/build.gradle.kts | 50 ++++++++ sample-auto-preview/consumer-rules.pro | 0 sample-auto-preview/proguard-rules.pro | 21 +++ .../sample/ExampleInstrumentedTest.kt | 24 ++++ .../src/main/AndroidManifest.xml | 4 + .../com/github/takahirom/sample/Preview.kt | 14 ++ .../takahirom/sample/ExampleUnitTest.kt | 17 +++ settings.gradle | 1 + 12 files changed, 262 insertions(+), 2 deletions(-) create mode 100644 include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/PreviewScreenshots.kt create mode 100644 sample-auto-preview/.gitignore create mode 100644 sample-auto-preview/build.gradle.kts create mode 100644 sample-auto-preview/consumer-rules.pro create mode 100644 sample-auto-preview/proguard-rules.pro create mode 100644 sample-auto-preview/src/androidTest/java/com/github/takahirom/sample/ExampleInstrumentedTest.kt create mode 100644 sample-auto-preview/src/main/AndroidManifest.xml create mode 100644 sample-auto-preview/src/main/java/com/github/takahirom/sample/Preview.kt create mode 100644 sample-auto-preview/src/test/java/com/github/takahirom/sample/ExampleUnitTest.kt diff --git a/build.gradle b/build.gradle index d8581a04..b83676c2 100644 --- a/build.gradle +++ b/build.gradle @@ -21,7 +21,7 @@ allprojects { def javaTargetVersion = JavaVersion.toVersion(javaVersion) def jvmTargetVersion = org.jetbrains.kotlin.gradle.dsl.JvmTarget.@Companion.fromTarget(javaVersion) - plugins.withId("java") { + plugins.withId("java") { java { toolchain { languageVersion.set(JavaLanguageVersion.of(toolchainVersion)) diff --git a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/PreviewScreenshots.kt b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/PreviewScreenshots.kt new file mode 100644 index 00000000..068b5dcc --- /dev/null +++ b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/PreviewScreenshots.kt @@ -0,0 +1,121 @@ +package io.github.takahirom.roborazzi + +import com.android.build.api.variant.Variant +import org.gradle.api.DefaultTask +import org.gradle.api.Project +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.TaskAction +import java.io.File +import javax.inject.Inject + + +open class AutoPreviewScreenshotsExtension @Inject constructor(objects: ObjectFactory) { + val enabled: Property = objects.property(Boolean::class.java) + val scanPackages: ListProperty = objects.listProperty(String::class.java) +} + +fun setupGeneratedScreenshotTest( + project: Project, + variant: Variant, + extension: AutoPreviewScreenshotsExtension +) { + val scanPackages = extension.scanPackages.get() + assert(scanPackages.isNotEmpty()) + addPreviewScreenshotLibraries(variant, project) + val generateTestsTask = project.tasks.register( + "generate${variant.name.capitalize()}PreviewScreenshotTests", + GeneratePreviewScreenshotTestsTask::class.java + ) { + it.outputDir.set(project.layout.buildDirectory.dir("generated/roborazzi/preview-screenshot")) + it.scanPackages.set(scanPackages) + } + variant.unitTest?.sources?.java?.addGeneratedSourceDirectory( + generateTestsTask, + GeneratePreviewScreenshotTestsTask::outputDir + ) +} + +private fun addPreviewScreenshotLibraries( + variant: Variant, + project: Project +) { + val configurationName = "${variant.name}Implementation" + + project.dependencies.add( + configurationName, + "io.github.takahirom.roborazzi:roborazzi-compose:1.20.0" + ) + project.dependencies.add(configurationName, "io.github.takahirom.roborazzi:roborazzi:1.20.0") + project.dependencies.add(configurationName, "junit:junit:4.13.2") + project.dependencies.add(configurationName, "org.robolectric:robolectric:4.12.2") + + project.repositories.add(project.repositories.maven { it.setUrl("https://jitpack.io") }) + project.repositories.add(project.repositories.mavenCentral()) + project.repositories.add(project.repositories.google()) + project.dependencies.add( + configurationName, + "com.github.sergio-sastre.ComposablePreviewScanner:android:0.1.2" + ) +} + +abstract class GeneratePreviewScreenshotTestsTask : DefaultTask() { + @get:OutputDirectory + abstract val outputDir: DirectoryProperty + + @get:Input + var scanPackages: ListProperty = project.objects.listProperty(String::class.java) + + @TaskAction + fun generateTests() { + val testDir = outputDir.get().asFile + testDir.mkdirs() + + val packagesExpr = scanPackages.get().joinToString(", ") { "\"$it\"" } + val scanPackageTreeExpr = ".scanPackageTrees($packagesExpr)" + + File(testDir, "GeneratedPreviewScreenshotTest.kt").writeText( + """ + import org.junit.Test + import org.junit.runner.RunWith + import org.robolectric.ParameterizedRobolectricTestRunner + import org.robolectric.annotation.Config + import org.robolectric.annotation.GraphicsMode + import com.github.takahirom.roborazzi.captureRoboImage + import sergio.sastre.composable.preview.scanner.core.preview.ComposablePreview + import sergio.sastre.composable.preview.scanner.android.AndroidPreviewInfo + import sergio.sastre.composable.preview.scanner.android.AndroidComposablePreviewScanner + + @RunWith(ParameterizedRobolectricTestRunner::class) + @GraphicsMode(GraphicsMode.Mode.NATIVE) + class PreviewParameterizedTests( + private val preview: ComposablePreview, + ) { + + companion object { + @JvmStatic + @ParameterizedRobolectricTestRunner.Parameters + fun values(): List> = + AndroidComposablePreviewScanner() + $scanPackageTreeExpr + .getPreviews() + } + + @GraphicsMode(GraphicsMode.Mode.NATIVE) + @Config(sdk = [30]) + @Test + fun snapshot() { + captureRoboImage() { + preview() + } + } + + } + """.trimIndent() + ) + } +} \ No newline at end of file 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 663aab28..b944dce3 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 @@ -46,8 +46,11 @@ private const val DEFAULT_TEMP_DIR = "intermediates/roborazzi" */ open class RoborazziExtension @Inject constructor(objects: ObjectFactory) { val outputDir: DirectoryProperty = objects.directoryProperty() + val autoPreviewScreenshots: AutoPreviewScreenshotsExtension = objects.newInstance(AutoPreviewScreenshotsExtension::class.java) + fun automaticPreviewScreenshots(action: Action) { + action.execute(autoPreviewScreenshots) + } } - @Suppress("unused") // From Paparazzi: https://github.com/cashapp/paparazzi/blob/a76702744a7f380480f323ffda124e845f2733aa/paparazzi/paparazzi-gradle-plugin/src/main/java/app/cash/paparazzi/gradle/PaparazziPlugin.kt abstract class RoborazziPlugin : Plugin { @@ -415,6 +418,10 @@ abstract class RoborazziPlugin : Plugin { val unitTest = variant.unitTest ?: return@onVariants val variantSlug = variant.name.capitalizeUS() val testVariantSlug = unitTest.name.capitalizeUS() + val isEnableAutomaticPreviewScreenshots = extension.autoPreviewScreenshots.enabled.convention(false).get() + if (isEnableAutomaticPreviewScreenshots) { + setupGeneratedScreenshotTest(project, variant, extension.autoPreviewScreenshots) + } // e.g. testDebugUnitTest -> recordRoborazziDebug configureRoborazziTasks( diff --git a/sample-auto-preview/.gitignore b/sample-auto-preview/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/sample-auto-preview/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/sample-auto-preview/build.gradle.kts b/sample-auto-preview/build.gradle.kts new file mode 100644 index 00000000..4f752865 --- /dev/null +++ b/sample-auto-preview/build.gradle.kts @@ -0,0 +1,50 @@ +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") + id("io.github.takahirom.roborazzi") +} + +roborazzi { + automaticPreviewScreenshots { + enabled = true + scanPackages = listOf("com.github.takahirom.sample") + } +} + +android { + namespace = "com.github.takahirom.sample" + compileSdk = 34 + + defaultConfig { + minSdk = 24 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get() + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } +} + +dependencies { + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.tooling) + + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.test.ext.junit) + androidTestImplementation(libs.androidx.test.espresso.core) +} \ No newline at end of file diff --git a/sample-auto-preview/consumer-rules.pro b/sample-auto-preview/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/sample-auto-preview/proguard-rules.pro b/sample-auto-preview/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/sample-auto-preview/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/sample-auto-preview/src/androidTest/java/com/github/takahirom/sample/ExampleInstrumentedTest.kt b/sample-auto-preview/src/androidTest/java/com/github/takahirom/sample/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..ee29d022 --- /dev/null +++ b/sample-auto-preview/src/androidTest/java/com/github/takahirom/sample/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.github.takahirom.sample + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.github.takahirom.sample.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/sample-auto-preview/src/main/AndroidManifest.xml b/sample-auto-preview/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/sample-auto-preview/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/sample-auto-preview/src/main/java/com/github/takahirom/sample/Preview.kt b/sample-auto-preview/src/main/java/com/github/takahirom/sample/Preview.kt new file mode 100644 index 00000000..03813057 --- /dev/null +++ b/sample-auto-preview/src/main/java/com/github/takahirom/sample/Preview.kt @@ -0,0 +1,14 @@ +package com.github.takahirom.sample + +import androidx.compose.foundation.layout.Box +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview + +@Preview +@Composable +fun Preview() { + Box { + Text("Hello, World!") + } +} diff --git a/sample-auto-preview/src/test/java/com/github/takahirom/sample/ExampleUnitTest.kt b/sample-auto-preview/src/test/java/com/github/takahirom/sample/ExampleUnitTest.kt new file mode 100644 index 00000000..6289244c --- /dev/null +++ b/sample-auto-preview/src/test/java/com/github/takahirom/sample/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +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/settings.gradle b/settings.gradle index 324894b7..7138ac61 100644 --- a/settings.gradle +++ b/settings.gradle @@ -26,6 +26,7 @@ include ':sample-android' include ':sample-android-without-compose' include ':sample-compose-desktop-multiplatform' include ':sample-compose-desktop-jvm' +include ':sample-auto-preview' includeBuild("include-build") { dependencySubstitution { From 887de838e2120d138b4be8ed49799c5aead1ba31 Mon Sep 17 00:00:00 2001 From: takahirom Date: Wed, 26 Jun 2024 12:41:08 +0900 Subject: [PATCH 02/24] Fix test dependency --- .../java/io/github/takahirom/roborazzi/PreviewScreenshots.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/PreviewScreenshots.kt b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/PreviewScreenshots.kt index 068b5dcc..cb62081f 100644 --- a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/PreviewScreenshots.kt +++ b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/PreviewScreenshots.kt @@ -44,7 +44,7 @@ private fun addPreviewScreenshotLibraries( variant: Variant, project: Project ) { - val configurationName = "${variant.name}Implementation" + val configurationName = "test${variant.name.capitalize()}Implementation" project.dependencies.add( configurationName, From ba06ac50f8f092a582b17d5ea9be78945bbcbae8 Mon Sep 17 00:00:00 2001 From: takahirom Date: Wed, 26 Jun 2024 13:12:26 +0900 Subject: [PATCH 03/24] Fix plugin version const problem --- gradle/libs.versions.toml | 2 + .../roborazzi-gradle-plugin/build.gradle | 45 +++++++++++++++++++ ....kt => AutoPreviewScreenshotsExtension.kt} | 11 ++--- 3 files changed, 53 insertions(+), 5 deletions(-) rename include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/{PreviewScreenshots.kt => AutoPreviewScreenshotsExtension.kt} (89%) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 69a37ed7..07265670 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -43,6 +43,8 @@ webjar-material-design-icons = "4.0.0" webjar-materialize = "1.0.0" webjars-locator-lite = "0.0.4" +composable-preview-scanner = "0.1.2" + [libraries] roborazzi = { module = "io.github.takahirom.roborazzi:roborazzi", version.ref = "roborazzi-for-replacing-by-include-build" } roborazzi-junit-rule = { module = "io.github.takahirom.roborazzi:roborazzi-junit-rule", version.ref = "roborazzi-for-replacing-by-include-build" } diff --git a/include-build/roborazzi-gradle-plugin/build.gradle b/include-build/roborazzi-gradle-plugin/build.gradle index 3d768fac..11f3a80f 100644 --- a/include-build/roborazzi-gradle-plugin/build.gradle +++ b/include-build/roborazzi-gradle-plugin/build.gradle @@ -10,6 +10,51 @@ apply plugin: 'java-gradle-plugin' def integrationTestDep = sourceSets.create('integrationTestDep') +def generatedSourcesDir = "${project.layout.buildDirectory.get()}/generated/sources/buildConfig/java/main" +sourceSets { + main { + java { + srcDir(generatedSourcesDir) + } + } +} + +tasks.register("generateBuildConfig") { + doLast { + def buildConfigFile = new File(generatedSourcesDir, "com/example/BuildConfig.kt") + buildConfigFile.parentFile.mkdirs() + + def libs = ["composable-preview-scanner", "junit", "robolectric"] +// def composablePreviewScannerVersion = project.extensions.getByType(VersionCatalogsExtension.class) +// .named("libs") +// .findVersion("composable-preview-scanner") +// .get() + def versionMapString = libs.collect { + def version = project.extensions.getByType(VersionCatalogsExtension.class) + .named("libs") + .findVersion(it) + .get() + "\"${it}\" to \"${version}\"," + }.join("\n") + println(version) + def libString = "val libraryVersionsMap = mapOf(" + "\"roborazzi\" to \"$VERSION_NAME\", \n" +versionMapString + ")\n" + "" + buildConfigFile.text = """ +package io.github.takahirom.roborazzi +class BuildConfig { + companion object { + ${libString} + } +} + """ + } +} + +tasks.named("compileKotlin") { + dependsOn("generateBuildConfig") +} + + dependencies { compileOnly gradleApi() compileOnly libs.android.tools.build.gradle diff --git a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/PreviewScreenshots.kt b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt similarity index 89% rename from include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/PreviewScreenshots.kt rename to include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt index cb62081f..5fa7684c 100644 --- a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/PreviewScreenshots.kt +++ b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt @@ -46,20 +46,21 @@ private fun addPreviewScreenshotLibraries( ) { val configurationName = "test${variant.name.capitalize()}Implementation" + val roborazziVersion = BuildConfig.libraryVersionsMap["roborazzi"] project.dependencies.add( configurationName, - "io.github.takahirom.roborazzi:roborazzi-compose:1.20.0" + "io.github.takahirom.roborazzi:roborazzi-compose:$roborazziVersion" ) - project.dependencies.add(configurationName, "io.github.takahirom.roborazzi:roborazzi:1.20.0") - project.dependencies.add(configurationName, "junit:junit:4.13.2") - project.dependencies.add(configurationName, "org.robolectric:robolectric:4.12.2") + project.dependencies.add(configurationName, "io.github.takahirom.roborazzi:roborazzi:$roborazziVersion") + project.dependencies.add(configurationName, "junit:junit:${BuildConfig.libraryVersionsMap["junit"]}") + project.dependencies.add(configurationName, "org.robolectric:robolectric:${BuildConfig.libraryVersionsMap["robolectric"]}") project.repositories.add(project.repositories.maven { it.setUrl("https://jitpack.io") }) project.repositories.add(project.repositories.mavenCentral()) project.repositories.add(project.repositories.google()) project.dependencies.add( configurationName, - "com.github.sergio-sastre.ComposablePreviewScanner:android:0.1.2" + "com.github.sergio-sastre.ComposablePreviewScanner:android:${BuildConfig.libraryVersionsMap["composable-preview-scanner"]}" ) } From f862534c72ad491ee20f453b6439e69a5f5c5779 Mon Sep 17 00:00:00 2001 From: takahirom Date: Wed, 26 Jun 2024 13:18:59 +0900 Subject: [PATCH 04/24] Fix timing of accessing VersionCatalogsExtension --- .../roborazzi-gradle-plugin/build.gradle | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/include-build/roborazzi-gradle-plugin/build.gradle b/include-build/roborazzi-gradle-plugin/build.gradle index 11f3a80f..1044700f 100644 --- a/include-build/roborazzi-gradle-plugin/build.gradle +++ b/include-build/roborazzi-gradle-plugin/build.gradle @@ -20,24 +20,21 @@ sourceSets { } tasks.register("generateBuildConfig") { - doLast { - def buildConfigFile = new File(generatedSourcesDir, "com/example/BuildConfig.kt") - buildConfigFile.parentFile.mkdirs() - - def libs = ["composable-preview-scanner", "junit", "robolectric"] -// def composablePreviewScannerVersion = project.extensions.getByType(VersionCatalogsExtension.class) -// .named("libs") -// .findVersion("composable-preview-scanner") -// .get() - def versionMapString = libs.collect { + def libs = ["composable-preview-scanner", "junit", "robolectric"] + def versionMapString = "\"roborazzi\" to \"$VERSION_NAME\", \n" + ( + libs.collect { def version = project.extensions.getByType(VersionCatalogsExtension.class) .named("libs") .findVersion(it) .get() "\"${it}\" to \"${version}\"," - }.join("\n") + }.join("\n")) + doLast { + def buildConfigFile = new File(generatedSourcesDir, "com/example/BuildConfig.kt") + buildConfigFile.parentFile.mkdirs() + println(version) - def libString = "val libraryVersionsMap = mapOf(" + "\"roborazzi\" to \"$VERSION_NAME\", \n" +versionMapString + ")\n" + def libString = "val libraryVersionsMap = mapOf(" + versionMapString + ")\n" "" buildConfigFile.text = """ package io.github.takahirom.roborazzi From d38cbaf62f430b7761ba68d4a6625809cfba10bb Mon Sep 17 00:00:00 2001 From: takahirom Date: Wed, 26 Jun 2024 13:29:53 +0900 Subject: [PATCH 05/24] Fix configuration cache --- include-build/roborazzi-gradle-plugin/build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/include-build/roborazzi-gradle-plugin/build.gradle b/include-build/roborazzi-gradle-plugin/build.gradle index 1044700f..f87aca72 100644 --- a/include-build/roborazzi-gradle-plugin/build.gradle +++ b/include-build/roborazzi-gradle-plugin/build.gradle @@ -29,12 +29,12 @@ tasks.register("generateBuildConfig") { .get() "\"${it}\" to \"${version}\"," }.join("\n")) + inputs.property("versionMapString", versionMapString) doLast { - def buildConfigFile = new File(generatedSourcesDir, "com/example/BuildConfig.kt") + def buildConfigFile = new File(generatedSourcesDir, "io/github/takahirom/roborazzi/BuildConfig.kt") buildConfigFile.parentFile.mkdirs() - println(version) - def libString = "val libraryVersionsMap = mapOf(" + versionMapString + ")\n" + def libString = "val libraryVersionsMap = mapOf(" + inputs.properties["versionMapString"] + ")\n" "" buildConfigFile.text = """ package io.github.takahirom.roborazzi From b4a21cf7281bea088797e2661e56064e55139435 Mon Sep 17 00:00:00 2001 From: takahirom Date: Wed, 26 Jun 2024 13:34:50 +0900 Subject: [PATCH 06/24] Fix file name --- .../takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt index 5fa7684c..6c2264b3 100644 --- a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt +++ b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt @@ -90,6 +90,8 @@ abstract class GeneratePreviewScreenshotTestsTask : DefaultTask() { import sergio.sastre.composable.preview.scanner.core.preview.ComposablePreview import sergio.sastre.composable.preview.scanner.android.AndroidPreviewInfo import sergio.sastre.composable.preview.scanner.android.AndroidComposablePreviewScanner + import com.github.takahirom.roborazzi.DEFAULT_ROBORAZZI_OUTPUT_DIR_PATH + @RunWith(ParameterizedRobolectricTestRunner::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) @@ -110,7 +112,9 @@ abstract class GeneratePreviewScreenshotTestsTask : DefaultTask() { @Config(sdk = [30]) @Test fun snapshot() { - captureRoboImage() { + val filePath = + DEFAULT_ROBORAZZI_OUTPUT_DIR_PATH + "/" + preview.methodName + ".png" + captureRoboImage(filePath = filePath) { preview() } } From 73f5509e8e81eaaed4e3ee3db5c2a7f094348e11 Mon Sep 17 00:00:00 2001 From: takahirom Date: Wed, 26 Jun 2024 17:43:46 +0900 Subject: [PATCH 07/24] Add comments and options --- .../AutoPreviewScreenshotsExtension.kt | 35 ++++++++++++++---- .../takahirom/roborazzi/RoborazziPlugin.kt | 37 ++++++++++++++----- 2 files changed, 56 insertions(+), 16 deletions(-) diff --git a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt index 6c2264b3..fd4f843e 100644 --- a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt +++ b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt @@ -10,6 +10,8 @@ import org.gradle.api.provider.Property import org.gradle.api.tasks.Input import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.TaskCollection +import org.gradle.api.tasks.testing.Test import java.io.File import javax.inject.Inject @@ -22,18 +24,29 @@ open class AutoPreviewScreenshotsExtension @Inject constructor(objects: ObjectFa fun setupGeneratedScreenshotTest( project: Project, variant: Variant, - extension: AutoPreviewScreenshotsExtension + extension: AutoPreviewScreenshotsExtension, + testTaskProvider: TaskCollection ) { val scanPackages = extension.scanPackages.get() assert(scanPackages.isNotEmpty()) addPreviewScreenshotLibraries(variant, project) + testTaskProvider.configureEach {testTask: Test -> + // see: https://github.com/takahirom/roborazzi?tab=readme-ov-file#roborazzirecordfilepathstrategy + testTask.systemProperties["roborazzi.record.filePathStrategy"] = "relativePathFromRoborazziContextOutputDirectory" + // see: https://github.com/takahirom/roborazzi?tab=readme-ov-file#robolectricpixelcopyrendermode + testTask.systemProperties["robolectric.pixelCopyRenderMode"] = "hardware" + } val generateTestsTask = project.tasks.register( "generate${variant.name.capitalize()}PreviewScreenshotTests", GeneratePreviewScreenshotTestsTask::class.java ) { + // It seems that this directory path is overridden by addGeneratedSourceDirectory. + // The generated tests will be located in build/JAVA/generate[VariantName]PreviewScreenshotTests. it.outputDir.set(project.layout.buildDirectory.dir("generated/roborazzi/preview-screenshot")) it.scanPackages.set(scanPackages) } + // We need to use Java here; otherwise, the generate task will not be executed. + // https://stackoverflow.com/a/76870110/4339442 variant.unitTest?.sources?.java?.addGeneratedSourceDirectory( generateTestsTask, GeneratePreviewScreenshotTestsTask::outputDir @@ -51,9 +64,18 @@ private fun addPreviewScreenshotLibraries( configurationName, "io.github.takahirom.roborazzi:roborazzi-compose:$roborazziVersion" ) - project.dependencies.add(configurationName, "io.github.takahirom.roborazzi:roborazzi:$roborazziVersion") - project.dependencies.add(configurationName, "junit:junit:${BuildConfig.libraryVersionsMap["junit"]}") - project.dependencies.add(configurationName, "org.robolectric:robolectric:${BuildConfig.libraryVersionsMap["robolectric"]}") + project.dependencies.add( + configurationName, + "io.github.takahirom.roborazzi:roborazzi:$roborazziVersion" + ) + project.dependencies.add( + configurationName, + "junit:junit:${BuildConfig.libraryVersionsMap["junit"]}" + ) + project.dependencies.add( + configurationName, + "org.robolectric:robolectric:${BuildConfig.libraryVersionsMap["robolectric"]}" + ) project.repositories.add(project.repositories.maven { it.setUrl("https://jitpack.io") }) project.repositories.add(project.repositories.mavenCentral()) @@ -79,7 +101,7 @@ abstract class GeneratePreviewScreenshotTestsTask : DefaultTask() { val packagesExpr = scanPackages.get().joinToString(", ") { "\"$it\"" } val scanPackageTreeExpr = ".scanPackageTrees($packagesExpr)" - File(testDir, "GeneratedPreviewScreenshotTest.kt").writeText( + File(testDir, "PreviewParameterizedTests.kt").writeText( """ import org.junit.Test import org.junit.runner.RunWith @@ -112,8 +134,7 @@ abstract class GeneratePreviewScreenshotTestsTask : DefaultTask() { @Config(sdk = [30]) @Test fun snapshot() { - val filePath = - DEFAULT_ROBORAZZI_OUTPUT_DIR_PATH + "/" + preview.methodName + ".png" + val filePath = preview.methodName + ".png" captureRoboImage(filePath = filePath) { preview() } 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 b944dce3..eca1e731 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 @@ -19,6 +19,7 @@ import org.gradle.api.tasks.InputDirectory import org.gradle.api.tasks.Optional import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.TaskCollection import org.gradle.api.tasks.options.Option import org.gradle.api.tasks.testing.AbstractTestTask import org.gradle.api.tasks.testing.Test @@ -46,11 +47,14 @@ private const val DEFAULT_TEMP_DIR = "intermediates/roborazzi" */ open class RoborazziExtension @Inject constructor(objects: ObjectFactory) { val outputDir: DirectoryProperty = objects.directoryProperty() - val autoPreviewScreenshots: AutoPreviewScreenshotsExtension = objects.newInstance(AutoPreviewScreenshotsExtension::class.java) + val autoPreviewScreenshots: AutoPreviewScreenshotsExtension = + objects.newInstance(AutoPreviewScreenshotsExtension::class.java) + fun automaticPreviewScreenshots(action: Action) { action.execute(autoPreviewScreenshots) } } + @Suppress("unused") // From Paparazzi: https://github.com/cashapp/paparazzi/blob/a76702744a7f380480f323ffda124e845f2733aa/paparazzi/paparazzi-gradle-plugin/src/main/java/app/cash/paparazzi/gradle/PaparazziPlugin.kt abstract class RoborazziPlugin : Plugin { @@ -119,6 +123,15 @@ abstract class RoborazziPlugin : Plugin { return roborazziProperties["roborazzi.test.record"] == "true" || roborazziProperties["roborazzi.test.verify"] == "true" || roborazziProperties["roborazzi.test.compare"] == "true" } + fun findTestTaskProvider( + testTaskClass: KClass, + testTaskName: String + ): TaskCollection = + project.tasks.withType(testTaskClass.java) + .matching { + it.name == testTaskName + } + fun configureRoborazziTasks( variantSlug: String, testTaskName: String, @@ -177,10 +190,7 @@ abstract class RoborazziPlugin : Plugin { isCompareRun.set(compareReportGenerateTaskProvider.map { graph.hasTask(it) }) } - val testTaskProvider = project.tasks.withType(testTaskClass.java) - .matching { - it.name == testTaskName - } + val testTaskProvider = findTestTaskProvider(testTaskClass, testTaskName) val roborazziProperties: Map = project.properties.filterKeys { it != "roborazzi" && it.startsWith("roborazzi") } @@ -263,7 +273,8 @@ abstract class RoborazziPlugin : Plugin { reportFile.parentFile.mkdirs() WebAssets.create().writeToRoborazziReportsDir(reportFile.parentFile) - val reportHtml = readIndexHtmlFile() ?: throw FileNotFoundException("index.html not found in resources/META-INF folder") + val reportHtml = readIndexHtmlFile() + ?: throw FileNotFoundException("index.html not found in resources/META-INF folder") reportFile.writeText( reportHtml.replace( oldValue = "REPORT_TEMPLATE_BODY", @@ -418,15 +429,23 @@ abstract class RoborazziPlugin : Plugin { val unitTest = variant.unitTest ?: return@onVariants val variantSlug = variant.name.capitalizeUS() val testVariantSlug = unitTest.name.capitalizeUS() - val isEnableAutomaticPreviewScreenshots = extension.autoPreviewScreenshots.enabled.convention(false).get() + val isEnableAutomaticPreviewScreenshots = + extension.autoPreviewScreenshots.enabled.convention(false).get() + val testTaskName = "test$testVariantSlug" if (isEnableAutomaticPreviewScreenshots) { - setupGeneratedScreenshotTest(project, variant, extension.autoPreviewScreenshots) + + setupGeneratedScreenshotTest( + project = project, + variant = variant, + extension = extension.autoPreviewScreenshots, + testTaskProvider = findTestTaskProvider(Test::class, testTaskName) + ) } // e.g. testDebugUnitTest -> recordRoborazziDebug configureRoborazziTasks( variantSlug = variantSlug, - testTaskName = "test$testVariantSlug", + testTaskName = testTaskName, ) } } From 82ff15dce5cc0bada42836fa4ad9edf59b8b3c6d Mon Sep 17 00:00:00 2001 From: takahirom Date: Wed, 26 Jun 2024 17:48:34 +0900 Subject: [PATCH 08/24] Rename scanPackages to scanPackageTrees --- .../roborazzi/AutoPreviewScreenshotsExtension.kt | 16 +++++++++------- .../takahirom/roborazzi/RoborazziPlugin.kt | 2 +- sample-auto-preview/build.gradle.kts | 2 +- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt index fd4f843e..28c1daaf 100644 --- a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt +++ b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt @@ -18,17 +18,19 @@ import javax.inject.Inject open class AutoPreviewScreenshotsExtension @Inject constructor(objects: ObjectFactory) { val enabled: Property = objects.property(Boolean::class.java) - val scanPackages: ListProperty = objects.listProperty(String::class.java) + val scanPackageTrees: ListProperty = objects.listProperty(String::class.java) } -fun setupGeneratedScreenshotTest( +fun setupAutoPreviewScreenshotTests( project: Project, variant: Variant, extension: AutoPreviewScreenshotsExtension, testTaskProvider: TaskCollection ) { - val scanPackages = extension.scanPackages.get() - assert(scanPackages.isNotEmpty()) + val scanPackageTrees = extension.scanPackageTrees.get() + assert(scanPackageTrees.isNotEmpty()) { + "Please set scanPackageTrees in the autoPreviewScreenshots extension. Please refer to https://github.com/sergio-sastre/ComposablePreviewScanner?tab=readme-ov-file#how-to-use for more information." + } addPreviewScreenshotLibraries(variant, project) testTaskProvider.configureEach {testTask: Test -> // see: https://github.com/takahirom/roborazzi?tab=readme-ov-file#roborazzirecordfilepathstrategy @@ -43,7 +45,7 @@ fun setupGeneratedScreenshotTest( // It seems that this directory path is overridden by addGeneratedSourceDirectory. // The generated tests will be located in build/JAVA/generate[VariantName]PreviewScreenshotTests. it.outputDir.set(project.layout.buildDirectory.dir("generated/roborazzi/preview-screenshot")) - it.scanPackages.set(scanPackages) + it.scanPackageTrees.set(scanPackageTrees) } // We need to use Java here; otherwise, the generate task will not be executed. // https://stackoverflow.com/a/76870110/4339442 @@ -91,14 +93,14 @@ abstract class GeneratePreviewScreenshotTestsTask : DefaultTask() { abstract val outputDir: DirectoryProperty @get:Input - var scanPackages: ListProperty = project.objects.listProperty(String::class.java) + var scanPackageTrees: ListProperty = project.objects.listProperty(String::class.java) @TaskAction fun generateTests() { val testDir = outputDir.get().asFile testDir.mkdirs() - val packagesExpr = scanPackages.get().joinToString(", ") { "\"$it\"" } + val packagesExpr = scanPackageTrees.get().joinToString(", ") { "\"$it\"" } val scanPackageTreeExpr = ".scanPackageTrees($packagesExpr)" File(testDir, "PreviewParameterizedTests.kt").writeText( 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 eca1e731..fe29502e 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 @@ -434,7 +434,7 @@ abstract class RoborazziPlugin : Plugin { val testTaskName = "test$testVariantSlug" if (isEnableAutomaticPreviewScreenshots) { - setupGeneratedScreenshotTest( + setupAutoPreviewScreenshotTests( project = project, variant = variant, extension = extension.autoPreviewScreenshots, diff --git a/sample-auto-preview/build.gradle.kts b/sample-auto-preview/build.gradle.kts index 4f752865..69f2bea1 100644 --- a/sample-auto-preview/build.gradle.kts +++ b/sample-auto-preview/build.gradle.kts @@ -7,7 +7,7 @@ plugins { roborazzi { automaticPreviewScreenshots { enabled = true - scanPackages = listOf("com.github.takahirom.sample") + scanPackageTrees = listOf("com.github.takahirom.sample") } } From affca18432d5bd335b13785e9de17317c5c6c7c1 Mon Sep 17 00:00:00 2001 From: takahirom Date: Wed, 26 Jun 2024 18:26:32 +0900 Subject: [PATCH 09/24] Prioritize user input for filePathStrategy and pixelCopyRenderMode --- .../roborazzi/AutoPreviewScreenshotsExtension.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt index 28c1daaf..19110b2d 100644 --- a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt +++ b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt @@ -32,11 +32,16 @@ fun setupAutoPreviewScreenshotTests( "Please set scanPackageTrees in the autoPreviewScreenshots extension. Please refer to https://github.com/sergio-sastre/ComposablePreviewScanner?tab=readme-ov-file#how-to-use for more information." } addPreviewScreenshotLibraries(variant, project) - testTaskProvider.configureEach {testTask: Test -> + testTaskProvider.configureEach { testTask: Test -> // see: https://github.com/takahirom/roborazzi?tab=readme-ov-file#roborazzirecordfilepathstrategy - testTask.systemProperties["roborazzi.record.filePathStrategy"] = "relativePathFromRoborazziContextOutputDirectory" + if (project.properties["roborazzi.record.filePathStrategy"] == null) { + testTask.systemProperties["roborazzi.record.filePathStrategy"] = + "relativePathFromRoborazziContextOutputDirectory" + } // see: https://github.com/takahirom/roborazzi?tab=readme-ov-file#robolectricpixelcopyrendermode - testTask.systemProperties["robolectric.pixelCopyRenderMode"] = "hardware" + if (testTask.systemProperties["robolectric.pixelCopyRenderMode"] == null) { + testTask.systemProperties["robolectric.pixelCopyRenderMode"] = "hardware" + } } val generateTestsTask = project.tasks.register( "generate${variant.name.capitalize()}PreviewScreenshotTests", From 2e96e1f20b34cb975104dda9638770948e13d7d9 Mon Sep 17 00:00:00 2001 From: takahirom Date: Wed, 26 Jun 2024 18:28:31 +0900 Subject: [PATCH 10/24] Divide functions for readability --- .../AutoPreviewScreenshotsExtension.kt | 42 +++++++++++++------ 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt index 19110b2d..b2ad9e4f 100644 --- a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt +++ b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt @@ -31,18 +31,16 @@ fun setupAutoPreviewScreenshotTests( assert(scanPackageTrees.isNotEmpty()) { "Please set scanPackageTrees in the autoPreviewScreenshots extension. Please refer to https://github.com/sergio-sastre/ComposablePreviewScanner?tab=readme-ov-file#how-to-use for more information." } - addPreviewScreenshotLibraries(variant, project) - testTaskProvider.configureEach { testTask: Test -> - // see: https://github.com/takahirom/roborazzi?tab=readme-ov-file#roborazzirecordfilepathstrategy - if (project.properties["roborazzi.record.filePathStrategy"] == null) { - testTask.systemProperties["roborazzi.record.filePathStrategy"] = - "relativePathFromRoborazziContextOutputDirectory" - } - // see: https://github.com/takahirom/roborazzi?tab=readme-ov-file#robolectricpixelcopyrendermode - if (testTask.systemProperties["robolectric.pixelCopyRenderMode"] == null) { - testTask.systemProperties["robolectric.pixelCopyRenderMode"] = "hardware" - } - } + addPreviewScreenshotLibraryDependencies(variant, project) + setupTestTask(testTaskProvider, project) + setupGenerateTestsTask(project, variant, scanPackageTrees) +} + +private fun setupGenerateTestsTask( + project: Project, + variant: Variant, + scanPackageTrees: List? +) { val generateTestsTask = project.tasks.register( "generate${variant.name.capitalize()}PreviewScreenshotTests", GeneratePreviewScreenshotTestsTask::class.java @@ -60,7 +58,24 @@ fun setupAutoPreviewScreenshotTests( ) } -private fun addPreviewScreenshotLibraries( +private fun setupTestTask( + testTaskProvider: TaskCollection, + project: Project +) { + testTaskProvider.configureEach { testTask: Test -> + // see: https://github.com/takahirom/roborazzi?tab=readme-ov-file#roborazzirecordfilepathstrategy + if (project.properties["roborazzi.record.filePathStrategy"] == null) { + testTask.systemProperties["roborazzi.record.filePathStrategy"] = + "relativePathFromRoborazziContextOutputDirectory" + } + // see: https://github.com/takahirom/roborazzi?tab=readme-ov-file#robolectricpixelcopyrendermode + if (testTask.systemProperties["robolectric.pixelCopyRenderMode"] == null) { + testTask.systemProperties["robolectric.pixelCopyRenderMode"] = "hardware" + } + } +} + +private fun addPreviewScreenshotLibraryDependencies( variant: Variant, project: Project ) { @@ -84,6 +99,7 @@ private fun addPreviewScreenshotLibraries( "org.robolectric:robolectric:${BuildConfig.libraryVersionsMap["robolectric"]}" ) + // For ComposablePreviewScanner project.repositories.add(project.repositories.maven { it.setUrl("https://jitpack.io") }) project.repositories.add(project.repositories.mavenCentral()) project.repositories.add(project.repositories.google()) From 04e2aa76cfda02bbeeb909541b00570f2b3e94df Mon Sep 17 00:00:00 2001 From: takahirom Date: Wed, 26 Jun 2024 18:29:59 +0900 Subject: [PATCH 11/24] Fix function name --- .../takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt index b2ad9e4f..98a254b2 100644 --- a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt +++ b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt @@ -32,7 +32,7 @@ fun setupAutoPreviewScreenshotTests( "Please set scanPackageTrees in the autoPreviewScreenshots extension. Please refer to https://github.com/sergio-sastre/ComposablePreviewScanner?tab=readme-ov-file#how-to-use for more information." } addPreviewScreenshotLibraryDependencies(variant, project) - setupTestTask(testTaskProvider, project) + setupTestTaskProperties(testTaskProvider, project) setupGenerateTestsTask(project, variant, scanPackageTrees) } @@ -58,7 +58,7 @@ private fun setupGenerateTestsTask( ) } -private fun setupTestTask( +private fun setupTestTaskProperties( testTaskProvider: TaskCollection, project: Project ) { From 9f2e1f7047a9ee1e717fbe6cf7c1de506ded494e Mon Sep 17 00:00:00 2001 From: takahirom Date: Thu, 27 Jun 2024 10:25:11 +0900 Subject: [PATCH 12/24] Make AutoPreviewScreenshotsExtension version customizable --- .../roborazzi-gradle-plugin/build.gradle | 2 +- .../AutoPreviewScreenshotsExtension.kt | 31 +++++++++++++++---- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/include-build/roborazzi-gradle-plugin/build.gradle b/include-build/roborazzi-gradle-plugin/build.gradle index f87aca72..17188d46 100644 --- a/include-build/roborazzi-gradle-plugin/build.gradle +++ b/include-build/roborazzi-gradle-plugin/build.gradle @@ -34,7 +34,7 @@ tasks.register("generateBuildConfig") { def buildConfigFile = new File(generatedSourcesDir, "io/github/takahirom/roborazzi/BuildConfig.kt") buildConfigFile.parentFile.mkdirs() - def libString = "val libraryVersionsMap = mapOf(" + inputs.properties["versionMapString"] + ")\n" + def libString = "val defaultLibraryVersionsMap = mapOf(" + inputs.properties["versionMapString"] + ")\n" "" buildConfigFile.text = """ package io.github.takahirom.roborazzi diff --git a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt index 98a254b2..c2e6e31d 100644 --- a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt +++ b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt @@ -19,6 +19,15 @@ import javax.inject.Inject open class AutoPreviewScreenshotsExtension @Inject constructor(objects: ObjectFactory) { val enabled: Property = objects.property(Boolean::class.java) val scanPackageTrees: ListProperty = objects.listProperty(String::class.java) + val libraryVersions: Property = + objects.property(LibraryVersionsExtension::class.java) +} + +open class LibraryVersionsExtension @Inject constructor(objects: ObjectFactory) { + val roborazzi: Property = objects.property(String::class.java) + val junit: Property = objects.property(String::class.java) + val robolectric: Property = objects.property(String::class.java) + val composablePreviewScanner: Property = objects.property(String::class.java) } fun setupAutoPreviewScreenshotTests( @@ -31,7 +40,7 @@ fun setupAutoPreviewScreenshotTests( assert(scanPackageTrees.isNotEmpty()) { "Please set scanPackageTrees in the autoPreviewScreenshots extension. Please refer to https://github.com/sergio-sastre/ComposablePreviewScanner?tab=readme-ov-file#how-to-use for more information." } - addPreviewScreenshotLibraryDependencies(variant, project) + addPreviewScreenshotLibraryDependencies(variant, project, extension.libraryVersions.get()) setupTestTaskProperties(testTaskProvider, project) setupGenerateTestsTask(project, variant, scanPackageTrees) } @@ -77,11 +86,17 @@ private fun setupTestTaskProperties( private fun addPreviewScreenshotLibraryDependencies( variant: Variant, - project: Project + project: Project, + libraryVersionsExtension: LibraryVersionsExtension ) { val configurationName = "test${variant.name.capitalize()}Implementation" - val roborazziVersion = BuildConfig.libraryVersionsMap["roborazzi"] + fun Property.getLibraryVersion(libraryName: String): String { + return convention(BuildConfig.defaultLibraryVersionsMap[libraryName]!!) + .get() + } + + val roborazziVersion = libraryVersionsExtension.roborazzi.getLibraryVersion("roborazzi") project.dependencies.add( configurationName, "io.github.takahirom.roborazzi:roborazzi-compose:$roborazziVersion" @@ -90,22 +105,26 @@ private fun addPreviewScreenshotLibraryDependencies( configurationName, "io.github.takahirom.roborazzi:roborazzi:$roborazziVersion" ) + val junitLibraryVersion = libraryVersionsExtension.junit.getLibraryVersion("junit") project.dependencies.add( configurationName, - "junit:junit:${BuildConfig.libraryVersionsMap["junit"]}" + "junit:junit:$junitLibraryVersion" ) + val robolectricLibraryVersion = libraryVersionsExtension.robolectric.getLibraryVersion("robolectric") project.dependencies.add( configurationName, - "org.robolectric:robolectric:${BuildConfig.libraryVersionsMap["robolectric"]}" + "org.robolectric:robolectric:$robolectricLibraryVersion" ) // For ComposablePreviewScanner project.repositories.add(project.repositories.maven { it.setUrl("https://jitpack.io") }) project.repositories.add(project.repositories.mavenCentral()) project.repositories.add(project.repositories.google()) + val composePreviewScannerLibraryVersion = + libraryVersionsExtension.composablePreviewScanner.getLibraryVersion("composable-preview-scanner") project.dependencies.add( configurationName, - "com.github.sergio-sastre.ComposablePreviewScanner:android:${BuildConfig.libraryVersionsMap["composable-preview-scanner"]}" + "com.github.sergio-sastre.ComposablePreviewScanner:android:$composePreviewScannerLibraryVersion" ) } From f7d4d57f967e809569f6fdc578906d5899ac4093 Mon Sep 17 00:00:00 2001 From: takahirom Date: Thu, 27 Jun 2024 10:41:27 +0900 Subject: [PATCH 13/24] Fix nullable --- .../AutoPreviewScreenshotsExtension.kt | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt index c2e6e31d..70eb3119 100644 --- a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt +++ b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt @@ -40,7 +40,7 @@ fun setupAutoPreviewScreenshotTests( assert(scanPackageTrees.isNotEmpty()) { "Please set scanPackageTrees in the autoPreviewScreenshots extension. Please refer to https://github.com/sergio-sastre/ComposablePreviewScanner?tab=readme-ov-file#how-to-use for more information." } - addPreviewScreenshotLibraryDependencies(variant, project, extension.libraryVersions.get()) + addPreviewScreenshotLibraryDependencies(variant, project, extension.libraryVersions.orNull) setupTestTaskProperties(testTaskProvider, project) setupGenerateTestsTask(project, variant, scanPackageTrees) } @@ -87,16 +87,19 @@ private fun setupTestTaskProperties( private fun addPreviewScreenshotLibraryDependencies( variant: Variant, project: Project, - libraryVersionsExtension: LibraryVersionsExtension + libraryVersionsExtension: LibraryVersionsExtension? ) { val configurationName = "test${variant.name.capitalize()}Implementation" - fun Property.getLibraryVersion(libraryName: String): String { + fun Property?.getLibraryVersion(libraryName: String): String { + if (this == null) { + return BuildConfig.defaultLibraryVersionsMap[libraryName]!! + } return convention(BuildConfig.defaultLibraryVersionsMap[libraryName]!!) .get() } - val roborazziVersion = libraryVersionsExtension.roborazzi.getLibraryVersion("roborazzi") + val roborazziVersion = libraryVersionsExtension?.roborazzi.getLibraryVersion("roborazzi") project.dependencies.add( configurationName, "io.github.takahirom.roborazzi:roborazzi-compose:$roborazziVersion" @@ -105,12 +108,12 @@ private fun addPreviewScreenshotLibraryDependencies( configurationName, "io.github.takahirom.roborazzi:roborazzi:$roborazziVersion" ) - val junitLibraryVersion = libraryVersionsExtension.junit.getLibraryVersion("junit") + val junitLibraryVersion = libraryVersionsExtension?.junit.getLibraryVersion("junit") project.dependencies.add( configurationName, "junit:junit:$junitLibraryVersion" ) - val robolectricLibraryVersion = libraryVersionsExtension.robolectric.getLibraryVersion("robolectric") + val robolectricLibraryVersion = libraryVersionsExtension?.robolectric.getLibraryVersion("robolectric") project.dependencies.add( configurationName, "org.robolectric:robolectric:$robolectricLibraryVersion" @@ -121,7 +124,7 @@ private fun addPreviewScreenshotLibraryDependencies( project.repositories.add(project.repositories.mavenCentral()) project.repositories.add(project.repositories.google()) val composePreviewScannerLibraryVersion = - libraryVersionsExtension.composablePreviewScanner.getLibraryVersion("composable-preview-scanner") + libraryVersionsExtension?.composablePreviewScanner.getLibraryVersion("composable-preview-scanner") project.dependencies.add( configurationName, "com.github.sergio-sastre.ComposablePreviewScanner:android:$composePreviewScannerLibraryVersion" From 5b04f57ae6bc1d4b16ab4e7181577ba4e4eb9192 Mon Sep 17 00:00:00 2001 From: takahirom Date: Thu, 27 Jun 2024 10:59:27 +0900 Subject: [PATCH 14/24] Use AndroidPreviewScreenshotIdBuilder --- .../AutoPreviewScreenshotsExtension.kt | 7 ++- .../com/github/takahirom/sample/Preview.kt | 51 +++++++++++++++++-- 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt index 70eb3119..b001f724 100644 --- a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt +++ b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt @@ -157,6 +157,7 @@ abstract class GeneratePreviewScreenshotTestsTask : DefaultTask() { import sergio.sastre.composable.preview.scanner.core.preview.ComposablePreview import sergio.sastre.composable.preview.scanner.android.AndroidPreviewInfo import sergio.sastre.composable.preview.scanner.android.AndroidComposablePreviewScanner + import sergio.sastre.composable.preview.scanner.android.screenshotid.AndroidPreviewScreenshotIdBuilder import com.github.takahirom.roborazzi.DEFAULT_ROBORAZZI_OUTPUT_DIR_PATH @@ -175,11 +176,15 @@ abstract class GeneratePreviewScreenshotTestsTask : DefaultTask() { .getPreviews() } + fun createScreenshotIdFor(preview: ComposablePreview) = + AndroidPreviewScreenshotIdBuilder(preview) + .build() + @GraphicsMode(GraphicsMode.Mode.NATIVE) @Config(sdk = [30]) @Test fun snapshot() { - val filePath = preview.methodName + ".png" + val filePath = createScreenshotIdFor(preview) + ".png" captureRoboImage(filePath = filePath) { preview() } diff --git a/sample-auto-preview/src/main/java/com/github/takahirom/sample/Preview.kt b/sample-auto-preview/src/main/java/com/github/takahirom/sample/Preview.kt index 03813057..61fabdc8 100644 --- a/sample-auto-preview/src/main/java/com/github/takahirom/sample/Preview.kt +++ b/sample-auto-preview/src/main/java/com/github/takahirom/sample/Preview.kt @@ -1,14 +1,59 @@ package com.github.takahirom.sample -import androidx.compose.foundation.layout.Box +import android.content.res.Configuration +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Card import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.Wallpapers +import androidx.compose.ui.unit.dp @Preview @Composable fun Preview() { - Box { - Text("Hello, World!") + Card( + Modifier + .width(100.dp) + .height(50.dp) + ) { + Text( + modifier = Modifier.padding(8.dp), + text = "Hello, World!" + ) } } + +@Preview( + name = "Preview Name", + // These properties are not supported by Roborazzi yet. + group = "Preview Group", + apiLevel = 30, + widthDp = 320, + heightDp = 640, + locale = "ja_JP", + fontScale = 1.5f, + showSystemUi = true, + showBackground = true, + backgroundColor = 0xFF0000FF, + uiMode = Configuration.UI_MODE_NIGHT_YES, + device = Devices.NEXUS_5, + wallpaper = Wallpapers.GREEN_DOMINATED_EXAMPLE, +) +@Composable +fun PreviewWithProperties() { + Card( + Modifier + .width(100.dp) + .height(50.dp) + ) { + Text( + modifier = Modifier.padding(8.dp), + text = "Hello, World!" + ) + } +} \ No newline at end of file From 9c5c433c84cd5c9720dc1d146d2fe2b38b9778c5 Mon Sep 17 00:00:00 2001 From: takahirom Date: Thu, 27 Jun 2024 10:59:48 +0900 Subject: [PATCH 15/24] Rename file name --- .../java/com/github/takahirom/sample/{Preview.kt => Previews.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename sample-auto-preview/src/main/java/com/github/takahirom/sample/{Preview.kt => Previews.kt} (100%) diff --git a/sample-auto-preview/src/main/java/com/github/takahirom/sample/Preview.kt b/sample-auto-preview/src/main/java/com/github/takahirom/sample/Previews.kt similarity index 100% rename from sample-auto-preview/src/main/java/com/github/takahirom/sample/Preview.kt rename to sample-auto-preview/src/main/java/com/github/takahirom/sample/Previews.kt From cdd2b32c802a226209cfd3c08ef4ddc0ee143d17 Mon Sep 17 00:00:00 2001 From: takahirom Date: Thu, 27 Jun 2024 11:58:32 +0900 Subject: [PATCH 16/24] Add ignoreClassName --- .../takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt index b001f724..23251676 100644 --- a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt +++ b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt @@ -178,6 +178,7 @@ abstract class GeneratePreviewScreenshotTestsTask : DefaultTask() { fun createScreenshotIdFor(preview: ComposablePreview) = AndroidPreviewScreenshotIdBuilder(preview) + .ignoreClassName() .build() @GraphicsMode(GraphicsMode.Mode.NATIVE) From 3d3a48e5cc55d8c228978107850ff66376fae805 Mon Sep 17 00:00:00 2001 From: takahirom Date: Thu, 27 Jun 2024 12:29:41 +0900 Subject: [PATCH 17/24] Adjust preview sample --- sample-auto-preview/build.gradle.kts | 4 +-- sample-auto-preview/consumer-rules.pro | 0 .../com/github/takahirom/sample/Previews.kt | 25 +++++++++++-------- 3 files changed, 16 insertions(+), 13 deletions(-) delete mode 100644 sample-auto-preview/consumer-rules.pro diff --git a/sample-auto-preview/build.gradle.kts b/sample-auto-preview/build.gradle.kts index 69f2bea1..cec0bb82 100644 --- a/sample-auto-preview/build.gradle.kts +++ b/sample-auto-preview/build.gradle.kts @@ -1,5 +1,6 @@ plugins { - id("com.android.library") + id("com.android.application") +// id("com.android.library") id("org.jetbrains.kotlin.android") id("io.github.takahirom.roborazzi") } @@ -19,7 +20,6 @@ android { minSdk = 24 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") } buildFeatures { compose = true diff --git a/sample-auto-preview/consumer-rules.pro b/sample-auto-preview/consumer-rules.pro deleted file mode 100644 index e69de29b..00000000 diff --git a/sample-auto-preview/src/main/java/com/github/takahirom/sample/Previews.kt b/sample-auto-preview/src/main/java/com/github/takahirom/sample/Previews.kt index 61fabdc8..4b3f9c23 100644 --- a/sample-auto-preview/src/main/java/com/github/takahirom/sample/Previews.kt +++ b/sample-auto-preview/src/main/java/com/github/takahirom/sample/Previews.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material3.Card +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -16,15 +17,17 @@ import androidx.compose.ui.unit.dp @Preview @Composable fun Preview() { - Card( + MaterialTheme { + Card( Modifier - .width(100.dp) - .height(50.dp) - ) { - Text( - modifier = Modifier.padding(8.dp), - text = "Hello, World!" - ) + .width(126.dp) + .height(80.dp) + ) { + Text( + modifier = Modifier.padding(8.dp), + text = "Auto Preview Sample" + ) + } } } @@ -47,9 +50,9 @@ fun Preview() { @Composable fun PreviewWithProperties() { Card( - Modifier - .width(100.dp) - .height(50.dp) + Modifier + .width(100.dp) + .height(50.dp) ) { Text( modifier = Modifier.padding(8.dp), From d90078c39bfd2db9c720b9726fa160ebdd47da3d Mon Sep 17 00:00:00 2001 From: takahirom Date: Fri, 28 Jun 2024 18:21:24 +0900 Subject: [PATCH 18/24] Change automaticPreviewScreenshots to androidSetup --- .../roborazzi/AndroidSetupExtension.kt | 261 ++++++++++++++++++ .../AutoPreviewScreenshotsExtension.kt | 198 ------------- .../takahirom/roborazzi/RoborazziPlugin.kt | 16 +- sample-auto-preview/build.gradle.kts | 8 +- 4 files changed, 275 insertions(+), 208 deletions(-) create mode 100644 include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AndroidSetupExtension.kt delete mode 100644 include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt diff --git a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AndroidSetupExtension.kt b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AndroidSetupExtension.kt new file mode 100644 index 00000000..7f6f432d --- /dev/null +++ b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AndroidSetupExtension.kt @@ -0,0 +1,261 @@ +package io.github.takahirom.roborazzi + +import com.android.build.api.variant.Variant +import com.android.build.gradle.TestedExtension +import org.gradle.api.DefaultTask +import org.gradle.api.Project +import org.gradle.api.artifacts.dsl.DependencyHandler +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.TaskCollection +import org.gradle.api.tasks.testing.Test +import java.io.File +import javax.inject.Inject + +open class AndroidSetupExtension @Inject constructor(objects: ObjectFactory) { + /** + * Default value is false. + */ + val enable: Property = objects.property(Boolean::class.java) + .convention(false) + val generatePreviewTests: GeneratePreviewTestsExtension = + objects.newInstance(GeneratePreviewTestsExtension::class.java) + + fun generatePreviewTests(action: GeneratePreviewTestsExtension.() -> Unit) { + generatePreviewTests.action() + } + + val libraryDependencies: LibraryDependenciesExtension = + objects.newInstance(LibraryDependenciesExtension::class.java) + + fun libraryDependencies(action: LibraryDependenciesExtension.() -> Unit) { + libraryDependencies.action() + } + + val testConfig: TestConfigExtension = objects.newInstance(TestConfigExtension::class.java) + + fun testConfig(action: TestConfigExtension.() -> Unit) { + testConfig.action() + } +} + +open class GeneratePreviewTestsExtension @Inject constructor(objects: ObjectFactory) { + val enable: Property = objects.property(Boolean::class.java) + val scanPackages: ListProperty = objects.listProperty(String::class.java) +} + +open class LibraryDependenciesExtension @Inject constructor(objects: ObjectFactory) { + val enable: Property = objects.property(Boolean::class.java) + val roborazziVersion: Property = objects.property(String::class.java) + val junitVersion: Property = objects.property(String::class.java) + val robolectricVersion: Property = objects.property(String::class.java) + val composablePreviewScannerVersion: Property = objects.property(String::class.java) + + companion object { + const val SKIP = "SKIP_DEPENDENCY" + } +} + +open class TestConfigExtension @Inject constructor(objects: ObjectFactory) { + val enable: Property = objects.property(Boolean::class.java) + val includeAndroidResources: Property = objects.property(Boolean::class.java) + val roborazziFilePathStrategy: Property = objects.property(String::class.java) + val robolectricRenderMode: Property = objects.property(String::class.java) +} + +fun setupAndroidSetupExtension( + project: Project, + variant: Variant, + extension: AndroidSetupExtension, + androidExtension: TestedExtension, + testTaskProvider: TaskCollection +) { + // Prioritize the enable property in the extension over the global enable property. + if ((extension.generatePreviewTests.enable.orNull ?: extension.enable.orNull) == true) { + setupGeneratePreviewTestsTask(project, variant, extension.generatePreviewTests) + } + if ((extension.libraryDependencies.enable.orNull ?: extension.enable.orNull) == true) { + addLibraryDependencies(variant, project, extension.libraryDependencies) + } + if ((extension.testConfig.enable.orNull ?: extension.enable.orNull) == true) { + setupTestConfig(testTaskProvider, extension.testConfig, androidExtension) + } +} + +private fun setupGeneratePreviewTestsTask( + project: Project, + variant: Variant, + extension: GeneratePreviewTestsExtension +) { + assert(extension.scanPackages.get().orEmpty().isNotEmpty()) { + "Please set androidSetup.generatePreviewTests.scanPackages in the generatePreviewTests extension or set androidSetup.generatePreviewTests.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()}PreviewScreenshotTests", + GeneratePreviewScreenshotTestsTask::class.java + ) { + // It seems that this directory path is overridden by addGeneratedSourceDirectory. + // The generated tests will be located in build/JAVA/generate[VariantName]PreviewScreenshotTests. + it.outputDir.set(project.layout.buildDirectory.dir("generated/roborazzi/preview-screenshot")) + it.scanPackageTrees.set(extension?.scanPackages) + } + // We need to use Java here; otherwise, the generate task will not be executed. + // https://stackoverflow.com/a/76870110/4339442 + variant.unitTest?.sources?.java?.addGeneratedSourceDirectory( + generateTestsTask, + GeneratePreviewScreenshotTestsTask::outputDir + ) +} + +private fun setupTestConfig( + testTaskProvider: TaskCollection, + testConfiguration: TestConfigExtension, + androidExtension: TestedExtension +) { + // Default true + if (testConfiguration.includeAndroidResources.orNull != false) { + androidExtension.testOptions.unitTests.isIncludeAndroidResources = true + } + testTaskProvider.configureEach { testTask: Test -> + // see: https://github.com/takahirom/roborazzi?tab=readme-ov-file#roborazzirecordfilepathstrategy + testTask.systemProperties["roborazzi.record.filePathStrategy"] = + testConfiguration.roborazziFilePathStrategy.orNull + ?: "relativePathFromRoborazziContextOutputDirectory" + // see: https://github.com/takahirom/roborazzi?tab=readme-ov-file#robolectricpixelcopyrendermode + testTask.systemProperties["robolectric.pixelCopyRenderMode"] = + testConfiguration.robolectricRenderMode.orNull + ?: "hardware" + } +} + +private fun addLibraryDependencies( + variant: Variant, + project: Project, + libraryVersionsExtension: LibraryDependenciesExtension? +) { + val configurationName = "test${variant.name.capitalize()}Implementation" + + fun Property?.getLibraryVersion(libraryName: String): String { + if (this == null) { + return BuildConfig.defaultLibraryVersionsMap[libraryName]!! + } + if (this.orNull == LibraryDependenciesExtension.SKIP) { + return LibraryDependenciesExtension.SKIP + } + return convention(BuildConfig.defaultLibraryVersionsMap[libraryName]!!).get() + } + + val roborazziVersion = libraryVersionsExtension?.roborazziVersion.getLibraryVersion("roborazzi") + fun DependencyHandler.addIfNotSkip(libraryName: String, version: String) { + if (version != LibraryDependenciesExtension.SKIP) { + add(configurationName, "$libraryName:$version") + } + } + project.dependencies.addIfNotSkip( + libraryName = "io.github.takahirom.roborazzi:roborazzi-compose", + version = roborazziVersion + ) + project.dependencies.addIfNotSkip( + libraryName = "io.github.takahirom.roborazzi:roborazzi", + version = roborazziVersion + ) + val junitLibraryVersion = libraryVersionsExtension?.junitVersion.getLibraryVersion("junit") + project.dependencies.addIfNotSkip( + "junit:junit", junitLibraryVersion + ) + val robolectricLibraryVersion = + libraryVersionsExtension?.robolectricVersion.getLibraryVersion("robolectric") + project.dependencies.addIfNotSkip( + libraryName = "org.robolectric:robolectric", + version = robolectricLibraryVersion + ) + + // For ComposablePreviewScanner + val composePreviewScannerLibraryVersion = + libraryVersionsExtension?.composablePreviewScannerVersion.getLibraryVersion("composable-preview-scanner") + if (composePreviewScannerLibraryVersion != LibraryDependenciesExtension.SKIP) { + project.repositories.add(project.repositories.maven { it.setUrl("https://jitpack.io") }) + project.repositories.add(project.repositories.mavenCentral()) + project.repositories.add(project.repositories.google()) + project.dependencies.addIfNotSkip( + libraryName = "com.github.sergio-sastre.ComposablePreviewScanner:android", + version = composePreviewScannerLibraryVersion + ) + } +} + +abstract class GeneratePreviewScreenshotTestsTask : DefaultTask() { + @get:OutputDirectory + abstract val outputDir: DirectoryProperty + + @get:Input + var scanPackageTrees: ListProperty = project.objects.listProperty(String::class.java) + + @TaskAction + fun generateTests() { + val testDir = outputDir.get().asFile + testDir.mkdirs() + + val packagesExpr = scanPackageTrees.get().joinToString(", ") { "\"$it\"" } + val scanPackageTreeExpr = ".scanPackageTrees($packagesExpr)" + + File(testDir, "PreviewParameterizedTests.kt").writeText( + """ + import org.junit.Test + import org.junit.runner.RunWith + import org.robolectric.ParameterizedRobolectricTestRunner + import org.robolectric.annotation.Config + import org.robolectric.annotation.GraphicsMode + import com.github.takahirom.roborazzi.captureRoboImage + import sergio.sastre.composable.preview.scanner.core.preview.ComposablePreview + import sergio.sastre.composable.preview.scanner.android.AndroidPreviewInfo + import sergio.sastre.composable.preview.scanner.android.AndroidComposablePreviewScanner + import sergio.sastre.composable.preview.scanner.android.screenshotid.AndroidPreviewScreenshotIdBuilder + import com.github.takahirom.roborazzi.DEFAULT_ROBORAZZI_OUTPUT_DIR_PATH + + + @RunWith(ParameterizedRobolectricTestRunner::class) + @GraphicsMode(GraphicsMode.Mode.NATIVE) + class PreviewParameterizedTests( + private val preview: ComposablePreview, + ) { + + companion object { + val previews: List> by lazy { + AndroidComposablePreviewScanner() + $scanPackageTreeExpr + .getPreviews() + } + @JvmStatic + @ParameterizedRobolectricTestRunner.Parameters + fun values(): List> = + previews + } + + fun createScreenshotIdFor(preview: ComposablePreview) = + AndroidPreviewScreenshotIdBuilder(preview) + .ignoreClassName() + .build() + + @GraphicsMode(GraphicsMode.Mode.NATIVE) + @Config(sdk = [30]) + @Test + fun test() { + val filePath = createScreenshotIdFor(preview) + ".png" + captureRoboImage(filePath = filePath) { + preview() + } + } + + } + """.trimIndent() + ) + } +} \ No newline at end of file diff --git a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt deleted file mode 100644 index 23251676..00000000 --- a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AutoPreviewScreenshotsExtension.kt +++ /dev/null @@ -1,198 +0,0 @@ -package io.github.takahirom.roborazzi - -import com.android.build.api.variant.Variant -import org.gradle.api.DefaultTask -import org.gradle.api.Project -import org.gradle.api.file.DirectoryProperty -import org.gradle.api.model.ObjectFactory -import org.gradle.api.provider.ListProperty -import org.gradle.api.provider.Property -import org.gradle.api.tasks.Input -import org.gradle.api.tasks.OutputDirectory -import org.gradle.api.tasks.TaskAction -import org.gradle.api.tasks.TaskCollection -import org.gradle.api.tasks.testing.Test -import java.io.File -import javax.inject.Inject - - -open class AutoPreviewScreenshotsExtension @Inject constructor(objects: ObjectFactory) { - val enabled: Property = objects.property(Boolean::class.java) - val scanPackageTrees: ListProperty = objects.listProperty(String::class.java) - val libraryVersions: Property = - objects.property(LibraryVersionsExtension::class.java) -} - -open class LibraryVersionsExtension @Inject constructor(objects: ObjectFactory) { - val roborazzi: Property = objects.property(String::class.java) - val junit: Property = objects.property(String::class.java) - val robolectric: Property = objects.property(String::class.java) - val composablePreviewScanner: Property = objects.property(String::class.java) -} - -fun setupAutoPreviewScreenshotTests( - project: Project, - variant: Variant, - extension: AutoPreviewScreenshotsExtension, - testTaskProvider: TaskCollection -) { - val scanPackageTrees = extension.scanPackageTrees.get() - assert(scanPackageTrees.isNotEmpty()) { - "Please set scanPackageTrees in the autoPreviewScreenshots extension. Please refer to https://github.com/sergio-sastre/ComposablePreviewScanner?tab=readme-ov-file#how-to-use for more information." - } - addPreviewScreenshotLibraryDependencies(variant, project, extension.libraryVersions.orNull) - setupTestTaskProperties(testTaskProvider, project) - setupGenerateTestsTask(project, variant, scanPackageTrees) -} - -private fun setupGenerateTestsTask( - project: Project, - variant: Variant, - scanPackageTrees: List? -) { - val generateTestsTask = project.tasks.register( - "generate${variant.name.capitalize()}PreviewScreenshotTests", - GeneratePreviewScreenshotTestsTask::class.java - ) { - // It seems that this directory path is overridden by addGeneratedSourceDirectory. - // The generated tests will be located in build/JAVA/generate[VariantName]PreviewScreenshotTests. - it.outputDir.set(project.layout.buildDirectory.dir("generated/roborazzi/preview-screenshot")) - it.scanPackageTrees.set(scanPackageTrees) - } - // We need to use Java here; otherwise, the generate task will not be executed. - // https://stackoverflow.com/a/76870110/4339442 - variant.unitTest?.sources?.java?.addGeneratedSourceDirectory( - generateTestsTask, - GeneratePreviewScreenshotTestsTask::outputDir - ) -} - -private fun setupTestTaskProperties( - testTaskProvider: TaskCollection, - project: Project -) { - testTaskProvider.configureEach { testTask: Test -> - // see: https://github.com/takahirom/roborazzi?tab=readme-ov-file#roborazzirecordfilepathstrategy - if (project.properties["roborazzi.record.filePathStrategy"] == null) { - testTask.systemProperties["roborazzi.record.filePathStrategy"] = - "relativePathFromRoborazziContextOutputDirectory" - } - // see: https://github.com/takahirom/roborazzi?tab=readme-ov-file#robolectricpixelcopyrendermode - if (testTask.systemProperties["robolectric.pixelCopyRenderMode"] == null) { - testTask.systemProperties["robolectric.pixelCopyRenderMode"] = "hardware" - } - } -} - -private fun addPreviewScreenshotLibraryDependencies( - variant: Variant, - project: Project, - libraryVersionsExtension: LibraryVersionsExtension? -) { - val configurationName = "test${variant.name.capitalize()}Implementation" - - fun Property?.getLibraryVersion(libraryName: String): String { - if (this == null) { - return BuildConfig.defaultLibraryVersionsMap[libraryName]!! - } - return convention(BuildConfig.defaultLibraryVersionsMap[libraryName]!!) - .get() - } - - val roborazziVersion = libraryVersionsExtension?.roborazzi.getLibraryVersion("roborazzi") - project.dependencies.add( - configurationName, - "io.github.takahirom.roborazzi:roborazzi-compose:$roborazziVersion" - ) - project.dependencies.add( - configurationName, - "io.github.takahirom.roborazzi:roborazzi:$roborazziVersion" - ) - val junitLibraryVersion = libraryVersionsExtension?.junit.getLibraryVersion("junit") - project.dependencies.add( - configurationName, - "junit:junit:$junitLibraryVersion" - ) - val robolectricLibraryVersion = libraryVersionsExtension?.robolectric.getLibraryVersion("robolectric") - project.dependencies.add( - configurationName, - "org.robolectric:robolectric:$robolectricLibraryVersion" - ) - - // For ComposablePreviewScanner - project.repositories.add(project.repositories.maven { it.setUrl("https://jitpack.io") }) - project.repositories.add(project.repositories.mavenCentral()) - project.repositories.add(project.repositories.google()) - val composePreviewScannerLibraryVersion = - libraryVersionsExtension?.composablePreviewScanner.getLibraryVersion("composable-preview-scanner") - project.dependencies.add( - configurationName, - "com.github.sergio-sastre.ComposablePreviewScanner:android:$composePreviewScannerLibraryVersion" - ) -} - -abstract class GeneratePreviewScreenshotTestsTask : DefaultTask() { - @get:OutputDirectory - abstract val outputDir: DirectoryProperty - - @get:Input - var scanPackageTrees: ListProperty = project.objects.listProperty(String::class.java) - - @TaskAction - fun generateTests() { - val testDir = outputDir.get().asFile - testDir.mkdirs() - - val packagesExpr = scanPackageTrees.get().joinToString(", ") { "\"$it\"" } - val scanPackageTreeExpr = ".scanPackageTrees($packagesExpr)" - - File(testDir, "PreviewParameterizedTests.kt").writeText( - """ - import org.junit.Test - import org.junit.runner.RunWith - import org.robolectric.ParameterizedRobolectricTestRunner - import org.robolectric.annotation.Config - import org.robolectric.annotation.GraphicsMode - import com.github.takahirom.roborazzi.captureRoboImage - import sergio.sastre.composable.preview.scanner.core.preview.ComposablePreview - import sergio.sastre.composable.preview.scanner.android.AndroidPreviewInfo - import sergio.sastre.composable.preview.scanner.android.AndroidComposablePreviewScanner - import sergio.sastre.composable.preview.scanner.android.screenshotid.AndroidPreviewScreenshotIdBuilder - import com.github.takahirom.roborazzi.DEFAULT_ROBORAZZI_OUTPUT_DIR_PATH - - - @RunWith(ParameterizedRobolectricTestRunner::class) - @GraphicsMode(GraphicsMode.Mode.NATIVE) - class PreviewParameterizedTests( - private val preview: ComposablePreview, - ) { - - companion object { - @JvmStatic - @ParameterizedRobolectricTestRunner.Parameters - fun values(): List> = - AndroidComposablePreviewScanner() - $scanPackageTreeExpr - .getPreviews() - } - - fun createScreenshotIdFor(preview: ComposablePreview) = - AndroidPreviewScreenshotIdBuilder(preview) - .ignoreClassName() - .build() - - @GraphicsMode(GraphicsMode.Mode.NATIVE) - @Config(sdk = [30]) - @Test - fun snapshot() { - val filePath = createScreenshotIdFor(preview) + ".png" - captureRoboImage(filePath = filePath) { - preview() - } - } - - } - """.trimIndent() - ) - } -} \ No newline at end of file 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 fe29502e..631863da 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 @@ -3,6 +3,7 @@ package io.github.takahirom.roborazzi import com.android.build.api.variant.AndroidComponentsExtension import com.android.build.api.variant.ApplicationAndroidComponentsExtension import com.android.build.api.variant.LibraryAndroidComponentsExtension +import com.android.build.gradle.TestedExtension import com.github.takahirom.roborazzi.CaptureResult import com.github.takahirom.roborazzi.CaptureResults import com.github.takahirom.roborazzi.InternalRoborazziApi @@ -47,11 +48,11 @@ private const val DEFAULT_TEMP_DIR = "intermediates/roborazzi" */ open class RoborazziExtension @Inject constructor(objects: ObjectFactory) { val outputDir: DirectoryProperty = objects.directoryProperty() - val autoPreviewScreenshots: AutoPreviewScreenshotsExtension = - objects.newInstance(AutoPreviewScreenshotsExtension::class.java) + val androidSetup: AndroidSetupExtension = + objects.newInstance(AndroidSetupExtension::class.java) - fun automaticPreviewScreenshots(action: Action) { - action.execute(autoPreviewScreenshots) + fun androidSetup(action: Action) { + action.execute(androidSetup) } } @@ -430,14 +431,15 @@ abstract class RoborazziPlugin : Plugin { val variantSlug = variant.name.capitalizeUS() val testVariantSlug = unitTest.name.capitalizeUS() val isEnableAutomaticPreviewScreenshots = - extension.autoPreviewScreenshots.enabled.convention(false).get() + extension.androidSetup.enable.convention(false).get() val testTaskName = "test$testVariantSlug" if (isEnableAutomaticPreviewScreenshots) { - setupAutoPreviewScreenshotTests( + setupAndroidSetupExtension( project = project, variant = variant, - extension = extension.autoPreviewScreenshots, + extension = extension.androidSetup, + androidExtension = project.extensions.getByType(TestedExtension::class.java), testTaskProvider = findTestTaskProvider(Test::class, testTaskName) ) } diff --git a/sample-auto-preview/build.gradle.kts b/sample-auto-preview/build.gradle.kts index cec0bb82..b7c9f73c 100644 --- a/sample-auto-preview/build.gradle.kts +++ b/sample-auto-preview/build.gradle.kts @@ -6,9 +6,11 @@ plugins { } roborazzi { - automaticPreviewScreenshots { - enabled = true - scanPackageTrees = listOf("com.github.takahirom.sample") + androidSetup { + enable = true + generatePreviewTests { + scanPackages.set(listOf("com.github.takahirom.sample")) + } } } From ef12f3b3f7c07e454d40dd74f5424df820ddde96 Mon Sep 17 00:00:00 2001 From: takahirom Date: Sun, 30 Jun 2024 10:53:00 +0900 Subject: [PATCH 19/24] Prototype base and advanced API --- ...on.kt => AdvancedAndroidSetupExtension.kt} | 4 +- .../takahirom/roborazzi/RoborazziPlugin.kt | 45 ++++++++++++++++--- sample-auto-preview/build.gradle.kts | 12 ++--- 3 files changed, 48 insertions(+), 13 deletions(-) rename include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/{AndroidSetupExtension.kt => AdvancedAndroidSetupExtension.kt} (98%) diff --git a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AndroidSetupExtension.kt b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AdvancedAndroidSetupExtension.kt similarity index 98% rename from include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AndroidSetupExtension.kt rename to include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AdvancedAndroidSetupExtension.kt index 7f6f432d..08888da0 100644 --- a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AndroidSetupExtension.kt +++ b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AdvancedAndroidSetupExtension.kt @@ -17,7 +17,7 @@ import org.gradle.api.tasks.testing.Test import java.io.File import javax.inject.Inject -open class AndroidSetupExtension @Inject constructor(objects: ObjectFactory) { +open class AdvancedAndroidSetupExtension @Inject constructor(objects: ObjectFactory) { /** * Default value is false. */ @@ -71,7 +71,7 @@ open class TestConfigExtension @Inject constructor(objects: ObjectFactory) { fun setupAndroidSetupExtension( project: Project, variant: Variant, - extension: AndroidSetupExtension, + extension: AdvancedAndroidSetupExtension, androidExtension: TestedExtension, testTaskProvider: TaskCollection ) { 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 631863da..b4aceb59 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 @@ -6,6 +6,7 @@ import com.android.build.api.variant.LibraryAndroidComponentsExtension import com.android.build.gradle.TestedExtension import com.github.takahirom.roborazzi.CaptureResult import com.github.takahirom.roborazzi.CaptureResults +import com.github.takahirom.roborazzi.ExperimentalRoborazziApi import com.github.takahirom.roborazzi.InternalRoborazziApi import com.github.takahirom.roborazzi.RoborazziReportConst import org.gradle.api.Action @@ -48,14 +49,46 @@ private const val DEFAULT_TEMP_DIR = "intermediates/roborazzi" */ open class RoborazziExtension @Inject constructor(objects: ObjectFactory) { val outputDir: DirectoryProperty = objects.directoryProperty() - val androidSetup: AndroidSetupExtension = - objects.newInstance(AndroidSetupExtension::class.java) - fun androidSetup(action: Action) { - action.execute(androidSetup) + // UseCase based APIs + sealed interface BaseSetupConfig { + fun setup(advancedAndroidSetupExtension: AdvancedAndroidSetupExtension) + fun name(): String + data class BestPractice(val previewScanPackages: List) : + BaseSetupConfig by AndroidAutomaticPreviewScreenshots(previewScanPackages) + + data class AndroidAutomaticPreviewScreenshots(val scanPackages: List) : + BaseSetupConfig { + override fun name(): String { + return "AndroidAutomaticPreviewScreenshots" + } + + override fun setup(advancedAndroidSetupExtension: AdvancedAndroidSetupExtension) { + advancedAndroidSetupExtension.enable.set(true) + advancedAndroidSetupExtension.generatePreviewTests { + scanPackages.set(this@AndroidAutomaticPreviewScreenshots.scanPackages) + } + } + } + } + + @ExperimentalRoborazziApi + fun baseSetupConfig(baseSetupConfig: BaseSetupConfig) { + baseSetupConfig.setup(advancedAndroidSetup) + } + + // Configuration based APIs + @ExperimentalRoborazziApi + val advancedAndroidSetup: AdvancedAndroidSetupExtension = + objects.newInstance(AdvancedAndroidSetupExtension::class.java) + + @ExperimentalRoborazziApi + fun advancedAndroidSetup(action: AdvancedAndroidSetupExtension.() -> Unit) { + action(advancedAndroidSetup) } } + @Suppress("unused") // From Paparazzi: https://github.com/cashapp/paparazzi/blob/a76702744a7f380480f323ffda124e845f2733aa/paparazzi/paparazzi-gradle-plugin/src/main/java/app/cash/paparazzi/gradle/PaparazziPlugin.kt abstract class RoborazziPlugin : Plugin { @@ -431,14 +464,14 @@ abstract class RoborazziPlugin : Plugin { val variantSlug = variant.name.capitalizeUS() val testVariantSlug = unitTest.name.capitalizeUS() val isEnableAutomaticPreviewScreenshots = - extension.androidSetup.enable.convention(false).get() + extension.advancedAndroidSetup.enable.convention(false).get() val testTaskName = "test$testVariantSlug" if (isEnableAutomaticPreviewScreenshots) { setupAndroidSetupExtension( project = project, variant = variant, - extension = extension.androidSetup, + extension = extension.advancedAndroidSetup, androidExtension = project.extensions.getByType(TestedExtension::class.java), testTaskProvider = findTestTaskProvider(Test::class, testTaskName) ) diff --git a/sample-auto-preview/build.gradle.kts b/sample-auto-preview/build.gradle.kts index b7c9f73c..96118a29 100644 --- a/sample-auto-preview/build.gradle.kts +++ b/sample-auto-preview/build.gradle.kts @@ -1,3 +1,5 @@ +import io.github.takahirom.roborazzi.RoborazziExtension.BaseSetupConfig.AndroidAutomaticPreviewScreenshots + plugins { id("com.android.application") // id("com.android.library") @@ -6,11 +8,11 @@ plugins { } roborazzi { - androidSetup { - enable = true - generatePreviewTests { - scanPackages.set(listOf("com.github.takahirom.sample")) - } + baseSetupConfig( + AndroidAutomaticPreviewScreenshots(listOf("com.github.takahirom.sample")) + ) + advancedAndroidSetup { + libraryDependencies.junitVersion = "4.13.2" } } From 12c92b95cc977fcd3ecf7b2e79202cfa73029529 Mon Sep 17 00:00:00 2001 From: takahirom Date: Tue, 2 Jul 2024 19:27:17 +0900 Subject: [PATCH 20/24] Use error and warning-based approach instead of manipulating user configurations --- .../roborazzi-gradle-plugin/build.gradle | 42 --- .../AdvancedAndroidSetupExtension.kt | 261 ------------------ ...enerateRobolectricPreviewTestsExtension.kt | 241 ++++++++++++++++ .../takahirom/roborazzi/RoborazziPlugin.kt | 54 +--- .../.gitignore | 0 .../build.gradle.kts | 30 +- .../proguard-rules.pro | 0 .../sample/ExampleInstrumentedTest.kt | 0 .../src/main/AndroidManifest.xml | 0 .../takahirom/preview/tests}/Previews.kt | 6 +- .../takahirom/sample/ExampleUnitTest.kt | 0 settings.gradle | 2 +- 12 files changed, 278 insertions(+), 358 deletions(-) delete mode 100644 include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AdvancedAndroidSetupExtension.kt create mode 100644 include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/GenerateRobolectricPreviewTestsExtension.kt rename {sample-auto-preview => sample-generate-preview-tests}/.gitignore (100%) rename {sample-auto-preview => sample-generate-preview-tests}/build.gradle.kts (57%) rename {sample-auto-preview => sample-generate-preview-tests}/proguard-rules.pro (100%) rename {sample-auto-preview => sample-generate-preview-tests}/src/androidTest/java/com/github/takahirom/sample/ExampleInstrumentedTest.kt (100%) rename {sample-auto-preview => sample-generate-preview-tests}/src/main/AndroidManifest.xml (100%) rename {sample-auto-preview/src/main/java/com/github/takahirom/sample => sample-generate-preview-tests/src/main/java/com/github/takahirom/preview/tests}/Previews.kt (92%) rename {sample-auto-preview => sample-generate-preview-tests}/src/test/java/com/github/takahirom/sample/ExampleUnitTest.kt (100%) diff --git a/include-build/roborazzi-gradle-plugin/build.gradle b/include-build/roborazzi-gradle-plugin/build.gradle index 17188d46..3d768fac 100644 --- a/include-build/roborazzi-gradle-plugin/build.gradle +++ b/include-build/roborazzi-gradle-plugin/build.gradle @@ -10,48 +10,6 @@ apply plugin: 'java-gradle-plugin' def integrationTestDep = sourceSets.create('integrationTestDep') -def generatedSourcesDir = "${project.layout.buildDirectory.get()}/generated/sources/buildConfig/java/main" -sourceSets { - main { - java { - srcDir(generatedSourcesDir) - } - } -} - -tasks.register("generateBuildConfig") { - def libs = ["composable-preview-scanner", "junit", "robolectric"] - def versionMapString = "\"roborazzi\" to \"$VERSION_NAME\", \n" + ( - libs.collect { - def version = project.extensions.getByType(VersionCatalogsExtension.class) - .named("libs") - .findVersion(it) - .get() - "\"${it}\" to \"${version}\"," - }.join("\n")) - inputs.property("versionMapString", versionMapString) - doLast { - def buildConfigFile = new File(generatedSourcesDir, "io/github/takahirom/roborazzi/BuildConfig.kt") - buildConfigFile.parentFile.mkdirs() - - def libString = "val defaultLibraryVersionsMap = mapOf(" + inputs.properties["versionMapString"] + ")\n" - "" - buildConfigFile.text = """ -package io.github.takahirom.roborazzi -class BuildConfig { - companion object { - ${libString} - } -} - """ - } -} - -tasks.named("compileKotlin") { - dependsOn("generateBuildConfig") -} - - dependencies { compileOnly gradleApi() compileOnly libs.android.tools.build.gradle diff --git a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AdvancedAndroidSetupExtension.kt b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AdvancedAndroidSetupExtension.kt deleted file mode 100644 index 08888da0..00000000 --- a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/AdvancedAndroidSetupExtension.kt +++ /dev/null @@ -1,261 +0,0 @@ -package io.github.takahirom.roborazzi - -import com.android.build.api.variant.Variant -import com.android.build.gradle.TestedExtension -import org.gradle.api.DefaultTask -import org.gradle.api.Project -import org.gradle.api.artifacts.dsl.DependencyHandler -import org.gradle.api.file.DirectoryProperty -import org.gradle.api.model.ObjectFactory -import org.gradle.api.provider.ListProperty -import org.gradle.api.provider.Property -import org.gradle.api.tasks.Input -import org.gradle.api.tasks.OutputDirectory -import org.gradle.api.tasks.TaskAction -import org.gradle.api.tasks.TaskCollection -import org.gradle.api.tasks.testing.Test -import java.io.File -import javax.inject.Inject - -open class AdvancedAndroidSetupExtension @Inject constructor(objects: ObjectFactory) { - /** - * Default value is false. - */ - val enable: Property = objects.property(Boolean::class.java) - .convention(false) - val generatePreviewTests: GeneratePreviewTestsExtension = - objects.newInstance(GeneratePreviewTestsExtension::class.java) - - fun generatePreviewTests(action: GeneratePreviewTestsExtension.() -> Unit) { - generatePreviewTests.action() - } - - val libraryDependencies: LibraryDependenciesExtension = - objects.newInstance(LibraryDependenciesExtension::class.java) - - fun libraryDependencies(action: LibraryDependenciesExtension.() -> Unit) { - libraryDependencies.action() - } - - val testConfig: TestConfigExtension = objects.newInstance(TestConfigExtension::class.java) - - fun testConfig(action: TestConfigExtension.() -> Unit) { - testConfig.action() - } -} - -open class GeneratePreviewTestsExtension @Inject constructor(objects: ObjectFactory) { - val enable: Property = objects.property(Boolean::class.java) - val scanPackages: ListProperty = objects.listProperty(String::class.java) -} - -open class LibraryDependenciesExtension @Inject constructor(objects: ObjectFactory) { - val enable: Property = objects.property(Boolean::class.java) - val roborazziVersion: Property = objects.property(String::class.java) - val junitVersion: Property = objects.property(String::class.java) - val robolectricVersion: Property = objects.property(String::class.java) - val composablePreviewScannerVersion: Property = objects.property(String::class.java) - - companion object { - const val SKIP = "SKIP_DEPENDENCY" - } -} - -open class TestConfigExtension @Inject constructor(objects: ObjectFactory) { - val enable: Property = objects.property(Boolean::class.java) - val includeAndroidResources: Property = objects.property(Boolean::class.java) - val roborazziFilePathStrategy: Property = objects.property(String::class.java) - val robolectricRenderMode: Property = objects.property(String::class.java) -} - -fun setupAndroidSetupExtension( - project: Project, - variant: Variant, - extension: AdvancedAndroidSetupExtension, - androidExtension: TestedExtension, - testTaskProvider: TaskCollection -) { - // Prioritize the enable property in the extension over the global enable property. - if ((extension.generatePreviewTests.enable.orNull ?: extension.enable.orNull) == true) { - setupGeneratePreviewTestsTask(project, variant, extension.generatePreviewTests) - } - if ((extension.libraryDependencies.enable.orNull ?: extension.enable.orNull) == true) { - addLibraryDependencies(variant, project, extension.libraryDependencies) - } - if ((extension.testConfig.enable.orNull ?: extension.enable.orNull) == true) { - setupTestConfig(testTaskProvider, extension.testConfig, androidExtension) - } -} - -private fun setupGeneratePreviewTestsTask( - project: Project, - variant: Variant, - extension: GeneratePreviewTestsExtension -) { - assert(extension.scanPackages.get().orEmpty().isNotEmpty()) { - "Please set androidSetup.generatePreviewTests.scanPackages in the generatePreviewTests extension or set androidSetup.generatePreviewTests.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()}PreviewScreenshotTests", - GeneratePreviewScreenshotTestsTask::class.java - ) { - // It seems that this directory path is overridden by addGeneratedSourceDirectory. - // The generated tests will be located in build/JAVA/generate[VariantName]PreviewScreenshotTests. - it.outputDir.set(project.layout.buildDirectory.dir("generated/roborazzi/preview-screenshot")) - it.scanPackageTrees.set(extension?.scanPackages) - } - // We need to use Java here; otherwise, the generate task will not be executed. - // https://stackoverflow.com/a/76870110/4339442 - variant.unitTest?.sources?.java?.addGeneratedSourceDirectory( - generateTestsTask, - GeneratePreviewScreenshotTestsTask::outputDir - ) -} - -private fun setupTestConfig( - testTaskProvider: TaskCollection, - testConfiguration: TestConfigExtension, - androidExtension: TestedExtension -) { - // Default true - if (testConfiguration.includeAndroidResources.orNull != false) { - androidExtension.testOptions.unitTests.isIncludeAndroidResources = true - } - testTaskProvider.configureEach { testTask: Test -> - // see: https://github.com/takahirom/roborazzi?tab=readme-ov-file#roborazzirecordfilepathstrategy - testTask.systemProperties["roborazzi.record.filePathStrategy"] = - testConfiguration.roborazziFilePathStrategy.orNull - ?: "relativePathFromRoborazziContextOutputDirectory" - // see: https://github.com/takahirom/roborazzi?tab=readme-ov-file#robolectricpixelcopyrendermode - testTask.systemProperties["robolectric.pixelCopyRenderMode"] = - testConfiguration.robolectricRenderMode.orNull - ?: "hardware" - } -} - -private fun addLibraryDependencies( - variant: Variant, - project: Project, - libraryVersionsExtension: LibraryDependenciesExtension? -) { - val configurationName = "test${variant.name.capitalize()}Implementation" - - fun Property?.getLibraryVersion(libraryName: String): String { - if (this == null) { - return BuildConfig.defaultLibraryVersionsMap[libraryName]!! - } - if (this.orNull == LibraryDependenciesExtension.SKIP) { - return LibraryDependenciesExtension.SKIP - } - return convention(BuildConfig.defaultLibraryVersionsMap[libraryName]!!).get() - } - - val roborazziVersion = libraryVersionsExtension?.roborazziVersion.getLibraryVersion("roborazzi") - fun DependencyHandler.addIfNotSkip(libraryName: String, version: String) { - if (version != LibraryDependenciesExtension.SKIP) { - add(configurationName, "$libraryName:$version") - } - } - project.dependencies.addIfNotSkip( - libraryName = "io.github.takahirom.roborazzi:roborazzi-compose", - version = roborazziVersion - ) - project.dependencies.addIfNotSkip( - libraryName = "io.github.takahirom.roborazzi:roborazzi", - version = roborazziVersion - ) - val junitLibraryVersion = libraryVersionsExtension?.junitVersion.getLibraryVersion("junit") - project.dependencies.addIfNotSkip( - "junit:junit", junitLibraryVersion - ) - val robolectricLibraryVersion = - libraryVersionsExtension?.robolectricVersion.getLibraryVersion("robolectric") - project.dependencies.addIfNotSkip( - libraryName = "org.robolectric:robolectric", - version = robolectricLibraryVersion - ) - - // For ComposablePreviewScanner - val composePreviewScannerLibraryVersion = - libraryVersionsExtension?.composablePreviewScannerVersion.getLibraryVersion("composable-preview-scanner") - if (composePreviewScannerLibraryVersion != LibraryDependenciesExtension.SKIP) { - project.repositories.add(project.repositories.maven { it.setUrl("https://jitpack.io") }) - project.repositories.add(project.repositories.mavenCentral()) - project.repositories.add(project.repositories.google()) - project.dependencies.addIfNotSkip( - libraryName = "com.github.sergio-sastre.ComposablePreviewScanner:android", - version = composePreviewScannerLibraryVersion - ) - } -} - -abstract class GeneratePreviewScreenshotTestsTask : DefaultTask() { - @get:OutputDirectory - abstract val outputDir: DirectoryProperty - - @get:Input - var scanPackageTrees: ListProperty = project.objects.listProperty(String::class.java) - - @TaskAction - fun generateTests() { - val testDir = outputDir.get().asFile - testDir.mkdirs() - - val packagesExpr = scanPackageTrees.get().joinToString(", ") { "\"$it\"" } - val scanPackageTreeExpr = ".scanPackageTrees($packagesExpr)" - - File(testDir, "PreviewParameterizedTests.kt").writeText( - """ - import org.junit.Test - import org.junit.runner.RunWith - import org.robolectric.ParameterizedRobolectricTestRunner - import org.robolectric.annotation.Config - import org.robolectric.annotation.GraphicsMode - import com.github.takahirom.roborazzi.captureRoboImage - import sergio.sastre.composable.preview.scanner.core.preview.ComposablePreview - import sergio.sastre.composable.preview.scanner.android.AndroidPreviewInfo - import sergio.sastre.composable.preview.scanner.android.AndroidComposablePreviewScanner - import sergio.sastre.composable.preview.scanner.android.screenshotid.AndroidPreviewScreenshotIdBuilder - import com.github.takahirom.roborazzi.DEFAULT_ROBORAZZI_OUTPUT_DIR_PATH - - - @RunWith(ParameterizedRobolectricTestRunner::class) - @GraphicsMode(GraphicsMode.Mode.NATIVE) - class PreviewParameterizedTests( - private val preview: ComposablePreview, - ) { - - companion object { - val previews: List> by lazy { - AndroidComposablePreviewScanner() - $scanPackageTreeExpr - .getPreviews() - } - @JvmStatic - @ParameterizedRobolectricTestRunner.Parameters - fun values(): List> = - previews - } - - fun createScreenshotIdFor(preview: ComposablePreview) = - AndroidPreviewScreenshotIdBuilder(preview) - .ignoreClassName() - .build() - - @GraphicsMode(GraphicsMode.Mode.NATIVE) - @Config(sdk = [30]) - @Test - fun test() { - val filePath = createScreenshotIdFor(preview) + ".png" - captureRoboImage(filePath = filePath) { - preview() - } - } - - } - """.trimIndent() - ) - } -} \ No newline at end of file diff --git a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/GenerateRobolectricPreviewTestsExtension.kt b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/GenerateRobolectricPreviewTestsExtension.kt new file mode 100644 index 00000000..20895372 --- /dev/null +++ b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/GenerateRobolectricPreviewTestsExtension.kt @@ -0,0 +1,241 @@ +package io.github.takahirom.roborazzi + +import com.android.build.api.variant.Variant +import com.android.build.gradle.TestedExtension +import org.gradle.api.DefaultTask +import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration +import org.gradle.api.artifacts.repositories.MavenArtifactRepository +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.logging.Logger +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.TaskCollection +import org.gradle.api.tasks.testing.Test +import java.io.File +import java.net.URLEncoder +import javax.inject.Inject + +open class GenerateRobolectricPreviewTestsExtension @Inject constructor(objects: ObjectFactory) { + /** + * Default value is false. + */ + val enable: Property = objects.property(Boolean::class.java) + .convention(false) + val packages: ListProperty = objects.listProperty(String::class.java) +} + +fun generateRobolectricPreviewTestsIfNeeded( + project: Project, + variant: Variant, + extension: GenerateRobolectricPreviewTestsExtension, + androidExtension: TestedExtension, + testTaskProvider: TaskCollection +) { + // Prioritize the enable property in the extension over the global enable property. + if ((extension.enable.orNull) != true) { + return + } + val logger = project.logger + setupGeneratePreviewTestsTask(project, variant, extension.packages) + project.afterEvaluate { + // Checks + assert(variant.unitTest == null) { + "Roborazzi: Please enable unit tests for the variant '${variant.name}' in the 'build.gradle' file." + } + verifyMavenRepository(project) + verifyLibraryDependencies(project.configurations.getByName("testImplementation")) + verifyTestConfig(testTaskProvider, androidExtension, logger) + } +} + +private fun setupGeneratePreviewTestsTask( + project: Project, + variant: Variant, + scanPackages: ListProperty +) { + assert(scanPackages.get().orEmpty().isNotEmpty()) { + "Please set roborazzi.generateRobolectricPreviewTests.packages in the generatePreviewTests extension or set roborazzi.generateRobolectricPreviewTests.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()}PreviewScreenshotTests", + GeneratePreviewScreenshotTestsTask::class.java + ) { + // It seems that this directory path is overridden by addGeneratedSourceDirectory. + // The generated tests will be located in build/JAVA/generate[VariantName]PreviewScreenshotTests. + it.outputDir.set(project.layout.buildDirectory.dir("generated/roborazzi/preview-screenshot")) + it.scanPackageTrees.set(scanPackages) + } + // We need to use Java here; otherwise, the generate task will not be executed. + // https://stackoverflow.com/a/76870110/4339442 + variant.unitTest?.sources?.java?.addGeneratedSourceDirectory( + generateTestsTask, + GeneratePreviewScreenshotTestsTask::outputDir + ) +} + +private fun verifyMavenRepository(project: Project) { + // Check if the jitpack repository is added. + val hasJitpackRepo = project.repositories.any { + it is MavenArtifactRepository && + it.url.toString().contains("https://jitpack.io") + } + if (!hasJitpackRepo) { + error( + "Roborazzi: Please add the following 'maven' repository to the 'repositories' block in the 'build.gradle' file.\n" + + "build.gradle: maven { url 'https://jitpack.io' }\n" + + "build.gradle.kts: maven { url = uri(\"https://jitpack.io\") }\n" + + "This is necessary to download the ComposablePreviewScanner." + ) + } +} + +private fun verifyTestConfig( + testTaskProvider: TaskCollection, + androidExtension: TestedExtension, + logger: Logger +) { + if (!androidExtension.testOptions.unitTests.isIncludeAndroidResources) { + logger.warn( + "Roborazzi: Please set 'android.testOptions.unitTests.isIncludeAndroidResources = true' in the 'build.gradle' file. " + + "This is advisable to avoid issues with ActivityNotFoundException." + ) + } + testTaskProvider.configureEach { testTask -> + if (testTask.systemProperties["roborazzi.record.filePathStrategy"] != "relativePathFromRoborazziContextOutputDirectory") { + logger.info( + "Roborazzi: Please set 'roborazzi.record.filePathStrategy=relativePathFromRoborazziContextOutputDirectory' in the 'gradle.properties' file. " + + "This is advisable to avoid unnecessary path manipulation. " + + "Please refer to 'https://github.com/takahirom/roborazzi?tab=readme-ov-file#roborazzirecordfilepathstrategy' for more information." + ) + } + if (testTask.systemProperties["robolectric.pixelCopyRenderMode"] != "hardware") { + val example = """ + testOptions { + unitTests { + isIncludeAndroidResources = true + all { + it.systemProperties["robolectric.pixelCopyRenderMode"] = "hardware" + } + } + } + """.trimIndent() + logger.warn( + "Roborazzi: Please set 'robolectric.pixelCopyRenderMode = hardware' in the 'testOptions' block in the 'build.gradle' file. " + + "This is advisable to avoid issues with the fidelity of the images.\n" + + "Please refer to 'https://github.com/takahirom/roborazzi?tab=readme-ov-file#q-the-images-taken-from-roborazzi-seem-broken' for more information.\n" + + "Example:\n$example" + ) + } + } +} + +private fun verifyLibraryDependencies( + runtimeConfiguration: Configuration, +) { + val dependencies = runtimeConfiguration.dependencies + .map { dependency -> dependency.group to dependency.name } + + fun checkExists(libraryName: String) { + if (!dependencies.contains(libraryName.split(":").let { it[0] to it[1] })) { + error( + "Roborazzi: Please add the following 'testImplementation' dependency to the 'dependencies' block in the 'build.gradle' file: '$libraryName' for the '${runtimeConfiguration.name}' configuration.\n" + + "For your convenience, visit https://www.google.com/search?q=" + URLEncoder.encode( + "$libraryName version", + "UTF-8" + ) + "\n" + + "testImplementation(\"$libraryName:version\")" + ) + } + } + + val requiredLibraries = listOf( + "io.github.takahirom.roborazzi:roborazzi-compose", + "io.github.takahirom.roborazzi:roborazzi", + "junit:junit", + "org.robolectric:robolectric", + "com.github.sergio-sastre.ComposablePreviewScanner:android", + ) + requiredLibraries.forEach { checkExists(it) } +} + +abstract class GeneratePreviewScreenshotTestsTask : DefaultTask() { + @get:OutputDirectory + abstract val outputDir: DirectoryProperty + + @get:Input + var scanPackageTrees: ListProperty = project.objects.listProperty(String::class.java) + + @TaskAction + fun generateTests() { + val testDir = outputDir.get().asFile + testDir.mkdirs() + + val packagesExpr = scanPackageTrees.get().joinToString(", ") { "\"$it\"" } + val scanPackageTreeExpr = ".scanPackageTrees($packagesExpr)" + + val className = "RoborazziPreviewParameterizedTests" + File(testDir, "$className.kt").writeText( + """ + import org.junit.Test + import org.junit.runner.RunWith + import org.robolectric.ParameterizedRobolectricTestRunner + import org.robolectric.annotation.Config + import org.robolectric.annotation.GraphicsMode + import com.github.takahirom.roborazzi.captureRoboImage + import sergio.sastre.composable.preview.scanner.core.preview.ComposablePreview + import sergio.sastre.composable.preview.scanner.android.AndroidPreviewInfo + import sergio.sastre.composable.preview.scanner.android.AndroidComposablePreviewScanner + import sergio.sastre.composable.preview.scanner.android.screenshotid.AndroidPreviewScreenshotIdBuilder + import com.github.takahirom.roborazzi.DEFAULT_ROBORAZZI_OUTPUT_DIR_PATH + + + @RunWith(ParameterizedRobolectricTestRunner::class) + @GraphicsMode(GraphicsMode.Mode.NATIVE) + class $className( + private val preview: ComposablePreview, + ) { + + companion object { + val previews: List> by lazy { + AndroidComposablePreviewScanner() + $scanPackageTreeExpr + .getPreviews() + } + @JvmStatic + @ParameterizedRobolectricTestRunner.Parameters + fun values(): List> = + previews + } + + fun createScreenshotIdFor(preview: ComposablePreview) = + AndroidPreviewScreenshotIdBuilder(preview) + .ignoreClassName() + .build() + + @GraphicsMode(GraphicsMode.Mode.NATIVE) + @Config(sdk = [30]) + @Test + fun test() { + val pathPrefix = if(com.github.takahirom.roborazzi.roborazziRecordFilePathStrategy() == com.github.takahirom.roborazzi.RoborazziRecordFilePathStrategy.RelativePathFromCurrentDirectory) { + DEFAULT_ROBORAZZI_OUTPUT_DIR_PATH + java.io.File.separator + } else { + "" + } + val filePath = pathPrefix + createScreenshotIdFor(preview) + ".png" + captureRoboImage(filePath = filePath) { + preview() + } + } + + } + """.trimIndent() + ) + } +} \ No newline at end of file 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 b4aceb59..04396fbd 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 @@ -50,41 +50,14 @@ private const val DEFAULT_TEMP_DIR = "intermediates/roborazzi" open class RoborazziExtension @Inject constructor(objects: ObjectFactory) { val outputDir: DirectoryProperty = objects.directoryProperty() - // UseCase based APIs - sealed interface BaseSetupConfig { - fun setup(advancedAndroidSetupExtension: AdvancedAndroidSetupExtension) - fun name(): String - data class BestPractice(val previewScanPackages: List) : - BaseSetupConfig by AndroidAutomaticPreviewScreenshots(previewScanPackages) - - data class AndroidAutomaticPreviewScreenshots(val scanPackages: List) : - BaseSetupConfig { - override fun name(): String { - return "AndroidAutomaticPreviewScreenshots" - } - - override fun setup(advancedAndroidSetupExtension: AdvancedAndroidSetupExtension) { - advancedAndroidSetupExtension.enable.set(true) - advancedAndroidSetupExtension.generatePreviewTests { - scanPackages.set(this@AndroidAutomaticPreviewScreenshots.scanPackages) - } - } - } - } - - @ExperimentalRoborazziApi - fun baseSetupConfig(baseSetupConfig: BaseSetupConfig) { - baseSetupConfig.setup(advancedAndroidSetup) - } - // Configuration based APIs @ExperimentalRoborazziApi - val advancedAndroidSetup: AdvancedAndroidSetupExtension = - objects.newInstance(AdvancedAndroidSetupExtension::class.java) + val generateRobolectricPreviewTests: GenerateRobolectricPreviewTestsExtension = + objects.newInstance(GenerateRobolectricPreviewTestsExtension::class.java) @ExperimentalRoborazziApi - fun advancedAndroidSetup(action: AdvancedAndroidSetupExtension.() -> Unit) { - action(advancedAndroidSetup) + fun generateRobolectricPreviewTests(action: GenerateRobolectricPreviewTestsExtension.() -> Unit) { + action(generateRobolectricPreviewTests) } } @@ -463,19 +436,14 @@ abstract class RoborazziPlugin : Plugin { val unitTest = variant.unitTest ?: return@onVariants val variantSlug = variant.name.capitalizeUS() val testVariantSlug = unitTest.name.capitalizeUS() - val isEnableAutomaticPreviewScreenshots = - extension.advancedAndroidSetup.enable.convention(false).get() val testTaskName = "test$testVariantSlug" - if (isEnableAutomaticPreviewScreenshots) { - - setupAndroidSetupExtension( - project = project, - variant = variant, - extension = extension.advancedAndroidSetup, - androidExtension = project.extensions.getByType(TestedExtension::class.java), - testTaskProvider = findTestTaskProvider(Test::class, testTaskName) - ) - } + generateRobolectricPreviewTestsIfNeeded( + project = project, + variant = variant, + extension = extension.generateRobolectricPreviewTests, + androidExtension = project.extensions.getByType(TestedExtension::class.java), + testTaskProvider = findTestTaskProvider(Test::class, testTaskName) + ) // e.g. testDebugUnitTest -> recordRoborazziDebug configureRoborazziTasks( diff --git a/sample-auto-preview/.gitignore b/sample-generate-preview-tests/.gitignore similarity index 100% rename from sample-auto-preview/.gitignore rename to sample-generate-preview-tests/.gitignore diff --git a/sample-auto-preview/build.gradle.kts b/sample-generate-preview-tests/build.gradle.kts similarity index 57% rename from sample-auto-preview/build.gradle.kts rename to sample-generate-preview-tests/build.gradle.kts index 96118a29..f0141e12 100644 --- a/sample-auto-preview/build.gradle.kts +++ b/sample-generate-preview-tests/build.gradle.kts @@ -1,5 +1,3 @@ -import io.github.takahirom.roborazzi.RoborazziExtension.BaseSetupConfig.AndroidAutomaticPreviewScreenshots - plugins { id("com.android.application") // id("com.android.library") @@ -8,16 +6,20 @@ plugins { } roborazzi { - baseSetupConfig( - AndroidAutomaticPreviewScreenshots(listOf("com.github.takahirom.sample")) - ) - advancedAndroidSetup { - libraryDependencies.junitVersion = "4.13.2" + generateRobolectricPreviewTests { + enable = true + packages = listOf("com.github.takahirom.preview.tests") } } +repositories { + mavenCentral() + google() + maven { url = uri("https://jitpack.io") } +} + android { - namespace = "com.github.takahirom.sample" + namespace = "com.github.takahirom.preview.tests" compileSdk = 34 defaultConfig { @@ -41,6 +43,14 @@ android { ) } } + testOptions { + unitTests { + isIncludeAndroidResources = true + all { + it.systemProperties["robolectric.pixelCopyRenderMode"] = "hardware" + } + } + } } dependencies { @@ -48,7 +58,11 @@ dependencies { implementation(libs.androidx.compose.ui) implementation(libs.androidx.compose.ui.tooling) + testImplementation("io.github.takahirom.roborazzi:roborazzi-compose:1.20.0") + testImplementation("io.github.takahirom.roborazzi:roborazzi:1.20.0") testImplementation(libs.junit) + testImplementation(libs.robolectric) + testImplementation("com.github.sergio-sastre.ComposablePreviewScanner:android:0.1.2") androidTestImplementation(libs.androidx.test.ext.junit) androidTestImplementation(libs.androidx.test.espresso.core) } \ No newline at end of file diff --git a/sample-auto-preview/proguard-rules.pro b/sample-generate-preview-tests/proguard-rules.pro similarity index 100% rename from sample-auto-preview/proguard-rules.pro rename to sample-generate-preview-tests/proguard-rules.pro diff --git a/sample-auto-preview/src/androidTest/java/com/github/takahirom/sample/ExampleInstrumentedTest.kt b/sample-generate-preview-tests/src/androidTest/java/com/github/takahirom/sample/ExampleInstrumentedTest.kt similarity index 100% rename from sample-auto-preview/src/androidTest/java/com/github/takahirom/sample/ExampleInstrumentedTest.kt rename to sample-generate-preview-tests/src/androidTest/java/com/github/takahirom/sample/ExampleInstrumentedTest.kt diff --git a/sample-auto-preview/src/main/AndroidManifest.xml b/sample-generate-preview-tests/src/main/AndroidManifest.xml similarity index 100% rename from sample-auto-preview/src/main/AndroidManifest.xml rename to sample-generate-preview-tests/src/main/AndroidManifest.xml diff --git a/sample-auto-preview/src/main/java/com/github/takahirom/sample/Previews.kt b/sample-generate-preview-tests/src/main/java/com/github/takahirom/preview/tests/Previews.kt similarity index 92% rename from sample-auto-preview/src/main/java/com/github/takahirom/sample/Previews.kt rename to sample-generate-preview-tests/src/main/java/com/github/takahirom/preview/tests/Previews.kt index 4b3f9c23..75c8a626 100644 --- a/sample-auto-preview/src/main/java/com/github/takahirom/sample/Previews.kt +++ b/sample-generate-preview-tests/src/main/java/com/github/takahirom/preview/tests/Previews.kt @@ -1,4 +1,4 @@ -package com.github.takahirom.sample +package com.github.takahirom.preview.tests import android.content.res.Configuration import androidx.compose.foundation.layout.height @@ -16,7 +16,7 @@ import androidx.compose.ui.unit.dp @Preview @Composable -fun Preview() { +fun Preview2() { MaterialTheme { Card( Modifier @@ -25,7 +25,7 @@ fun Preview() { ) { Text( modifier = Modifier.padding(8.dp), - text = "Auto Preview Sample" + text = "Generate Preview Test Sample" ) } } diff --git a/sample-auto-preview/src/test/java/com/github/takahirom/sample/ExampleUnitTest.kt b/sample-generate-preview-tests/src/test/java/com/github/takahirom/sample/ExampleUnitTest.kt similarity index 100% rename from sample-auto-preview/src/test/java/com/github/takahirom/sample/ExampleUnitTest.kt rename to sample-generate-preview-tests/src/test/java/com/github/takahirom/sample/ExampleUnitTest.kt diff --git a/settings.gradle b/settings.gradle index 7138ac61..4691dc2b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -26,7 +26,7 @@ include ':sample-android' include ':sample-android-without-compose' include ':sample-compose-desktop-multiplatform' include ':sample-compose-desktop-jvm' -include ':sample-auto-preview' +include ':sample-generate-preview-tests' includeBuild("include-build") { dependencySubstitution { From 5560e7280f2d898e915745037775a547af631f11 Mon Sep 17 00:00:00 2001 From: takahirom Date: Tue, 2 Jul 2024 22:12:12 +0900 Subject: [PATCH 21/24] Add import com.github.takahirom.roborazzi.* and use roborazziSystemPropertyOutputDirectory() --- .../roborazzi/GenerateRobolectricPreviewTestsExtension.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/GenerateRobolectricPreviewTestsExtension.kt b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/GenerateRobolectricPreviewTestsExtension.kt index 20895372..e02f297c 100644 --- a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/GenerateRobolectricPreviewTestsExtension.kt +++ b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/GenerateRobolectricPreviewTestsExtension.kt @@ -188,12 +188,11 @@ abstract class GeneratePreviewScreenshotTestsTask : DefaultTask() { import org.robolectric.ParameterizedRobolectricTestRunner import org.robolectric.annotation.Config import org.robolectric.annotation.GraphicsMode - import com.github.takahirom.roborazzi.captureRoboImage + import com.github.takahirom.roborazzi.* import sergio.sastre.composable.preview.scanner.core.preview.ComposablePreview import sergio.sastre.composable.preview.scanner.android.AndroidPreviewInfo import sergio.sastre.composable.preview.scanner.android.AndroidComposablePreviewScanner import sergio.sastre.composable.preview.scanner.android.screenshotid.AndroidPreviewScreenshotIdBuilder - import com.github.takahirom.roborazzi.DEFAULT_ROBORAZZI_OUTPUT_DIR_PATH @RunWith(ParameterizedRobolectricTestRunner::class) @@ -223,8 +222,8 @@ abstract class GeneratePreviewScreenshotTestsTask : DefaultTask() { @Config(sdk = [30]) @Test fun test() { - val pathPrefix = if(com.github.takahirom.roborazzi.roborazziRecordFilePathStrategy() == com.github.takahirom.roborazzi.RoborazziRecordFilePathStrategy.RelativePathFromCurrentDirectory) { - DEFAULT_ROBORAZZI_OUTPUT_DIR_PATH + java.io.File.separator + val pathPrefix = if(roborazziRecordFilePathStrategy() == RoborazziRecordFilePathStrategy.RelativePathFromCurrentDirectory) { + roborazziSystemPropertyOutputDirectory() + java.io.File.separator } else { "" } From c2b9b27477a6811124c2df476eb9a3eec787cba3 Mon Sep 17 00:00:00 2001 From: takahirom Date: Wed, 3 Jul 2024 10:13:17 +0900 Subject: [PATCH 22/24] Fix outdated comments --- .../roborazzi/GenerateRobolectricPreviewTestsExtension.kt | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/GenerateRobolectricPreviewTestsExtension.kt b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/GenerateRobolectricPreviewTestsExtension.kt index e02f297c..7e697f6f 100644 --- a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/GenerateRobolectricPreviewTestsExtension.kt +++ b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/GenerateRobolectricPreviewTestsExtension.kt @@ -21,9 +21,6 @@ import java.net.URLEncoder import javax.inject.Inject open class GenerateRobolectricPreviewTestsExtension @Inject constructor(objects: ObjectFactory) { - /** - * Default value is false. - */ val enable: Property = objects.property(Boolean::class.java) .convention(false) val packages: ListProperty = objects.listProperty(String::class.java) @@ -36,14 +33,13 @@ fun generateRobolectricPreviewTestsIfNeeded( androidExtension: TestedExtension, testTaskProvider: TaskCollection ) { - // Prioritize the enable property in the extension over the global enable property. if ((extension.enable.orNull) != true) { return } val logger = project.logger setupGeneratePreviewTestsTask(project, variant, extension.packages) project.afterEvaluate { - // Checks + // We use afterEvaluate only for verify assert(variant.unitTest == null) { "Roborazzi: Please enable unit tests for the variant '${variant.name}' in the 'build.gradle' file." } @@ -72,7 +68,7 @@ private fun setupGeneratePreviewTestsTask( it.outputDir.set(project.layout.buildDirectory.dir("generated/roborazzi/preview-screenshot")) it.scanPackageTrees.set(scanPackages) } - // We need to use Java here; otherwise, the generate task will not be executed. + // 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, From a7984d09452715d7ffdd18e3b4cc21795d47ee78 Mon Sep 17 00:00:00 2001 From: takahirom Date: Tue, 9 Jul 2024 09:40:31 +0900 Subject: [PATCH 23/24] Add (Robolectric 4.12.2+) to the warning text recommending 'robolectric.pixelCopyRenderMode = hardware' --- .../roborazzi/GenerateRobolectricPreviewTestsExtension.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/GenerateRobolectricPreviewTestsExtension.kt b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/GenerateRobolectricPreviewTestsExtension.kt index 7e697f6f..967f5397 100644 --- a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/GenerateRobolectricPreviewTestsExtension.kt +++ b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/GenerateRobolectricPreviewTestsExtension.kt @@ -123,7 +123,7 @@ private fun verifyTestConfig( } """.trimIndent() logger.warn( - "Roborazzi: Please set 'robolectric.pixelCopyRenderMode = hardware' in the 'testOptions' block in the 'build.gradle' file. " + + "Roborazzi: Please set 'robolectric.pixelCopyRenderMode = hardware' (Robolectric 4.12.2+) in the 'testOptions' block in the 'build.gradle' file. " + "This is advisable to avoid issues with the fidelity of the images.\n" + "Please refer to 'https://github.com/takahirom/roborazzi?tab=readme-ov-file#q-the-images-taken-from-roborazzi-seem-broken' for more information.\n" + "Example:\n$example" From 66224435e65ccaf5870871ea7c5831ce930816ce Mon Sep 17 00:00:00 2001 From: Takahiro Menju Date: Fri, 12 Jul 2024 19:31:21 +0900 Subject: [PATCH 24/24] Add roborazzi-compose-preview-scanner-support module to handle preview settings (#427) * Add roborazzi-compose-preview-scanner-support module to handle previews * Remove confusing option of generatedClassFQDN * Add comment * Fix integration tests * Add PreviewDarkMode preview * Make it more customizable * Fix space * Fix indent of generated code * Remove height of preview * Use Config(qualifiers) instead of setQualifiers to be able to setup by user * Rename customTestClassFQDN to customTestQualifiedClassName and add document * Add comments * Try ja-rJP * Add README section of Compose Preview Support * Use roborazziDefaultNamingStrategy() for Preview * Fix naming of parameterized tests * Divide the preview into two preview to fix too long file name * Revert unneeded change * Add The supported `@Preview` annotation options section * Change generateRobolectricPreviewTests to generateRobolectricComposePreviewTests * Rename generateRobolectricPreviewTests to generateRobolectricComposePreviewTests * Fix some warning and errors --- README.md | 71 ++++++++++++ README.template.md | 2 + build.gradle | 8 ++ docs/roborazzi-docs.tree | 1 + docs/topics/preview_support.md | 66 +++++++++++ gradle/libs.versions.toml | 3 + .../roborazzi/DefaultFileNameGenerator.kt | 28 ++--- ...obolectricComposePreviewTestsExtension.kt} | 106 ++++++++++++------ .../takahirom/roborazzi/RoborazziPlugin.kt | 15 +-- .../.gitignore | 1 + .../build.gradle | 28 +++++ .../consumer-rules.pro | 0 .../proguard-rules.pro | 21 ++++ .../src/main/AndroidManifest.xml | 5 + .../RobolectricPreviewInfosApplier.kt | 30 +++++ .../RoborazziPreviewScannerSupport.kt | 68 +++++++++++ .../idea/preview/RoborazziPreviewTool.kt | 1 - .../build.gradle.kts | 9 +- .../takahirom/preview/tests/Previews.kt | 54 +++++++-- settings.gradle | 1 + 20 files changed, 449 insertions(+), 69 deletions(-) create mode 100644 docs/topics/preview_support.md rename include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/{GenerateRobolectricPreviewTestsExtension.kt => GenerateRobolectricComposePreviewTestsExtension.kt} (71%) create mode 100644 roborazzi-compose-preview-scanner-support/.gitignore create mode 100644 roborazzi-compose-preview-scanner-support/build.gradle create mode 100644 roborazzi-compose-preview-scanner-support/consumer-rules.pro create mode 100644 roborazzi-compose-preview-scanner-support/proguard-rules.pro create mode 100644 roborazzi-compose-preview-scanner-support/src/main/AndroidManifest.xml create mode 100644 roborazzi-compose-preview-scanner-support/src/main/java/com/github/takahirom/roborazzi/RobolectricPreviewInfosApplier.kt create mode 100644 roborazzi-compose-preview-scanner-support/src/main/java/com/github/takahirom/roborazzi/RoborazziPreviewScannerSupport.kt diff --git a/README.md b/README.md index 5d1ed66c..e99f478c 100644 --- a/README.md +++ b/README.md @@ -944,6 +944,77 @@ If you are having trouble debugging your test, try Dump mode as follows. ![image](https://user-images.githubusercontent.com/1386930/226364158-a07a0fb0-d8e7-46b7-a495-8dd217faaadb.png) + +
+ + +# 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. + +## Generate Compose Preview screenshot tests + +You first need to add the Roborazzi plugin to your project. Please refer to the [setup guide](https://takahirom.github.io/roborazzi/build-setup.html) for more information. +Then you can enable the Compose Preview screenshot test generation feature by adding the following configuration to your `build.gradle.kts` file: + +```kotlin +roborazzi { + generateComposePreviewRobolectricTests { + enable = true + } +} +``` + +The plugin will not automatically change your settings or add dependencies to prevent conflicts with your existing setup. However, it will provide instructions on what to do next, such as adding dependencies and required code. + +After that, you can run the `recordRoborazziDebug` task to generate screenshots using the generated tests, as described in the [setup guide](https://takahirom.github.io/roborazzi/build-setup.html). + +### Customizing the Preview screenshot test + +You can customize the generated test by adding the following configuration to your `build.gradle` file: + +```kotlin +roborazzi { + generateComposePreviewRobolectricTests { + 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.RobolectricPreviewTest]. + customTestQualifiedClassName = "com.example.MyCustomRobolectricPreviewTest" + // 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", + ) + } +} +``` + +## Manually adding Compose Preview screenshot tests + +Roborazzi provides a helper function for ComposePreviewScanner. +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.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). + +```kotlin +fun ComposablePreview.captureRoboImage( + filePath: String, + roborazziOptions: RoborazziOptions +) +``` + +### The supported `@Preview` annotation options + +Currently, we don't support all the annotation options provided by the Compose Preview. +You can check the supported annotations in the [source code](https://github.com/takahirom/roborazzi/blob/main/roborazzi-compose-preview-scanner-support/src/main/java/com/github/takahirom/roborazzi/RobolectricPreviewInfosApplier.kt). +We are looking forward to your contributions to support more annotation options. +
diff --git a/README.template.md b/README.template.md index ba49aafa..84bbd4d9 100644 --- a/README.template.md +++ b/README.template.md @@ -7,6 +7,8 @@
+
+
diff --git a/build.gradle b/build.gradle index b83676c2..714484d8 100644 --- a/build.gradle +++ b/build.gradle @@ -83,6 +83,14 @@ allprojects { } } } + + configurations.all { + resolutionStrategy { + dependencySubstitution { + substitute(module("io.github.takahirom.roborazzi:roborazzi-compose-preview-scanner-support")).using(project(":roborazzi-compose-preview-scanner-support")) + } + } + } } class Topic { diff --git a/docs/roborazzi-docs.tree b/docs/roborazzi-docs.tree index c4e8d197..30e3e59b 100644 --- a/docs/roborazzi-docs.tree +++ b/docs/roborazzi-docs.tree @@ -10,6 +10,7 @@ + diff --git a/docs/topics/preview_support.md b/docs/topics/preview_support.md new file mode 100644 index 00000000..d37c11bb --- /dev/null +++ b/docs/topics/preview_support.md @@ -0,0 +1,66 @@ +# 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. + +## Generate Compose Preview screenshot tests + +You first need to add the Roborazzi plugin to your project. Please refer to the [setup guide](https://takahirom.github.io/roborazzi/build-setup.html) for more information. +Then you can enable the Compose Preview screenshot test generation feature by adding the following configuration to your `build.gradle.kts` file: + +```kotlin +roborazzi { + generateComposePreviewRobolectricTests { + enable = true + } +} +``` + +The plugin will not automatically change your settings or add dependencies to prevent conflicts with your existing setup. However, it will provide instructions on what to do next, such as adding dependencies and required code. + +After that, you can run the `recordRoborazziDebug` task to generate screenshots using the generated tests, as described in the [setup guide](https://takahirom.github.io/roborazzi/build-setup.html). + +### Customizing the Preview screenshot test + +You can customize the generated test by adding the following configuration to your `build.gradle` file: + +```kotlin +roborazzi { + generateComposePreviewRobolectricTests { + 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.RobolectricPreviewTest]. + customTestQualifiedClassName = "com.example.MyCustomRobolectricPreviewTest" + // 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", + ) + } +} +``` + +## Manually adding Compose Preview screenshot tests + +Roborazzi provides a helper function for ComposePreviewScanner. +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.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). + +```kotlin +fun ComposablePreview.captureRoboImage( + filePath: String, + roborazziOptions: RoborazziOptions +) +``` + +### The supported `@Preview` annotation options + +Currently, we don't support all the annotation options provided by the Compose Preview. +You can check the supported annotations in the [source code](https://github.com/takahirom/roborazzi/blob/main/roborazzi-compose-preview-scanner-support/src/main/java/com/github/takahirom/roborazzi/RobolectricPreviewInfosApplier.kt). +We are looking forward to your contributions to support more annotation options. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 07265670..85b0da26 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -55,6 +55,9 @@ kotlin-stdlib-jdk8 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test" } kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit" } +# for sample +composable-preview-scanner = { module = "com.github.sergio-sastre.ComposablePreviewScanner:android", version.ref = "composable-preview-scanner" } + androidx-activity = { module = "androidx.activity:activity", version.ref = "androidx-activity" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" } androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } diff --git a/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/DefaultFileNameGenerator.kt b/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/DefaultFileNameGenerator.kt index 4abb6f65..5464bd99 100644 --- a/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/DefaultFileNameGenerator.kt +++ b/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/DefaultFileNameGenerator.kt @@ -11,6 +11,19 @@ object DefaultFileNameGenerator { EscapedTestPackageAndClassAndMethod("escapedTestPackageAndClassAndMethod"), TestClassAndMethod("testClassAndMethod"); + @ExperimentalRoborazziApi + fun generateOutputName(className: String, methodName: String?): String { + return when (this) { + TestPackageAndClassAndMethod -> "$className.$methodName" + EscapedTestPackageAndClassAndMethod -> className.replace( + ".", + "_" + ) + "." + methodName + + TestClassAndMethod -> className.substringAfterLast(".") + "." + methodName + } + } + companion object { fun fromOptionName(optionName: String): DefaultNamingStrategy { return values().firstOrNull { it.optionName == optionName } ?: TestPackageAndClassAndMethod @@ -89,7 +102,7 @@ object DefaultFileNameGenerator { } ?: throw IllegalArgumentException("Roborazzi can't find method of test. Please specify file name or use Rule") val testName = - generateOutputName(stackTraceElement.className, stackTraceElement.methodName) + defaultNamingStrategy.generateOutputName(stackTraceElement.className, stackTraceElement.methodName) return testName } @@ -112,19 +125,8 @@ object DefaultFileNameGenerator { internal fun generateOutputNameWithDescription(description: Description): String { val className = description.className val methodName = description.methodName - val testName = generateOutputName(className, methodName) + val testName = defaultNamingStrategy.generateOutputName(className, methodName) return testName } - private fun generateOutputName(className: String, methodName: String?): String { - return when (defaultNamingStrategy) { - DefaultNamingStrategy.TestPackageAndClassAndMethod -> "$className.$methodName" - DefaultNamingStrategy.EscapedTestPackageAndClassAndMethod -> className.replace( - ".", - "_" - ) + "." + methodName - - DefaultNamingStrategy.TestClassAndMethod -> className.substringAfterLast(".") + "." + methodName - } - } } diff --git a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/GenerateRobolectricPreviewTestsExtension.kt b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/GenerateRobolectricComposePreviewTestsExtension.kt similarity index 71% rename from include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/GenerateRobolectricPreviewTestsExtension.kt rename to include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/GenerateRobolectricComposePreviewTestsExtension.kt index 967f5397..51e45e69 100644 --- a/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/GenerateRobolectricPreviewTestsExtension.kt +++ b/include-build/roborazzi-gradle-plugin/src/main/java/io/github/takahirom/roborazzi/GenerateRobolectricComposePreviewTestsExtension.kt @@ -10,6 +10,7 @@ import org.gradle.api.file.DirectoryProperty import org.gradle.api.logging.Logger import org.gradle.api.model.ObjectFactory import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.MapProperty import org.gradle.api.provider.Property import org.gradle.api.tasks.Input import org.gradle.api.tasks.OutputDirectory @@ -20,16 +21,38 @@ import java.io.File import java.net.URLEncoder import javax.inject.Inject -open class GenerateRobolectricPreviewTestsExtension @Inject constructor(objects: ObjectFactory) { +open class GenerateComposePreviewRobolectricTestsExtension @Inject constructor(objects: ObjectFactory) { val enable: Property = objects.property(Boolean::class.java) .convention(false) + + /** + * The package names to scan for the Composable Previews. + */ val packages: ListProperty = objects.listProperty(String::class.java) + + /** + * The fully qualified class name of the custom test class that implements [com.github.takahirom.roborazzi.RobolectricPreviewTest]. + */ + val customTestQualifiedClassName: Property = objects.property(String::class.java) + .convention("com.github.takahirom.roborazzi.DefaultRobolectricPreviewTest") + + /** + * [robolectricConfig] will be passed to the Robolectric's @Config annotation in the generated test class. + * See https://robolectric.org/configuring/ for more information. + */ + val robolectricConfig: MapProperty = objects.mapProperty(String::class.java, String::class.java) + .convention( + mapOf( + "sdk" to "[33]", + "qualifiers" to "RobolectricDeviceQualifiers.Pixel4a", + ) + ) } fun generateRobolectricPreviewTestsIfNeeded( project: Project, variant: Variant, - extension: GenerateRobolectricPreviewTestsExtension, + extension: GenerateComposePreviewRobolectricTestsExtension, androidExtension: TestedExtension, testTaskProvider: TaskCollection ) { @@ -37,7 +60,13 @@ fun generateRobolectricPreviewTestsIfNeeded( return } val logger = project.logger - setupGeneratePreviewTestsTask(project, variant, extension.packages) + setupGeneratePreviewTestsTask( + project = project, + variant = variant, + scanPackages = extension.packages, + customTestQualifiedClassName = extension.customTestQualifiedClassName, + robolectricConfig = extension.robolectricConfig + ) project.afterEvaluate { // We use afterEvaluate only for verify assert(variant.unitTest == null) { @@ -52,7 +81,9 @@ fun generateRobolectricPreviewTestsIfNeeded( private fun setupGeneratePreviewTestsTask( project: Project, variant: Variant, - scanPackages: ListProperty + scanPackages: ListProperty, + customTestQualifiedClassName: Property, + robolectricConfig: MapProperty, ) { assert(scanPackages.get().orEmpty().isNotEmpty()) { "Please set roborazzi.generateRobolectricPreviewTests.packages in the generatePreviewTests extension or set roborazzi.generateRobolectricPreviewTests.enable = false." + @@ -67,6 +98,8 @@ private fun setupGeneratePreviewTestsTask( // The generated tests will be located in build/JAVA/generate[VariantName]PreviewScreenshotTests. it.outputDir.set(project.layout.buildDirectory.dir("generated/roborazzi/preview-screenshot")) it.scanPackageTrees.set(scanPackages) + it.customTestQualifiedClassName.set(customTestQualifiedClassName) + 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 @@ -85,8 +118,8 @@ private fun verifyMavenRepository(project: Project) { if (!hasJitpackRepo) { error( "Roborazzi: Please add the following 'maven' repository to the 'repositories' block in the 'build.gradle' file.\n" + - "build.gradle: maven { url 'https://jitpack.io' }\n" + - "build.gradle.kts: maven { url = uri(\"https://jitpack.io\") }\n" + + "build.gradle: \nrepositories {\nmaven { url 'https://jitpack.io' } \n}\n" + + "build.gradle.kts: \nrepositories {\nmaven { url = uri(\"https://jitpack.io\") } \n}\n" + "This is necessary to download the ComposablePreviewScanner." ) } @@ -113,11 +146,13 @@ private fun verifyTestConfig( } if (testTask.systemProperties["robolectric.pixelCopyRenderMode"] != "hardware") { val example = """ - testOptions { - unitTests { - isIncludeAndroidResources = true - all { - it.systemProperties["robolectric.pixelCopyRenderMode"] = "hardware" + android { + testOptions { + unitTests { + isIncludeAndroidResources = true + all { + it.systemProperties["robolectric.pixelCopyRenderMode"] = "hardware" + } } } } @@ -152,8 +187,7 @@ private fun verifyLibraryDependencies( } val requiredLibraries = listOf( - "io.github.takahirom.roborazzi:roborazzi-compose", - "io.github.takahirom.roborazzi:roborazzi", + "io.github.takahirom.roborazzi:roborazzi-compose-preview-scanner-support", "junit:junit", "org.robolectric:robolectric", "com.github.sergio-sastre.ComposablePreviewScanner:android", @@ -168,17 +202,32 @@ abstract class GeneratePreviewScreenshotTestsTask : DefaultTask() { @get:Input var scanPackageTrees: ListProperty = project.objects.listProperty(String::class.java) + @get:Input + abstract val customTestQualifiedClassName: Property + + @get:Input + abstract val robolectricConfig: MapProperty + @TaskAction fun generateTests() { val testDir = outputDir.get().asFile testDir.mkdirs() val packagesExpr = scanPackageTrees.get().joinToString(", ") { "\"$it\"" } - val scanPackageTreeExpr = ".scanPackageTrees($packagesExpr)" - val className = "RoborazziPreviewParameterizedTests" - File(testDir, "$className.kt").writeText( + val generatedClassFQDN = "com.github.takahirom.roborazzi.RoborazziPreviewParameterizedTests" + val packageName = generatedClassFQDN.substringBeforeLast(".") + val className = generatedClassFQDN.substringAfterLast(".") + val directory = File(testDir, packageName.replace(".", "/")) + directory.mkdirs() + val robolectricConfigString = + "@Config(" + robolectricConfig.get().entries.joinToString(", ") { (key, value) -> + "$key = $value" + } + ")" + val customTestQualifiedClassNameString = customTestQualifiedClassName.get() + File(directory, "$className.kt").writeText( """ + package $packageName import org.junit.Test import org.junit.runner.RunWith import org.robolectric.ParameterizedRobolectricTestRunner @@ -198,35 +247,24 @@ abstract class GeneratePreviewScreenshotTestsTask : DefaultTask() { ) { companion object { + // lazy for performance val previews: List> by lazy { - AndroidComposablePreviewScanner() - $scanPackageTreeExpr - .getPreviews() + getRobolectricPreviewTest("$customTestQualifiedClassNameString").previews( + $packagesExpr + ) } @JvmStatic - @ParameterizedRobolectricTestRunner.Parameters + @ParameterizedRobolectricTestRunner.Parameters(name = "{0}") fun values(): List> = previews } - fun createScreenshotIdFor(preview: ComposablePreview) = - AndroidPreviewScreenshotIdBuilder(preview) - .ignoreClassName() - .build() @GraphicsMode(GraphicsMode.Mode.NATIVE) - @Config(sdk = [30]) + $robolectricConfigString @Test fun test() { - val pathPrefix = if(roborazziRecordFilePathStrategy() == RoborazziRecordFilePathStrategy.RelativePathFromCurrentDirectory) { - roborazziSystemPropertyOutputDirectory() + java.io.File.separator - } else { - "" - } - val filePath = pathPrefix + createScreenshotIdFor(preview) + ".png" - captureRoboImage(filePath = filePath) { - preview() - } + getRobolectricPreviewTest("$customTestQualifiedClassNameString").test(preview) } } 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 04396fbd..004a5caa 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 @@ -43,21 +43,16 @@ import kotlin.reflect.KClass private const val DEFAULT_OUTPUT_DIR = "outputs/roborazzi" private const val DEFAULT_TEMP_DIR = "intermediates/roborazzi" -/** - * Experimental API - * This class can be changed without notice. - */ open class RoborazziExtension @Inject constructor(objects: ObjectFactory) { val outputDir: DirectoryProperty = objects.directoryProperty() - // Configuration based APIs @ExperimentalRoborazziApi - val generateRobolectricPreviewTests: GenerateRobolectricPreviewTestsExtension = - objects.newInstance(GenerateRobolectricPreviewTestsExtension::class.java) + val generateComposePreviewRobolectricTests: GenerateComposePreviewRobolectricTestsExtension = + objects.newInstance(GenerateComposePreviewRobolectricTestsExtension::class.java) @ExperimentalRoborazziApi - fun generateRobolectricPreviewTests(action: GenerateRobolectricPreviewTestsExtension.() -> Unit) { - action(generateRobolectricPreviewTests) + fun generateComposePreviewRobolectricTests(action: GenerateComposePreviewRobolectricTestsExtension.() -> Unit) { + action(generateComposePreviewRobolectricTests) } } @@ -440,7 +435,7 @@ abstract class RoborazziPlugin : Plugin { generateRobolectricPreviewTestsIfNeeded( project = project, variant = variant, - extension = extension.generateRobolectricPreviewTests, + extension = extension.generateComposePreviewRobolectricTests, androidExtension = project.extensions.getByType(TestedExtension::class.java), testTaskProvider = findTestTaskProvider(Test::class, testTaskName) ) diff --git a/roborazzi-compose-preview-scanner-support/.gitignore b/roborazzi-compose-preview-scanner-support/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/roborazzi-compose-preview-scanner-support/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/roborazzi-compose-preview-scanner-support/build.gradle b/roborazzi-compose-preview-scanner-support/build.gradle new file mode 100644 index 00000000..32947ef6 --- /dev/null +++ b/roborazzi-compose-preview-scanner-support/build.gradle @@ -0,0 +1,28 @@ +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' +} +if (System.getenv("INTEGRATION_TEST") != "true") { + pluginManager.apply("com.vanniktech.maven.publish") +} +repositories { + google() + mavenCentral() + maven { url = uri("https://jitpack.io") } +} + +android.buildFeatures.compose = true + +// TODO: Use build-logic +apply from: rootProject.file('gradle/android.gradle') + +android.namespace 'com.github.takahirom.roborazzi.preview.support' + +dependencies { + api project(":roborazzi") + api project(":roborazzi-compose") + + compileOnly libs.androidx.compose.runtime + compileOnly libs.composable.preview.scanner + compileOnly libs.robolectric +} \ No newline at end of file diff --git a/roborazzi-compose-preview-scanner-support/consumer-rules.pro b/roborazzi-compose-preview-scanner-support/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/roborazzi-compose-preview-scanner-support/proguard-rules.pro b/roborazzi-compose-preview-scanner-support/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/roborazzi-compose-preview-scanner-support/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/roborazzi-compose-preview-scanner-support/src/main/AndroidManifest.xml b/roborazzi-compose-preview-scanner-support/src/main/AndroidManifest.xml new file mode 100644 index 00000000..151554b4 --- /dev/null +++ b/roborazzi-compose-preview-scanner-support/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/roborazzi-compose-preview-scanner-support/src/main/java/com/github/takahirom/roborazzi/RobolectricPreviewInfosApplier.kt b/roborazzi-compose-preview-scanner-support/src/main/java/com/github/takahirom/roborazzi/RobolectricPreviewInfosApplier.kt new file mode 100644 index 00000000..fb829713 --- /dev/null +++ b/roborazzi-compose-preview-scanner-support/src/main/java/com/github/takahirom/roborazzi/RobolectricPreviewInfosApplier.kt @@ -0,0 +1,30 @@ +package com.github.takahirom.roborazzi + +import android.content.res.Configuration +import org.robolectric.RuntimeEnvironment.setFontScale +import org.robolectric.RuntimeEnvironment.setQualifiers +import sergio.sastre.composable.preview.scanner.android.AndroidPreviewInfo +import sergio.sastre.composable.preview.scanner.core.preview.ComposablePreview + +@ExperimentalRoborazziApi +fun ComposablePreview.applyToRobolectricConfiguration() { + val preview = this + + fun setUiMode(uiMode: Int) { + val nightMode = + when (uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES) { + true -> "night" + false -> "notnight" + } + setQualifiers("+$nightMode") + } + setUiMode(preview.previewInfo.uiMode) + + fun setLocale(locale: String) { + val localeWithFallback = locale.ifBlank { "en" } + setQualifiers("+$localeWithFallback") + } + setLocale(preview.previewInfo.locale) + + setFontScale(preview.previewInfo.fontScale) +} 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 new file mode 100644 index 00000000..b104f1bb --- /dev/null +++ b/roborazzi-compose-preview-scanner-support/src/main/java/com/github/takahirom/roborazzi/RoborazziPreviewScannerSupport.kt @@ -0,0 +1,68 @@ +package com.github.takahirom.roborazzi + +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 +import sergio.sastre.composable.preview.scanner.core.preview.ComposablePreview + +@ExperimentalRoborazziApi +fun ComposablePreview.captureRoboImage( + filePath: String = DefaultFileNameGenerator.generateFilePath("png"), + roborazziOptions: RoborazziOptions = RoborazziOptions(), +) { + val composablePreview = this + composablePreview.applyToRobolectricConfiguration() + captureRoboImage(filePath = filePath, roborazziOptions = roborazziOptions) { + composablePreview() + } +} + +@ExperimentalRoborazziApi +interface RobolectricPreviewTest { + fun previews(vararg packages: String): List> + + fun test( + preview: ComposablePreview, + ) +} + +@InternalRoborazziApi +class DefaultRobolectricPreviewTest : RobolectricPreviewTest { + override fun previews(vararg packages: String): List> { + return AndroidComposablePreviewScanner() + .scanPackageTrees(*packages) + .getPreviews() + } + + override fun test(preview: ComposablePreview) { + val pathPrefix = + if (roborazziRecordFilePathStrategy() == RoborazziRecordFilePathStrategy.RelativePathFromCurrentDirectory) { + roborazziSystemPropertyOutputDirectory() + java.io.File.separator + } else { + "" + } + val name = roborazziDefaultNamingStrategy().generateOutputName(preview.declaringClass, createScreenshotIdFor(preview)) + val filePath = "$pathPrefix$name.png" + preview.captureRoboImage(filePath) + } + + private fun createScreenshotIdFor(preview: ComposablePreview) = + AndroidPreviewScreenshotIdBuilder(preview) + .ignoreClassName() + .build() +} + +@InternalRoborazziApi +fun getRobolectricPreviewTest(customTestQualifiedClassName: String): RobolectricPreviewTest { + val customTestClass = try { + Class.forName(customTestQualifiedClassName) + } catch (e: ClassNotFoundException) { + throw IllegalArgumentException("The class $customTestQualifiedClassName not found") + } + if (!RobolectricPreviewTest::class.java.isAssignableFrom(customTestClass)) { + throw IllegalArgumentException("The class $customTestQualifiedClassName must implement RobolectricPreviewCapturer") + } + val robolectricPreviewTest = + customTestClass.getDeclaredConstructor().newInstance() as RobolectricPreviewTest + return robolectricPreviewTest +} diff --git a/roborazzi-idea-plugin/src/main/kotlin/com/github/takahirom/roborazzi/idea/preview/RoborazziPreviewTool.kt b/roborazzi-idea-plugin/src/main/kotlin/com/github/takahirom/roborazzi/idea/preview/RoborazziPreviewTool.kt index 58d41a5a..f1a5c79d 100644 --- a/roborazzi-idea-plugin/src/main/kotlin/com/github/takahirom/roborazzi/idea/preview/RoborazziPreviewTool.kt +++ b/roborazzi-idea-plugin/src/main/kotlin/com/github/takahirom/roborazzi/idea/preview/RoborazziPreviewTool.kt @@ -13,7 +13,6 @@ import com.intellij.openapi.fileEditor.FileEditorManagerListener import com.intellij.openapi.options.ShowSettingsUtil import com.intellij.openapi.project.Project import com.intellij.openapi.roots.ProjectRootManager -import com.intellij.openapi.util.IconLoader import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.wm.ToolWindow import com.intellij.openapi.wm.ToolWindowFactory diff --git a/sample-generate-preview-tests/build.gradle.kts b/sample-generate-preview-tests/build.gradle.kts index f0141e12..5d56f701 100644 --- a/sample-generate-preview-tests/build.gradle.kts +++ b/sample-generate-preview-tests/build.gradle.kts @@ -6,7 +6,7 @@ plugins { } roborazzi { - generateRobolectricPreviewTests { + generateComposePreviewRobolectricTests { enable = true packages = listOf("com.github.takahirom.preview.tests") } @@ -57,12 +57,13 @@ dependencies { implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.ui) implementation(libs.androidx.compose.ui.tooling) + implementation(libs.androidx.compose.runtime) - testImplementation("io.github.takahirom.roborazzi:roborazzi-compose:1.20.0") - testImplementation("io.github.takahirom.roborazzi:roborazzi:1.20.0") + // replaced by dependency substitution + testImplementation("io.github.takahirom.roborazzi:roborazzi-compose-preview-scanner-support:0.1.0") testImplementation(libs.junit) testImplementation(libs.robolectric) - testImplementation("com.github.sergio-sastre.ComposablePreviewScanner:android:0.1.2") + testImplementation(libs.composable.preview.scanner) androidTestImplementation(libs.androidx.test.ext.junit) androidTestImplementation(libs.androidx.test.espresso.core) } \ No newline at end of file diff --git a/sample-generate-preview-tests/src/main/java/com/github/takahirom/preview/tests/Previews.kt b/sample-generate-preview-tests/src/main/java/com/github/takahirom/preview/tests/Previews.kt index 75c8a626..1d8e46af 100644 --- a/sample-generate-preview-tests/src/main/java/com/github/takahirom/preview/tests/Previews.kt +++ b/sample-generate-preview-tests/src/main/java/com/github/takahirom/preview/tests/Previews.kt @@ -1,12 +1,14 @@ package com.github.takahirom.preview.tests import android.content.res.Configuration -import androidx.compose.foundation.layout.height +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material3.Card import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Devices @@ -16,12 +18,36 @@ import androidx.compose.ui.unit.dp @Preview @Composable -fun Preview2() { +fun PreviewNormal() { MaterialTheme { Card( Modifier - .width(126.dp) - .height(80.dp) + .width(180.dp) + ) { + Text( + modifier = Modifier.padding(8.dp), + text = "Generate Preview Test Sample" + ) + } + } +} + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_YES +) +@Composable +fun PreviewDarkMode() { + val isSystemInDarkTheme = isSystemInDarkTheme() + MaterialTheme( + colorScheme = if (isSystemInDarkTheme) { + darkColorScheme() + } else { + lightColorScheme() + } + ) { + Card( + Modifier + .width(180.dp) ) { Text( modifier = Modifier.padding(8.dp), @@ -38,8 +64,23 @@ fun Preview2() { apiLevel = 30, widthDp = 320, heightDp = 640, - locale = "ja_JP", + locale = "ja-rJP", fontScale = 1.5f, +) +@Composable +fun PreviewWithProperties1() { + Card( + Modifier + .width(100.dp) + ) { + Text( + modifier = Modifier.padding(8.dp), + text = "Hello, World!" + ) + } +} + +@Preview( showSystemUi = true, showBackground = true, backgroundColor = 0xFF0000FF, @@ -48,11 +89,10 @@ fun Preview2() { wallpaper = Wallpapers.GREEN_DOMINATED_EXAMPLE, ) @Composable -fun PreviewWithProperties() { +fun PreviewWithProperties2() { Card( Modifier .width(100.dp) - .height(50.dp) ) { Text( modifier = Modifier.padding(8.dp), diff --git a/settings.gradle b/settings.gradle index 4691dc2b..20e68d9a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -19,6 +19,7 @@ include ':roborazzi-compose-desktop' include ':roborazzi-compose-ios' include ':roborazzi-compose' include ':roborazzi-painter' +include ':roborazzi-compose-preview-scanner-support' include ':roborazzi-idea-plugin'