From c689b4ab8b0e8c51599958b54d0b23aec4003748 Mon Sep 17 00:00:00 2001 From: Till Hellmund Date: Thu, 26 Oct 2023 17:03:25 -0400 Subject: [PATCH] Add screenshot tests for `CardBrand` composable --- payments-core/build.gradle | 1 + payments-core/detekt-baseline.xml | 3 +- .../com/stripe/android/view/CardBrandView.kt | 2 +- .../android/utils/PaparazziConfigOption.kt | 33 +++++ .../com/stripe/android/utils/PaparazziRule.kt | 135 ++++++++++++++++++ .../android/view/CardBrandScreenshotTest.kt | 68 +++++++++ ...reenshotTest_testKnownBrand[DarkTheme].png | Bin 0 -> 5441 bytes ...eenshotTest_testKnownBrand[LightTheme].png | Bin 0 -> 5430 bytes ...enshotTest_testUnknownBrand[DarkTheme].png | Bin 0 -> 1159 bytes ...nshotTest_testUnknownBrand[LightTheme].png | Bin 0 -> 1387 bytes ...hotTest_testWithCbcEligible[DarkTheme].png | Bin 0 -> 1285 bytes ...otTest_testWithCbcEligible[LightTheme].png | Bin 0 -> 1532 bytes ...dScreenshotTest_testWithCbc[DarkTheme].png | Bin 0 -> 1285 bytes ...ScreenshotTest_testWithCbc[LightTheme].png | Bin 0 -> 1532 bytes 14 files changed, 239 insertions(+), 3 deletions(-) create mode 100644 payments-core/src/test/java/com/stripe/android/utils/PaparazziConfigOption.kt create mode 100644 payments-core/src/test/java/com/stripe/android/utils/PaparazziRule.kt create mode 100644 payments-core/src/test/java/com/stripe/android/view/CardBrandScreenshotTest.kt create mode 100644 payments-core/src/test/snapshots/images/com.stripe.android.view_CardBrandScreenshotTest_testKnownBrand[DarkTheme].png create mode 100644 payments-core/src/test/snapshots/images/com.stripe.android.view_CardBrandScreenshotTest_testKnownBrand[LightTheme].png create mode 100644 payments-core/src/test/snapshots/images/com.stripe.android.view_CardBrandScreenshotTest_testUnknownBrand[DarkTheme].png create mode 100644 payments-core/src/test/snapshots/images/com.stripe.android.view_CardBrandScreenshotTest_testUnknownBrand[LightTheme].png create mode 100644 payments-core/src/test/snapshots/images/com.stripe.android.view_CardBrandScreenshotTest_testWithCbcEligible[DarkTheme].png create mode 100644 payments-core/src/test/snapshots/images/com.stripe.android.view_CardBrandScreenshotTest_testWithCbcEligible[LightTheme].png create mode 100644 payments-core/src/test/snapshots/images/com.stripe.android.view_CardBrandScreenshotTest_testWithCbc[DarkTheme].png create mode 100644 payments-core/src/test/snapshots/images/com.stripe.android.view_CardBrandScreenshotTest_testWithCbc[LightTheme].png diff --git a/payments-core/build.gradle b/payments-core/build.gradle index 3f3237144d8..848ffa30d4d 100644 --- a/payments-core/build.gradle +++ b/payments-core/build.gradle @@ -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") diff --git a/payments-core/detekt-baseline.xml b/payments-core/detekt-baseline.xml index 243f706d005..139d6b9f656 100644 --- a/payments-core/detekt-baseline.xml +++ b/payments-core/detekt-baseline.xml @@ -7,7 +7,6 @@ ComplexCondition:ExpiryDateEditText.kt$ExpiryDateEditText.<no name provided>$expirationDate.month.length == 2 && latestInsertionSize > 0 && !inErrorState || rawNumericInput.length > 2 ConstructorParameterNaming:Source.kt$Source$private val _klarna: Klarna? = null ConstructorParameterNaming:Source.kt$Source$private val _weChat: WeChat? = null - CyclomaticComplexMethod:CardBrandView.kt$@Composable private fun CardBrand( isLoading: Boolean, currentBrand: CardBrand, possibleBrands: List<CardBrand>, shouldShowCvc: Boolean, shouldShowErrorIcon: Boolean, tintColorInt: Int, isCbcEligible: Boolean, modifier: Modifier = Modifier, onBrandSelected: (CardBrand?) -> Unit, ) CyclomaticComplexMethod:NextActionDataParser.kt$NextActionDataParser$override fun parse( json: JSONObject ): StripeIntent.NextActionData? CyclomaticComplexMethod:PaymentMethodJsonParser.kt$PaymentMethodJsonParser$override fun parse(json: JSONObject): PaymentMethod CyclomaticComplexMethod:Source.kt$Source.Companion$@SourceType @JvmStatic fun asSourceType(sourceType: String?): String @@ -37,7 +36,7 @@ LargeClass:StripeApiRepository.kt$StripeApiRepository : StripeRepository LargeClass:StripeApiRepositoryTest.kt$StripeApiRepositoryTest LargeClass:StripeKtxTest.kt$StripeKtxTest - LongMethod:CardBrandView.kt$@Composable private fun CardBrand( isLoading: Boolean, currentBrand: CardBrand, possibleBrands: List<CardBrand>, shouldShowCvc: Boolean, shouldShowErrorIcon: Boolean, tintColorInt: Int, isCbcEligible: Boolean, modifier: Modifier = Modifier, onBrandSelected: (CardBrand?) -> Unit, ) + LongMethod:CardBrandView.kt$@Composable internal fun CardBrand( isLoading: Boolean, currentBrand: CardBrand, possibleBrands: List<CardBrand>, shouldShowCvc: Boolean, shouldShowErrorIcon: Boolean, tintColorInt: Int, isCbcEligible: Boolean, modifier: Modifier = Modifier, onBrandSelected: (CardBrand?) -> Unit, ) LongMethod:CardFormView.kt$CardFormView$private fun setupCardWidget() LongMethod:CardFormViewTest.kt$CardFormViewTest$@Test fun testCardValidCallback() LongMethod:CardInputWidget.kt$CardInputWidget$private fun initView(attrs: AttributeSet?) diff --git a/payments-core/src/main/java/com/stripe/android/view/CardBrandView.kt b/payments-core/src/main/java/com/stripe/android/view/CardBrandView.kt index 1848850bf6b..fdc892066ac 100644 --- a/payments-core/src/main/java/com/stripe/android/view/CardBrandView.kt +++ b/payments-core/src/main/java/com/stripe/android/view/CardBrandView.kt @@ -211,7 +211,7 @@ internal class CardBrandView @JvmOverloads constructor( } @Composable -private fun CardBrand( +internal fun CardBrand( isLoading: Boolean, currentBrand: CardBrand, possibleBrands: List, diff --git a/payments-core/src/test/java/com/stripe/android/utils/PaparazziConfigOption.kt b/payments-core/src/test/java/com/stripe/android/utils/PaparazziConfigOption.kt new file mode 100644 index 00000000000..e006d26c338 --- /dev/null +++ b/payments-core/src/test/java/com/stripe/android/utils/PaparazziConfigOption.kt @@ -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, + ) + } +} diff --git a/payments-core/src/test/java/com/stripe/android/utils/PaparazziRule.kt b/payments-core/src/test/java/com/stripe/android/utils/PaparazziRule.kt new file mode 100644 index 00000000000..5f37a4ce5f9 --- /dev/null +++ b/payments-core/src/test/java/com/stripe/android/utils/PaparazziRule.kt @@ -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, + private val boxModifier: Modifier = Modifier.defaultBoxModifier(), +) : TestRule { + + private val testCases: List = 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>.toTestCases(): List { + return createPermutations(this).map { TestCase(it) } +} + +private fun createPermutations( + options: Array>, +): List> { + return (options.toSet()).fold(listOf(listOf())) { acc, set -> + acc.flatMap { list -> set.map { element -> list + element } } + } +} + +private data class TestCase( + val configOptions: List, +) { + + 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) + } + } +} diff --git a/payments-core/src/test/java/com/stripe/android/view/CardBrandScreenshotTest.kt b/payments-core/src/test/java/com/stripe/android/view/CardBrandScreenshotTest.kt new file mode 100644 index 00000000000..bc913008ad8 --- /dev/null +++ b/payments-core/src/test/java/com/stripe/android/view/CardBrandScreenshotTest.kt @@ -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 = {}, + ) + } + } +} diff --git a/payments-core/src/test/snapshots/images/com.stripe.android.view_CardBrandScreenshotTest_testKnownBrand[DarkTheme].png b/payments-core/src/test/snapshots/images/com.stripe.android.view_CardBrandScreenshotTest_testKnownBrand[DarkTheme].png new file mode 100644 index 0000000000000000000000000000000000000000..f6c805669912e870e9299036e2140321cc3ace4b GIT binary patch literal 5441 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 b{blXVta@z|vN#u5z%Y2a`njxgN@xNACZ*Bw literal 0 HcmV?d00001 diff --git a/payments-core/src/test/snapshots/images/com.stripe.android.view_CardBrandScreenshotTest_testUnknownBrand[LightTheme].png b/payments-core/src/test/snapshots/images/com.stripe.android.view_CardBrandScreenshotTest_testUnknownBrand[LightTheme].png new file mode 100644 index 0000000000000000000000000000000000000000..fdac52065a4ed7c85006538d42e0ba2ec721cd82 GIT binary patch literal 1387 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 literal 0 HcmV?d00001 diff --git a/payments-core/src/test/snapshots/images/com.stripe.android.view_CardBrandScreenshotTest_testWithCbc[DarkTheme].png b/payments-core/src/test/snapshots/images/com.stripe.android.view_CardBrandScreenshotTest_testWithCbc[DarkTheme].png new file mode 100644 index 0000000000000000000000000000000000000000..2825dd994661b7873f60997ff508eeadffdded45 GIT binary patch literal 1285 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 literal 0 HcmV?d00001