Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PaywallData validation tests #1310

Merged
merged 9 commits into from
Oct 5, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ui/revenuecatui/build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
plugins {
alias libs.plugins.android.library
alias libs.plugins.kotlin.android
alias libs.plugins.kotlin.serialization
}


Expand Down Expand Up @@ -56,4 +57,5 @@ dependencies {

testImplementation libs.bundles.test
testImplementation libs.coroutines.test
testImplementation libs.kotlinx.serialization.json
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.revenuecat.purchases.ui.revenuecatui.data.testdata

import android.content.Context
import androidx.compose.material3.ColorScheme
import androidx.compose.ui.graphics.Color
import androidx.lifecycle.ViewModel
import com.revenuecat.purchases.Offering
import com.revenuecat.purchases.Package
Expand Down Expand Up @@ -42,8 +43,60 @@ internal object TestData {
)

val assetBaseURL = URL("https://assets.pawwalls.com")

val localization = PaywallData.LocalizedConfiguration(
title = "Call to action for _better_ conversion.",
subtitle = "Lorem ipsum is simply dummy text of the ~printing and~ typesetting industry.",
callToAction = "Subscribe for {{ sub_price_per_month }}/mo",
offerDetails = "{{ total_price_and_per_month }}",
offerDetailsWithIntroOffer = "{{ total_price_and_per_month }} after {{ sub_offer_duration }} trial",
offerName = "{{ sub_period }}",
features = emptyList(),
)

val currentColorScheme = ColorScheme(
primary = Color.White,
onPrimary = Color.White,
primaryContainer = Color.White,
onPrimaryContainer = Color.White,
inversePrimary = Color.Green,
secondary = Color.Black,
onSecondary = Color.Black,
secondaryContainer = Color.Black,
onSecondaryContainer = Color.Black,
tertiary = Color.Cyan,
onTertiary = Color.Black,
tertiaryContainer = Color.Gray,
onTertiaryContainer = Color.White,
background = Color.White,
onBackground = Color.Black,
surface = Color.Gray,
onSurface = Color.Black,
surfaceVariant = Color.DarkGray,
onSurfaceVariant = Color.White,
surfaceTint = Color.LightGray,
inverseSurface = Color.Black,
inverseOnSurface = Color.White,
error = Color.Red,
onError = Color.White,
errorContainer = Color.Red,
onErrorContainer = Color.White,
outline = Color.Transparent,
outlineVariant = Color.LightGray,
scrim = Color.Gray,
)
}

val offeringWithNoPaywall = Offering(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏻

identifier = "Template1",
availablePackages = listOf(
Packages.monthly,
),
metadata = mapOf(),
paywall = null,
serverDescription = "",
)

