Skip to content

Commit

Permalink
Adds foldable tests and CI changes
Browse files Browse the repository at this point in the history
Change-Id: Ie0a37f53046a3219f6ea9ae4deccacb48e531fe9
  • Loading branch information
JoseAlcerreca committed May 24, 2024
1 parent 7fd5d47 commit a14b142
Show file tree
Hide file tree
Showing 7 changed files with 230 additions and 62 deletions.
135 changes: 91 additions & 44 deletions .github/workflows/Build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ jobs:
timeout-minutes: 55
strategy:
matrix:
api-level: [27, 31]
api-level: [26, 30]

steps:
- name: Delete unnecessary tools 🔧
Expand Down Expand Up @@ -189,56 +189,15 @@ 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 }}
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 --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
Expand Down Expand Up @@ -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/[email protected]
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 }}"
4 changes: 4 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ android {
unitTests {
isIncludeAndroidResources = true
}
// Espresso Device
emulatorControl {
enable = true
}
}
namespace = "com.google.samples.apps.nowinandroid"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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")
Expand All @@ -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
);
}
}
Original file line number Diff line number Diff line change
@@ -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
4 changes: 3 additions & 1 deletion app/src/debug/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
3 changes: 3 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit a14b142

Please sign in to comment.