Skip to content

Commit

Permalink
Add screenshot tests for CardBrand composable
Browse files Browse the repository at this point in the history
  • Loading branch information
tillh-stripe committed Oct 26, 2023
1 parent 82cfaf0 commit 289fba1
Show file tree
Hide file tree
Showing 13 changed files with 238 additions and 1 deletion.
1 change: 1 addition & 0 deletions payments-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ apply from: configs.androidLibrary
apply plugin: 'com.google.devtools.ksp'
apply plugin: 'checkstyle'
apply plugin: 'org.jetbrains.kotlin.plugin.parcelize'
apply plugin: 'app.cash.paparazzi'

dependencies {
api project(":stripe-core")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ internal class CardBrandView @JvmOverloads constructor(
}

@Composable
private fun CardBrand(
internal fun CardBrand(
isLoading: Boolean,
currentBrand: CardBrand,
possibleBrands: List<CardBrand>,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.stripe.android.utils

import app.cash.paparazzi.DeviceConfig
import com.android.resources.NightMode

interface PaparazziConfigOption {

fun apply(deviceConfig: DeviceConfig): DeviceConfig = deviceConfig

fun initialize() {
// Nothing to do
}
}

enum class SystemAppearance(private val nightMode: NightMode) : PaparazziConfigOption {
LightTheme(NightMode.NOTNIGHT),
DarkTheme(NightMode.NIGHT);

override fun apply(deviceConfig: DeviceConfig): DeviceConfig {
return deviceConfig.copy(nightMode = nightMode)
}
}

enum class FontSize(val scaleFactor: Float) : PaparazziConfigOption {
DefaultFont(scaleFactor = 1f),
LargeFont(scaleFactor = 1.5f);

override fun apply(deviceConfig: DeviceConfig): DeviceConfig {
return deviceConfig.copy(
fontScale = scaleFactor,
)
}
}
135 changes: 135 additions & 0 deletions payments-core/src/test/java/com/stripe/android/utils/PaparazziRule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package com.stripe.android.utils

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import app.cash.paparazzi.DeviceConfig
import app.cash.paparazzi.Paparazzi
import com.android.ide.common.rendering.api.SessionParams
import com.stripe.android.uicore.StripeTheme
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement

class PaparazziRule(
vararg configOptions: Array<out PaparazziConfigOption>,
private val boxModifier: Modifier = Modifier.defaultBoxModifier(),
) : TestRule {

private val testCases: List<TestCase> = configOptions.toTestCases()

private val defaultDeviceConfig = createPaparazziDeviceConfig()
private val paparazzi = createPaparazzi(defaultDeviceConfig)

private var description: Description? = null

override fun apply(base: Statement, description: Description): Statement {
this.description = description
return object : Statement() {
override fun evaluate() {
base.evaluate()
}
}
}

fun snapshot(
content: @Composable () -> Unit,
) {
val description = requireNotNull(description) {
"Description in PaparazziRule can't be null"
}

for (testCase in testCases) {
testCase.initialize()

// We need to update the entire Description to prevent Paparazzi from converting the
// name to lowercase.
val newDescription = Description.createTestDescription(
description.className,
description.methodName + testCase.name,
)

paparazzi.prepare(newDescription)

try {
val deviceConfig = testCase.apply(defaultDeviceConfig)
if (deviceConfig != defaultDeviceConfig) {
paparazzi.unsafeUpdateConfig(deviceConfig)
}

paparazzi.snapshot {
StripeTheme {
Surface(color = MaterialTheme.colors.surface) {
Box(
contentAlignment = Alignment.Center,
modifier = boxModifier,
) {
content()
}
}
}
}
} finally {
paparazzi.close()
}
}
}

private fun createPaparazziDeviceConfig(): DeviceConfig {
return DeviceConfig.PIXEL_6.copy(softButtons = false)
}

private fun createPaparazzi(deviceConfig: DeviceConfig): Paparazzi {
return Paparazzi(
deviceConfig = deviceConfig,
// Needed to shrink the screenshot to the height of the composable
renderingMode = SessionParams.RenderingMode.SHRINK,
showSystemUi = false,
)
}
}

private fun Modifier.defaultBoxModifier(): Modifier {
return padding(PaddingValues(vertical = 16.dp))
.fillMaxWidth()
}

private fun Array<out Array<out PaparazziConfigOption>>.toTestCases(): List<TestCase> {
return createPermutations(this).map { TestCase(it) }
}

private fun createPermutations(
options: Array<out Array<out PaparazziConfigOption>>,
): List<List<PaparazziConfigOption>> {
return (options.toSet()).fold(listOf(listOf())) { acc, set ->
acc.flatMap { list -> set.map { element -> list + element } }
}
}

private data class TestCase(
val configOptions: List<PaparazziConfigOption>,
) {

val name: String
get() {
val optionsSuffix = configOptions.joinToString(separator = ",") { it.toString() }
return "[$optionsSuffix]"
}

fun initialize() {
configOptions.forEach { it.initialize() }
}

fun apply(deviceConfig: DeviceConfig): DeviceConfig {
return configOptions.fold(deviceConfig) { acc, option ->
option.apply(acc)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.stripe.android.view

import android.graphics.Color
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.stripe.android.model.CardBrand
import com.stripe.android.utils.PaparazziRule
import com.stripe.android.utils.SystemAppearance
import org.junit.Rule
import org.junit.Test

class CardBrandScreenshotTest {

@get:Rule
val paparazziRule = PaparazziRule(
configOptions = arrayOf(SystemAppearance.values()),
boxModifier = Modifier.padding(16.dp),
)

@Test
fun testUnknownBrand() {
paparazziRule.snapshot {
CardBrand(
isLoading = false,
currentBrand = CardBrand.Unknown,
possibleBrands = emptyList(),
shouldShowCvc = false,
shouldShowErrorIcon = false,
tintColorInt = Color.GRAY,
isCbcEligible = false,
onBrandSelected = {},
)
}
}

@Test
fun testKnownBrand() {
paparazziRule.snapshot {
CardBrand(
isLoading = false,
currentBrand = CardBrand.CartesBancaires,
possibleBrands = emptyList(),
shouldShowCvc = false,
shouldShowErrorIcon = false,
tintColorInt = Color.GRAY,
isCbcEligible = false,
onBrandSelected = {},
)
}
}

@Test
fun testWithCbcEligible() {
paparazziRule.snapshot {
CardBrand(
isLoading = false,
currentBrand = CardBrand.Unknown,
possibleBrands = listOf(CardBrand.CartesBancaires, CardBrand.Visa),
shouldShowCvc = false,
shouldShowErrorIcon = false,
tintColorInt = Color.GRAY,
isCbcEligible = true,
onBrandSelected = {},
)
}
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 289fba1

Please sign in to comment.