Skip to content

Commit d6cf123

Browse files
nak5ivegeoff-powell
authored andcommitted
Adding preview snapshot test apis
1 parent 5ac7af8 commit d6cf123

File tree

5 files changed

+279
-0
lines changed

5 files changed

+279
-0
lines changed

gradle/libs.versions.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ bytebuddy-agent = { module = "net.bytebuddy:byte-buddy-agent", version.ref = "by
2525
bytebuddy-core = { module = "net.bytebuddy:byte-buddy", version.ref = "bytebuddy" }
2626

2727
compose-runtime = { module = "androidx.compose.runtime:runtime", version.ref = "compose" }
28+
composeUi-foundation = { module = "androidx.compose.foundation:foundation" }
2829
composeUi-material = { module = "androidx.compose.material:material", version.ref = "compose" }
2930
composeUi-uiTooling = { module = "androidx.compose.ui:ui-tooling" }
3031

paparazzi/api/paparazzi.api

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,3 +235,41 @@ public final class app/cash/paparazzi/accessibility/AccessibilityRenderExtension
235235
public fun renderView (Landroid/view/View;)Landroid/view/View;
236236
}
237237

238+
public final class app/cash/paparazzi/preview/ComposableSingletons$SnapshotKt {
239+
public static final field INSTANCE Lapp/cash/paparazzi/preview/ComposableSingletons$SnapshotKt;
240+
public static field lambda-1 Lkotlin/jvm/functions/Function3;
241+
public fun <init> ()V
242+
public final fun getLambda-1$paparazzi ()Lkotlin/jvm/functions/Function3;
243+
}
244+
245+
public final class app/cash/paparazzi/preview/ComposableSingletons$UtilsKt {
246+
public static final field INSTANCE Lapp/cash/paparazzi/preview/ComposableSingletons$UtilsKt;
247+
public static field lambda-1 Lkotlin/jvm/functions/Function3;
248+
public static field lambda-2 Lkotlin/jvm/functions/Function3;
249+
public fun <init> ()V
250+
public final fun getLambda-1$paparazzi ()Lkotlin/jvm/functions/Function3;
251+
public final fun getLambda-2$paparazzi ()Lkotlin/jvm/functions/Function3;
252+
}
253+
254+
public final class app/cash/paparazzi/preview/DefaultLocaleRule : org/junit/rules/TestRule {
255+
public static final field $stable I
256+
public fun <init> (Ljava/lang/String;)V
257+
public fun apply (Lorg/junit/runners/model/Statement;Lorg/junit/runner/Description;)Lorg/junit/runners/model/Statement;
258+
public final fun getLocale ()Ljava/lang/String;
259+
}
260+
261+
public class app/cash/paparazzi/preview/PaparazziValuesProvider : com/google/testing/junit/testparameterinjector/TestParameter$TestParameterValuesProvider {
262+
public static final field $stable I
263+
public fun <init> (Ljava/util/List;)V
264+
public fun provideValues ()Ljava/util/List;
265+
}
266+
267+
public final class app/cash/paparazzi/preview/SnapshotKt {
268+
public static final fun deviceConfig (Lapp/cash/paparazzi/annotations/PaparazziPreviewData;Lapp/cash/paparazzi/DeviceConfig;)Lapp/cash/paparazzi/DeviceConfig;
269+
public static synthetic fun deviceConfig$default (Lapp/cash/paparazzi/annotations/PaparazziPreviewData;Lapp/cash/paparazzi/DeviceConfig;ILjava/lang/Object;)Lapp/cash/paparazzi/DeviceConfig;
270+
public static final fun flatten (Ljava/util/List;)Ljava/util/List;
271+
public static final fun locale (Lapp/cash/paparazzi/annotations/PaparazziPreviewData;)Ljava/lang/String;
272+
public static final fun snapshot (Lapp/cash/paparazzi/Paparazzi;Lapp/cash/paparazzi/annotations/PaparazziPreviewData;Ljava/lang/String;ZLkotlin/jvm/functions/Function3;)V
273+
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
274+
}
275+

