-
Notifications
You must be signed in to change notification settings - Fork 220
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
5ac7af8
commit d6cf123
Showing
5 changed files
with
279 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
104 changes: 104 additions & 0 deletions
104
paparazzi/src/main/java/app/cash/paparazzi/preview/Snapshot.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
133
paparazzi/src/main/java/app/cash/paparazzi/preview/Utils.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) | ||
} | ||
} |