Adds foldable tests and CI changes

Change-Id: Ie0a37f53046a3219f6ea9ae4deccacb48e531fe9
ben/dropshots
Jose Alcérreca 1 month ago
parent 7fd5d47056
commit a14b142ea5

@ -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 }}"

@ -67,6 +67,10 @@ android {
unitTests {
isIncludeAndroidResources = true
}
// Espresso Device
emulatorControl {
enable = true
}
}
namespace = "com.google.samples.apps.nowinandroid"
}

@ -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
);
}
}

@ -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

@ -19,5 +19,7 @@
<!-- Needed for Dropshots on API 26 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<!-- Needed for Espresso Device -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</manifest>

@ -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

@ -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"

Loading…
Cancel
Save