Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add preview param support for @Paparazzi #1554

Open
wants to merge 62 commits into
base: akent/simple-preview/snapshots
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
9f13a50
adding data api for annotation manifest
nak5ive Apr 18, 2024
ef9d189
Cleanup API
nak5ive Apr 18, 2024
ec46772
adding data api for annotation manifest
nak5ive Apr 18, 2024
c97793b
Cleanup API
nak5ive Apr 18, 2024
39a2c20
Cleanup API
nak5ive Apr 18, 2024
62ee20e
adding data api for annotation manifest
nak5ive Apr 18, 2024
79a3841
Cleanup API
nak5ive Apr 18, 2024
1e48eb2
adding preview processor
nak5ive Apr 18, 2024
fb60d19
Cleanup processor to remove preview param support
geoff-powell May 14, 2024
fee7db6
Cleanup API
nak5ive Apr 18, 2024
71a292c
adding data api for annotation manifest
nak5ive Apr 18, 2024
62aab16
Cleanup API
nak5ive Apr 18, 2024
5127b0a
Adding preview snapshot test apis
nak5ive Apr 18, 2024
a12b89e
Cleanup preview param
geoff-powell May 14, 2024
5bffdfd
Adding preview snapshot test apis
nak5ive Apr 18, 2024
7a93d6d
Cleanup preview param
geoff-powell May 14, 2024
5e51ed5
Cleanup API
nak5ive Apr 18, 2024
5ffcabe
Cleanup preview param
geoff-powell May 14, 2024
2f6608c
Adding preview snapshot test apis
nak5ive Apr 18, 2024
54a84fc
adding plugin extension stub
nak5ive Apr 18, 2024
aa8e232
Cleanup preview param
geoff-powell May 14, 2024
69fd8ea
Cleanup from upstream changes
geoff-powell Nov 20, 2024
7cd7987
Cleanup preview param
geoff-powell May 14, 2024
4781840
Adding preview snapshot test apis
nak5ive Apr 18, 2024
7b48d78
Cleanup preview param
geoff-powell May 14, 2024
e23d64e
adding plugin extension stub
nak5ive Apr 18, 2024
6cfedab
Cleanup preview param
geoff-powell May 14, 2024
af39b23
Adding preview snapshot test apis
nak5ive Apr 18, 2024
84b069c
starting plugin implementation for preview support
nak5ive Apr 29, 2024
2da32db
Add plugin tests for annotation
geoff-powell May 15, 2024
de7b1b1
Cleanup from upstream changes
geoff-powell Aug 21, 2024
e065b69
Add support for configuration caching
geoff-powell Aug 23, 2024
75af218
Remove paparazzi dsl extension for gradle prop
geoff-powell Aug 30, 2024
18d8c32
Cleanup deprecated TestParameterValuesProvider
geoff-powell Oct 8, 2024
063679f
Adding preview snapshot test apis
nak5ive Apr 18, 2024
5c2837e
Cleanup preview param
geoff-powell May 14, 2024
48282a2
adding plugin extension stub
nak5ive Apr 18, 2024
d2f117f
starting plugin implementation for preview support
nak5ive Apr 29, 2024
dd828e6
Add plugin tests for annotation
geoff-powell May 15, 2024
34ae55a
Cleanup from upstream changes
geoff-powell Aug 21, 2024
5823f5d
Add support for configuration caching
geoff-powell Aug 23, 2024
95c515c
Remove paparazzi dsl extension for gradle prop
geoff-powell Aug 30, 2024
1e44d3c
Cleanup from upstream changes
geoff-powell Aug 21, 2024
9c3586d
Cleanup preview param
geoff-powell May 14, 2024
59a3194
Add support for configuration caching
geoff-powell Aug 23, 2024
037c1a8
Cleanup from upstream changes
geoff-powell Aug 21, 2024
5ec8095
Add support for configuration caching
geoff-powell Aug 23, 2024
03c0d3a
Add plugin tests for annotation
geoff-powell May 15, 2024
21af873
Cleanup from upstream changes
geoff-powell Aug 21, 2024
e6abb86
Add support for configuration caching
geoff-powell Aug 23, 2024
21afa18
starting plugin implementation for preview support
nak5ive Apr 29, 2024
e59fdd5
Cleanup from upstream changes
geoff-powell Aug 21, 2024
3024ecc
Add support for configuration caching
geoff-powell Aug 23, 2024
ebc2d3a
Add preview param support
geoff-powell Aug 23, 2024
1f6776d
Cleanup tests
geoff-powell Sep 3, 2024
05eddda
Cleanup
geoff-powell Nov 13, 2024
592795c
Remove layoutlib dep
geoff-powell Nov 20, 2024
788b5a1
Remove lint check for now supported preview params
geoff-powell Nov 21, 2024
43f1916
Cleanup annotation processor and codegen
geoff-powell Jan 8, 2025
920188b
Fix module deployment and plugin
geoff-powell Jan 8, 2025
1d43fa3
Update API documentation
geoff-powell Jan 8, 2025
6cb31a2
Fix module dependency and package name
geoff-powell Jan 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ bytebuddy-agent = { module = "net.bytebuddy:byte-buddy-agent", version.ref = "by
bytebuddy-core = { module = "net.bytebuddy:byte-buddy", version.ref = "bytebuddy" }

compose-runtime = { module = "androidx.compose.runtime:runtime", version.ref = "compose" }
composeUi-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose" }
composeUi-material = { module = "androidx.compose.material:material", version.ref = "compose" }
composeUi-material3-android = { module = "androidx.compose.material3:material3-android", version = "1.3.1" }
composeUi-material-icons = { module = "androidx.compose.material:material-icons-core", version.ref = "compose" }
Expand Down
112 changes: 112 additions & 0 deletions paparazzi-annotations/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# `@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 `gradle.properties` file.

```properties
app.cash.paparazzi.annotation.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<String> {
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
```
2 changes: 1 addition & 1 deletion paparazzi-annotations/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ apply plugin: 'org.jetbrains.kotlin.plugin.compose'
apply plugin: 'com.vanniktech.maven.publish'

dependencies {
implementation libs.compose.runtime
compileOnly libs.compose.runtime
}
9 changes: 9 additions & 0 deletions paparazzi-gradle-plugin/api/paparazzi-gradle-plugin.api
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
public abstract class app/cash/paparazzi/gradle/GeneratePreviewTestFileTask : org/gradle/api/DefaultTask {
public fun <init> ()V
public final fun createFile ()V
public abstract fun getNamespace ()Lorg/gradle/api/provider/Property;
public abstract fun getPaparazziPreviewsFile ()Lorg/gradle/api/file/RegularFileProperty;
public abstract fun getPreviewTestOutputDir ()Lorg/gradle/api/file/DirectoryProperty;
public abstract fun getPreviewTestOutputFile ()Lorg/gradle/api/file/RegularFileProperty;
}

public final class app/cash/paparazzi/gradle/PaparazziPlugin : org/gradle/api/Plugin {
public fun <init> (Lorg/gradle/api/provider/ProviderFactory;Lorg/gradle/internal/operations/BuildOperationRunner;Lorg/gradle/internal/operations/BuildOperationExecutor;)V
public synthetic fun apply (Ljava/lang/Object;)V
Expand Down
5 changes: 5 additions & 0 deletions paparazzi-gradle-plugin/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ dependencies {
compileOnly libs.plugin.android

implementation platform(libs.kotlin.bom)
implementation libs.plugin.ksp
implementation(libs.tools.sdkCommon) {
because "SymbolUtils.getPackageNameFromManifest removed in AGP 7.0. Replace?"
}
Expand Down Expand Up @@ -63,6 +64,10 @@ buildConfig {

tasks.withType(Test).configureEach {
dependsOn(':paparazzi:publishMavenPublicationToProjectLocalMavenRepository')
dependsOn(':paparazzi-annotations:publishMavenPublicationToProjectLocalMavenRepository')
dependsOn(':paparazzi-preview-processor:publishMavenPublicationToProjectLocalMavenRepository')
dependsOn(':paparazzi-preview-runtime:publishMavenPublicationToProjectLocalMavenRepository')
dependsOn(':paparazzi-preview-test-junit:publishMavenPublicationToProjectLocalMavenRepository')
}

// When cleaning this project, we also want to clean the test projects.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package app.cash.paparazzi.gradle

import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.CacheableTask
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.SkipWhenEmpty
import org.gradle.api.tasks.TaskAction

@CacheableTask
public abstract class GeneratePreviewTestFileTask : DefaultTask() {
@get:Input
public abstract val namespace: Property<String>

@get:Optional
@get:SkipWhenEmpty
@get:InputFile
@get:PathSensitive(PathSensitivity.NONE)
public abstract val paparazziPreviewsFile: RegularFileProperty

@get:OutputDirectory
public abstract val previewTestOutputDir: DirectoryProperty

@get:OutputFile
public abstract val previewTestOutputFile: RegularFileProperty

@TaskAction
public fun createFile() {
val previewDataFile = paparazziPreviewsFile.asFile.get()
val previewTestFile = previewTestOutputFile.asFile.get()

if (!previewDataFile.exists()) {
logger.warn("Preview data file not found: $previewDataFile")
return
}

previewTestFile.writeText(previewTestSource(namespace.get()))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package app.cash.paparazzi.gradle
import app.cash.paparazzi.gradle.instrumentation.ResourcesCompatVisitorFactory
import app.cash.paparazzi.gradle.reporting.PaparazziTestReporter
import app.cash.paparazzi.gradle.utils.artifactViewFor
import app.cash.paparazzi.gradle.utils.registerGeneratePreviewTask
import app.cash.paparazzi.gradle.utils.relativize
import com.android.build.api.instrumentation.FramesComputationMode
import com.android.build.api.instrumentation.InstrumentationScope
Expand All @@ -27,6 +28,8 @@ import com.android.build.api.variant.DynamicFeatureAndroidComponentsExtension
import com.android.build.api.variant.HasUnitTest
import com.android.build.api.variant.LibraryAndroidComponentsExtension
import com.android.build.gradle.BaseExtension
import com.google.devtools.ksp.gradle.KspExtension
import com.google.devtools.ksp.gradle.KspGradleSubplugin
import org.gradle.api.DefaultTask
import org.gradle.api.Plugin
import org.gradle.api.Project
Expand Down Expand Up @@ -86,6 +89,7 @@ public class PaparazziPlugin @Inject constructor(
else -> error("${androidComponents.javaClass.name} from $plugin is not supported in Paparazzi")
}
setupPaparazzi(project, androidComponents)
setupPreviewProcessor(project, androidComponents)
}
}
}
Expand Down Expand Up @@ -238,7 +242,7 @@ public class PaparazziPlugin @Inject constructor(
test.inputs.dir(
isVerifyRun.flatMap {
project.objects.directoryProperty().apply {
set(if (it) snapshotOutputDir else null)
set(if (it && snapshotOutputDir.asFile.exists()) snapshotOutputDir else null)
}
}
).withPropertyName("paparazzi.snapshot.input.dir")
Expand Down Expand Up @@ -278,6 +282,23 @@ public class PaparazziPlugin @Inject constructor(
}
}

private fun setupPreviewProcessor(project: Project, extension: AndroidComponentsExtension<*, *, *>) {
project.pluginManager.apply(KspGradleSubplugin::class.java)

project.addAnnotationsDependency()
project.addPreviewRuntimeDependency()
project.addProcessorDependency()
project.addPreviewTestDependency()
project.registerGeneratePreviewTask(extension)

project.afterEvaluate {
// pass the namespace to the processor
val kspExtension = project.extensions.getByType(KspExtension::class.java)
val android = project.extensions.getByType(BaseExtension::class.java)
kspExtension.arg(KSP_ARG_NAMESPACE, android.packageName())
}
}

public abstract class PaparazziTask : DefaultTask() {
@Option(option = "tests", description = "Sets test class or method name to be included, '*' is supported.")
public open fun setTestNameIncludePatterns(testNamePattern: List<String>): PaparazziTask {
Expand Down Expand Up @@ -345,6 +366,46 @@ public class PaparazziPlugin @Inject constructor(
configurations.getByName("testImplementation").dependencies.add(dependency)
}

private fun Project.addAnnotationsDependency() {
val dependency = if (isInternal()) {
dependencies.project(mapOf("path" to ":paparazzi-annotations"))
} else {
dependencies.create("app.cash.paparazzi:paparazzi-annotations:$VERSION")
}
configurations.getByName("implementation").dependencies.add(dependency)
}

private fun Project.addPreviewRuntimeDependency() {
val dependency = if (isInternal()) {
dependencies.project(mapOf("path" to ":paparazzi-preview-runtime"))
} else {
dependencies.create("app.cash.paparazzi:paparazzi-preview-runtime:$VERSION")
}
configurations.getByName("implementation").dependencies.add(dependency)
}

private fun Project.addProcessorDependency() {
val dependency = if (isInternal()) {
dependencies.project(mapOf("path" to ":paparazzi-preview-processor"))
} else {
dependencies.create("app.cash.paparazzi:paparazzi-preview-processor:$VERSION")
}
if (project.plugins.hasPlugin("org.jetbrains.kotlin.multiplatform")) {
configurations.getByName("kspCommonMainMetadata").dependencies.add(dependency)
} else {
configurations.getByName("ksp").dependencies.add(dependency)
}
}

private fun Project.addPreviewTestDependency() {
val dependency = if (isInternal()) {
dependencies.project(mapOf("path" to ":paparazzi-preview-test-junit"))
} else {
dependencies.create("app.cash.paparazzi:paparazzi-preview-test-junit:$VERSION")
}
configurations.getByName("testImplementation").dependencies.add(dependency)
}

private fun Project.isInternal(): Boolean = providers.gradleProperty("app.cash.paparazzi.internal").orNull == "true"

private fun BaseExtension.packageName(): String = namespace ?: ""
Expand All @@ -359,3 +420,4 @@ public class PaparazziPlugin @Inject constructor(
}

private const val DEFAULT_COMPILE_SDK_VERSION = 34
private const val KSP_ARG_NAMESPACE = "app.cash.paparazzi.preview.namespace"
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package app.cash.paparazzi.gradle

internal fun previewTestSource(packageName: String) =
"""
package $packageName

import app.cash.paparazzi.Paparazzi
import app.cash.paparazzi.preview.DefaultLocaleRule
import app.cash.paparazzi.preview.PaparazziValuesProvider
import app.cash.paparazzi.preview.deviceConfig
import app.cash.paparazzi.preview.locale
import app.cash.paparazzi.preview.snapshot
import app.cash.paparazzi.preview.runtime.PaparazziPreviewData
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(
deviceConfig = preview.deviceConfig(),
renderingMode = SHRINK,
maxPercentDifference = 0.11,
)

@get:Rule
val localeRule = DefaultLocaleRule(preview.locale())

@Test
fun preview() {
paparazzi.snapshot(preview)
}
}
"""
Loading
Loading