val template1Offering = Offering(
identifier = "Template1",
availablePackages = listOf(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@ internal val TestData.template2: PaywallData
templateName = PaywallTemplate.TEMPLATE_2.id,
config = PaywallData.Configuration(
packages = listOf(
PackageType.WEEKLY.identifier!!,
PackageType.MONTHLY.identifier!!,
PackageType.ANNUAL.identifier!!,
PackageType.MONTHLY.identifier!!,
),
defaultPackage = PackageType.MONTHLY.identifier!!,
images = TestData.Constants.images,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package com.revenuecat.purchases.ui.revenuecatui

import androidx.test.ext.junit.runners.AndroidJUnit4
import com.revenuecat.purchases.Offering
import com.revenuecat.purchases.paywalls.PaywallColor
import com.revenuecat.purchases.paywalls.PaywallData
import com.revenuecat.purchases.ui.revenuecatui.data.testdata.TestData
import com.revenuecat.purchases.ui.revenuecatui.data.testdata.offerings.offeringWithMultiPackagePaywall
import com.revenuecat.purchases.ui.revenuecatui.errors.PaywallValidationError
import com.revenuecat.purchases.ui.revenuecatui.helpers.validatedPaywall
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import java.io.File
import java.net.URL

@RunWith(AndroidJUnit4::class)
class PaywallDataValidationTest {

@Test
fun `Validate an offering without paywall`() {
val offering = TestData.offeringWithNoPaywall
val paywallValidationResult = getPaywallValidationResult(offering)
assertThat(paywallValidationResult.error).isEqualTo(PaywallValidationError.MissingPaywall)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome

}

@Test
fun `Validate a valid paywall`() {
val offering = TestData.template1Offering
val paywallValidationResult = getPaywallValidationResult(offering)
assertThat(paywallValidationResult.error).isNull()
}

@Test
fun `Unrecognized template name generates default paywall`() {
val templateName = "unrecognized_template"
val originalOffering = TestData.template2Offering
val offering = originalOffering.copy(paywall = originalOffering.paywall!!.copy(templateName = templateName))

val paywallValidationResult = getPaywallValidationResult(offering)

verifyPackages(paywallValidationResult.displayablePaywall, originalOffering.paywall!!)
compareWithDefaultTemplate(paywallValidationResult.displayablePaywall)
assertThat(paywallValidationResult.error).isEqualTo(PaywallValidationError.InvalidTemplate(templateName))
}

@Test
fun `Unrecognized variable generates default paywall`() {
val originalOffering = TestData.template2Offering

val paywall = originalOffering.paywall!!.let { originalPaywall ->
val (locale, originalLocalizedConfiguration) = originalPaywall.localizedConfiguration
val localizedConfiguration = originalLocalizedConfiguration.copy(
title = "Title with {{ unrecognized_variable }}",
callToAction = "{{ future_variable }}",
)
originalPaywall.copy(localization = mapOf(locale.toString() to localizedConfiguration))
}

val offering = originalOffering.copy(paywall = paywall)
val paywallValidationResult = getPaywallValidationResult(offering)
verifyPackages(paywallValidationResult.displayablePaywall, originalOffering.paywall!!)
compareWithDefaultTemplate(paywallValidationResult.displayablePaywall)
assertThat(paywallValidationResult.error).isEqualTo(
PaywallValidationError.InvalidVariables(setOf("unrecognized_variable", "future_variable"))
)
}

@Test
fun `Unrecognized variables in features generate default paywall`() {
val originalOffering = TestData.template2Offering

val paywall = originalOffering.paywall!!.let { originalPaywall ->
val (locale, originalLocalizedConfiguration) = originalPaywall.localizedConfiguration
val localizedConfiguration = originalLocalizedConfiguration.copy(
features = listOf(
PaywallData.LocalizedConfiguration.Feature(
title = "{{ future_variable }}",
content = "{{ new_variable }}"
),
PaywallData.LocalizedConfiguration.Feature(
title = "{{ another_one }}"
)
)
)
originalPaywall.copy(localization = mapOf(locale.toString() to localizedConfiguration))
}

val offering = originalOffering.copy(paywall = paywall)
val paywallValidationResult = getPaywallValidationResult(offering)
verifyPackages(paywallValidationResult.displayablePaywall, originalOffering.paywall!!)
compareWithDefaultTemplate(paywallValidationResult.displayablePaywall)
assertThat(paywallValidationResult.error).isEqualTo(
PaywallValidationError.InvalidVariables(setOf("future_variable", "new_variable", "another_one"))
)
}

@Test
fun `Unrecognized icons generate default paywall`() {
val originalOffering = TestData.template2Offering

val paywall = originalOffering.paywall!!.let { originalPaywall ->
val (locale, originalLocalizedConfiguration) = originalPaywall.localizedConfiguration
val localizedConfiguration = originalLocalizedConfiguration.copy(
features = listOf(
PaywallData.LocalizedConfiguration.Feature(
title = "Feature Title",
iconID = "an_unrecognized_icon",
),
)
)
originalPaywall.copy(localization = mapOf(locale.toString() to localizedConfiguration))
}

val offering = originalOffering.copy(paywall = paywall)
val paywallValidationResult = getPaywallValidationResult(offering)
verifyPackages(paywallValidationResult.displayablePaywall, originalOffering.paywall!!)
compareWithDefaultTemplate(paywallValidationResult.displayablePaywall)
assertThat(paywallValidationResult.error).isEqualTo(
PaywallValidationError.InvalidIcons(setOf("an_unrecognized_icon"))
)
}

private fun getPaywallValidationResult(offering: Offering) = offering.validatedPaywall(
currentColorScheme = TestData.Constants.currentColorScheme,
)

private fun verifyPackages(actual: PaywallData, expectation: PaywallData) {
assertThat(actual.config.packages).isEqualTo(expectation.config.packages)
}

private fun compareWithDefaultTemplate(displayablePaywall: PaywallData) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!! This is a great idea

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes... the only issue I am seeing wiht this approach is that it's very hard to see what fails in tests... It will just say that both paywall datas are different and print them both, without printing the actual difference. I might replace it with asserting all properties to be honest

val json = File(javaClass.classLoader!!.getResource("default_template.json").file).readText()
val defaultPaywall: PaywallData = Json.decodeFromString(json)

assertThat(displayablePaywall.assetBaseURL).isEqualTo(defaultPaywall.assetBaseURL)
assertThat(displayablePaywall.templateName).isEqualTo(defaultPaywall.templateName)
assertThat(displayablePaywall.revision).isEqualTo(defaultPaywall.revision)

(displayablePaywall.config to defaultPaywall.config).let { (config, defaultConfig) ->
assertThat(config.blurredBackgroundImage).isEqualTo(defaultConfig.blurredBackgroundImage)
assertColors(config.colors.light, defaultConfig.colors.light)
assertColors(config.colors.dark!!, defaultConfig.colors.dark!!)
assertThat(config.displayRestorePurchases).isEqualTo(defaultConfig.displayRestorePurchases)
assertThat(config.images.background).isEqualTo(defaultConfig.images.background)
assertThat(config.images.header).isEqualTo(defaultConfig.images.header)
assertThat(config.images.icon).isEqualTo(defaultConfig.images.icon)
assertThat(config.packages).containsExactly(*defaultConfig.packages.toTypedArray())
assertThat(config.defaultPackage).isEqualTo(defaultConfig.defaultPackage)
assertThat(config.termsOfServiceURL).isEqualTo(defaultConfig.termsOfServiceURL)
assertThat(config.privacyURL).isEqualTo(defaultConfig.privacyURL)
}

assertThat(displayablePaywall.localizedConfiguration).isEqualTo(defaultPaywall.localizedConfiguration)
}

private fun assertColors(
actualColors: PaywallData.Configuration.Colors,
defaultColors: PaywallData.Configuration.Colors,
) {
assertThat(actualColors.accent1).isEqualTo(defaultColors.accent1)
assertThat(actualColors.accent2).isEqualTo(defaultColors.accent2)
assertThat(actualColors.background).isEqualTo(defaultColors.background)
assertThat(actualColors.callToActionBackground).isEqualTo(defaultColors.callToActionBackground)
assertThat(actualColors.callToActionForeground).isEqualTo(defaultColors.callToActionForeground)
assertThat(actualColors.text1).isEqualTo(defaultColors.text1)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,17 @@ internal class TemplateConfigurationFactoryTest {
val packageConfiguration = template2Configuration.packages as TemplateConfiguration.PackageConfiguration.Multiple

val expectedConfiguration = TemplateConfiguration.PackageConfiguration.Multiple(
first = getPackageInfo(TestData.Packages.weekly),
first = getPackageInfo(TestData.Packages.annual),
default = getPackageInfo(TestData.Packages.monthly),
all = listOf(
getPackageInfo(TestData.Packages.weekly),
getPackageInfo(TestData.Packages.annual),
getPackageInfo(TestData.Packages.monthly),
getPackageInfo(TestData.Packages.annual)
),
)
assertThat(packageConfiguration).isEqualTo(expectedConfiguration)

assertThat(packageConfiguration.first).isEqualTo(expectedConfiguration.first)
assertThat(packageConfiguration.default).isEqualTo(expectedConfiguration.default)
assertThat(packageConfiguration.all).containsExactly(*expectedConfiguration.all.toTypedArray())
}

@Test
Expand Down
52 changes: 52 additions & 0 deletions ui/revenuecatui/src/test/resources/default_template.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👏🏻

"asset_base_url" : "https:",
"config" : {
"blurred_background_image" : true,
"colors" : {
"light" : {
"accent_1" : "#000000",
"accent_2" : "#FFFFFF",
"background" : "#FFFFFF",
"call_to_action_background" : "#000000",
"call_to_action_foreground" : "#FFFFFF",
"text_1" : "#FFFFFF"
},
"dark" : {
"accent_1" : "#000000",
"accent_2" : "#FFFFFF",
"background" : "#000000",
"call_to_action_background" : "#000000",
"call_to_action_foreground" : "#000000",
"text_1" : "#FFFFFF"
}
},
"display_restore_purchases" : true,
"images" : {
"background" : "default_background",
"header" : null,
"icon" : "revenuecatui_default_paywall_app_icon"
},
"packages" : [
"$rc_annual",
"$rc_monthly"
],
"privacy_url" : null,
"tos_url" : null
},
"localized_strings" : {
"en_US" : {
"call_to_action" : "Continue",
"call_to_action_with_intro_offer" : null,
"features" : [

],
"offer_details" : "{{ total_price_and_per_month }}",
"offer_details_with_intro_offer" : "Start your {{ sub_offer_duration }} trial, then {{ total_price_and_per_month }}.",
"offer_name" : null,
"subtitle" : null,
"title" : "{{ app_name }}"
}
},
"revision" : -1,
"template_name" : "2"
}