diff --git a/.gitattributes b/.gitattributes index 2ba7789cc2..0865287a9d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,3 +3,4 @@ **/docs/**/*.png filter=lfs diff=lfs merge=lfs -text *.mp4 filter=lfs diff=lfs merge=lfs -text *.jpg filter=lfs diff=lfs merge=lfs -text +**/test/screenshots/**.png filter=lfs diff=lfs merge=lfs -text diff --git a/build.gradle.kts b/build.gradle.kts index 51fd8f95ab..6d9978b7fc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -47,6 +47,7 @@ plugins { alias(libs.plugins.protobuf) apply false alias(libs.plugins.gradleMavenPublishPlugin) alias(libs.plugins.dependencyAnalysis) + alias(libs.plugins.roborazzi) apply false } apply(plugin = "org.jetbrains.dokka") diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0e5cb4e141..2e5f18719a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -52,6 +52,7 @@ okio = "3.8.0" org-robolectric = "4.11.1" playServicesAuth = "21.0.0" protobuf = "3.25.2" +roborazzi = "1.11.0" room = "2.6.1" runtimeTracing = "1.0.0-beta01" snapshot-android = "1.0.4" @@ -199,6 +200,9 @@ retrofit2-convertermoshi = { module = "com.squareup.retrofit2:converter-moshi", retrofit2-retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "com-squareup-retrofit2" } robolectric = { module = "org.robolectric:robolectric", version.ref = "org-robolectric" } robolectric-shadows = { module = "org.robolectric:shadows-framework", version.ref = "org-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" } room-common = { module = "androidx.room:room-common", version.ref = "room" } room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } @@ -219,3 +223,4 @@ kotlinGradle = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } protobuf = "com.google.protobuf:0.9.4" spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } +roborazzi = { id = "io.github.takahirom.roborazzi", version.ref = "roborazzi" } diff --git a/roboscreenshots/build.gradle.kts b/roboscreenshots/build.gradle.kts index 4d36ef6789..f90f46dda3 100644 --- a/roboscreenshots/build.gradle.kts +++ b/roboscreenshots/build.gradle.kts @@ -102,6 +102,10 @@ dependencies { implementation(libs.wearcompose.material) implementation(libs.wearcompose.foundation) + implementation(libs.roborazzi) + implementation(libs.roborazzi.compose) + implementation(libs.roborazzi.rule) + testImplementation(libs.robolectric) } diff --git a/roboscreenshots/src/main/java/com/google/android/horologist/screenshots/rng/WearDevice.kt b/roboscreenshots/src/main/java/com/google/android/horologist/screenshots/rng/WearDevice.kt new file mode 100644 index 0000000000..0560976638 --- /dev/null +++ b/roboscreenshots/src/main/java/com/google/android/horologist/screenshots/rng/WearDevice.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.horologist.screenshots.rng + +import kotlin.math.roundToInt + +public enum class WearDevice( + public val id: String, + public val modelName: String, + public val screenSizePx: Int, + public val density: Float, + public val fontScale: Float = 1f, +) { + MobvoiTicWatchPro5( + id = "ticwatch_pro_5", + modelName = "Mobvoi TicWatch Pro 5", + screenSizePx = 466, + density = 2.0f, + ), + SamsungGalaxyWatch5( + id = "galaxy_watch_5", + modelName = "Samsung Galaxy Watch 5", + screenSizePx = 396, + density = 2.0f, + ), + SamsungGalaxyWatch6( + id = "galaxy_watch_6", + modelName = "Samsung Galaxy Watch 6 Large", + screenSizePx = 480, + density = 2.125f, + ), + GooglePixelWatch( + id = "pixel_watch", + modelName = "Google Pixel Watch", + screenSizePx = 384, + density = 2.0f, + ), + GenericSmallRound( + id = "small_round", + modelName = "Generic Small Round", + screenSizePx = 384, + density = 2.0f, + ), + GenericLargeRound( + id = "large_round", + modelName = "Generic Large Round", + screenSizePx = 454, + density = 2.0f, + ), + SamsungGalaxyWatch6SmallFont( + id = "galaxy_watch_6_small_font", + modelName = "Samsung Galaxy Watch 6 Large", + screenSizePx = 480, + density = 2.125f, + fontScale = 0.94f, + ), + GooglePixelWatchLargeFont( + id = "pixel_watch_large_font", + modelName = "Google Pixel Watch", + screenSizePx = 384, + density = 2.0f, + fontScale = 1.24f, + ), + ; + + public val dp: Int = (screenSizePx / density).roundToInt() +} diff --git a/roboscreenshots/src/main/java/com/google/android/horologist/screenshots/rng/WearDeviceScreenshotTest.kt b/roboscreenshots/src/main/java/com/google/android/horologist/screenshots/rng/WearDeviceScreenshotTest.kt new file mode 100644 index 0000000000..12b65a7920 --- /dev/null +++ b/roboscreenshots/src/main/java/com/google/android/horologist/screenshots/rng/WearDeviceScreenshotTest.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.horologist.screenshots.rng + +import androidx.compose.runtime.Composable +import org.junit.runner.RunWith +import org.robolectric.ParameterizedRobolectricTestRunner + +@RunWith(ParameterizedRobolectricTestRunner::class) +public abstract class WearDeviceScreenshotTest(override val device: WearDevice) : WearScreenshotTest() { + public override val tolerance: Float = 0.02f + + public fun runTest(content: @Composable () -> Unit) { + runTest(suffix = null, content) + } + + public companion object { + @JvmStatic + @ParameterizedRobolectricTestRunner.Parameters + public fun devices(): List = WearDevice.entries + } +} diff --git a/roboscreenshots/src/main/java/com/google/android/horologist/screenshots/rng/WearScreenshotTest.kt b/roboscreenshots/src/main/java/com/google/android/horologist/screenshots/rng/WearScreenshotTest.kt new file mode 100644 index 0000000000..d9fe22e107 --- /dev/null +++ b/roboscreenshots/src/main/java/com/google/android/horologist/screenshots/rng/WearScreenshotTest.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalRoborazziApi::class) + +package com.google.android.horologist.screenshots.rng + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.test.junit4.ComposeContentTestRule +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onRoot +import androidx.wear.compose.material.MaterialTheme +import com.github.takahirom.roborazzi.ExperimentalRoborazziApi +import com.github.takahirom.roborazzi.RoborazziOptions +import com.github.takahirom.roborazzi.ThresholdValidator +import com.github.takahirom.roborazzi.captureRoboImage +import org.junit.Rule +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config +import org.robolectric.annotation.GraphicsMode + +@Config( + sdk = [33], + qualifiers = "w227dp-h227dp-small-notlong-round-watch-xhdpi-keyshidden-nonav", +) +@GraphicsMode(GraphicsMode.Mode.NATIVE) +public abstract class WearScreenshotTest { + @get:Rule + public val composeRule: ComposeContentTestRule = createComposeRule() + + public abstract val device: WearDevice + + // Allow for individual tolerances to be set on each test, should be between 0.0 and 1.0 + public open val tolerance: Float = 0.0f + + public fun runTest(suffix: String? = null, content: @Composable () -> Unit) { + RuntimeEnvironment.setQualifiers("+w${device.dp}dp-h${device.dp}dp") + RuntimeEnvironment.setFontScale(device.fontScale) + + composeRule.setContent { + Box(modifier = Modifier.fillMaxSize().background(MaterialTheme.colors.background)) { + content() + } + } + + captureScreenshot(suffix.orEmpty()) + } + + public fun captureScreenshot(suffix: String) { + composeRule.onRoot().captureRoboImage( + filePath = "src/test/screenshots/${this.javaClass.simpleName}_${device.id}$suffix.png", + roborazziOptions = RoborazziOptions( + recordOptions = RoborazziOptions.RecordOptions( + applyDeviceCrop = true, + ), + compareOptions = RoborazziOptions.CompareOptions( + resultValidator = ThresholdValidator(tolerance), + ), + ), + ) + } +} diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index ea589bc317..1b10b49407 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -17,6 +17,7 @@ plugins { id("com.android.application") kotlin("android") + alias(libs.plugins.roborazzi) } android { diff --git a/sample/src/test/kotlin/com/google/android/horologist/screensizes/CheckYourPhoneTest.kt b/sample/src/test/kotlin/com/google/android/horologist/screensizes/CheckYourPhoneTest.kt new file mode 100644 index 0000000000..7511030375 --- /dev/null +++ b/sample/src/test/kotlin/com/google/android/horologist/screensizes/CheckYourPhoneTest.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.horologist.screensizes + +import com.google.android.horologist.auth.composables.screens.CheckYourPhoneScreen +import com.google.android.horologist.screenshots.rng.WearDevice +import com.google.android.horologist.screenshots.rng.WearDeviceScreenshotTest +import org.junit.Test + +class CheckYourPhoneTest(device: WearDevice) : WearDeviceScreenshotTest(device = device) { + public override val tolerance: Float = 0.01f + + @Test + fun initial() = runTest { + CheckYourPhoneScreen() + } +} diff --git a/sample/src/test/screenshots/CheckYourPhoneTest_galaxy_watch_5.png b/sample/src/test/screenshots/CheckYourPhoneTest_galaxy_watch_5.png new file mode 100644 index 0000000000..1aa55c59b2 --- /dev/null +++ b/sample/src/test/screenshots/CheckYourPhoneTest_galaxy_watch_5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:323c76a80ddc67dcacb1bb272f9975eccb934ce3a4f1c9bc144722d5d7b488f9 +size 11595 diff --git a/sample/src/test/screenshots/CheckYourPhoneTest_galaxy_watch_6.png b/sample/src/test/screenshots/CheckYourPhoneTest_galaxy_watch_6.png new file mode 100644 index 0000000000..9b7e9f9d7b --- /dev/null +++ b/sample/src/test/screenshots/CheckYourPhoneTest_galaxy_watch_6.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:892fefc0a91567201302ce1c9bc59aff49b727fb20c86ea8133740ab03a06ef0 +size 12650 diff --git a/sample/src/test/screenshots/CheckYourPhoneTest_galaxy_watch_6_small_font.png b/sample/src/test/screenshots/CheckYourPhoneTest_galaxy_watch_6_small_font.png new file mode 100644 index 0000000000..7894e56990 --- /dev/null +++ b/sample/src/test/screenshots/CheckYourPhoneTest_galaxy_watch_6_small_font.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de70c601479790fa165e023e64b8fcb25c2b86913fc01a75d715cdb4269b5ba8 +size 12477 diff --git a/sample/src/test/screenshots/CheckYourPhoneTest_large_round.png b/sample/src/test/screenshots/CheckYourPhoneTest_large_round.png new file mode 100644 index 0000000000..07b16a0626 --- /dev/null +++ b/sample/src/test/screenshots/CheckYourPhoneTest_large_round.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a7946c516ba85afd15544201132de49e19dd68c8d98c5ea35c6a5d22f33ec2e +size 12682 diff --git a/sample/src/test/screenshots/CheckYourPhoneTest_pixel_watch.png b/sample/src/test/screenshots/CheckYourPhoneTest_pixel_watch.png new file mode 100644 index 0000000000..013d2e62cb --- /dev/null +++ b/sample/src/test/screenshots/CheckYourPhoneTest_pixel_watch.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:54d3fd295cc70ea42b9876f72f4fb591d4c4956001db078e3050a45f55c172da +size 11447 diff --git a/sample/src/test/screenshots/CheckYourPhoneTest_pixel_watch_large_font.png b/sample/src/test/screenshots/CheckYourPhoneTest_pixel_watch_large_font.png new file mode 100644 index 0000000000..1ba99ab43d --- /dev/null +++ b/sample/src/test/screenshots/CheckYourPhoneTest_pixel_watch_large_font.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a11f1c3b86626b07c4343e2f4c7894446f1c3d809bac08a9d704cb0e878f89c5 +size 12305 diff --git a/sample/src/test/screenshots/CheckYourPhoneTest_small_round.png b/sample/src/test/screenshots/CheckYourPhoneTest_small_round.png new file mode 100644 index 0000000000..013d2e62cb --- /dev/null +++ b/sample/src/test/screenshots/CheckYourPhoneTest_small_round.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:54d3fd295cc70ea42b9876f72f4fb591d4c4956001db078e3050a45f55c172da +size 11447 diff --git a/sample/src/test/screenshots/CheckYourPhoneTest_ticwatch_pro_5.png b/sample/src/test/screenshots/CheckYourPhoneTest_ticwatch_pro_5.png new file mode 100644 index 0000000000..7f77872607 --- /dev/null +++ b/sample/src/test/screenshots/CheckYourPhoneTest_ticwatch_pro_5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e28fcbfc171b4a44a145aa496680a239cb2b0389db0c00080e2f43cab3c4d53 +size 12850