Skip to content

Commit

Permalink
Merge pull request #222 from thomaskioko/screenshot-testing
Browse files Browse the repository at this point in the history
Android Screenshot Testing
  • Loading branch information
thomaskioko committed May 27, 2024
2 parents 183c33a + 90d5f50 commit ad527dd
Show file tree
Hide file tree
Showing 120 changed files with 584 additions and 137 deletions.
53 changes: 52 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,57 @@ jobs:
name: common-test-report
path: ./**/build/reports/tests/

android_screenshot_test:
runs-on: macos-latest

permissions:
contents: write
pull-requests: write

steps:
- name: checkout
uses: actions/checkout@v4

- name: Set up JDK
uses: actions/setup-java@v4
with:
distribution: ${{ env.DISTRIBUTION }}
java-version: ${{ env.JDK_VERSION }}

- name: Roborazzi screenshot tests
id: screenshotsverify
continue-on-error: true
run: ./gradlew verifyRoborazziDemoDebug -Proborazzi.test.record=true

- name: Prevent pushing new screenshots if this is a fork
id: checkfork_screenshots
continue-on-error: false
if: steps.screenshotsverify.outcome == 'failure' && github.event.pull_request.head.repo.full_name != github.repository
run: |
echo "::error::Screenshot tests failed, please create a PR in your fork first." && exit 1
# Runs if previous job failed
- name: Generate new screenshots if verification failed and it's a PR
id: screenshotsrecord
if: steps.screenshotsverify.outcome == 'failure' && github.event_name == 'pull_request'
run: |
./gradlew recordRoborazziDemoDebug -Proborazzi.test.record=true
- name: Push new screenshots if available
uses: stefanzweifel/git-auto-commit-action@v5
if: steps.screenshotsrecord.outcome == 'success'
with:
file_pattern: '*/*.png'
disable_globbing: true
commit_message: "🤖 Beep Beep: Update screenshots 🤖"

- name: Upload screenshot results
if: always()
uses: actions/upload-artifact@v4
with:
name: screenshot-test-results
path: '**/build/outputs/roborazzi/*_compare.png'

iOS:
runs-on: macos-latest
steps:
Expand All @@ -153,7 +204,7 @@ jobs:
path: ./**/build/reports/tests/

create-release:
needs: [android, android_lint, common_test, iOS, spotless, jvm_test, ]
needs: [android, android_lint, android_screenshot_test, common_test, iOS, spotless, jvm_test, ]
if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') }}
runs-on: ubuntu-latest
steps:
Expand Down
22 changes: 22 additions & 0 deletions .idea/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion android/app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import com.thomaskioko.tvmaniac.extensions.TvManiacBuildType

plugins {
id("tvmaniac.application")
alias(libs.plugins.tvmaniac.application)
alias(libs.plugins.ksp)
}

Expand Down
2 changes: 1 addition & 1 deletion android/designsystem/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
plugins { id("plugin.tvmaniac.compose.library") }
plugins { alias(libs.plugins.tvmaniac.compose.library) }

android { namespace = "com.thomaskioko.tvmaniac.compose" }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ fun TvManiacBackground(
}
}

@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Light theme")
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark theme")
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Light Theme", showBackground = true)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark Theme", showBackground = true)
annotation class ThemePreviews

