From 9a6d41598d33e872c7766061b1edf0bf2180393b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Alc=C3=A9rreca?= Date: Tue, 21 May 2024 14:59:51 +0000 Subject: [PATCH] Adds Dropshots and edge-to-edge tests Change-Id: I4e0d5d0c9dfbf106acea951266c04139fab93c53 # Conflicts: # build.gradle.kts --- .github/workflows/Build.yaml | 38 +++++- app/build.gradle.kts | 3 + .../apps/nowinandroid/ui/EdgeToEdgeTest.kt | 113 ++++++++++++++++++ .../AndroidApplicationConventionPlugin.kt | 1 + build.gradle.kts | 1 + gradle.properties | 2 +- gradle/libs.versions.toml | 8 +- 7 files changed, 162 insertions(+), 4 deletions(-) create mode 100644 app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/EdgeToEdgeTest.kt diff --git a/.github/workflows/Build.yaml b/.github/workflows/Build.yaml index 6bee0ddfb..878be4d11 100644 --- a/.github/workflows/Build.yaml +++ b/.github/workflows/Build.yaml @@ -189,7 +189,8 @@ jobs: - name: Setup Gradle uses: gradle/gradle-build-action@v3 - - name: Build projects and run instrumentation tests + - name: Build projects and run instrumentation tests including screenshots + id: dropshotsverify uses: reactivecircus/android-emulator-runner@v2 with: api-level: ${{ matrix.api-level }} @@ -197,8 +198,43 @@ jobs: disable-animations: true disk-size: 6000M heap-size: 600M + profile: pixel_5 + ram-size: 4096M + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none script: ./gradlew connectedDemoDebugAndroidTest --daemon + - name: Prevent pushing new screenshots if this is a fork (instrumented) + id: checkfork_screenshots_instrumented + continue-on-error: false + if: steps.dropshotsverify.outcome == 'failure' && github.event.pull_request.head.repo.full_name != github.repository + run: | + echo "::error::Instrumented screenshot tests failed, please create a PR in your fork first." && exit 1 + + + - name: Record new instrumented screenshots + id: screenshotsrecordinstrumented + if: steps.dropshotsverify.outcome == 'failure' && github.event_name == 'pull_request' + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ matrix.api-level }} + arch: x86_64 + disable-animations: true + disk-size: 6000M + heap-size: 600M + profile: pixel_5 + force-avd-creation: false + ram-size: 4096M + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + script: adb shell rm -rf /storage/emulated/0/Download/screenshots && ./gradlew connectedDemoDebugAndroidTest -Pdropshots.record --stacktrace + + - name: Push new screenshots if available + uses: stefanzweifel/git-auto-commit-action@4b8a201e31cadd9829df349894b28c54e6c19fe6 + if: steps.screenshotsrecordinstrumented.outcome == 'success' + with: + file_pattern: '*/*.png' + disable_globbing: true + commit_message: "🤖 Updates instrumented screenshots" + - name: Run local tests (including Roborazzi) for the combined coverage report (only API 30) if: matrix.api-level == 30 # There is no need to verify Roborazzi tests to generate coverage. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 355ec42c0..e9919a368 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -126,9 +126,12 @@ dependencies { androidTestImplementation(projects.core.dataTest) androidTestImplementation(projects.core.datastoreTest) androidTestImplementation(libs.androidx.test.espresso.core) + androidTestImplementation(libs.androidx.test.core) androidTestImplementation(libs.androidx.navigation.testing) androidTestImplementation(libs.androidx.compose.ui.test) androidTestImplementation(libs.hilt.android.testing) + androidTestImplementation(libs.androidx.test.espresso.device) + androidTestImplementation(libs.androidx.test.uiautomator) baselineProfile(projects.benchmarks) } diff --git a/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/EdgeToEdgeTest.kt b/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/EdgeToEdgeTest.kt new file mode 100644 index 000000000..51f5bc1ec --- /dev/null +++ b/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/EdgeToEdgeTest.kt @@ -0,0 +1,113 @@ +/* + * 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.samples.apps.nowinandroid.ui + +import androidx.test.core.app.takeScreenshot +import androidx.test.espresso.device.common.executeShellCommand +import androidx.test.espresso.device.filter.RequiresDisplay +import androidx.test.espresso.device.sizeclass.HeightSizeClass.Companion.HeightSizeClassEnum +import androidx.test.espresso.device.sizeclass.WidthSizeClass.Companion.WidthSizeClassEnum +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.filters.SdkSuppress +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.dropbox.dropshots.Dropshots +import com.google.samples.apps.nowinandroid.MainActivity +import com.google.samples.apps.nowinandroid.core.rules.GrantPostNotificationsPermissionRule +import dagger.hilt.android.testing.BindValue +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder + +@HiltAndroidTest +class EdgeToEdgeTest { + /** + * Manages the components' state and is used to perform injection on your test + */ + @get:Rule(order = 0) + val hiltRule = HiltAndroidRule(this) + + /** + * Create a temporary folder used to create a Data Store file. This guarantees that + * the file is removed in between each test, preventing a crash. + */ + @BindValue + @get:Rule(order = 1) + val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build() + + /** + * Grant [android.Manifest.permission.POST_NOTIFICATIONS] permission. + */ + @get:Rule(order = 2) + val postNotificationsPermission = GrantPostNotificationsPermissionRule() + + @get:Rule(order = 3) + val activityScenarioRule = ActivityScenarioRule(MainActivity::class.java) + + @get:Rule(order = 4) + val dropshots = Dropshots() + + @Before + fun setup() = hiltRule.inject() + + @Before + fun enableDemoMode() { + UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).apply { + executeShellCommand("cmd overlay enable-exclusive com.android.internal.systemui.navbar.threebutton") + executeShellCommand("settings put global sysui_demo_allowed 1") + executeShellCommand("am broadcast -a com.android.systemui.demo -e command enter") + executeShellCommand("am broadcast -a com.android.systemui.demo -e command notifications -e visible false") + executeShellCommand("am broadcast -a com.android.systemui.demo -e command clock -e hhmm 1234") + executeShellCommand("am broadcast -a com.android.systemui.demo -e command network -e wifi hide") + executeShellCommand("am broadcast -a com.android.systemui.demo -e command network -e mobile hide") + } + } + + @After + fun resetDemoMode() { + executeShellCommand("am broadcast -a com.android.systemui.demo -e command exit") + } + + @RequiresDisplay(WidthSizeClassEnum.COMPACT, HeightSizeClassEnum.MEDIUM) + @SdkSuppress(minSdkVersion = 26, maxSdkVersion = 26) + @Test + fun edgeToEdge_Phone_Api26() { + testEdgeToEdge("edgeToEdge_Phone_Api26") + } + + @RequiresDisplay(WidthSizeClassEnum.COMPACT, HeightSizeClassEnum.MEDIUM) + @SdkSuppress(minSdkVersion = 30, maxSdkVersion = 30) + @Test + fun edgeToEdge_Phone_Api30() { + testEdgeToEdge("edgeToEdge_Phone_Api30") + } + + @RequiresDisplay(WidthSizeClassEnum.EXPANDED, HeightSizeClassEnum.MEDIUM) + @SdkSuppress(minSdkVersion = 30, maxSdkVersion = 30) + @Test + fun edgeToEdge_Tablet_Api30() { + testEdgeToEdge("edgeToEdge_Tablet_Api30") + } + + private fun testEdgeToEdge(screenshotFileName: String) { + dropshots.assertSnapshot(takeScreenshot(), screenshotFileName) + } +} diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt index f4d5bb0d0..1b39887f8 100644 --- a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt @@ -34,6 +34,7 @@ class AndroidApplicationConventionPlugin : Plugin { apply("org.jetbrains.kotlin.android") apply("nowinandroid.android.lint") apply("com.dropbox.dependency-guard") + apply("com.dropbox.dropshots") } extensions.configure { diff --git a/build.gradle.kts b/build.gradle.kts index dffc0c0dd..e1b477eb6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -40,6 +40,7 @@ plugins { alias(libs.plugins.kotlin.jvm) apply false alias(libs.plugins.kotlin.serialization) apply false alias(libs.plugins.dependencyGuard) apply false + alias(libs.plugins.dropshots) apply false alias(libs.plugins.firebase.crashlytics) apply false alias(libs.plugins.firebase.perf) apply false alias(libs.plugins.gms) apply false diff --git a/gradle.properties b/gradle.properties index 97f940e2e..16906e249 100644 --- a/gradle.properties +++ b/gradle.properties @@ -37,7 +37,7 @@ kotlin.code.style=official # Disable build features that are enabled by default, # https://developer.android.com/build/releases/gradle-plugin#default-changes -android.defaults.buildfeatures.resvalues=false +#android.defaults.buildfeatures.resvalues=false android.defaults.buildfeatures.shaders=false # Run Roborazzi screenshot tests with the local tests diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e0b938e75..740baa0cc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,22 +17,24 @@ androidxCore = "1.12.0" androidxCoreSplashscreen = "1.0.1" androidxDataStore = "1.0.0" androidxEspresso = "3.5.1" +androidxEspressoDevice = "1.0.0-beta01" androidxHiltNavigationCompose = "1.2.0" androidxLifecycle = "2.7.0" androidxMacroBenchmark = "1.2.4" androidxMetrics = "1.0.0-alpha04" androidxNavigation = "2.8.0-alpha06" androidxProfileinstaller = "1.3.1" -androidxTestCore = "1.5.0" +androidxTestCore = "1.6.0-beta01" androidxTestExt = "1.1.5" androidxTestRules = "1.5.0" androidxTestRunner = "1.5.2" androidxTracing = "1.3.0-alpha02" -androidxUiAutomator = "2.2.0" +androidxUiAutomator = "2.3.0" androidxWindowManager = "1.3.0-alpha03" androidxWork = "2.9.0" coil = "2.6.0" dependencyGuard = "0.5.0" +dropshots = "0.4.1" firebaseBom = "32.4.0" firebaseCrashlyticsPlugin = "2.9.9" firebasePerfPlugin = "1.4.2" @@ -98,6 +100,7 @@ androidx-navigation-testing = { group = "androidx.navigation", name = "navigatio androidx-profileinstaller = { group = "androidx.profileinstaller", name = "profileinstaller", version.ref = "androidxProfileinstaller" } androidx-test-core = { group = "androidx.test", name = "core", version.ref = "androidxTestCore" } androidx-test-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidxEspresso" } +androidx-test-espresso-device = { group = "androidx.test.espresso", name = "espresso-device", version.ref = "androidxEspressoDevice" } androidx-test-ext = { group = "androidx.test.ext", name = "junit-ktx", version.ref = "androidxTestExt" } androidx-test-rules = { group = "androidx.test", name = "rules", version.ref = "androidxTestRules" } androidx-test-runner = { group = "androidx.test", name = "runner", version.ref = "androidxTestRunner" } @@ -160,6 +163,7 @@ android-test = { id = "com.android.test", version.ref = "androidGradlePlugin" } baselineprofile = { id = "androidx.baselineprofile", version.ref = "androidxMacroBenchmark"} compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } dependencyGuard = { id = "com.dropbox.dependency-guard", version.ref = "dependencyGuard" } +dropshots = { id = "com.dropbox.dropshots", version.ref = "dropshots" } firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebaseCrashlyticsPlugin" } firebase-perf = { id = "com.google.firebase.firebase-perf", version.ref = "firebasePerfPlugin" } gms = { id = "com.google.gms.google-services", version.ref = "gmsPlugin" }