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 timeout-minutes: 55
strategy: strategy:
matrix: matrix:
api-level: [27, 31] api-level: [26, 30]
steps: steps:
- name: Delete unnecessary tools 🔧 - name: Delete unnecessary tools 🔧
@ -189,9 +189,7 @@ jobs:
- name: Setup Gradle - name: Setup Gradle
uses: gradle/gradle-build-action@v3 uses: gradle/gradle-build-action@v3
- name: Build projects and run instrumentation tests including screenshots - name: Build projects and run instrumentation tests
id: dropshotsverify
continue-on-error: true
uses: reactivecircus/android-emulator-runner@v2 uses: reactivecircus/android-emulator-runner@v2
with: with:
api-level: ${{ matrix.api-level }} api-level: ${{ matrix.api-level }}
@ -199,46 +197,7 @@ jobs:
disable-animations: true disable-animations: true
disk-size: 6000M disk-size: 6000M
heap-size: 600M heap-size: 600M
profile: pixel_5 script: ./gradlew connectedDemoDebugAndroidTest --daemon
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 }}"
- name: Run local tests (including Roborazzi) for the combined coverage report (only API 30) - name: Run local tests (including Roborazzi) for the combined coverage report (only API 30)
if: matrix.api-level == 30 if: matrix.api-level == 30
@ -278,3 +237,91 @@ jobs:
compression-level: 1 compression-level: 1
overwrite: false overwrite: false
path: '**/build/reports/jacoco/' 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 { unitTests {
isIncludeAndroidResources = true isIncludeAndroidResources = true
} }
// Espresso Device
emulatorControl {
enable = true
}
} }
namespace = "com.google.samples.apps.nowinandroid" namespace = "com.google.samples.apps.nowinandroid"
} }

@ -16,8 +16,16 @@
package com.google.samples.apps.nowinandroid.ui package com.google.samples.apps.nowinandroid.ui
import android.graphics.Bitmap
import android.view.WindowInsets
import androidx.test.core.app.takeScreenshot 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.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.filter.RequiresDisplay
import androidx.test.espresso.device.sizeclass.HeightSizeClass.Companion.HeightSizeClassEnum import androidx.test.espresso.device.sizeclass.HeightSizeClass.Companion.HeightSizeClassEnum
import androidx.test.espresso.device.sizeclass.WidthSizeClass.Companion.WidthSizeClassEnum 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.BindValue
import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.After import org.junit.AfterClass
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TemporaryFolder 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 @HiltAndroidTest
@InstrumentedScreenshotTests
class EdgeToEdgeTest { class EdgeToEdgeTest {
/** /**
* Manages the components' state and is used to perform injection on your test * Manages the components' state and is used to perform injection on your test
@ -72,7 +87,6 @@ class EdgeToEdgeTest {
@Before @Before
fun enableDemoMode() { fun enableDemoMode() {
UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).apply { 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("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 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 notifications -e visible false")
@ -82,33 +96,109 @@ class EdgeToEdgeTest {
} }
} }
@After companion object {
fun resetDemoMode() { @JvmStatic
executeShellCommand("am broadcast -a com.android.systemui.demo -e command exit") @AfterClass
fun resetDemoMode(): Unit {
executeShellCommand("am broadcast -a com.android.systemui.demo -e command exit")
}
} }
@RequiresDisplay(WidthSizeClassEnum.COMPACT, HeightSizeClassEnum.MEDIUM) @RequiresDisplay(WidthSizeClassEnum.COMPACT, HeightSizeClassEnum.MEDIUM)
@SdkSuppress(minSdkVersion = 27, maxSdkVersion = 27) @SdkSuppress(minSdkVersion = 27, maxSdkVersion = 27)
@Test @Test
fun edgeToEdge_Phone_Api27() { fun edgeToEdge_Phone_Api27() {
testEdgeToEdge("edgeToEdge_Phone_Api27") screenshotSystemBar("edgeToEdge_Phone_systemBar_Api27")
screenshotNavigationBar("edgeToEdge_Phone_navBar_Api27")
} }
@RequiresDisplay(WidthSizeClassEnum.COMPACT, HeightSizeClassEnum.MEDIUM) @RequiresDeviceMode(mode = FLAT)
@SdkSuppress(minSdkVersion = 31, maxSdkVersion = 31) @RequiresDeviceMode(mode = CLOSED)
@SdkSuppress(minSdkVersion = 33, maxSdkVersion = 33)
@Test @Test
fun edgeToEdge_Phone_Api31() { fun edgeToEdge_Foldable_api33() {
testEdgeToEdge("edgeToEdge_Phone_Api31") runFoldableTests(apiName = "api33")
} }
@RequiresDisplay(WidthSizeClassEnum.EXPANDED, HeightSizeClassEnum.MEDIUM) @RequiresDeviceMode(mode = FLAT)
@SdkSuppress(minSdkVersion = 30, maxSdkVersion = 30) @RequiresDeviceMode(mode = CLOSED)
@SdkSuppress(minSdkVersion = 35, codeName = "VanillaIceCream")
@Test @Test
fun edgeToEdge_Tablet_Api30() { fun edgeToEdge_Foldable_api35() {
testEdgeToEdge("edgeToEdge_Tablet_Api30") 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) { private fun waitForWindowUpdate() {
dropshots.assertSnapshot(takeScreenshot(), screenshotFileName) // 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 --> <!-- Needed for Dropshots on API 26 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" /> android:maxSdkVersion="32" />
<!-- Needed for Espresso Device -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</manifest> </manifest>

@ -42,3 +42,6 @@ android.defaults.buildfeatures.shaders=false
# Run Roborazzi screenshot tests with the local tests # Run Roborazzi screenshot tests with the local tests
roborazzi.test.verify=true roborazzi.test.verify=true
# Espresso Device
android.experimental.androidTest.enableEmulatorControl=true

@ -34,7 +34,7 @@ androidxWindowManager = "1.3.0-alpha03"
androidxWork = "2.9.0" androidxWork = "2.9.0"
coil = "2.6.0" coil = "2.6.0"
dependencyGuard = "0.5.0" dependencyGuard = "0.5.0"
dropshots = "0.4.1" dropshots = "0.4.2"
firebaseBom = "32.4.0" firebaseBom = "32.4.0"
firebaseCrashlyticsPlugin = "2.9.9" firebaseCrashlyticsPlugin = "2.9.9"
firebasePerfPlugin = "1.4.2" firebasePerfPlugin = "1.4.2"

Loading…
Cancel
Save