From 563618fdbdcc8f5d2d0057f87d751afbbe22b266 Mon Sep 17 00:00:00 2001 From: Geoff Powell Date: Fri, 23 Aug 2024 15:24:26 -0400 Subject: [PATCH] Add preview param support --- paparazzi-annotations/README.md | 114 +++++++++++++ .../api/paparazzi-annotations.api | 88 +++++++++- paparazzi-annotations/build.gradle | 1 + .../annotations/PaparazziPreviewData.kt | 104 +++++++++++- paparazzi-gradle-plugin/build.gradle | 1 + .../cash/paparazzi/gradle/PaparazziPlugin.kt | 10 ++ .../app/cash/paparazzi/gradle/PreviewTests.kt | 4 +- .../paparazzi/gradle/utils/PreviewUtils.kt | 94 ++++------- .../paparazzi/gradle/PaparazziPluginTest.kt | 2 +- ...zzi_HelloPaparazziParameterized,text0].png | Bin 0 -> 2995 bytes ...zzi_HelloPaparazziParameterized,text1].png | Bin 0 -> 2910 bytes ...viewConfig,fs_1.5,Dark,Normal,Nexus_6].png | Bin 0 -> 4787 bytes ...Paparazzi_HelloPaparazziPreviewConfig].png | Bin 4583 -> 0 bytes ...preview[HelloPaparazzi_HelloPaparazzi].png | Bin 2535 -> 2410 bytes .../api/paparazzi-preview-processor.api | 4 + .../preview/processor/PaparazziPoet.kt | 116 +++++++++++--- .../cash/paparazzi/preview/processor/Utils.kt | 40 ++++- .../api/paparazzi-annotations.api | 39 +++++ .../api/paparazzi-preview-test.api | 43 +++++ paparazzi-preview-test/build.gradle | 10 ++ paparazzi-preview-test/gradle.properties | 4 + .../app/cash/paparazzi/preview/Snapshot.kt | 104 ++++++++++++ .../java/app/cash/paparazzi/preview/Utils.kt | 151 ++++++++++++++++++ paparazzi/api/paparazzi.api | 25 --- .../app/cash/paparazzi/preview/Snapshot.kt | 36 ----- .../java/app/cash/paparazzi/preview/Utils.kt | 16 -- .../cash/paparazzi/sample/HelloPaparazzi.kt | 45 ++++++ settings.gradle | 1 + 28 files changed, 874 insertions(+), 178 deletions(-) create mode 100644 paparazzi-annotations/README.md create mode 100644 paparazzi-gradle-plugin/src/test/projects/preview-annotation-sample/src/test/snapshots/images/app.cash.paparazzi.plugin.test_PreviewTests_preview[HelloPaparazzi_HelloPaparazziParameterized,text0].png create mode 100644 paparazzi-gradle-plugin/src/test/projects/preview-annotation-sample/src/test/snapshots/images/app.cash.paparazzi.plugin.test_PreviewTests_preview[HelloPaparazzi_HelloPaparazziParameterized,text1].png create mode 100644 paparazzi-gradle-plugin/src/test/projects/preview-annotation-sample/src/test/snapshots/images/app.cash.paparazzi.plugin.test_PreviewTests_preview[HelloPaparazzi_HelloPaparazziPreviewConfig,fs_1.5,Dark,Normal,Nexus_6].png delete mode 100644 paparazzi-gradle-plugin/src/test/projects/preview-annotation-sample/src/test/snapshots/images/app.cash.paparazzi.plugin.test_PreviewTests_preview[HelloPaparazzi_HelloPaparazziPreviewConfig].png create mode 100644 paparazzi-preview-test/api/paparazzi-annotations.api create mode 100644 paparazzi-preview-test/api/paparazzi-preview-test.api create mode 100644 paparazzi-preview-test/build.gradle create mode 100644 paparazzi-preview-test/gradle.properties create mode 100644 paparazzi-preview-test/src/main/java/app/cash/paparazzi/preview/Snapshot.kt create mode 100644 paparazzi-preview-test/src/main/java/app/cash/paparazzi/preview/Utils.kt delete mode 100644 paparazzi/src/main/java/app/cash/paparazzi/preview/Snapshot.kt delete mode 100644 paparazzi/src/main/java/app/cash/paparazzi/preview/Utils.kt diff --git a/paparazzi-annotations/README.md b/paparazzi-annotations/README.md new file mode 100644 index 0000000000..dddff45184 --- /dev/null +++ b/paparazzi-annotations/README.md @@ -0,0 +1,114 @@ +# `@Paparazzi` +An annotation used to generate Paparazzi snapshots for composable preview functions. + +## Installation +Add the following to your `build.gradle` file + +```groovy +apply plugin: 'app.cash.paparazzi.preview' +``` + +## Basic Usage +Apply the annotation alongside an existing preview method. The annotation processor will generate a manifest of information about this method and the previews applied. + +```kotlin +import app.cash.paparazzi.preview.Paparazzi + +@Paparazzi +@Preview +@Composable +fun MyViewPreview() { + MyView(title = "Hello, Paparazzi Annotation") +} +``` + +Run `:recordPaparazziDebug` in your module to generate preview snapshots (and optionally verify them using `:verifyPaparazziDebug`) as you normally would. + +A test class to generate snapshots for annotated previews will automatically be generated. +If you prefer to define a custom snapshot test, you mey disable test generation by adding the following to your `build.gradle` file. + +```groovy +paparazziPreview { + generateTestClass = false +} +``` + +You may implement your own test class, as shown below, to create snapshots for all previews included in the generated manifest (`paparazziAnnotations`). + +```kotlin +import app.cash.paparazzi.Paparazzi +import app.cash.paparazzi.preview.PaparazziPreviewData +import app.cash.paparazzi.preview.PaparazziValuesProvider +import app.cash.paparazzi.preview.deviceConfig +import app.cash.paparazzi.preview.snapshot +import com.android.ide.common.rendering.api.SessionParams.RenderingMode.SHRINK +import com.google.testing.junit.testparameterinjector.TestParameter +import com.google.testing.junit.testparameterinjector.TestParameterInjector +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(TestParameterInjector::class) +class PreviewTests( + @TestParameter(valuesProvider = PreviewConfigValuesProvider::class) + private val preview: PaparazziPreviewData, +) { + private class PreviewConfigValuesProvider : PaparazziValuesProvider(paparazziPreviews) + + @get:Rule + val paparazzi = Paparazzi( + deviceConfig = preview.deviceConfig(), + renderingMode = SHRINK, + ) + + @Test + fun preview() { + paparazzi.snapshot(preview) + } +} +``` + +## Preview Parameter +If your preview function accepts a parameter using `@PreviewParameter`, then snapshots will be created for each combination of preview / param. + +```kotlin +@Paparazzi +@Preview +@Composable +fun MyViewPreview(@PreviewParameter(MyTitleProvider::class) title: String) { + MyView(title = title) +} + +class MyTitleProvider : PreviewParameterProvider { + override val values = sequenceOf("Hello", "Paparazzi", "Annotation") +} +``` + +## Composable Wrapping +If you need to apply additional UI treatment around your previews, you may provide a composable wrapper within the test. + +```kotlin +paparazzi.snapshot(preview) { content -> + Box(modifier = Modifier.background(Color.Gray)) { + content() + } +} +``` + +## Preview Composition +If you have multiple preview annotations applied to a function, or have them nested behind a custom annotation, they will all be included in the snapshot manifest. + +```kotlin +@Paparazzi +@ScaledThemedPreviews +@Composable +fun MyViewPreview() { + MyView(title = "Hello, Paparazzi Annotation") +} + +@Preview(name = "small light", fontScale = 1f, uiMode = Configuration.UI_MODE_NIGHT_NO, device = PIXEL_3_XL) +@Preview(name = "small dark", fontScale = 1f, uiMode = Configuration.UI_MODE_NIGHT_YES, device = PIXEL_3_XL) +@Preview(name = "large light", fontScale = 2f, uiMode = Configuration.UI_MODE_NIGHT_NO, device = PIXEL_3_XL) +@Preview(name = "large dark", fontScale = 2f, uiMode = Configuration.UI_MODE_NIGHT_YES, device = PIXEL_3_XL) +annotation class ScaledThemedPreviews +``` diff --git a/paparazzi-annotations/api/paparazzi-annotations.api b/paparazzi-annotations/api/paparazzi-annotations.api index 913e18bbf8..80927b48fb 100644 --- a/paparazzi-annotations/api/paparazzi-annotations.api +++ b/paparazzi-annotations/api/paparazzi-annotations.api @@ -5,13 +5,15 @@ public abstract interface class app/cash/paparazzi/annotations/PaparazziPreviewD } public final class app/cash/paparazzi/annotations/PaparazziPreviewData$Default : app/cash/paparazzi/annotations/PaparazziPreviewData { - public fun (Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V + public fun (Ljava/lang/String;Lapp/cash/paparazzi/annotations/PreviewData;Lkotlin/jvm/functions/Function0;)V public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Lkotlin/jvm/functions/Function0; - public final fun copy (Ljava/lang/String;Lkotlin/jvm/functions/Function0;)Lapp/cash/paparazzi/annotations/PaparazziPreviewData$Default; - public static synthetic fun copy$default (Lapp/cash/paparazzi/annotations/PaparazziPreviewData$Default;Ljava/lang/String;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lapp/cash/paparazzi/annotations/PaparazziPreviewData$Default; + public final fun component2 ()Lapp/cash/paparazzi/annotations/PreviewData; + public final fun component3 ()Lkotlin/jvm/functions/Function0; + public final fun copy (Ljava/lang/String;Lapp/cash/paparazzi/annotations/PreviewData;Lkotlin/jvm/functions/Function0;)Lapp/cash/paparazzi/annotations/PaparazziPreviewData$Default; + public static synthetic fun copy$default (Lapp/cash/paparazzi/annotations/PaparazziPreviewData$Default;Ljava/lang/String;Lapp/cash/paparazzi/annotations/PreviewData;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lapp/cash/paparazzi/annotations/PaparazziPreviewData$Default; public fun equals (Ljava/lang/Object;)Z public final fun getComposable ()Lkotlin/jvm/functions/Function0; + public final fun getPreview ()Lapp/cash/paparazzi/annotations/PreviewData; public final fun getSnapshotName ()Ljava/lang/String; public fun hashCode ()I public fun toString ()Ljava/lang/String; @@ -25,15 +27,85 @@ public final class app/cash/paparazzi/annotations/PaparazziPreviewData$Empty : a } public final class app/cash/paparazzi/annotations/PaparazziPreviewData$Error : app/cash/paparazzi/annotations/PaparazziPreviewData { - public fun (Ljava/lang/String;Ljava/lang/String;)V + public fun (Ljava/lang/String;Lapp/cash/paparazzi/annotations/PreviewData;Ljava/lang/String;)V public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Ljava/lang/String; - public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lapp/cash/paparazzi/annotations/PaparazziPreviewData$Error; - public static synthetic fun copy$default (Lapp/cash/paparazzi/annotations/PaparazziPreviewData$Error;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lapp/cash/paparazzi/annotations/PaparazziPreviewData$Error; + public final fun component2 ()Lapp/cash/paparazzi/annotations/PreviewData; + public final fun component3 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Lapp/cash/paparazzi/annotations/PreviewData;Ljava/lang/String;)Lapp/cash/paparazzi/annotations/PaparazziPreviewData$Error; + public static synthetic fun copy$default (Lapp/cash/paparazzi/annotations/PaparazziPreviewData$Error;Ljava/lang/String;Lapp/cash/paparazzi/annotations/PreviewData;Ljava/lang/String;ILjava/lang/Object;)Lapp/cash/paparazzi/annotations/PaparazziPreviewData$Error; public fun equals (Ljava/lang/Object;)Z public final fun getMessage ()Ljava/lang/String; + public final fun getPreview ()Lapp/cash/paparazzi/annotations/PreviewData; public final fun getSnapshotName ()Ljava/lang/String; public fun hashCode ()I public fun toString ()Ljava/lang/String; } +public final class app/cash/paparazzi/annotations/PaparazziPreviewData$Provider : app/cash/paparazzi/annotations/PaparazziPreviewData { + public static final field $stable I + public fun (Ljava/lang/String;Lapp/cash/paparazzi/annotations/PreviewData;Lkotlin/jvm/functions/Function3;Lapp/cash/paparazzi/annotations/PreviewParameterData;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Lapp/cash/paparazzi/annotations/PreviewData; + public final fun component3 ()Lkotlin/jvm/functions/Function3; + public final fun component4 ()Lapp/cash/paparazzi/annotations/PreviewParameterData; + public final fun copy (Ljava/lang/String;Lapp/cash/paparazzi/annotations/PreviewData;Lkotlin/jvm/functions/Function3;Lapp/cash/paparazzi/annotations/PreviewParameterData;)Lapp/cash/paparazzi/annotations/PaparazziPreviewData$Provider; + public static synthetic fun copy$default (Lapp/cash/paparazzi/annotations/PaparazziPreviewData$Provider;Ljava/lang/String;Lapp/cash/paparazzi/annotations/PreviewData;Lkotlin/jvm/functions/Function3;Lapp/cash/paparazzi/annotations/PreviewParameterData;ILjava/lang/Object;)Lapp/cash/paparazzi/annotations/PaparazziPreviewData$Provider; + public fun equals (Ljava/lang/Object;)Z + public final fun getComposable ()Lkotlin/jvm/functions/Function3; + public final fun getPreview ()Lapp/cash/paparazzi/annotations/PreviewData; + public final fun getPreviewParameter ()Lapp/cash/paparazzi/annotations/PreviewParameterData; + public final fun getSnapshotName ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; + public final fun withPreviewParameterIndex (I)Lapp/cash/paparazzi/annotations/PaparazziPreviewData$Provider; +} + +public final class app/cash/paparazzi/annotations/PaparazziPreviewDefaults { + public static final field $stable I + public static final field DEVICE_ID Ljava/lang/String; + public static final field INSTANCE Lapp/cash/paparazzi/annotations/PaparazziPreviewDefaults; +} + +public final class app/cash/paparazzi/annotations/PreviewData { + public static final field $stable I + public fun ()V + public fun (Ljava/lang/Float;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/Float;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/Float; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/Integer; + public final fun component4 ()Ljava/lang/Integer; + public final fun component5 ()Ljava/lang/Integer; + public final fun component6 ()Ljava/lang/String; + public final fun component7 ()Ljava/lang/String; + public final fun copy (Ljava/lang/Float;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;)Lapp/cash/paparazzi/annotations/PreviewData; + public static synthetic fun copy$default (Lapp/cash/paparazzi/annotations/PreviewData;Ljava/lang/Float;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lapp/cash/paparazzi/annotations/PreviewData; + public fun equals (Ljava/lang/Object;)Z + public final fun getBackgroundColor ()Ljava/lang/String; + public final fun getDevice ()Ljava/lang/String; + public final fun getFontScale ()Ljava/lang/Float; + public final fun getHeightDp ()Ljava/lang/Integer; + public final fun getLocale ()Ljava/lang/String; + public final fun getUiMode ()Ljava/lang/Integer; + public final fun getWidthDp ()Ljava/lang/Integer; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class app/cash/paparazzi/annotations/PreviewParameterData { + public static final field $stable I + public fun (Ljava/lang/String;Lkotlin/sequences/Sequence;I)V + public synthetic fun (Ljava/lang/String;Lkotlin/sequences/Sequence;IILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Lkotlin/sequences/Sequence; + public final fun component3 ()I + public final fun copy (Ljava/lang/String;Lkotlin/sequences/Sequence;I)Lapp/cash/paparazzi/annotations/PreviewParameterData; + public static synthetic fun copy$default (Lapp/cash/paparazzi/annotations/PreviewParameterData;Ljava/lang/String;Lkotlin/sequences/Sequence;IILjava/lang/Object;)Lapp/cash/paparazzi/annotations/PreviewParameterData; + public fun equals (Ljava/lang/Object;)Z + public final fun getIndex ()I + public final fun getName ()Ljava/lang/String; + public final fun getValues ()Lkotlin/sequences/Sequence; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + diff --git a/paparazzi-annotations/build.gradle b/paparazzi-annotations/build.gradle index f97b16750b..6a9c7e28e8 100644 --- a/paparazzi-annotations/build.gradle +++ b/paparazzi-annotations/build.gradle @@ -4,4 +4,5 @@ apply plugin: 'com.vanniktech.maven.publish' dependencies { compileOnly libs.compose.runtime + compileOnly libs.tools.layoutlib } diff --git a/paparazzi-annotations/src/main/java/app/cash/paparazzi/annotations/PaparazziPreviewData.kt b/paparazzi-annotations/src/main/java/app/cash/paparazzi/annotations/PaparazziPreviewData.kt index c52825bce1..283ad3f2ad 100644 --- a/paparazzi-annotations/src/main/java/app/cash/paparazzi/annotations/PaparazziPreviewData.kt +++ b/paparazzi-annotations/src/main/java/app/cash/paparazzi/annotations/PaparazziPreviewData.kt @@ -1,14 +1,38 @@ package app.cash.paparazzi.annotations +import android.content.res.Configuration import androidx.compose.runtime.Composable +public object PaparazziPreviewDefaults { + public const val DEVICE_ID: String = "id:pixel_5" +} + public sealed interface PaparazziPreviewData { public data class Default( val snapshotName: String, + val preview: PreviewData, val composable: @Composable () -> Unit ) : PaparazziPreviewData { - override fun toString(): String = snapshotName + override fun toString(): String = buildList { + add(snapshotName) + preview.toString().takeIf { it.isNotEmpty() }?.let(::add) + }.joinToString(",") + } + + public data class Provider( + val snapshotName: String, + val preview: PreviewData, + val composable: @Composable (T) -> Unit, + val previewParameter: PreviewParameterData + ) : PaparazziPreviewData { + override fun toString(): String = buildList { + add(snapshotName) + preview.toString().takeIf { it.isNotEmpty() }?.let(::add) + add(previewParameter.toString()) + }.joinToString(",") + + public fun withPreviewParameterIndex(index: Int): Provider = copy(previewParameter = previewParameter.copy(index = index)) } public data object Empty : PaparazziPreviewData { @@ -17,8 +41,84 @@ public sealed interface PaparazziPreviewData { public data class Error( val snapshotName: String, + val preview: PreviewData, val message: String ) : PaparazziPreviewData { - override fun toString(): String = snapshotName + override fun toString(): String = buildList { + add(snapshotName) + preview.toString().takeIf { it.isNotEmpty() }?.let(::add) + }.joinToString(",") + } +} + +public data class PreviewData( + val fontScale: Float? = null, + val device: String? = null, + val widthDp: Int? = null, + val heightDp: Int? = null, + val uiMode: Int? = null, + val locale: String? = null, + val backgroundColor: String? = null +) { + override fun toString(): String = buildList { + fontScale?.fontScale()?.displayName()?.let(::add) + uiMode?.lightDarkName()?.let(::add) + uiMode?.uiModeName()?.let(::add) + device?.let { + if (it != PaparazziPreviewDefaults.DEVICE_ID) { + add(it.substringAfterLast(":")) + } + } + widthDp?.let { add("w_$it") } + heightDp?.let { add("h_$it") } + locale?.let(::add) + backgroundColor?.let { add("bg_$it") } + }.takeIf { it.isNotEmpty() } + ?.joinToString(",") + ?: "" +} + +public data class PreviewParameterData( + val name: String, + val values: Sequence, + val index: Int = 0 +) { + override fun toString(): String = "$name$index" +} + +/** + * Maps [fontScale] to enum values similar to Preview + * see: +https://android.googlesource.com/platform/tools/adt/idea/+/refs/heads/mirror-goog-studio-main/compose-designer/src/com/android/tools/idea/compose/pickers/preview/enumsupport/PsiEnumValues.kt + */ +internal fun Float.fontScale() = + FontScale.values().find { this == it.value } ?: FontScale.CUSTOM.apply { value = this@fontScale } + +internal enum class FontScale(var value: Float?) { + DEFAULT(1f), + SMALL(0.85f), + LARGE(1.15f), + LARGEST(1.30f), + CUSTOM(null); + + fun displayName() = when (this) { + CUSTOM -> "fs_$value" + else -> name } } + +internal fun Int.lightDarkName() = when (this and Configuration.UI_MODE_NIGHT_MASK) { + Configuration.UI_MODE_NIGHT_NO -> "Light" + Configuration.UI_MODE_NIGHT_YES -> "Dark" + else -> null +} + +internal fun Int.uiModeName() = when (this and Configuration.UI_MODE_TYPE_MASK) { + Configuration.UI_MODE_TYPE_NORMAL -> "Normal" + Configuration.UI_MODE_TYPE_CAR -> "Car" + Configuration.UI_MODE_TYPE_DESK -> "Desk" + Configuration.UI_MODE_TYPE_APPLIANCE -> "Appliance" + Configuration.UI_MODE_TYPE_WATCH -> "Watch" + Configuration.UI_MODE_TYPE_VR_HEADSET -> "VR_Headset" + else -> null +} diff --git a/paparazzi-gradle-plugin/build.gradle b/paparazzi-gradle-plugin/build.gradle index e5ae3979b9..5ea547b5f3 100644 --- a/paparazzi-gradle-plugin/build.gradle +++ b/paparazzi-gradle-plugin/build.gradle @@ -66,6 +66,7 @@ tasks.withType(Test).configureEach { dependsOn(':paparazzi:publishMavenPublicationToProjectLocalMavenRepository') dependsOn(':paparazzi-annotations:publishMavenPublicationToProjectLocalMavenRepository') dependsOn(':paparazzi-preview-processor:publishMavenPublicationToProjectLocalMavenRepository') + dependsOn(':paparazzi-preview-test:publishMavenPublicationToProjectLocalMavenRepository') } // When cleaning this project, we also want to clean the test projects. diff --git a/paparazzi-gradle-plugin/src/main/java/app/cash/paparazzi/gradle/PaparazziPlugin.kt b/paparazzi-gradle-plugin/src/main/java/app/cash/paparazzi/gradle/PaparazziPlugin.kt index 4b4d93dd3d..63784fefc8 100644 --- a/paparazzi-gradle-plugin/src/main/java/app/cash/paparazzi/gradle/PaparazziPlugin.kt +++ b/paparazzi-gradle-plugin/src/main/java/app/cash/paparazzi/gradle/PaparazziPlugin.kt @@ -251,6 +251,7 @@ public class PaparazziPlugin @Inject constructor( project.addAnnotationsDependency() project.addProcessorDependency() + project.addPreviewTestDependency() project.registerGeneratePreviewTask(config, extension) project.afterEvaluate { @@ -343,6 +344,15 @@ public class PaparazziPlugin @Inject constructor( configurations.getByName("ksp").dependencies.add(dependency) } + private fun Project.addPreviewTestDependency() { + val dependency = if (isInternal()) { + dependencies.project(mapOf("path" to ":paparazzi-preview-test")) + } else { + dependencies.create("app.cash.paparazzi:paparazzi-preview-test:$VERSION") + } + configurations.getByName("testImplementation").dependencies.add(dependency) + } + private fun Project.isInternal(): Boolean = providers.gradleProperty("app.cash.paparazzi.internal").orNull == "true" diff --git a/paparazzi-gradle-plugin/src/main/java/app/cash/paparazzi/gradle/PreviewTests.kt b/paparazzi-gradle-plugin/src/main/java/app/cash/paparazzi/gradle/PreviewTests.kt index 8c34bfe1cc..0b353e49f5 100644 --- a/paparazzi-gradle-plugin/src/main/java/app/cash/paparazzi/gradle/PreviewTests.kt +++ b/paparazzi-gradle-plugin/src/main/java/app/cash/paparazzi/gradle/PreviewTests.kt @@ -1,9 +1,9 @@ package app.cash.paparazzi.gradle -private const val PREVIEW_TEST_SOURCE = """ +internal const val PREVIEW_TEST_SOURCE = """ import app.cash.paparazzi.Paparazzi +import app.cash.paparazzi.annotations.PaparazziPreviewData import app.cash.paparazzi.preview.DefaultLocaleRule -import app.cash.paparazzi.preview.PaparazziPreviewData import app.cash.paparazzi.preview.PaparazziValuesProvider import app.cash.paparazzi.preview.deviceConfig import app.cash.paparazzi.preview.locale diff --git a/paparazzi-gradle-plugin/src/main/java/app/cash/paparazzi/gradle/utils/PreviewUtils.kt b/paparazzi-gradle-plugin/src/main/java/app/cash/paparazzi/gradle/utils/PreviewUtils.kt index 881f9c48f5..9de33c65b2 100644 --- a/paparazzi-gradle-plugin/src/main/java/app/cash/paparazzi/gradle/utils/PreviewUtils.kt +++ b/paparazzi-gradle-plugin/src/main/java/app/cash/paparazzi/gradle/utils/PreviewUtils.kt @@ -1,5 +1,6 @@ package app.cash.paparazzi.gradle.utils +import app.cash.paparazzi.gradle.PREVIEW_TEST_SOURCE import app.cash.paparazzi.gradle.PaparazziExtension import com.android.build.api.variant.AndroidComponentsExtension import com.android.build.api.variant.HasUnitTest @@ -21,30 +22,33 @@ internal fun Project.registerGeneratePreviewTask( val testVariant = (variant as? HasUnitTest)?.unitTest ?: return@onVariants val testVariantSlug = testVariant.name.capitalize() - val buildType = testVariant.buildType - val buildTypeCap = testVariant.buildType?.capitalize() + val buildType = testVariant.buildType + val buildTypeCap = testVariant.buildType?.capitalize() - val taskName = "paparazziGeneratePreview${testVariantSlug}Kotlin" - val taskProvider = tasks.register(taskName) { task -> + val taskName = "paparazziGeneratePreview${testVariantSlug}Kotlin" + val taskProvider = tasks.register(taskName) { task -> task.group = VERIFICATION_GROUP - task.description = "Generates the preview test class to the test source set for $testVariantSlug" + task.description = + "Generates the preview test class to the test source set for $testVariantSlug" task.dependsOn("ksp${buildTypeCap}Kotlin") } - val testSourceDir = "$projectDir${File.separator}$TEST_SOURCE_DIR${File.separator}${buildType}UnitTest" + val testSourceDir = + "$projectDir${File.separator}$TEST_SOURCE_DIR${File.separator}${buildType}UnitTest" testVariant.sources.java?.addStaticSourceDirectory(testSourceDir) // test compilation depends on the task - project.tasks.named { it == "compile${testVariantSlug}Kotlin" } - .configureEach { it.dependsOn(taskProvider) } - project.tasks.named { it == "generate${testVariantSlug}LintModel" } - .configureEach { it.dependsOn(taskProvider) } + project.tasks.named { + it == "compile${testVariantSlug}Kotlin" || + it == "generate${testVariantSlug}LintModel" || + it == "lintAnalyze$testVariantSlug" + }.configureEach { it.dependsOn(taskProvider) } // run task before processing symbols project.tasks.named { it == "ksp${testVariantSlug}Kotlin" } .configureEach { it.mustRunAfter(taskProvider) } - gradle.taskGraph.whenReady { + gradle.taskGraph.whenReady { taskProvider.configure { task -> // Test variant appends .test to the namespace val namespace = testVariant.namespace.get().replace(".test$".toRegex(), "") @@ -52,27 +56,22 @@ internal fun Project.registerGeneratePreviewTask( val previewTestDir = "$testSourceDir${File.separator}$namespaceDir" task.enabled = config.generatePreviewTestClass.get() - task.inputs.file( - "$projectDir${File.separator}$KSP_SOURCE_DIR${File.separator}${buildType}${File.separator}kotlin${File.separator}$namespaceDir${File.separator}$PREVIEW_DATA_FILE" + task.inputs.file( + "$projectDir${File.separator}$KSP_SOURCE_DIR${File.separator}${buildType}${File.separator}kotlin${File.separator}$namespaceDir${File.separator}$PREVIEW_DATA_FILE" + ) + task.enabled = config.generatePreviewTestClass.get() + task.outputs.dir(previewTestDir) + task.outputs.file("$previewTestDir${File.separator}$PREVIEW_TEST_FILE") + task.outputs.cacheIf { true } + + task.doLast { + File(previewTestDir).mkdirs() + File(previewTestDir, PREVIEW_TEST_FILE).writeText( + buildString { + appendLine("package $namespace") + append(PREVIEW_TEST_SOURCE) + } ) - task.enabled = config.generatePreviewTestClass.get()task.outputs.dir(previewTestDir) - task.outputs.file("$previewTestDir${File.separator}$PREVIEW_TEST_FILE") - task.outputs.cacheIf { true } - - // test compilation depends on the task - tasks.findByName("compile${typeNameCap}UnitTestKotlin")?.dependsOn(taskName) - // run task before processing symbols - tasks.findByName("ksp${typeNameCap}UnitTestKotlin")?.mustRunAfter(taskName) - - task.doLast { - File(previewTestDir).mkdirs() - File(previewTestDir, PREVIEW_TEST_FILE).writeText( - buildString { - appendLine("package $namespace") - append(PREVIEW_TEST_SOURCE) - } - ) - } } } } @@ -81,36 +80,3 @@ internal fun Project.registerGeneratePreviewTask( private fun String.capitalize() = replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.ROOT) else it.toString() } - -private const val PREVIEW_TEST_SOURCE = """ -import app.cash.paparazzi.Paparazzi -import app.cash.paparazzi.annotations.PaparazziPreviewData -import app.cash.paparazzi.preview.PaparazziValuesProvider -import app.cash.paparazzi.preview.snapshot -import com.android.ide.common.rendering.api.SessionParams.RenderingMode.SHRINK -import com.google.testing.junit.testparameterinjector.TestParameter -import com.google.testing.junit.testparameterinjector.TestParameterInjector -import org.junit.Assume.assumeTrue -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(TestParameterInjector::class) -class PreviewTests( - @TestParameter(valuesProvider = PreviewConfigValuesProvider::class) - private val preview: PaparazziPreviewData, -) { - private class PreviewConfigValuesProvider : PaparazziValuesProvider(paparazziPreviews) - - @get:Rule - val paparazzi = Paparazzi( - renderingMode = SHRINK, - ) - - @Test - fun preview() { - assumeTrue(preview !is PaparazziPreviewData.Empty) - paparazzi.snapshot(preview) - } -} -""" diff --git a/paparazzi-gradle-plugin/src/test/java/app/cash/paparazzi/gradle/PaparazziPluginTest.kt b/paparazzi-gradle-plugin/src/test/java/app/cash/paparazzi/gradle/PaparazziPluginTest.kt index d888559a41..a2ed72cca4 100644 --- a/paparazzi-gradle-plugin/src/test/java/app/cash/paparazzi/gradle/PaparazziPluginTest.kt +++ b/paparazzi-gradle-plugin/src/test/java/app/cash/paparazzi/gradle/PaparazziPluginTest.kt @@ -1470,7 +1470,7 @@ class PaparazziPluginTest { val result = gradleRunner .forwardOutput() .withArguments("verifyPaparazziDebug", "--stacktrace") - .runFixture(fixtureRoot) { buildAndFail() } // Currently fails because of preview parameter usage + .runFixture(fixtureRoot) { build() } // Currently fails because of preview parameter usage assertThat(result.task(":paparazziGeneratePreviewDebugUnitTestKotlin")).isNotNull() diff --git a/paparazzi-gradle-plugin/src/test/projects/preview-annotation-sample/src/test/snapshots/images/app.cash.paparazzi.plugin.test_PreviewTests_preview[HelloPaparazzi_HelloPaparazziParameterized,text0].png b/paparazzi-gradle-plugin/src/test/projects/preview-annotation-sample/src/test/snapshots/images/app.cash.paparazzi.plugin.test_PreviewTests_preview[HelloPaparazzi_HelloPaparazziParameterized,text0].png new file mode 100644 index 0000000000000000000000000000000000000000..9b577c683c0fd1adc01c9094985f5b3961ef0342 GIT binary patch literal 2995 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 }hcQLG59Kz2jt`xb(=fwgl>2HkrLD zbnfck+A|Oyj7Z~v5DeqQZh$U&AGJ+$N~umHe)BzE$_`*=GwYR(zK>cH&r@D3jZVX| zGmG7J5Hes(=l<6YYV6jDP}>4;fOpG5XR0o9piW*6Wl|@IzuSCc+B{d69(J~ZdbABT zH#e;?Y2#X@mCDGnwgzgOURTP6o~!%X)4QV?VaGU!sMJ$m{1;w$G0g&*5*sT#gfkOY0K)ibWSveb(aHmo*z&FRugzxct6QR$@Fc-%y{k6i!=FR5i0T_HOa zqgq=#@UDozx26+KrRR+(e`GmJ*Hviy+LsaP=;opZM^VYXvcE9Vd;8he)O+A16bk(g zP#;>F-9iZGw0K(q>QE>^y$P|5(b!ebuX_ZjL!nTB+EK*U(}}5`jfwzuC=?1%A83=I z<%015btn`HP=^B4p-_N26#74>mmK4y0Cgx7pbmvXp?_BY0T;yeavwsTE&u=k07*qo IM6N<$g5UVMssI20 literal 0 HcmV?d00001 diff --git a/paparazzi-gradle-plugin/src/test/projects/preview-annotation-sample/src/test/snapshots/images/app.cash.paparazzi.plugin.test_PreviewTests_preview[HelloPaparazzi_HelloPaparazziPreviewConfig,fs_1.5,Dark,Normal,Nexus_6].png b/paparazzi-gradle-plugin/src/test/projects/preview-annotation-sample/src/test/snapshots/images/app.cash.paparazzi.plugin.test_PreviewTests_preview[HelloPaparazzi_HelloPaparazziPreviewConfig,fs_1.5,Dark,Normal,Nexus_6].png new file mode 100644 index 0000000000000000000000000000000000000000..08449fa476f6728d9789066edad436e1dc0e7217 GIT binary patch literal 4787 zcmY*-2Q(Yp|32ChRV1iAf*P%&R*V|8SLu9f#EO-YSWzo6TGZZqw>3(YiqW75jaH&Y zN{iSnv5VTfe)_)ObN=Ul&OP^j&OOh)_xY@I&v|0+nj11-G+X9`b$zOhZ!_$IHt@VQ zg){WeanLSV>$G4JA97Tn)!4CKq!kvryD>`)VZ*IYw+pGg_wJaD1F6!*TqHh)y&g$H6=_6NXMr!OC2BGnsVm8gFxf^@-?V%do&2&r6L0hih z)aVZ;b*6o65Jx+|f9irDAe_tb22tp3$=mQ7Y{*EE)z>^#ji5ru2Ed1*%H*>*HS}i8 zJ@7B#w;f(+VEkh&g}s<>Q7^6t>M^4~WCCuC7aJxE#$sc!2Q>PI82LN`C(W$O4!hwk zYlU)&ng?`7T{|ly^&g@{+Zk8|`R92I(J{O8e}qsC<}UbKUB4)@_V`xr6N7D(6p7M1WIi!}pbjPz`&P!&KqR|6=zS zc@2@6B*lY3VD}DJ$l9)*?&SgWc-X}5qyVhb>Q6kEEQ_)R7)aH1*c-dg;0A%2zDEIz z^)dVGT22yphJ^@ci~hpL#>aYEV2NLWRlU}4O`Y&;`zkaZ+U>viXBR*tqa|GqUZ5G$ zr4P^j9~_ygX;j)TMz2~hr3o&~t5(|5oO+&~R)`jjB3w%~t7=oh^f=jSCHV(3_Hh5PYtbljaZa=B6ROoFe{Sp4-4Ic zblN9ha6R|*7nysyaMA61i^gcUr`ei#_)zd44%+M7eUpBCuJ9 z356{4Vj@JBjAJaYY1)*Yi-pP0Da&(E_|16Bjg*c)#+;M@nbG{6;^-#*1lpwhfad)o zo9dTo!u*r6Qy=%h*0tFOmPaf zmRon}W{MzfT_(OKspipC6fr`nA&3xVysc86^>%w8k~#dN&HG^wUR&vqGyW+}?eFo9 z(F5}Sk`3hsDPl-(V9N88#xgZM!3aJL`W8!{9l`jttwMJewPJe_BoV5B*nyQMQfF{e zX;*IF|1n`mLX@s9oPR>z+X|-g&!_#V>bpFv)G+S{)?<=;!Dt$knHt)=tU*%5zb(Vt zBq0SOurQW3O~9&l=oC7Vzie6^)_HzDN9ugS=SW%y_~(&Axo1iX{Kk}*G{GejVsSbu zfb|*j1PgDJCsnkegi566kzB%aRRhrjkTij!#wO{4VK!s8%#p|AS3U`Jm`XGDjBQfs+Nh?=e!p}O;aR6#9r>&OU<>$5>*fU&K#kr_d+ ze!_cYr9{v3|MQ^IB6PysIQ0NP8yB}|?x~)_Bjn;J<4(Mx`uUD9>JJO4C}7Bq!7_h( zQ7(gUlES=w-T=_W5;e8#<=p;zUHXR~BM)`h`e6$OjGR*X_Lda{Vf)}LfZemq@RQCC z{d%ot04L;BuWHjQ>Ck6|PA*#=kUFfz-IW=C=X6<3aFu9+X1z}UsWn8b&y%m~t79OH zb_Dv98M#?_uwggX9LY8jt3BecOW8M(a_1ScyeZxdHM<#y^$C~E2zHCUFxwSwpI#0W z)?$gzrgq(&DNDJ@37{?)+KVGi)l$>D2c_RG%26+6mQ!_~w4?nl%0F|;om?AcJWIm% zvTjBq)vSN8RJ86p#OuLdS`O&R7CIf$Uhxh1Lq!@D3Bp>3Sf6lWZ*o!P3RJKUf)63<(S^f z%k5`~PnydbU#hFa1Y}Qy_JQSLTT|vUlj!c>ecb2xu8|+eDICtxCdluFeVQ*WpOj{Y ztmiEaMH}%k5+_jCznE8KkfuaxB*)1i)uZs1(OScjn3q9*@M?3xxA#vkX$_a`f#;8# zd5z;lkc$lI4{EA{*lnTqt(?BT4Mr4$Vm(SziS#RnDYrR)dQfCy=ScNP%|UO-C%#BT z0c(F+&9JW^TyU}r4NHZ`pF`%MoJfO^JWMT~*j(s)%C69DYw%?{N^L@X>^Ci=c0FlFo z9|)_e;Wh}cdkg8TE^b`qKEONma8vH~DdG|Si>)l~2QCU@h5D4@k=r$zdSgZnbGc9z zl@@Wt;asLg!)l}dOO7`FkDu(gv0ARHqmbB;sf&&mK~|xRvJ*XTy>Hc0?*9%}XNdmm>L})rTEYHD{e3 zD#;~X59)gI39)MU4#!KjstHkr!1Cva=$)c(&?Ie&O4@kc!3z_LtF77(k!Z_XxX|0o z1*a*d#|dC$Fk~`4qRXUp7qa0rJ5f zLxPSo!4OsoZHGV)(bYckU~NUBKUl8Kb*ptVPLN9i<=Zd7UZdAEIN8*cg;jaFQ`4P$tXMTR#yd4U1p4=zh8pkoW}(9bdam;B0n9?+A|>pa?g~vh4{s zDnIFLH#OmTYZx*7$gKsHfda41v5iC(fo}avhWz!oMW<4saRu1yb~L9}h%nJzEu2Jx zuDAuPxM&nJLK_kzx9arWMjvhY91YgqO%_N*i*@21o*p!Uz?dr}mcgWF%G_C{*Dud` z#1_l8f#rDXk10ird|XVPu{t;)Bx!fv91PK>G@+F^S=7cO3Q|M>`O;rv^5iwPI^%Ar z5jaQEJefo2`9{HvPaCjpIbNiRo*~N*>N4n_`B%1h97~`L)6z0 z<(70XL~}>;v6qbZr>2}7F~i5o?9GoklW*GAVP9+0#E}!xSuyTRu+GDCJhw@y3uZt_=&mpq^C&NDc1*wepSgoA1n0b5 z0qcTA&|4?62(s0;S2dT%X;JQ>!r}YqRwWQF2qJI6A|Vhz*1YF=y=^m3Nd~&k?0;e4 z5{4ftat#u|dhc3j*!%6l&I(ERx|9mH&^Xz~bM(3v4o>J&fBo6wtokkbe{urGCB+KQ z${J1!!yF~(lNV-0FShhQ(Nw=0#U-VQPHo1=8ZLfK6vR^B$iK;zRyLffvuHiQ10$u&OQo-K zve5NcD#qUPiX9u%m%f}5WM{x1-S4l71gf(Ni4y8wS4EE)*V~wcME7k-_QXC zA?~dfslnoLsb%YKK|Qq=cJ*DKyEmqGGBKf5JF&&Nd_^{EkE=-=m;Sn#g;6CseS4WM zo!q`2hn-s%4idl6Y}{R|yMZFg+>2z6CyCe3tebj;>-G2GO2XR4s|zRw)U?Yfw^ zCr@!f(F1N|P7ck2oqNtIao3!Js%iLsNLSYtvIm50Gj2-idn)TvPy}P+LRI+l?isk5 zNs#=#Na=t}nf}ONeSW$Qx<)3a?|j8XZ_T?#_@5UXpH1>~v8aNR>EQd4Z$6IfX|%{g z0J}H$-Ia{2JSA+#HOT*w%Qnlyp_6v4I}p;9wjf*}pOuvCSG3}^XbsQoj5Vr=xb|RM z4IXcRzf;ho-P2|fA4J$2s}+}Ewf}T@p?EVMkwDti%YvZ27v?4P1BJOL858vA zSnSXGETpp5AI*F$j80V@Q6lAi>+a0NjJBZ8o8`bOFe$mE?{L7&hkbkoaA$lMav^=( zMf%wSomQhB1+~?4>dDxX21*v}%qu9137tza$G}Dm9=3=eeUa2OU&8B`bAv|7TM2?( zT6WVYz0c=UnSL9jLXxt4ge+eKo?w#imz+@a!AJ_}x51dfgb5$K1}|s-CDUq@17;c* zQ9-M)&zo}Bs;4^U3KFtTT5*trB=~HHYjv42?DyvlI27r4@Q#4*5bu3~80uKb7(mPa9w48{Ak14Z$YA{i zeT&P3<@XG<7r=nm`_ zzn8})Xd(3Hj6*VHGlQt)Xnun$hn+&#o7`8DCX_E5@0_k$*&UQ_s+=|;?rc@Jnyt7& zcvSn^``ur)gr7Zlb_^>2`JZ83pdRJ?nfxIfHaM_(dJu#i{!RyiXh)izDI!2#MDh&b z*`7Ruy~^*56cCL4KP>EXS@IJfI<9dOFS7WLyP(EHG1fQNd#B?N{(qwJH`4$B literal 0 HcmV?d00001 diff --git a/paparazzi-gradle-plugin/src/test/projects/preview-annotation-sample/src/test/snapshots/images/app.cash.paparazzi.plugin.test_PreviewTests_preview[HelloPaparazzi_HelloPaparazziPreviewConfig].png b/paparazzi-gradle-plugin/src/test/projects/preview-annotation-sample/src/test/snapshots/images/app.cash.paparazzi.plugin.test_PreviewTests_preview[HelloPaparazzi_HelloPaparazziPreviewConfig].png deleted file mode 100644 index 828d6d4c56bf037919b6ceadc685b7d9d82d121e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4583 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 diff --git a/paparazzi-gradle-plugin/src/test/projects/preview-annotation-sample/src/test/snapshots/images/app.cash.paparazzi.plugin.test_PreviewTests_preview[HelloPaparazzi_HelloPaparazzi].png b/paparazzi-gradle-plugin/src/test/projects/preview-annotation-sample/src/test/snapshots/images/app.cash.paparazzi.plugin.test_PreviewTests_preview[HelloPaparazzi_HelloPaparazzi].png index d838caf940a54b48f6f2091488d866ddd9b3a36e..da18192acb268e3dadb8f70882e07878565b85d7 100644 GIT binary patch literal 2410 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 - val visibilityCheck = checkVisibility(func) + functions.process { func, preview, previewParam -> + val visibilityCheck = checkVisibility(func, previewParam) val snapshotName = func.snapshotName(env) when { @@ -53,21 +55,19 @@ internal object PaparazziPoet { visibilityCheck = visibilityCheck, function = func, snapshotName = snapshotName, - buildErrorMessage = { - "$it is private. Make it internal or public to generate a snapshot." - } + preview = preview, + previewParam = previewParam ) - previewParam != null -> addError( - visibilityCheck = visibilityCheck, + previewParam != null -> addProvider( function = func, snapshotName = snapshotName, - buildErrorMessage = { - "$it preview uses PreviewParameters which aren't currently supported." - } + preview = preview, + previewParam = previewParam ) else -> addDefault( function = func, - snapshotName = snapshotName + snapshotName = snapshotName, + preview = preview ) } } @@ -84,32 +84,56 @@ internal object PaparazziPoet { } private fun Sequence.process( - block: (KSFunctionDeclaration, KSValueParameter?) -> Unit + block: (KSFunctionDeclaration, PreviewModel, KSValueParameter?) -> Unit ) = flatMap { func -> val previewParam = func.previewParam() func.findDistinctPreviews() - .map { Pair(func, previewParam) } - }.forEach { (func, previewParam) -> - block(func, previewParam) + .map { AcceptableAnnotationsProcessData(func, it, previewParam) } + }.forEach { (func, preview, previewParam) -> + block(func, preview, previewParam) } + private data class AcceptableAnnotationsProcessData( + val func: KSFunctionDeclaration, + val model: PreviewModel, + val previewParam: KSValueParameter? + ) + private fun CodeBlock.Builder.addError( visibilityCheck: VisibilityCheck, function: KSFunctionDeclaration, snapshotName: String, - buildErrorMessage: (String?) -> String + preview: PreviewModel, + previewParam: KSValueParameter? ) { val qualifiedName = if (visibilityCheck.isFunctionPrivate) { function.qualifiedName?.asString() } else { - null + previewParam?.previewParamProvider()?.qualifiedName?.asString() } addStatement("%L.PaparazziPreviewData.Error(", PACKAGE_NAME) indent() addStatement("snapshotName = %S,", snapshotName) - addStatement("message = %S,", buildErrorMessage(qualifiedName)) + addStatement("message = %S,", "$qualifiedName is private. Make it internal or public to generate a snapshot.") + addPreviewData(preview) + unindent() + addStatement("),") + } + + private fun CodeBlock.Builder.addProvider( + function: KSFunctionDeclaration, + snapshotName: String, + preview: PreviewModel, + previewParam: KSValueParameter + ) { + addStatement("%L.PaparazziPreviewData.Provider(", PACKAGE_NAME) + indent() + addStatement("snapshotName = %S,", snapshotName) + addStatement("composable = { %L(it) },", function.qualifiedName?.asString()) + addPreviewParameterData(previewParam) + addPreviewData(preview) unindent() addStatement("),") } @@ -128,12 +152,55 @@ internal object PaparazziPoet { private fun CodeBlock.Builder.addDefault( function: KSFunctionDeclaration, - snapshotName: String + snapshotName: String, + preview: PreviewModel ) { addStatement("%L.PaparazziPreviewData.Default(", PACKAGE_NAME) indent() addStatement("snapshotName = %S,", snapshotName) addStatement("composable = { %L() },", function.qualifiedName?.asString()) + addPreviewData(preview) + unindent() + addStatement("),") + } + + private fun CodeBlock.Builder.addPreviewData(preview: PreviewModel) { + addStatement("preview = %L.PreviewData(", PACKAGE_NAME) + indent() + + preview.fontScale.takeIf { it != 1f } + ?.let { addStatement("fontScale = %Lf,", it) } + + preview.device.takeIf { it.isNotEmpty() } + ?.let { addStatement("device = %S,", it) } + + preview.widthDp.takeIf { it > -1 } + ?.let { addStatement("widthDp = %L,", it) } + + preview.heightDp.takeIf { it > -1 } + ?.let { addStatement("heightDp = %L,", it) } + + preview.uiMode.takeIf { it != 0 } + ?.let { addStatement("uiMode = %L,", it) } + + preview.locale.takeIf { it.isNotEmpty() } + ?.let { addStatement("locale = %S,", it) } + + preview.backgroundColor.takeIf { it != 0L && preview.showBackground } + ?.let { addStatement("backgroundColor = %S", it.toString(16)) } + + unindent() + addStatement("),") + } + + private fun CodeBlock.Builder.addPreviewParameterData(previewParam: KSValueParameter) { + addStatement("previewParameter = %L.PreviewParameterData(", PACKAGE_NAME) + indent() + addStatement("name = %S,", previewParam.name?.asString()) + val previewParamProvider = previewParam.previewParamProvider() + val isClassObject = previewParamProvider.closestClassDeclaration()?.classKind == ClassKind.OBJECT + val previewParamProviderInstantiation = "${previewParamProvider.qualifiedName?.asString()}${if (isClassObject) "" else "()"}" + addStatement("values = %L.values,", previewParamProviderInstantiation) unindent() addStatement("),") } @@ -149,14 +216,17 @@ internal object PaparazziPoet { }.joinToString("_") private fun checkVisibility( - function: KSFunctionDeclaration + function: KSFunctionDeclaration, + previewParam: KSValueParameter? ) = VisibilityCheck( - isFunctionPrivate = function.getVisibility() == Visibility.PRIVATE + isFunctionPrivate = function.getVisibility() == Visibility.PRIVATE, + isPreviewParamProviderPrivate = previewParam?.previewParamProvider()?.getVisibility() == Visibility.PRIVATE ) } internal data class VisibilityCheck( - val isFunctionPrivate: Boolean + val isFunctionPrivate: Boolean, + val isPreviewParamProviderPrivate: Boolean ) { val isPrivate = isFunctionPrivate } diff --git a/paparazzi-preview-processor/src/main/java/app/cash/paparazzi/preview/processor/Utils.kt b/paparazzi-preview-processor/src/main/java/app/cash/paparazzi/preview/processor/Utils.kt index 0deb57adcd..e6f0559da9 100644 --- a/paparazzi-preview-processor/src/main/java/app/cash/paparazzi/preview/processor/Utils.kt +++ b/paparazzi-preview-processor/src/main/java/app/cash/paparazzi/preview/processor/Utils.kt @@ -4,6 +4,8 @@ import com.google.devtools.ksp.symbol.FunctionKind.TOP_LEVEL import com.google.devtools.ksp.symbol.KSAnnotated import com.google.devtools.ksp.symbol.KSAnnotation import com.google.devtools.ksp.symbol.KSFunctionDeclaration +import com.google.devtools.ksp.symbol.KSType +import com.google.devtools.ksp.symbol.KSValueParameter internal const val PACKAGE_NAME = "app.cash.paparazzi.annotations" @@ -14,6 +16,11 @@ internal fun KSAnnotation.isPreviewParameter() = qualifiedName() == "androidx.co internal fun KSAnnotation.qualifiedName() = declaration().qualifiedName?.asString() ?: "" internal fun KSAnnotation.declaration() = annotationType.resolve().declaration +@Suppress("UNCHECKED_CAST") +public fun KSAnnotation.previewArg(name: String): T = arguments + .first { it.name?.asString() == name } + .let { it.value as T } + internal fun Sequence.findPaparazzi() = filterIsInstance() .filter { @@ -35,12 +42,43 @@ internal fun Sequence.findPreviews(stack: Set = setO return direct.plus(indirect) } -internal fun KSFunctionDeclaration.findDistinctPreviews() = annotations.findPreviews().distinct() +internal fun KSFunctionDeclaration.findDistinctPreviews() = annotations.findPreviews().toList() + .map { preview -> + PreviewModel( + fontScale = preview.previewArg("fontScale"), + device = preview.previewArg("device"), + widthDp = preview.previewArg("widthDp"), + heightDp = preview.previewArg("heightDp"), + uiMode = preview.previewArg("uiMode"), + locale = preview.previewArg("locale"), + backgroundColor = preview.previewArg("backgroundColor"), + showBackground = preview.previewArg("showBackground") + ) + } + .distinct() internal fun KSFunctionDeclaration.previewParam() = parameters.firstOrNull { param -> param.annotations.any { it.isPreviewParameter() } } +internal fun KSValueParameter.previewParamProvider() = annotations + .first { it.isPreviewParameter() } + .arguments + .first { arg -> arg.name?.asString() == "provider" } + .let { it.value as KSType } + .declaration + +internal data class PreviewModel( + val fontScale: Float, + val device: String, + val widthDp: Int, + val heightDp: Int, + val uiMode: Int, + val locale: String, + val backgroundColor: Long, + val showBackground: Boolean +) + internal data class EnvironmentOptions( val namespace: String ) diff --git a/paparazzi-preview-test/api/paparazzi-annotations.api b/paparazzi-preview-test/api/paparazzi-annotations.api new file mode 100644 index 0000000000..913e18bbf8 --- /dev/null +++ b/paparazzi-preview-test/api/paparazzi-annotations.api @@ -0,0 +1,39 @@ +public abstract interface annotation class app/cash/paparazzi/annotations/Paparazzi : java/lang/annotation/Annotation { +} + +public abstract interface class app/cash/paparazzi/annotations/PaparazziPreviewData { +} + +public final class app/cash/paparazzi/annotations/PaparazziPreviewData$Default : app/cash/paparazzi/annotations/PaparazziPreviewData { + public fun (Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Lkotlin/jvm/functions/Function0; + public final fun copy (Ljava/lang/String;Lkotlin/jvm/functions/Function0;)Lapp/cash/paparazzi/annotations/PaparazziPreviewData$Default; + public static synthetic fun copy$default (Lapp/cash/paparazzi/annotations/PaparazziPreviewData$Default;Ljava/lang/String;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lapp/cash/paparazzi/annotations/PaparazziPreviewData$Default; + public fun equals (Ljava/lang/Object;)Z + public final fun getComposable ()Lkotlin/jvm/functions/Function0; + public final fun getSnapshotName ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class app/cash/paparazzi/annotations/PaparazziPreviewData$Empty : app/cash/paparazzi/annotations/PaparazziPreviewData { + public static final field INSTANCE Lapp/cash/paparazzi/annotations/PaparazziPreviewData$Empty; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class app/cash/paparazzi/annotations/PaparazziPreviewData$Error : app/cash/paparazzi/annotations/PaparazziPreviewData { + public fun (Ljava/lang/String;Ljava/lang/String;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lapp/cash/paparazzi/annotations/PaparazziPreviewData$Error; + public static synthetic fun copy$default (Lapp/cash/paparazzi/annotations/PaparazziPreviewData$Error;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lapp/cash/paparazzi/annotations/PaparazziPreviewData$Error; + public fun equals (Ljava/lang/Object;)Z + public final fun getMessage ()Ljava/lang/String; + public final fun getSnapshotName ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + diff --git a/paparazzi-preview-test/api/paparazzi-preview-test.api b/paparazzi-preview-test/api/paparazzi-preview-test.api new file mode 100644 index 0000000000..59a26fe6ad --- /dev/null +++ b/paparazzi-preview-test/api/paparazzi-preview-test.api @@ -0,0 +1,43 @@ +public final class app/cash/paparazzi/preview/ComposableSingletons$SnapshotKt { + public static final field INSTANCE Lapp/cash/paparazzi/preview/ComposableSingletons$SnapshotKt; + public static field lambda-1 Lkotlin/jvm/functions/Function3; + public fun ()V + public final fun getLambda-1$paparazzi_preview_test ()Lkotlin/jvm/functions/Function3; +} + +public final class app/cash/paparazzi/preview/ComposableSingletons$UtilsKt { + public static final field INSTANCE Lapp/cash/paparazzi/preview/ComposableSingletons$UtilsKt; + public static field lambda-1 Lkotlin/jvm/functions/Function3; + public static field lambda-2 Lkotlin/jvm/functions/Function3; + public static field lambda-3 Lkotlin/jvm/functions/Function3; + public fun ()V + public final fun getLambda-1$paparazzi_preview_test ()Lkotlin/jvm/functions/Function3; + public final fun getLambda-2$paparazzi_preview_test ()Lkotlin/jvm/functions/Function3; + public final fun getLambda-3$paparazzi_preview_test ()Lkotlin/jvm/functions/Function3; +} + +public final class app/cash/paparazzi/preview/DefaultLocaleRule : org/junit/rules/TestRule { + public static final field $stable I + public fun (Ljava/lang/String;)V + public fun apply (Lorg/junit/runners/model/Statement;Lorg/junit/runner/Description;)Lorg/junit/runners/model/Statement; + public final fun getLocale ()Ljava/lang/String; +} + +public class app/cash/paparazzi/preview/PaparazziValuesProvider : com/google/testing/junit/testparameterinjector/TestParameter$TestParameterValuesProvider { + public static final field $stable I + public fun (Ljava/util/List;)V + public fun provideValues ()Ljava/util/List; +} + +public final class app/cash/paparazzi/preview/SnapshotKt { + public static final fun deviceConfig (Lapp/cash/paparazzi/annotations/PaparazziPreviewData;Lapp/cash/paparazzi/DeviceConfig;)Lapp/cash/paparazzi/DeviceConfig; + public static synthetic fun deviceConfig$default (Lapp/cash/paparazzi/annotations/PaparazziPreviewData;Lapp/cash/paparazzi/DeviceConfig;ILjava/lang/Object;)Lapp/cash/paparazzi/DeviceConfig; + public static final fun locale (Lapp/cash/paparazzi/annotations/PaparazziPreviewData;)Ljava/lang/String; + public static final fun snapshot (Lapp/cash/paparazzi/Paparazzi;Lapp/cash/paparazzi/annotations/PaparazziPreviewData;Ljava/lang/String;ZLkotlin/jvm/functions/Function3;)V + public static synthetic fun snapshot$default (Lapp/cash/paparazzi/Paparazzi;Lapp/cash/paparazzi/annotations/PaparazziPreviewData;Ljava/lang/String;ZLkotlin/jvm/functions/Function3;ILjava/lang/Object;)V +} + +public final class app/cash/paparazzi/preview/UtilsKt { + public static final fun deviceConfig (Lapp/cash/paparazzi/annotations/PreviewData;Lapp/cash/paparazzi/DeviceConfig;)Lapp/cash/paparazzi/DeviceConfig; +} + diff --git a/paparazzi-preview-test/build.gradle b/paparazzi-preview-test/build.gradle new file mode 100644 index 0000000000..0bdd9b278b --- /dev/null +++ b/paparazzi-preview-test/build.gradle @@ -0,0 +1,10 @@ +apply plugin: 'org.jetbrains.kotlin.jvm' +apply plugin: 'org.jetbrains.kotlin.plugin.compose' +apply plugin: 'com.vanniktech.maven.publish' + +dependencies { + compileOnly libs.compose.runtime + compileOnly libs.composeUi.foundation + compileOnly projects.paparazzi + implementation projects.paparazziAnnotations +} diff --git a/paparazzi-preview-test/gradle.properties b/paparazzi-preview-test/gradle.properties new file mode 100644 index 0000000000..2c41f7932f --- /dev/null +++ b/paparazzi-preview-test/gradle.properties @@ -0,0 +1,4 @@ +POM_ARTIFACT_ID=paparazzi-preview-test +POM_NAME=Paparazzi Preview Test +POM_DESCRIPTION=Helper classes for running JUnit tests for Paparazzi with generated Composable @Previews +POM_PACKAGING=jar diff --git a/paparazzi-preview-test/src/main/java/app/cash/paparazzi/preview/Snapshot.kt b/paparazzi-preview-test/src/main/java/app/cash/paparazzi/preview/Snapshot.kt new file mode 100644 index 0000000000..ef48dd3e20 --- /dev/null +++ b/paparazzi-preview-test/src/main/java/app/cash/paparazzi/preview/Snapshot.kt @@ -0,0 +1,104 @@ +// Copyright Square, Inc. +package app.cash.paparazzi.preview + +import androidx.compose.runtime.Composable +import app.cash.paparazzi.DeviceConfig +import app.cash.paparazzi.Paparazzi +import app.cash.paparazzi.annotations.PaparazziPreviewData +import com.google.testing.junit.testparameterinjector.TestParameter.TestParameterValuesProvider +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement +import java.util.Locale + +/** + * Take a snapshot of the given [previewData]. + */ +public fun Paparazzi.snapshot( + previewData: PaparazziPreviewData, + name: String? = null, + localInspectionMode: Boolean = true, + wrapper: @Composable (@Composable () -> Unit) -> Unit = { it() } +) { + when (previewData) { + is PaparazziPreviewData.Default -> snapshotDefault(previewData, name, localInspectionMode, wrapper) + is PaparazziPreviewData.Provider<*> -> snapshotProvider(previewData, name, localInspectionMode, wrapper) + is PaparazziPreviewData.Empty -> Unit + is PaparazziPreviewData.Error -> throw Exception(previewData.message) + } +} + +/** + * Generate a Paparazzi DeviceConfig for the given preview + * using the given [default] DeviceConfig. + * + * default: The IDE renders a preview with a higher resolution than + * the default device set by Paparazzi (which is currently Nexus 5). Defaulting to + * a larger device brings the previews and snapshots closer in parity. + */ +public fun PaparazziPreviewData.deviceConfig( + default: DeviceConfig = DeviceConfig.PIXEL_5 +): DeviceConfig = when (this) { + is PaparazziPreviewData.Default -> preview.deviceConfig(default) + is PaparazziPreviewData.Provider<*> -> preview.deviceConfig(default) + else -> default +} + +/** + * Returns a locale for the given preview, or null if error or empty. + */ +public fun PaparazziPreviewData.locale(): String? = when (this) { + is PaparazziPreviewData.Default -> preview.locale + is PaparazziPreviewData.Provider<*> -> preview.locale + else -> null +} + +/** + * Convert a list of generated [PaparazziPreviewData] + * to a flat list of [PaparazziPreviewData]s. + */ +internal fun List.flatten() = flatMap { + when (it) { + is PaparazziPreviewData.Provider<*> -> List(it.previewParameter.values.count()) { i -> + it.withPreviewParameterIndex(i) + } + else -> listOf(it) + } +} + +/** + * A `@TestParameter` values provider for the given [annotations]. + * + * Example usage: + * ``` + * private class ValuesProvider : PaparazziValuesProvider(paparazziAnnotations) + * ``` + */ +public open class PaparazziValuesProvider( + private val annotations: List +) : TestParameterValuesProvider { + override fun provideValues(): List = annotations.flatten() +} + +/** + * Enforce a particular default locale for a test. Resets back to default on completion. + */ +public class DefaultLocaleRule(public val locale: String?) : TestRule { + override fun apply( + base: Statement, + description: Description + ): Statement { + return object : Statement() { + override fun evaluate() { + val default = Locale.getDefault() + + try { + locale?.let { Locale.setDefault(Locale.forLanguageTag(it)) } + base.evaluate() + } finally { + Locale.setDefault(default) + } + } + } + } +} diff --git a/paparazzi-preview-test/src/main/java/app/cash/paparazzi/preview/Utils.kt b/paparazzi-preview-test/src/main/java/app/cash/paparazzi/preview/Utils.kt new file mode 100644 index 0000000000..c3404418f4 --- /dev/null +++ b/paparazzi-preview-test/src/main/java/app/cash/paparazzi/preview/Utils.kt @@ -0,0 +1,151 @@ +// Copyright Square, Inc. +package app.cash.paparazzi.preview + +import android.content.res.Configuration +import android.util.DisplayMetrics +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalInspectionMode +import app.cash.paparazzi.DeviceConfig +import app.cash.paparazzi.Paparazzi +import app.cash.paparazzi.annotations.PaparazziPreviewData +import app.cash.paparazzi.annotations.PreviewData +import com.android.resources.NightMode +import com.android.resources.UiMode +import java.util.Locale +import kotlin.math.roundToInt + +internal fun String.deviceConfig() = when (this) { + "id:Nexus 7" -> DeviceConfig.NEXUS_7 + "id:Nexus 7 2013" -> DeviceConfig.NEXUS_7_2012 + "id:Nexus 5" -> DeviceConfig.NEXUS_5 + "id:Nexus 6" -> DeviceConfig.NEXUS_7 + "id:Nexus 9" -> DeviceConfig.NEXUS_10 + "name:Nexus 10" -> DeviceConfig.NEXUS_10 + "id:Nexus 5X" -> DeviceConfig.NEXUS_5 + "id:Nexus 6P" -> DeviceConfig.NEXUS_7 + "id:pixel_c" -> DeviceConfig.PIXEL_C + "id:pixel" -> DeviceConfig.PIXEL + "id:pixel_xl" -> DeviceConfig.PIXEL_XL + "id:pixel_2" -> DeviceConfig.PIXEL_2 + "id:pixel_2_xl" -> DeviceConfig.PIXEL_2_XL + "id:pixel_3" -> DeviceConfig.PIXEL_3 + "id:pixel_3_xl" -> DeviceConfig.PIXEL_3_XL + "id:pixel_3a" -> DeviceConfig.PIXEL_3A + "id:pixel_3a_xl" -> DeviceConfig.PIXEL_3A_XL + "id:pixel_4" -> DeviceConfig.PIXEL_4 + "id:pixel_4_xl" -> DeviceConfig.PIXEL_4_XL + "id:pixel_5" -> DeviceConfig.PIXEL_5 + "id:pixel_6" -> DeviceConfig.PIXEL_6 + "id:pixel_6_pro" -> DeviceConfig.PIXEL_6_PRO + "id:wearos_small_round" -> DeviceConfig.WEAR_OS_SMALL_ROUND + "id:wearos_square" -> DeviceConfig.WEAR_OS_SQUARE + else -> null +} + +internal fun Int.uiMode() = when (this and Configuration.UI_MODE_TYPE_MASK) { + Configuration.UI_MODE_TYPE_NORMAL -> UiMode.NORMAL + Configuration.UI_MODE_TYPE_CAR -> UiMode.CAR + Configuration.UI_MODE_TYPE_DESK -> UiMode.DESK + Configuration.UI_MODE_TYPE_APPLIANCE -> UiMode.APPLIANCE + Configuration.UI_MODE_TYPE_WATCH -> UiMode.WATCH + Configuration.UI_MODE_TYPE_VR_HEADSET -> UiMode.VR_HEADSET + else -> null +} + +internal fun Int.nightMode() = when (this and Configuration.UI_MODE_NIGHT_MASK) { + Configuration.UI_MODE_NIGHT_NO -> NightMode.NOTNIGHT + Configuration.UI_MODE_NIGHT_YES -> NightMode.NIGHT + else -> null +} + +internal fun String.localeQualifierString() = + Locale.forLanguageTag(this).run { + "$language-r$country" + } + +public fun PreviewData?.deviceConfig(defaultDeviceConfig: DeviceConfig): DeviceConfig = + (this?.device?.deviceConfig() ?: defaultDeviceConfig).let { config -> + config.copy( + screenWidth = this?.widthDp?.toPx(config.density.dpiValue) ?: config.screenWidth, + screenHeight = this?.heightDp?.toPx(config.density.dpiValue) ?: config.screenHeight, + fontScale = this?.fontScale ?: config.fontScale, + uiMode = this?.uiMode?.uiMode() ?: config.uiMode, + nightMode = this?.uiMode?.nightMode() ?: config.nightMode, + locale = this?.locale?.localeQualifierString() ?: config.locale + ) + } + +private fun Int.toPx(dpi: Int) = + (this * (dpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)).roundToInt() + +internal fun Paparazzi.snapshotDefault( + previewData: PaparazziPreviewData.Default, + name: String?, + localInspectionMode: Boolean, + wrapper: @Composable (@Composable () -> Unit) -> Unit = { it() } +) { + snapshot( + name = name, + backgroundColor = previewData.preview.backgroundColor, + composable = { previewData.composable() }, + localInspectionMode = localInspectionMode, + wrapper = wrapper + ) +} + +internal fun Paparazzi.snapshotProvider( + previewData: PaparazziPreviewData.Provider, + name: String?, + localInspectionMode: Boolean, + wrapper: @Composable (@Composable () -> Unit) -> Unit = { it() } +) { + val paramValue = previewData.previewParameter.values + .elementAt(previewData.previewParameter.index) + + snapshot( + name = name, + backgroundColor = previewData.preview.backgroundColor, + composable = { previewData.composable(paramValue) }, + localInspectionMode = localInspectionMode, + wrapper = wrapper + ) +} + +private fun Paparazzi.snapshot( + name: String?, + backgroundColor: String?, + composable: @Composable () -> Unit, + localInspectionMode: Boolean, + wrapper: @Composable (@Composable () -> Unit) -> Unit = { it() } +) { + snapshot(name) { + PreviewWrapper(backgroundColor, localInspectionMode) { + wrapper { composable() } + } + } +} + +@Composable +private fun PreviewWrapper( + backgroundColor: String?, + localInspectionMode: Boolean, + content: @Composable BoxScope.() -> Unit +) { + CompositionLocalProvider(LocalInspectionMode provides localInspectionMode) { + Box( + modifier = Modifier + .then( + backgroundColor?.toLong(16) + ?.let { Modifier.background(Color(it)) } + ?: Modifier + ), + content = content + ) + } +} diff --git a/paparazzi/api/paparazzi.api b/paparazzi/api/paparazzi.api index b7ab491035..01a6bf3947 100644 --- a/paparazzi/api/paparazzi.api +++ b/paparazzi/api/paparazzi.api @@ -235,28 +235,3 @@ public final class app/cash/paparazzi/accessibility/AccessibilityRenderExtension public fun renderView (Landroid/view/View;)Landroid/view/View; } -public final class app/cash/paparazzi/preview/ComposableSingletons$SnapshotKt { - public static final field INSTANCE Lapp/cash/paparazzi/preview/ComposableSingletons$SnapshotKt; - public static field lambda-1 Lkotlin/jvm/functions/Function3; - public fun ()V - public final fun getLambda-1$paparazzi ()Lkotlin/jvm/functions/Function3; -} - -public final class app/cash/paparazzi/preview/ComposableSingletons$UtilsKt { - public static final field INSTANCE Lapp/cash/paparazzi/preview/ComposableSingletons$UtilsKt; - public static field lambda-1 Lkotlin/jvm/functions/Function3; - public fun ()V - public final fun getLambda-1$paparazzi ()Lkotlin/jvm/functions/Function3; -} - -public class app/cash/paparazzi/preview/PaparazziValuesProvider : com/google/testing/junit/testparameterinjector/TestParameter$TestParameterValuesProvider { - public static final field $stable I - public fun (Ljava/util/List;)V - public fun provideValues ()Ljava/util/List; -} - -public final class app/cash/paparazzi/preview/SnapshotKt { - public static final fun snapshot (Lapp/cash/paparazzi/Paparazzi;Lapp/cash/paparazzi/annotations/PaparazziPreviewData;Ljava/lang/String;Lkotlin/jvm/functions/Function3;)V - public static synthetic fun snapshot$default (Lapp/cash/paparazzi/Paparazzi;Lapp/cash/paparazzi/annotations/PaparazziPreviewData;Ljava/lang/String;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)V -} - diff --git a/paparazzi/src/main/java/app/cash/paparazzi/preview/Snapshot.kt b/paparazzi/src/main/java/app/cash/paparazzi/preview/Snapshot.kt deleted file mode 100644 index 253d2c1773..0000000000 --- a/paparazzi/src/main/java/app/cash/paparazzi/preview/Snapshot.kt +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright Square, Inc. -package app.cash.paparazzi.preview - -import androidx.compose.runtime.Composable -import app.cash.paparazzi.Paparazzi -import app.cash.paparazzi.annotations.PaparazziPreviewData -import com.google.testing.junit.testparameterinjector.TestParameter.TestParameterValuesProvider - -/** - * Take a snapshot of the given [previewData]. - */ -public fun Paparazzi.snapshot( - previewData: PaparazziPreviewData, - name: String? = null, - wrapper: @Composable (@Composable () -> Unit) -> Unit = { it() } -) { - when (previewData) { - is PaparazziPreviewData.Default -> snapshotDefault(previewData, name, wrapper) - is PaparazziPreviewData.Empty -> Unit - is PaparazziPreviewData.Error -> throw Exception(previewData.message) - } -} - -/** - * A `@TestParameter` values provider for the given [annotations]. - * - * Example usage: - * ``` - * private class ValuesProvider : PaparazziValuesProvider(paparazziAnnotations) - * ``` - */ -public open class PaparazziValuesProvider( - private val annotations: List -) : TestParameterValuesProvider { - override fun provideValues(): List = annotations -} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/preview/Utils.kt b/paparazzi/src/main/java/app/cash/paparazzi/preview/Utils.kt deleted file mode 100644 index 97c0a4a51e..0000000000 --- a/paparazzi/src/main/java/app/cash/paparazzi/preview/Utils.kt +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright Square, Inc. -package app.cash.paparazzi.preview - -import androidx.compose.runtime.Composable -import app.cash.paparazzi.Paparazzi -import app.cash.paparazzi.annotations.PaparazziPreviewData - -internal fun Paparazzi.snapshotDefault( - previewData: PaparazziPreviewData.Default, - name: String?, - wrapper: @Composable (@Composable () -> Unit) -> Unit = { it() } -) { - snapshot(name) { - wrapper { previewData.composable() } - } -} diff --git a/sample/src/main/java/app/cash/paparazzi/sample/HelloPaparazzi.kt b/sample/src/main/java/app/cash/paparazzi/sample/HelloPaparazzi.kt index c140df2349..8187d3974a 100644 --- a/sample/src/main/java/app/cash/paparazzi/sample/HelloPaparazzi.kt +++ b/sample/src/main/java/app/cash/paparazzi/sample/HelloPaparazzi.kt @@ -1,5 +1,6 @@ package app.cash.paparazzi.sample +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize @@ -13,6 +14,8 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider import app.cash.paparazzi.annotations.Paparazzi @Paparazzi @@ -50,3 +53,45 @@ fun HelloPaparazzi() { ) } } + +@Paparazzi +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview +@Composable +fun HelloPaparazzi( + @PreviewParameter(TextProvider::class) text: String +) { + Column( + Modifier + .background(Color.White) + .wrapContentSize() + ) { + Text(text) + Text(text, style = TextStyle(fontFamily = FontFamily.Cursive)) + Text( + text = text, + style = TextStyle(textDecoration = TextDecoration.LineThrough) + ) + Text( + text = text, + style = TextStyle(textDecoration = TextDecoration.Underline) + ) + Text( + text = text, + style = TextStyle( + textDecoration = TextDecoration.combine( + listOf( + TextDecoration.Underline, + TextDecoration.LineThrough + ) + ), + fontWeight = FontWeight.Bold + ) + ) + } +} + +object TextProvider : PreviewParameterProvider { + override val values: Sequence = + sequenceOf("Hello Paparazzi", "Hello World", "Hello Compose") +} diff --git a/settings.gradle b/settings.gradle index 47b4876ac9..2676f3e16b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -19,6 +19,7 @@ rootProject.name = 'paparazzi-root' include ':paparazzi' include ':paparazzi-annotations' include ':paparazzi-preview-processor' +include ':paparazzi-preview-test' include ':paparazzi-gradle-plugin' include ':sample'