From a14b142ea5176c1aa20790f01b7576d71eb17f56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Alc=C3=A9rreca?= Date: Fri, 24 May 2024 13:10:49 +0000 Subject: [PATCH] Adds foldable tests and CI changes Change-Id: Ie0a37f53046a3219f6ea9ae4deccacb48e531fe9 --- .github/workflows/Build.yaml | 135 ++++++++++++------ app/build.gradle.kts | 4 + .../apps/nowinandroid/ui/EdgeToEdgeTest.kt | 122 +++++++++++++--- .../ui/InstrumentedScreenshotTests.kt | 22 +++ app/src/debug/AndroidManifest.xml | 4 +- gradle.properties | 3 + gradle/libs.versions.toml | 2 +- 7 files changed, 230 insertions(+), 62 deletions(-) create mode 100644 app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/InstrumentedScreenshotTests.kt diff --git a/.github/workflows/Build.yaml b/.github/workflows/Build.yaml index 97274dc7e..6abd444cd 100644 --- a/.github/workflows/Build.yaml +++ b/.github/workflows/Build.yaml @@ -153,7 +153,7 @@ jobs: timeout-minutes: 55 strategy: matrix: - api-level: [27, 31] + api-level: [26, 30] steps: - name: Delete unnecessary tools 🔧 @@ -189,9 +189,7 @@ jobs: - name: Setup Gradle uses: gradle/gradle-build-action@v3 - - name: Build projects and run instrumentation tests including screenshots - id: dropshotsverify - continue-on-error: true + - name: Build projects and run instrumentation tests uses: reactivecircus/android-emulator-runner@v2 with: api-level: ${{ matrix.api-level }} @@ -199,46 +197,7 @@ jobs: disable-animations: true disk-size: 6000M heap-size: 600M - profile: pixel_5 - emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none - # Run tests, if they fail, record screenshots and exit with a failure - script: ./gradlew connectedDemoDebugAndroidTest --daemon || echo "Recording new screenshots" ; ./gradlew connectedDemoDebugAndroidTest -Pdropshots.record --daemon --stacktrace ; echo "Done recording new screenshots, exiting with failure" ; exit 5 - - - 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 -# 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: Checkout new changes (in case another job already uploaded screenshots) - uses: actions/checkout@v4 - with: - clean: false - - - name: Push new device screenshots if available - uses: stefanzweifel/git-auto-commit-action@4b8a201e31cadd9829df349894b28c54e6c19fe6 - if: steps.dropshotsverify.outcome == 'failure' - with: - file_pattern: 'app/src/androidTest/screenshots/*.png' - disable_globbing: true - commit_message: "🤖 Updates instrumented screenshots. API ${{ matrix.api-level }}" + script: ./gradlew connectedDemoDebugAndroidTest --daemon - name: Run local tests (including Roborazzi) for the combined coverage report (only API 30) if: matrix.api-level == 30 @@ -278,3 +237,91 @@ jobs: compression-level: 1 overwrite: false path: '**/build/reports/jacoco/' + + + androidTestScreenshots: + runs-on: ubuntu-latest + timeout-minutes: 55 + strategy: + matrix: + include: + - api-level: 27 + profile: pixel_5 + - api-level: 33 + profile: pixel_fold + - api-level: "VanillaIceCream" + profile: pixel_fold + + steps: + - name: Delete unnecessary tools 🔧 + uses: jlumbroso/free-disk-space@v1.3.1 + with: + android: false # Don't remove Android tools + tool-cache: true # Remove image tool cache - rm -rf "$AGENT_TOOLSDIRECTORY" + dotnet: true # rm -rf /usr/share/dotnet + haskell: true # rm -rf /opt/ghc... + swap-storage: true # rm -f /mnt/swapfile (4GiB) + docker-images: false # Takes 16s, enable if needed in the future + large-packages: false # includes google-cloud-sdk and it's slow + + - name: Enable KVM group perms + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + ls /dev/kvm + + - name: Checkout + uses: actions/checkout@v4 + + - name: Copy CI gradle.properties + run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: 17 + + - name: Setup Gradle + uses: gradle/gradle-build-action@v3 + + - name: Build projects and run instrumentation tests including screenshots + id: dropshotsverify + continue-on-error: true + 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 + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + # Run tests, if they fail, record screenshots and exit with a failure + script: | + ./gradlew connectedDemoDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.annotation=com.google.samples.apps.nowinandroid.ui.InstrumentedScreenshotTests --daemon + || echo "Recording new screenshots" + ; ./gradlew connectedDemoDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.annotation=com.google.samples.apps.nowinandroid.ui.InstrumentedScreenshotTests -Pdropshots.record --daemon --stacktrace + ; echo "Done recording new screenshots, exiting with failure" + ; exit 5 + + - 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: Checkout new changes (in case another job already uploaded screenshots) + uses: actions/checkout@v4 + with: + clean: false + + - name: Push new device screenshots if available + uses: stefanzweifel/git-auto-commit-action@4b8a201e31cadd9829df349894b28c54e6c19fe6 + if: steps.dropshotsverify.outcome == 'failure' + with: + file_pattern: 'app/src/androidTest/screenshots/*.png' + disable_globbing: true + commit_message: "🤖 Updates instrumented screenshots. API ${{ matrix.api-level }}" diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e9919a368..75e1432c5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -67,6 +67,10 @@ android { unitTests { isIncludeAndroidResources = true } + // Espresso Device + emulatorControl { + enable = true + } } namespace = "com.google.samples.apps.nowinandroid" } 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 index 4308796ce..8caa4e9a3 100644 --- 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 @@ -16,8 +16,16 @@ package com.google.samples.apps.nowinandroid.ui +import android.graphics.Bitmap +import android.view.WindowInsets import androidx.test.core.app.takeScreenshot +import androidx.test.espresso.device.DeviceInteraction.Companion.setClosedMode +import androidx.test.espresso.device.DeviceInteraction.Companion.setFlatMode +import androidx.test.espresso.device.EspressoDevice.Companion.onDevice import androidx.test.espresso.device.common.executeShellCommand +import androidx.test.espresso.device.controller.DeviceMode.CLOSED +import androidx.test.espresso.device.controller.DeviceMode.FLAT +import androidx.test.espresso.device.filter.RequiresDeviceMode 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 @@ -31,13 +39,20 @@ import com.google.samples.apps.nowinandroid.core.rules.GrantPostNotificationsPer 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.AfterClass import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder +/** + * These tests must be run on the following devices: + * - A phone on API 27 (pixel_5) + * - A foldable on API 33 (pixel_fold) + * - A foldable on API 35 (pixel_fold) + */ @HiltAndroidTest +@InstrumentedScreenshotTests class EdgeToEdgeTest { /** * Manages the components' state and is used to perform injection on your test @@ -72,7 +87,6 @@ class EdgeToEdgeTest { @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") @@ -82,33 +96,109 @@ class EdgeToEdgeTest { } } - @After - fun resetDemoMode() { - executeShellCommand("am broadcast -a com.android.systemui.demo -e command exit") + companion object { + @JvmStatic + @AfterClass + fun resetDemoMode(): Unit { + executeShellCommand("am broadcast -a com.android.systemui.demo -e command exit") + } } @RequiresDisplay(WidthSizeClassEnum.COMPACT, HeightSizeClassEnum.MEDIUM) @SdkSuppress(minSdkVersion = 27, maxSdkVersion = 27) @Test fun edgeToEdge_Phone_Api27() { - testEdgeToEdge("edgeToEdge_Phone_Api27") + screenshotSystemBar("edgeToEdge_Phone_systemBar_Api27") + screenshotNavigationBar("edgeToEdge_Phone_navBar_Api27") } - @RequiresDisplay(WidthSizeClassEnum.COMPACT, HeightSizeClassEnum.MEDIUM) - @SdkSuppress(minSdkVersion = 31, maxSdkVersion = 31) + @RequiresDeviceMode(mode = FLAT) + @RequiresDeviceMode(mode = CLOSED) + @SdkSuppress(minSdkVersion = 33, maxSdkVersion = 33) @Test - fun edgeToEdge_Phone_Api31() { - testEdgeToEdge("edgeToEdge_Phone_Api31") + fun edgeToEdge_Foldable_api33() { + runFoldableTests(apiName = "api33") } - @RequiresDisplay(WidthSizeClassEnum.EXPANDED, HeightSizeClassEnum.MEDIUM) - @SdkSuppress(minSdkVersion = 30, maxSdkVersion = 30) + @RequiresDeviceMode(mode = FLAT) + @RequiresDeviceMode(mode = CLOSED) + @SdkSuppress(minSdkVersion = 35, codeName = "VanillaIceCream") @Test - fun edgeToEdge_Tablet_Api30() { - testEdgeToEdge("edgeToEdge_Tablet_Api30") + fun edgeToEdge_Foldable_api35() { + runFoldableTests(apiName = "api35") + } + + private fun runFoldableTests(apiName: String) { + onDevice().setClosedMode() + screenshotSystemBar("edgeToEdge_Foldable_closed_system_${apiName}") + forceThreeButtonNavigation() + screenshotNavigationBar("edgeToEdge_Foldable_closed_nav3button_${apiName}") + forceGestureNavigation() + screenshotNavigationBar("edgeToEdge_Foldable_closed_navGesture_${apiName}") + + onDevice().setFlatMode() + enableDemoMode() // Flat mode resets demo mode! + screenshotSystemBar("edgeToEdge_Foldable_flat_system_${apiName}") + forceThreeButtonNavigation() + screenshotNavigationBar("edgeToEdge_Foldable_flat_nav3button_${apiName}") + forceGestureNavigation() + screenshotNavigationBar("edgeToEdge_Foldable_flat_navGesture_${apiName}") + } + + private fun screenshotSystemBar(screenshotFileName: String) { + var topInset: Int? = null + var width: Int? = null + waitForWindowUpdate() + activityScenarioRule.scenario.onActivity { activity -> + topInset = activity.windowManager.maximumWindowMetrics.windowInsets.getInsets( + WindowInsets.Type.systemBars()).top + width = activity.windowManager.maximumWindowMetrics.bounds.width() + } + // Crop the top, adding extra pixels to check continuity + val bitmap = takeScreenshot().let { + Bitmap.createBitmap(it, 0, 0, width!!, (topInset!! * 2)) + } + dropshots.assertSnapshot(bitmap, screenshotFileName) + } + + private fun screenshotNavigationBar(screenshotFileName: String) { + var bottomInset: Int? = null + var width: Int? = null + var height: Int? = null + waitForWindowUpdate() + activityScenarioRule.scenario.onActivity { activity -> + bottomInset = activity.windowManager.maximumWindowMetrics.windowInsets.getInsets( + WindowInsets.Type.navigationBars()).bottom + width = activity.windowManager.maximumWindowMetrics.bounds.width() + height = activity.windowManager.maximumWindowMetrics.bounds.height() + } + // Crop the top, adding extra pixels to check continuity + val bitmap = takeScreenshot().let { + Bitmap.createBitmap(it, 0, height!! - (bottomInset!! * 2), width!!, (bottomInset!! * 2)) + } + dropshots.assertSnapshot(bitmap, screenshotFileName) + } + + private fun forceThreeButtonNavigation() { + UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).apply { + executeShellCommand("cmd overlay enable-exclusive " + + "com.android.internal.systemui.navbar.threebutton") + } + } + + private fun forceGestureNavigation() { + UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).apply { + executeShellCommand("cmd overlay enable-exclusive " + + "com.android.internal.systemui.navbar.gestural") + } } - private fun testEdgeToEdge(screenshotFileName: String) { - dropshots.assertSnapshot(takeScreenshot(), screenshotFileName) + private fun waitForWindowUpdate() { + // TODO: This works but it's unclear if it's making it wait too long. Investigate. + UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + .waitForWindowUpdate( + InstrumentationRegistry.getInstrumentation().targetContext.packageName, + 4000 + ); } } diff --git a/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/InstrumentedScreenshotTests.kt b/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/InstrumentedScreenshotTests.kt new file mode 100644 index 000000000..c279130cb --- /dev/null +++ b/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/InstrumentedScreenshotTests.kt @@ -0,0 +1,22 @@ +/* + * 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 + +/** + * TODO: Move to a test module. + */ +annotation class InstrumentedScreenshotTests diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml index bc214c778..36313049e 100644 --- a/app/src/debug/AndroidManifest.xml +++ b/app/src/debug/AndroidManifest.xml @@ -19,5 +19,7 @@ - + + + diff --git a/gradle.properties b/gradle.properties index 16906e249..8e2896cd5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -42,3 +42,6 @@ android.defaults.buildfeatures.shaders=false # Run Roborazzi screenshot tests with the local tests roborazzi.test.verify=true + +# Espresso Device +android.experimental.androidTest.enableEmulatorControl=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f805b7e76..79890d62a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -34,7 +34,7 @@ androidxWindowManager = "1.3.0-alpha03" androidxWork = "2.9.0" coil = "2.6.0" dependencyGuard = "0.5.0" -dropshots = "0.4.1" +dropshots = "0.4.2" firebaseBom = "32.4.0" firebaseCrashlyticsPlugin = "2.9.9" firebasePerfPlugin = "1.4.2"