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

Setup Paparazzi for composable screenshot tests #3407

Merged
merged 3 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from all 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 .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@
*.properties text
*.xml text
*.yml text

**/snapshots/**/*.png filter=lfs diff=lfs merge=lfs -text
3 changes: 2 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 0
lfs: 'true'
- name: Cache Maven
uses: actions/cache@v4
with:
Expand All @@ -101,7 +102,7 @@ jobs:
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
- name: Run Unit Tests
run: ./gradlew test koverXmlReportDebug koverXmlReportProductionDebug --max-workers 1 --scan
run: ./gradlew test verifyPaparazzi koverXmlReportDebug koverXmlReportProductionDebug --max-workers 1 --scan
- name: Codecov
uses: codecov/codecov-action@v4
with:
Expand Down
15 changes: 15 additions & 0 deletions .github/workflows/git-lfs-validation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: "Validate Git LFS"

on:
push:
branches: [develop, master, feature/*]
pull_request:
branches: [develop, master, feature/*]

jobs:
validate-lfs-pointers:
name: "Validate Git LFS pointers"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: git lfs fsck --pointers
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ GodTools Android

[![codecov](https://codecov.io/gh/CruGlobal/godtools-android/branch/develop/graph/badge.svg)](https://codecov.io/gh/CruGlobal/godtools-android)

# Git LFS

We use [Git LFS](https://git-lfs.com/) for storing Paparazzi snapshots. You will need to setup [Git LFS](https://git-lfs.com/) on your local machine in order to store new paparazzi snapshots or validate existing paparazzi snapshots.

# OneSky

To enable OneSky translation downloads/uploads configure the following [gradle properties](https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties):
Expand Down
2 changes: 2 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ plugins {
alias(libs.plugins.grgit)
alias(libs.plugins.hilt)
alias(libs.plugins.ksp)
alias(libs.plugins.paparazzi)
}

android {
Expand Down Expand Up @@ -232,6 +233,7 @@ dependencies {
testImplementation(libs.androidx.arch.core.testing)
testImplementation(libs.androidx.lifecycle.runtime.testing)
testImplementation(libs.androidx.test.espresso.core)
testImplementation(libs.coil.test)
testImplementation(libs.gtoSupport.testing.dagger)
testImplementation(libs.hilt.testing)
testImplementation(libs.kotlin.coroutines.test)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package org.cru.godtools.ui.languages.downloadable

import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
Expand All @@ -14,20 +17,23 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp

private val DEFAULT_ICON_SIZE = 24.dp

@Composable
internal fun LanguageDownloadProgressIndicator(
isPinned: Boolean,
downloaded: Int,
total: Int,
modifier: Modifier = Modifier,
iconSize: Dp = 24.dp,
) {
val total = total.coerceAtLeast(0)
val downloaded = downloaded.coerceIn(0, total)
val contentModifier = modifier.size(iconSize)
val contentModifier = modifier
.size(DEFAULT_ICON_SIZE)
.aspectRatio(1f)

when {
!isPinned -> Icon(
Expand All @@ -51,15 +57,19 @@ internal fun LanguageDownloadProgressIndicator(
},
)

val iconPadding = iconSize / 12
CircularProgressIndicator(
progress = { progress },
color = MaterialTheme.colorScheme.primary,
strokeWidth = (iconSize / 2) - iconPadding,
modifier = contentModifier
.padding(iconPadding)
.border(iconSize / 12, MaterialTheme.colorScheme.primary, CircleShape)
)
BoxWithConstraints(contentModifier) {
val size = with(LocalDensity.current) { constraints.maxWidth.toDp() }
val iconPadding = size / 12
CircularProgressIndicator(
progress = { progress },
color = MaterialTheme.colorScheme.primary,
strokeWidth = (size / 2) - iconPadding,
modifier = Modifier
.fillMaxSize()
.padding(iconPadding)
.border(size / 12, MaterialTheme.colorScheme.primary, CircleShape)
)
}
}
}
}
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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.cru.godtools.ui.languages.downloadable

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import app.cash.paparazzi.Paparazzi
import kotlin.test.Test
import org.cru.godtools.base.ui.theme.GodToolsTheme
import org.junit.Rule

class LanguageDownloadProgressIndicatorPaparazziTest {
@get:Rule
val paparazzi = Paparazzi()

@Test
fun `LanguageDownloadProgressIndicator()`() {
paparazzi.snapshot {
GodToolsTheme(disableDagger = true) {
Box {
Row(Modifier.align(Alignment.Center)) {
for (it in 0..5) {
LanguageDownloadProgressIndicator(isPinned = true, downloaded = it, total = 5)
}
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package org.cru.godtools.ui.tools

import android.graphics.drawable.Drawable
import androidx.compose.foundation.layout.Box
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import app.cash.paparazzi.Paparazzi
import coil.Coil
import coil.ImageLoader
import coil.annotation.ExperimentalCoilApi
import coil.test.FakeImageLoaderEngine
import io.mockk.mockk
import java.util.Locale
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.cru.godtools.base.ui.theme.GodToolsTheme
import org.cru.godtools.downloadmanager.DownloadProgress
import org.cru.godtools.model.Language
import org.cru.godtools.model.Tool
import org.cru.godtools.model.randomTool
import org.cru.godtools.model.randomTranslation
import org.junit.Rule

class SquareToolCardPaparazziTest {
@get:Rule
val paparazzi = Paparazzi()

private val toolState = ToolCard.State(
tool = randomTool(
name = "Tool Title",
category = Tool.CATEGORY_GOSPEL,
),
banner = mockk(),
secondLanguage = Language(Locale.FRENCH),
secondTranslation = randomTranslation()
)

@BeforeTest
@OptIn(ExperimentalCoilApi::class, ExperimentalCoroutinesApi::class)
fun setup() {
Dispatchers.setMain(UnconfinedTestDispatcher())
val banner = Drawable.createFromStream(javaClass.getResourceAsStream("banner.jpg"), "banner.jpg")!!
Coil.setImageLoader(
ImageLoader.Builder(paparazzi.context)
.components {
add(
FakeImageLoaderEngine.Builder()
.intercept(toolState.banner!!, banner)
.build()
)
}
.build()
)
}

@AfterTest
@OptIn(ExperimentalCoroutinesApi::class)
fun cleanup() {
Coil.reset()
Dispatchers.resetMain()
}

@Test
fun `SquareToolCard() - Default`() {
paparazzi.snapshot {
GodToolsTheme(disableDagger = true) {
Box {
SquareToolCard(
state = toolState,
modifier = Modifier.align(Alignment.Center),
)
}
}
}
}

@Test
fun `SquareToolCard() - Downloading`() {
paparazzi.snapshot {
GodToolsTheme(disableDagger = true) {
Box {
SquareToolCard(
state = toolState.copy(
downloadProgress = DownloadProgress(2, 5)
),
modifier = Modifier.align(Alignment.Center),
)
}
}
}
}

@Test
fun `SquareToolCard() - Show Second Language`() {
paparazzi.snapshot {
GodToolsTheme(disableDagger = true) {
Box {
SquareToolCard(
state = toolState,
showSecondLanguage = true,
modifier = Modifier.align(Alignment.Center),
)
}
}
}
}
}
3 changes: 3 additions & 0 deletions build-logic/src/main/kotlin/AndroidTestConfiguration.kt
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,7 @@ internal fun TestedExtension.configureTestOptions(project: Project) {
}
}
}

// Paparazzi
project.fixGuavaDependencyForPaparazzi()
}
24 changes: 24 additions & 0 deletions build-logic/src/main/kotlin/PaparazziConfiguration.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import org.gradle.api.Project
import org.gradle.api.attributes.java.TargetJvmEnvironment

internal fun Project.fixGuavaDependencyForPaparazzi() {
plugins.withId("app.cash.paparazzi") {
// Defer until afterEvaluate so that testImplementation is created by Android plugin.
afterEvaluate {
dependencies.constraints {
add("testImplementation", "com.google.guava:guava") {
attributes {
attribute(
TargetJvmEnvironment.TARGET_JVM_ENVIRONMENT_ATTRIBUTE,
objects.named(TargetJvmEnvironment::class.java, TargetJvmEnvironment.STANDARD_JVM)
)
}
because(
"LayoutLib and sdk-common depend on Guava's -jre published variant. " +
"See https://github.com/cashapp/paparazzi/issues/906."
)
}
}
}
}
}
5 changes: 4 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ androidx-room = "2.6.1"
androidx-viewpager2 = "1.0.0"
androidx-work = "2.9.0"
circuit = "0.19.1"
coil = "2.6.0"
dagger = "2.51"
eventbus = "3.3.1"
facebook = "16.3.0"
Expand Down Expand Up @@ -88,7 +89,8 @@ circuit-foundation = { module = "com.slack.circuit:circuit-foundation", version.
circuit-test = { module = "com.slack.circuit:circuit-test", version.ref = "circuit" }
circuitx-android = { module = "com.slack.circuit:circuitx-android", version.ref = "circuit" }
circuitx-effects = { module = "com.slack.circuit:circuitx-effects", version.ref = "circuit" }
coil-compose = "io.coil-kt:coil-compose:2.6.0"
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
coil-test = { module = "io.coil-kt:coil-test", version.ref = "coil" }
compose-reorderable = "org.burnoutcrew.composereorderable:reorderable:0.9.6"
dagger = { module = "com.google.dagger:dagger", version.ref = "dagger" }
dagger-compiler = { module = "com.google.dagger:dagger-compiler", version.ref = "dagger" }
Expand Down Expand Up @@ -240,3 +242,4 @@ grgit = { id = "org.ajoberstar.grgit", version = "5.2.2" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "dagger" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlintGradle" }
paparazzi = { id = "app.cash.paparazzi", version = "1.3.3" }
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ import org.greenrobot.eventbus.EventBus
val LocalEventBus = staticCompositionLocalOf { EventBus() }

@Composable
internal fun CompositionLocals(content: @Composable () -> Unit) {
internal fun CompositionLocals(disableDagger: Boolean = false, content: @Composable () -> Unit) {
val context = LocalContext.current
val daggerComponents = when {
LocalInspectionMode.current -> object : ComposeEntryPoint {
override val eventBus = EventBus()
LocalInspectionMode.current || disableDagger -> remember {
object : ComposeEntryPoint {
override val eventBus = EventBus()
}
}
else -> remember { EntryPointAccessors.fromApplication<ComposeEntryPoint>(context) }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ object GodToolsTheme {
}

@Composable
fun GodToolsTheme(content: @Composable () -> Unit) {
fun GodToolsTheme(disableDagger: Boolean = false, content: @Composable () -> Unit) {
val isDarkTheme = isSystemInDarkTheme() && BuildConfig.DEBUG
MaterialTheme(
colorScheme = when {
Expand All @@ -123,7 +123,7 @@ fun GodToolsTheme(content: @Composable () -> Unit) {
},
typography = GodToolsTheme.typography
) {
CompositionLocals {
CompositionLocals(disableDagger) {
CompositionLocalProvider(
LocalLightColorSchemeActive provides !isDarkTheme,
LocalContentColor provides contentColorFor(MaterialTheme.colorScheme.background),
Expand Down