paparazzi/build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ dependencies {
3232
testImplementationAarAsJar libs.androidx.compose.ui.android
3333
compileOnlyAarAsJar libs.androidx.compose.ui.android
3434
compileOnlyAarAsJar libs.androidx.activity
35+
compileOnlyAarAsJar libs.composeUi.foundation
3536

3637
implementation libs.bytebuddy.agent
3738
implementation libs.bytebuddy.core
@@ -47,10 +48,12 @@ dependencies {
4748
api libs.guava
4849
api libs.kotlinx.coroutines.android
4950
api libs.okio
51+
api libs.testParameterInjector
5052
api platform(libs.kotlin.bom)
5153
implementation libs.moshi.core
5254
implementation libs.moshi.adapters
5355
implementation libs.moshi.kotlinReflect
56+
implementation projects.paparazziAnnotations
5457

5558
def osName = System.getProperty("os.name").toLowerCase(Locale.US)
5659
def osLabel
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Copyright Square, Inc.
2+
package app.cash.paparazzi.preview
3+
4+
import androidx.compose.runtime.Composable
5+
import app.cash.paparazzi.DeviceConfig
6+
import app.cash.paparazzi.Paparazzi
7+
import app.cash.paparazzi.annotations.PaparazziPreviewData
8+
import com.google.testing.junit.testparameterinjector.TestParameter.TestParameterValuesProvider
9+
import org.junit.rules.TestRule
10+
import org.junit.runner.Description
11+
import org.junit.runners.model.Statement
12+
import java.util.Locale
13+
14+
/**
15+
* Take a snapshot of the given [previewData].
16+
*/
17+
public fun Paparazzi.snapshot(
18+
previewData: PaparazziPreviewData,
19+
name: String? = null,
20+
localInspectionMode: Boolean = true,
21+
wrapper: @Composable (@Composable () -> Unit) -> Unit = { it() }
22+
) {
23+
when (previewData) {
24+
is PaparazziPreviewData.Default -> snapshotDefault(previewData, name, localInspectionMode, wrapper)
25+
is PaparazziPreviewData.Provider<*> -> snapshotProvider(previewData, name, localInspectionMode, wrapper)
26+
is PaparazziPreviewData.Empty -> Unit
27+
is PaparazziPreviewData.Error -> throw Exception(previewData.message)
28+
}
29+
}
30+
31+
/**
32+
* Generate a Paparazzi DeviceConfig for the given preview
33+
* using the given [default] DeviceConfig.
34+
*
35+
* default: The IDE renders a preview with a higher resolution than
36+
* the default device set by Paparazzi (which is currently Nexus 5). Defaulting to
37+
* a larger device brings the previews and snapshots closer in parity.
38+
*/
39+
public fun PaparazziPreviewData.deviceConfig(
40+
default: DeviceConfig = DeviceConfig.PIXEL_5
41+
): DeviceConfig = when (this) {
42+
is PaparazziPreviewData.Default -> preview.deviceConfig(default)
43+
is PaparazziPreviewData.Provider<*> -> preview.deviceConfig(default)
44+
else -> default
45+
}
46+
47+
/**
48+
* Returns a locale for the given preview, or null if error or empty.
49+
*/
50+
public fun PaparazziPreviewData.locale(): String? = when (this) {
51+
is PaparazziPreviewData.Default -> preview.locale
52+
is PaparazziPreviewData.Provider<*> -> preview.locale
53+
else -> null
54+
}
55+
56+
/**
57+
* Convert a list of generated [PaparazziPreviewData]
58+
* to a flat list of [PaparazziPreviewData]s.
59+
*/
60+
public fun List<PaparazziPreviewData>.flatten(): List<PaparazziPreviewData> = flatMap {
61+
when (it) {
62+
is PaparazziPreviewData.Provider<*> -> List(it.previewParameter.values.count()) { i ->
63+
it.withPreviewParameterIndex(i)
64+
}
65+
else -> listOf(it)
66+
}
67+
}
68+
69+
/**
70+
* A `@TestParameter` values provider for the given [annotations].
71+
*
72+
* Example usage:
73+
* ```
74+
* private class ValuesProvider : PaparazziValuesProvider(paparazziAnnotations)
75+
* ```
76+
*/
77+
public open class PaparazziValuesProvider(
78+
private val annotations: List<PaparazziPreviewData>
79+
) : TestParameterValuesProvider {
80+
override fun provideValues(): List<PaparazziPreviewData> = annotations.flatten()
81+
}
82+
83+
/**
84+
* Enforce a particular default locale for a test. Resets back to default on completion.
85+
*/
86+
public class DefaultLocaleRule(public val locale: String?) : TestRule {
87+
override fun apply(
88+
base: Statement,
89+
description: Description
90+
): Statement {
91+
return object : Statement() {
92+
override fun evaluate() {
93+
val default = Locale.getDefault()
94+
95+
try {
96+
locale?.let { Locale.setDefault(Locale.forLanguageTag(it)) }
97+
base.evaluate()
98+
} finally {
99+
Locale.setDefault(default)
100+
}
101+
}
102+
}
103+
}
104+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// Copyright Square, Inc.
2+
package app.cash.paparazzi.preview
3+
4+
import android.content.res.Configuration
5+
import android.util.DisplayMetrics
6+
import androidx.compose.foundation.background
7+
import androidx.compose.foundation.layout.Box
8+
import androidx.compose.foundation.layout.BoxScope
9+
import androidx.compose.runtime.Composable
10+
import androidx.compose.runtime.CompositionLocalProvider
11+
import androidx.compose.ui.Modifier
12+
import androidx.compose.ui.graphics.Color
13+
import androidx.compose.ui.platform.LocalInspectionMode
14+
import app.cash.paparazzi.DeviceConfig
15+
import app.cash.paparazzi.Paparazzi
16+
import app.cash.paparazzi.annotations.PaparazziPreviewData
17+
import app.cash.paparazzi.annotations.PreviewData
18+
import com.android.resources.NightMode
19+
import com.android.resources.UiMode
20+
import java.util.Locale
21+
import kotlin.math.roundToInt
22+
23+
internal fun String.deviceConfig() = when (this) {
24+
"id:Nexus 7" -> DeviceConfig.NEXUS_7
25+
"id:Nexus 7 2013" -> DeviceConfig.NEXUS_7_2012
26+
"id:Nexus 5" -> DeviceConfig.NEXUS_5
27+
"id:Nexus 6" -> DeviceConfig.NEXUS_7
28+
"id:Nexus 9" -> DeviceConfig.NEXUS_10
29+
"name:Nexus 10" -> DeviceConfig.NEXUS_10
30+
"id:Nexus 5X" -> DeviceConfig.NEXUS_5
31+
"id:Nexus 6P" -> DeviceConfig.NEXUS_7
32+
"id:pixel_c" -> DeviceConfig.PIXEL_C
33+
"id:pixel" -> DeviceConfig.PIXEL
34+
"id:pixel_xl" -> DeviceConfig.PIXEL_XL
35+
"id:pixel_2" -> DeviceConfig.PIXEL_2
36+
"id:pixel_2_xl" -> DeviceConfig.PIXEL_2_XL
37+
"id:pixel_3" -> DeviceConfig.PIXEL_3
38+
"id:pixel_3_xl" -> DeviceConfig.PIXEL_3_XL
39+
"id:pixel_3a" -> DeviceConfig.PIXEL_3A
40+
"id:pixel_3a_xl" -> DeviceConfig.PIXEL_3A_XL
41+
"id:pixel_4" -> DeviceConfig.PIXEL_4
42+
"id:pixel_4_xl" -> DeviceConfig.PIXEL_4_XL
43+
"id:pixel_5" -> DeviceConfig.PIXEL_5
44+
"id:pixel_6" -> DeviceConfig.PIXEL_6
45+
"id:pixel_6_pro" -> DeviceConfig.PIXEL_6_PRO
46+
"id:wearos_small_round" -> DeviceConfig.WEAR_OS_SMALL_ROUND
47+
"id:wearos_square" -> DeviceConfig.WEAR_OS_SQUARE
48+
else -> null
49+
}
50+
51+
internal fun Int.uiMode() = when (this and Configuration.UI_MODE_TYPE_MASK) {
52+
Configuration.UI_MODE_TYPE_NORMAL -> UiMode.NORMAL
53+
Configuration.UI_MODE_TYPE_CAR -> UiMode.CAR
54+
Configuration.UI_MODE_TYPE_DESK -> UiMode.DESK
55+
Configuration.UI_MODE_TYPE_APPLIANCE -> UiMode.APPLIANCE
56+
Configuration.UI_MODE_TYPE_WATCH -> UiMode.WATCH
57+
Configuration.UI_MODE_TYPE_VR_HEADSET -> UiMode.VR_HEADSET
58+
else -> null
59+
}
60+
61+
internal fun Int.nightMode() = when (this and Configuration.UI_MODE_NIGHT_MASK) {
62+
Configuration.UI_MODE_NIGHT_NO -> NightMode.NOTNIGHT
63+
Configuration.UI_MODE_NIGHT_YES -> NightMode.NIGHT
64+
else -> null
65+
}
66+
67+
internal fun String.localeQualifierString() =
68+
Locale.forLanguageTag(this).run {
69+
"$language-r$country"
70+
}
71+
72+
internal fun PreviewData?.deviceConfig(defaultDeviceConfig: DeviceConfig) =
73+
(this?.device?.deviceConfig() ?: defaultDeviceConfig).let { config ->
74+
config.copy(
75+
screenWidth = this?.widthDp?.toPx(config.density.dpiValue) ?: config.screenWidth,
76+
screenHeight = this?.heightDp?.toPx(config.density.dpiValue) ?: config.screenHeight,
77+
fontScale = this?.fontScale ?: config.fontScale,
78+
uiMode = this?.uiMode?.uiMode() ?: config.uiMode,
79+
nightMode = this?.uiMode?.nightMode() ?: config.nightMode,
80+
locale = this?.locale?.localeQualifierString() ?: config.locale
81+
)
82+
}
83+
84+
private fun Int.toPx(dpi: Int) =
85+
(this * (dpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)).roundToInt()
86+
87+
internal fun Paparazzi.snapshotDefault(
88+
previewData: PaparazziPreviewData.Default,
89+
name: String?,
90+
localInspectionMode: Boolean,
91+
wrapper: @Composable (@Composable () -> Unit) -> Unit = { it() }
92+
) {
93+
snapshot(name) {
94+
PreviewWrapper(previewData.preview.backgroundColor, localInspectionMode) {
95+
wrapper { previewData.composable() }
96+
}
97+
}
98+
}
99+
100+
internal fun <T> Paparazzi.snapshotProvider(
101+
previewData: PaparazziPreviewData.Provider<T>,
102+
name: String?,
103+
localInspectionMode: Boolean,
104+
wrapper: @Composable (@Composable () -> Unit) -> Unit = { it() }
105+
) {
106+
val paramValue = previewData.previewParameter.values
107+
.elementAt(previewData.previewParameter.index)
108+
109+
snapshot(name) {
110+
PreviewWrapper(previewData.preview.backgroundColor, localInspectionMode) {
111+
wrapper { previewData.composable(paramValue) }
112+
}
113+
}
114+
}
115+
116+
@Composable
117+
private fun PreviewWrapper(
118+
backgroundColor: String?,
119+
localInspectionMode: Boolean,
120+
content: @Composable BoxScope.() -> Unit
121+
) {
122+
CompositionLocalProvider(LocalInspectionMode provides localInspectionMode) {
123+
Box(
124+
modifier = Modifier
125+
.then(
126+
backgroundColor?.toLong(16)
127+
?.let { Modifier.background(Color(it)) }
128+
?: Modifier
129+
),
130+
content = content
131+
)
132+
}
133+
}

0 commit comments

Comments
 (0)