diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f3333f9a34..47efc33227 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -140,6 +140,7 @@ window-core = { module = "androidx.window:window-core", version.ref = "window" } coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" } coil-svg = { module = "io.coil-kt:coil-svg", version.ref = "coil" } +coil-test = { module = "io.coil-kt:coil-test", version.ref = "coil" } commonmark = { module = "org.commonmark:commonmark", version.ref = "commonmark" } commonmark-strikethrough = { module = "org.commonmark:commonmark-ext-gfm-strikethrough", version.ref = "commonmark" } diff --git a/ui/revenuecatui/build.gradle b/ui/revenuecatui/build.gradle index 570a221fd0..891b039767 100644 --- a/ui/revenuecatui/build.gradle +++ b/ui/revenuecatui/build.gradle @@ -83,6 +83,7 @@ dependencies { debugImplementation libs.androidx.test.compose.manifest testImplementation libs.bundles.test + testImplementation libs.coil.test testImplementation libs.coroutines.test testImplementation libs.kotlinx.serialization.json testImplementation libs.androidx.test.compose diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/properties/BackgroundStyle.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/properties/BackgroundStyle.kt index 55ad69648b..1673a0a60d 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/properties/BackgroundStyle.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/properties/BackgroundStyle.kt @@ -37,9 +37,9 @@ internal fun Background.toBackgroundStyle(): BackgroundStyle = val imageUrls = value.urlsForCurrentTheme BackgroundStyle.Image( painter = rememberAsyncImagePainter( - model = imageUrls.webp, + model = imageUrls.webp.toString(), placeholder = rememberAsyncImagePainter( - model = imageUrls.webpLowRes, + model = imageUrls.webpLowRes.toString(), error = null, fallback = null, contentScale = ContentScale.Crop, @@ -136,4 +136,5 @@ private fun Background_Preview_ColorGradientRadial() { ) } -// We cannot use a network image in Compose previews, so we don't have a preview for Background.Image. +// We cannot use a network image in Compose previews, so we don't have a preview for Background.Image. Instead, we have +// some tests in BackgroundTests. diff --git a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/modifier/BackgroundTests.kt b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/modifier/BackgroundTests.kt new file mode 100644 index 0000000000..6c2ed49a53 --- /dev/null +++ b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/modifier/BackgroundTests.kt @@ -0,0 +1,145 @@ +package com.revenuecat.purchases.ui.revenuecatui.components.modifier + +import android.graphics.drawable.ColorDrawable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.requiredSize +import androidx.compose.material3.Text +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTag +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import coil.Coil +import coil.ImageLoader +import coil.annotation.ExperimentalCoilApi +import coil.test.FakeImageLoaderEngine +import com.revenuecat.purchases.paywalls.components.common.Background +import com.revenuecat.purchases.paywalls.components.properties.ImageUrls +import com.revenuecat.purchases.paywalls.components.properties.ThemeImageUrls +import com.revenuecat.purchases.ui.revenuecatui.assertions.assertPixelColorEquals +import com.revenuecat.purchases.ui.revenuecatui.assertions.assertPixelColorPercentage +import com.revenuecat.purchases.ui.revenuecatui.components.properties.toBackgroundStyle +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.Config +import org.robolectric.annotation.GraphicsMode +import org.robolectric.shadows.ShadowPixelCopy +import java.net.URL + +@GraphicsMode(GraphicsMode.Mode.NATIVE) +@Config(shadows = [ShadowPixelCopy::class], sdk = [26]) +@OptIn(ExperimentalCoilApi::class) +@RunWith(AndroidJUnit4::class) +class BackgroundTests { + + private enum class TestUrl(val urlString: String, val color: Color) { + Blue(urlString = "https://blue", color = Color.Blue), + ; + } + + @get:Rule + val composeTestRule = createComposeRule() + + @Before + fun setup() { + val engine = FakeImageLoaderEngine.Builder() + .interceptAllTestUrls() + .build() + val imageLoader = ImageLoader.Builder(InstrumentationRegistry.getInstrumentation().targetContext) + .components { add(engine) } + .build() + + Coil.setImageLoader(imageLoader) + } + + @After + fun teardown() { + Coil.reset() + } + + @Test + fun `Should properly set an image background`(): Unit = with(composeTestRule) { + // Arrange + val sizePx = 100 + val testUrl = TestUrl.Blue + val expectedColor = testUrl.color + val background = Background.Image(ThemeImageUrls(light = testUrl.toImageUrls(size = sizePx))) + setContent { + val backgroundStyle = background.toBackgroundStyle() + val sizeDp = with(LocalDensity.current) { sizePx.toDp() } + + // Act + Box( + modifier = Modifier + .requiredSize(sizeDp) + .background(backgroundStyle) + .semantics { testTag = "box" } + ) + } + + // Assert + onNodeWithTag("box") + .assertIsDisplayed() + .assertPixelColorEquals(startX = 0, startY = 0, width = sizePx, height = sizePx, color = expectedColor) + } + + @Test + fun `Should draw image background behind content`(): Unit = with(composeTestRule) { + // Arrange + val sizePx = 100 + val testUrl = TestUrl.Blue + val expectedColor = testUrl.color + val background = Background.Image(ThemeImageUrls(light = testUrl.toImageUrls(size = sizePx))) + setContent { + val backgroundStyle = background.toBackgroundStyle() + val sizeDp = with(LocalDensity.current) { sizePx.toDp() } + + // Act + Text( + text = "Hello", + modifier = Modifier + .requiredSize(sizeDp) + .background(backgroundStyle) + .semantics { testTag = "box" } + ) + } + + // Assert + onNodeWithTag("box") + .assertIsDisplayed() + .assertPixelColorPercentage( + startX = 0, + startY = 0, + width = sizePx, + height = sizePx, + color = expectedColor, + // Text rendering might not be fully deterministic (e.g. due to anti aliasing, font settings, etc.) so + // we're just verifying that the majority of the Composable shows the background, but not all of it. + predicate = { percentage -> percentage in 0.6f..0.99f } + ) + } + + private fun FakeImageLoaderEngine.Builder.interceptAllTestUrls(): FakeImageLoaderEngine.Builder = apply { + TestUrl.values().forEach { testUrl -> + intercept(testUrl.urlString, ColorDrawable(testUrl.color.toArgb())) + } + } + + private fun TestUrl.toImageUrls(size: Int): ImageUrls = + ImageUrls( + original = URL(urlString), + webp = URL(urlString), + webpLowRes = URL(urlString), + width = size.toUInt(), + height = size.toUInt(), + ) +}