Skip to content

Commit

Permalink
Adding preview snapshot test apis
Browse files Browse the repository at this point in the history
  • Loading branch information
nak5ive authored and geoff-powell committed May 14, 2024
1 parent 66494fd commit 15c9974
Show file tree
Hide file tree
Showing 5 changed files with 279 additions and 0 deletions.
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ bytebuddy-core = { module = "net.bytebuddy:byte-buddy", version.ref = "bytebuddy

compose-compiler = { module = "androidx.compose.compiler:compiler", version.ref = "composeCompiler" }
compose-runtime = { module = "androidx.compose.runtime:runtime", version.ref = "compose" }
composeUi-foundation = { module = "androidx.compose.foundation:foundation" }
composeUi-material = { module = "androidx.compose.material:material", version.ref = "compose" }
composeUi-uiTooling = { module = "androidx.compose.ui:ui-tooling" }

Expand Down
38 changes: 38 additions & 0 deletions paparazzi/api/paparazzi.api
Original file line number Diff line number Diff line change
Expand Up @@ -235,3 +235,41 @@ 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 <init> ()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 static field lambda-2 Lkotlin/jvm/functions/Function3;
public fun <init> ()V
public final fun getLambda-1$paparazzi ()Lkotlin/jvm/functions/Function3;
public final fun getLambda-2$paparazzi ()Lkotlin/jvm/functions/Function3;
}

public final class app/cash/paparazzi/preview/DefaultLocaleRule : org/junit/rules/TestRule {
public static final field $stable I
public fun <init> (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 <init> (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 flatten (Ljava/util/List;)Ljava/util/List;
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
}

3 changes: 3 additions & 0 deletions paparazzi/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ dependencies {
testImplementationAarAsJar libs.androidx.compose.ui.android
compileOnlyAarAsJar libs.androidx.compose.ui.android
compileOnlyAarAsJar libs.androidx.activity
compileOnlyAarAsJar libs.composeUi.foundation

implementation libs.bytebuddy.agent
implementation libs.bytebuddy.core
Expand All @@ -30,10 +31,12 @@ dependencies {
api libs.guava
api libs.kotlinx.coroutines.core
api libs.okio
api libs.testParameterInjector
api platform(libs.kotlin.bom)
implementation libs.moshi.core
implementation libs.moshi.adapters
implementation libs.moshi.kotlinReflect
implementation projects.paparazziAnnotations

def osName = System.getProperty("os.name").toLowerCase(Locale.US)
if (osName.startsWith("mac")) {
Expand Down
104 changes: 104 additions & 0 deletions paparazzi/src/main/java/app/cash/paparazzi/preview/Snapshot.kt
Original file line number Diff line number Diff line change
@@ -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.
*/
public fun List<PaparazziPreviewData>.flatten(): List<PaparazziPreviewData> = 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<PaparazziPreviewData>
) : TestParameterValuesProvider {
override fun provideValues(): List<PaparazziPreviewData> = 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)
}
}
}
}
}
133 changes: 133 additions & 0 deletions paparazzi/src/main/java/app/cash/paparazzi/preview/Utils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// 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"
}

internal fun PreviewData?.deviceConfig(defaultDeviceConfig: 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) {
PreviewWrapper(previewData.preview.backgroundColor, localInspectionMode) {
wrapper { previewData.composable() }
}
}
}

internal fun <T> Paparazzi.snapshotProvider(
previewData: PaparazziPreviewData.Provider<T>,
name: String?,
localInspectionMode: Boolean,
wrapper: @Composable (@Composable () -> Unit) -> Unit = { it() }
) {
val paramValue = previewData.previewParameter.values
.elementAt(previewData.previewParameter.index)

snapshot(name) {
PreviewWrapper(previewData.preview.backgroundColor, localInspectionMode) {
wrapper { previewData.composable(paramValue) }
}
}
}

@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
)
}
}

0 comments on commit 15c9974

Please sign in to comment.