@ThemePreviews
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,6 @@ private fun TvManiacAlphaTextButtonPreview() {
contentColor = MaterialTheme.colorScheme.onSecondary,
containerColor = MaterialTheme.colorScheme.secondary.copy(alpha = 0.08f),
),
modifier =
Modifier.background(color = MaterialTheme.colorScheme.secondary.copy(alpha = 0.08f)),
) {
Text(
text = "Horror",
Expand Down
2 changes: 1 addition & 1 deletion android/resources/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
<string name="dialog_message_unwatched">Are you sure you want to mark the entire season as unwatched?</string>
<string name="dialog_message_watched">Are you sure you want to mark the entire season as watched?</string>

<string name="following">Follow show</string>
<string name="following">Follow</string>
<string name="unfollow">Unfollow</string>
<string name="btn_trailer">Watch Trailer</string>
<string name="title_recommended">Recommendations</string>
Expand Down
1 change: 1 addition & 0 deletions android/screenshot-tests/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
14 changes: 14 additions & 0 deletions android/screenshot-tests/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
plugins { alias(libs.plugins.tvmaniac.compose.library) }

android { namespace = "com.thomaskioko.tvmaniac.screenshottesting" }

dependencies {
implementation(projects.android.designsystem)

api(libs.roborazzi)
implementation(libs.androidx.compose.activity)
implementation(libs.androidx.compose.ui.test)
implementation(libs.robolectric)

debugImplementation(libs.androidx.compose.ui.test.manifest)
}
2 changes: 2 additions & 0 deletions android/screenshot-tests/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package com.thomaskioko.tvmaniac.screenshottests

import androidx.activity.ComponentActivity
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.onRoot
import androidx.test.ext.junit.rules.ActivityScenarioRule
import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers
import com.github.takahirom.roborazzi.RoborazziOptions
import com.github.takahirom.roborazzi.RoborazziOptions.CompareOptions
import com.github.takahirom.roborazzi.RoborazziOptions.RecordOptions
import com.github.takahirom.roborazzi.captureRoboImage
import com.thomaskioko.tvmaniac.compose.theme.TvManiacTheme
import org.robolectric.RuntimeEnvironment

val DefaultRoborazziOptions =
RoborazziOptions(
// Pixel-perfect matching
compareOptions = CompareOptions(changeThreshold = 0f),
// Reduce the size of the PNGs
recordOptions = RecordOptions(resizeScale = 0.5),
)

enum class DefaultTestDevices(val spec: String) {
Pixel7(RobolectricDeviceQualifiers.Pixel7),
}

fun <A : ComponentActivity> AndroidComposeTestRule<ActivityScenarioRule<A>, A>.captureMultiDevice(
name: String,
content: @Composable () -> Unit,
) {
DefaultTestDevices.entries.forEach {
this.captureMultiTheme(
deviceSpec = it.spec,
name = name,
content = content,
)
}
}

/** Takes two screenshots combining light/dark themes. */
fun <A : ComponentActivity> AndroidComposeTestRule<ActivityScenarioRule<A>, A>.captureMultiTheme(
name: String,
deviceSpec: String,
overrideFileName: String? = null,
shouldCompareDarkMode: Boolean = true,
content: @Composable () -> Unit,
) {

// Set qualifiers from specs
RuntimeEnvironment.setQualifiers(deviceSpec)

val darkModeValues = if (shouldCompareDarkMode) listOf(true, false) else listOf(false)

var darkMode by mutableStateOf(true)

this.setContent {
CompositionLocalProvider(
LocalInspectionMode provides true,
) {
TvManiacTheme(
darkTheme = darkMode,
) {
content()
}
}
}

darkModeValues.forEach { isDarkMode ->
darkMode = isDarkMode
val darkModeDesc = if (isDarkMode) "dark" else "light"

val filename = overrideFileName ?: name

this.onRoot()
.captureRoboImage(
"src/test/screenshots/" + filename + "_$darkModeDesc" + ".png",
roborazziOptions = DefaultRoborazziOptions,
)
}
}
10 changes: 9 additions & 1 deletion android/ui/discover/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
plugins { id("plugin.tvmaniac.compose.library") }
plugins {
alias(libs.plugins.tvmaniac.compose.library)
alias(libs.plugins.roborazzi)
}

android { namespace = "com.thomaskioko.tvmaniac.ui.discover" }

Expand All @@ -14,4 +17,9 @@ dependencies {
implementation(libs.androidx.compose.runtime)
implementation(libs.decompose.extensions.compose)
implementation(libs.snapper)

testImplementation(projects.android.screenshotTests)
testImplementation(libs.androidx.compose.ui.test)
testImplementation(libs.robolectric)
testDemoImplementation(libs.roborazzi)
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
Expand Down Expand Up @@ -474,16 +473,14 @@ private fun DiscoverScreenPreview(
) {
TvManiacTheme {
TvManiacBackground {
Surface(Modifier.fillMaxWidth()) {
val pagerState = rememberPagerState(pageCount = { 5 })
val snackBarHostState = remember { SnackbarHostState() }
DiscoverScreen(
state = state,
pagerState = pagerState,
snackBarHostState = snackBarHostState,
onAction = {},
)
}
val pagerState = rememberPagerState(pageCount = { 5 })
val snackBarHostState = remember { SnackbarHostState() }
DiscoverScreen(
state = state,
pagerState = pagerState,
snackBarHostState = snackBarHostState,
onAction = {},
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.thomaskioko.tvmaniac.ui.discover

import androidx.activity.ComponentActivity
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.remember
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import com.thomaskioko.tvmaniac.compose.components.TvManiacBackground
import com.thomaskioko.tvmaniac.presentation.discover.EmptyState
import com.thomaskioko.tvmaniac.presentation.discover.ErrorState
import com.thomaskioko.tvmaniac.presentation.discover.Loading
import com.thomaskioko.tvmaniac.screenshottests.captureMultiDevice
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.robolectric.annotation.GraphicsMode
import org.robolectric.annotation.LooperMode

@RunWith(RobolectricTestRunner::class)
@Config(sdk = [33])
@GraphicsMode(GraphicsMode.Mode.NATIVE)
@LooperMode(LooperMode.Mode.PAUSED)
class DiscoverScreenshotTest {

@get:Rule val composeTestRule = createAndroidComposeRule<ComponentActivity>()

@Test
fun discoverScreenEmptyState() {
composeTestRule.captureMultiDevice("DiscoverScreenEmptyState") {
TvManiacBackground {
DiscoverScreen(
state = EmptyState,
pagerState = rememberPagerState(pageCount = { 5 }),
snackBarHostState = remember { SnackbarHostState() },
onAction = {},
)
}
}
}

@Test
fun discoverScreenLoading() {
composeTestRule.captureMultiDevice("DiscoverScreenLoading") {
TvManiacBackground {
DiscoverScreen(
state = Loading,
pagerState = rememberPagerState(pageCount = { 5 }),
snackBarHostState = remember { SnackbarHostState() },
onAction = {},
)
}
}
}

@Test
fun discoverScreenErrorState() {
composeTestRule.captureMultiDevice("DiscoverScreenErrorState") {
TvManiacBackground {
DiscoverScreen(
state = ErrorState(errorMessage = "Opps! Something went wrong"),
pagerState = rememberPagerState(pageCount = { 5 }),
snackBarHostState = remember { SnackbarHostState() },
onAction = {},
)
}
}
}

@Test
fun discoverScreenDataLoaded() {
composeTestRule.captureMultiDevice("DiscoverScreenDataLoaded") {
TvManiacBackground {
DiscoverScreen(
state = discoverContentSuccess,
pagerState = rememberPagerState(pageCount = { 5 }),
snackBarHostState = remember { SnackbarHostState() },
onAction = {},
)
}
}
}
}
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.
10 changes: 9 additions & 1 deletion android/ui/library/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
plugins { id("plugin.tvmaniac.compose.library") }
plugins {
alias(libs.plugins.tvmaniac.compose.library)
alias(libs.plugins.roborazzi)
}

android { namespace = "com.thomaskioko.tvmaniac.ui.library" }

Expand All @@ -13,4 +16,9 @@ dependencies {
implementation(libs.androidx.compose.runtime)
implementation(libs.decompose.extensions.compose)
implementation(libs.kotlinx.collections)

testImplementation(projects.android.screenshotTests)
testImplementation(libs.androidx.compose.ui.test)
testImplementation(libs.robolectric)
testDemoImplementation(libs.roborazzi)
}
Loading

0 comments on commit ad527dd

Please sign in to comment.