diff --git a/.github/workflows/build.yml b/.github/workflows/verify.yml similarity index 78% rename from .github/workflows/build.yml rename to .github/workflows/verify.yml index da3ceb7..0a5a0a5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/verify.yml @@ -1,4 +1,4 @@ -name: Build cahier +name: Build and verify cahier on: workflow_dispatch: @@ -11,7 +11,8 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Checkout codebase + uses: actions/checkout@v4 - name: Set Up JDK uses: actions/setup-java@v4 @@ -31,7 +32,7 @@ jobs: test: runs-on: ubuntu-latest steps: - - name: checkout + - name: Checkout codebase uses: actions/checkout@v4 - name: Enable KVM @@ -50,3 +51,9 @@ jobs: device: desktop_large arch: x86_64 script: ./gradlew :app:connectedAndroidTest + + - name: Run Roborazzi + id: verify-test + run: | + # If there is a difference between the screenshots, the test will fail. + ./gradlew app:verifyRoborazziDebug --stacktrace \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 266099f..5a8be7c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -17,6 +17,7 @@ plugins { alias(libs.plugins.androidApplication) alias(libs.plugins.jetbrainsKotlinAndroid) alias(libs.plugins.ksp) + alias(libs.plugins.roborazzi) id("org.jetbrains.kotlin.plugin.compose") version "2.2.20" id("kotlin-parcelize") kotlin("plugin.serialization") @@ -61,6 +62,14 @@ android { excludes += "/META-INF/{AL2.0,LGPL2.1}" } } + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } + roborazzi { + outputDir.set(file("../screenshots")) + } } dependencies { @@ -97,6 +106,12 @@ dependencies { testImplementation(libs.androidx.arch.core.testing) testImplementation(libs.kotlinx.coroutines.test) testImplementation(libs.turbine) + testImplementation(libs.robolectric) + testImplementation(libs.roborazzi) + testImplementation(libs.roborazzi.compose) + testImplementation(libs.roborazzi.rule) + testImplementation(platform(libs.androidx.compose.bom)) + testImplementation(libs.androidx.ui.test.junit4) // Android Testing - For instrumented tests androidTestImplementation(libs.androidx.junit) diff --git a/app/src/androidTest/java/com/example/cahier/CahierListDetailTest.kt b/app/src/androidTest/java/com/example/cahier/CahierListDetailTest.kt index 507ba65..08ec8b7 100644 --- a/app/src/androidTest/java/com/example/cahier/CahierListDetailTest.kt +++ b/app/src/androidTest/java/com/example/cahier/CahierListDetailTest.kt @@ -1,5 +1,6 @@ package com.example.cahier +import androidx.compose.runtime.Composable import androidx.compose.ui.test.DeviceConfigurationOverride import androidx.compose.ui.test.ForcedSize import androidx.compose.ui.test.junit4.createComposeRule @@ -25,19 +26,9 @@ class CahierListDetailTest { fun homeScreen_showListOnly() { composeTestRule.setContent { DeviceConfigurationOverride( - DeviceConfigurationOverride.ForcedSize( - DpSize( - width = 400.dp, - height = 900.dp - ) - ) + DeviceConfigurationOverride.ForcedSize(compatWidthWindow) ) { - HomePane( - navigateToCanvas = { _ -> }, - navigateToDrawingCanvas = { _ -> }, - navigateUp = {}, - homeScreenViewModel = fakeViewModel - ) + HomeContent() } } @@ -49,23 +40,34 @@ class CahierListDetailTest { fun homeScreen_showListAndDetail() { composeTestRule.setContent { DeviceConfigurationOverride( - DeviceConfigurationOverride.ForcedSize( - DpSize( - width = 1200.dp, - height = 900.dp - ) - ) + DeviceConfigurationOverride.ForcedSize(mediumWidthWindow) ) { - HomePane( - navigateToCanvas = { _ -> }, - navigateToDrawingCanvas = { _ -> }, - navigateUp = {}, - homeScreenViewModel = fakeViewModel - ) + HomeContent() } } composeTestRule.onNodeWithTag("List").assertExists() composeTestRule.onNodeWithTag("Detail").assertExists() } + + + private val mediumWidthWindow = DpSize( + width = 1200.dp, + height = 900.dp + ) + + private val compatWidthWindow = DpSize( + width = 400.dp, + height = 900.dp + ) + + @Composable + private fun HomeContent() { + HomePane( + navigateToCanvas = { _ -> }, + navigateToDrawingCanvas = { _ -> }, + navigateUp = {}, + homeScreenViewModel = fakeViewModel + ) + } } \ No newline at end of file diff --git a/app/src/test/java/com/example/cahier/ScreenshotTest.kt b/app/src/test/java/com/example/cahier/ScreenshotTest.kt new file mode 100644 index 0000000..6e13ca7 --- /dev/null +++ b/app/src/test/java/com/example/cahier/ScreenshotTest.kt @@ -0,0 +1,53 @@ +package com.example.cahier + + +import androidx.compose.runtime.Composable +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onRoot +import com.example.cahier.data.FakeNotesRepository +import com.example.cahier.ui.HomePane +import com.example.cahier.ui.viewmodels.HomeScreenViewModel +import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers +import com.github.takahirom.roborazzi.captureRoboImage +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 + +@GraphicsMode(GraphicsMode.Mode.NATIVE) +@RunWith(RobolectricTestRunner::class) +class ScreenshotTest { + + @get:Rule + val composeTestRule = createComposeRule() + + private val fakeViewModel = HomeScreenViewModel(FakeNotesRepository()) + + @Composable + private fun HomeContent() { + HomePane( + navigateToCanvas = { _ -> }, + navigateToDrawingCanvas = { _ -> }, + navigateUp = {}, + homeScreenViewModel = fakeViewModel + ) + } + + @Config(qualifiers = RobolectricDeviceQualifiers.MediumTablet) + @Test + fun globalNavigation_showNavRail() { + composeTestRule.setContent { HomeContent() } + + composeTestRule.onRoot().captureRoboImage("reference_screenshot_navrail.png") + } + + @Config(qualifiers = RobolectricDeviceQualifiers.Pixel7Pro) + @Test + fun globalNavigation_showBottomNavBar() { + composeTestRule.setContent { HomeContent() } + + composeTestRule.onRoot().captureRoboImage("reference_screenshot_bottomnavbar.png") + } +} \ No newline at end of file diff --git a/app/src/test/resources/robolectric.properties b/app/src/test/resources/robolectric.properties new file mode 100644 index 0000000..4b38ad5 --- /dev/null +++ b/app/src/test/resources/robolectric.properties @@ -0,0 +1 @@ +sdk=34 \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 4616b11..03979a9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,4 +21,6 @@ kotlin.code.style=official # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library android.nonTransitiveRClass=true -android.enableJetifier=false \ No newline at end of file +android.enableJetifier=false +# Sets Roborazzi output folder to the one specified in the +roborazzi.record.filePathStrategy=relativePathFromRoborazziContextOutputDirectory \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 289cdf3..2cde4ba 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,6 @@ agp = "8.13.0" appcompatV7 = "1.7.1" coilCompose = "3.3.0" foundation = "1.9.2" -gson = "2.13.2" hiltAndroid = "2.57.1" hiltNavigationCompose = "1.3.0" ink = "1.0.0-beta01" @@ -36,6 +35,8 @@ coreTesting = "2.2.0" kotlinxCoroutinesTest = "1.10.2" turbine = "1.2.1" material3WindowSizeClass = "1.4.0" +robolectric = "4.11.1" +roborazzi = "1.51.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -54,7 +55,6 @@ appcompat-v7 = { module = "androidx.appcompat:appcompat", version.ref = "appcomp appcompat-v7-resources = { module = "androidx.appcompat:appcompat-resources", version.ref = "appcompatV7" } coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coilCompose" } coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coilCompose" } -gson = { module = "com.google.code.gson:gson", version.ref = "gson" } hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" } hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hiltAndroid" } junit = { group = "junit", name = "junit", version.ref = "junit" } @@ -94,9 +94,14 @@ hilt-android-testing = { module = "com.google.dagger:hilt-android-testing", vers turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" } mockito-android = { module = "org.mockito:mockito-android", version.ref = "mockito" } androidx-compose-material3-window-size-class1 = { group = "androidx.compose.material3", name = "material3-window-size-class", version.ref = "material3WindowSizeClass" } +robolectric = { group = "org.robolectric", name = "robolectric", version.ref = "robolectric" } +roborazzi = { group = "io.github.takahirom.roborazzi", name = "roborazzi", version.ref = "roborazzi" } +roborazzi-compose = { group = "io.github.takahirom.roborazzi", name = "roborazzi-compose", version.ref = "roborazzi" } +roborazzi-rule = { group = "io.github.takahirom.roborazzi", name = "roborazzi-junit-rule", version.ref = "roborazzi" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } androidLibrary = { id = "com.android.library", version.ref = "agp" } jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } -ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } \ No newline at end of file +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } +roborazzi = { id = "io.github.takahirom.roborazzi", version.ref = "roborazzi" } \ No newline at end of file diff --git a/screenshots/reference_screenshot_bottomnavbar.png b/screenshots/reference_screenshot_bottomnavbar.png new file mode 100644 index 0000000..68a61c8 Binary files /dev/null and b/screenshots/reference_screenshot_bottomnavbar.png differ diff --git a/screenshots/reference_screenshot_navrail.png b/screenshots/reference_screenshot_navrail.png new file mode 100644 index 0000000..811a6c3 Binary files /dev/null and b/screenshots/reference_screenshot_navrail.png differ