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

Android Screenshot Testing #222

Merged
merged 9 commits into from
May 27, 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
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
Loading