Merge 2fb91c52b9
into 30a5af5b1f
@ -0,0 +1,338 @@
|
||||
/*
|
||||
* 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 android.graphics.Bitmap
|
||||
import android.util.Log
|
||||
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.DeviceInteraction.Companion.setScreenOrientation
|
||||
import androidx.test.espresso.device.EspressoDevice.Companion.onDevice
|
||||
import androidx.test.espresso.device.action.ScreenOrientation.LANDSCAPE
|
||||
import androidx.test.espresso.device.action.ScreenOrientation.PORTRAIT
|
||||
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
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule
|
||||
import androidx.test.filters.SdkSuppress
|
||||
import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread
|
||||
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.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
|
||||
*/
|
||||
@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 to prevent the
|
||||
* permissions dialog from showing on top.
|
||||
*/
|
||||
@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() {
|
||||
executeShellCommand(
|
||||
"settings put global development_settings_enabled 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 " +
|
||||
"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",
|
||||
)
|
||||
executeShellCommand(
|
||||
"am broadcast -a com.android.systemui.demo -e command " +
|
||||
"network -e satellite hide",
|
||||
)
|
||||
}
|
||||
|
||||
@After
|
||||
fun disableDemoMode() {
|
||||
exitDemoMode()
|
||||
executeShellCommand(
|
||||
"settings put global sysui_demo_allowed 0",
|
||||
)
|
||||
executeShellCommand(
|
||||
"settings put global development_settings_enabled 0",
|
||||
)
|
||||
}
|
||||
|
||||
private fun exitDemoMode() {
|
||||
executeShellCommand(
|
||||
"am broadcast -a com.android.systemui.demo -e command exit",
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
@AfterClass
|
||||
fun resetDemoMode() {
|
||||
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() {
|
||||
screenshotSystemBar("edgeToEdge_Phone_systemBar_Api27")
|
||||
screenshotNavigationBar("edgeToEdge_Phone_navBar_Api27")
|
||||
}
|
||||
|
||||
@RequiresDeviceMode(mode = FLAT)
|
||||
@RequiresDeviceMode(mode = CLOSED)
|
||||
@SdkSuppress(minSdkVersion = 33, maxSdkVersion = 33)
|
||||
@Test
|
||||
fun edgeToEdge_Foldable_api33() {
|
||||
runFoldableTests(apiName = "api33")
|
||||
}
|
||||
|
||||
@RequiresDeviceMode(mode = FLAT)
|
||||
@RequiresDeviceMode(mode = CLOSED)
|
||||
@SdkSuppress(minSdkVersion = 35, codeName = "VanillaIceCream")
|
||||
@Test
|
||||
fun edgeToEdge_Foldable_api35() {
|
||||
runFoldableTests(apiName = "api35")
|
||||
}
|
||||
|
||||
@RequiresDeviceMode(mode = FLAT)
|
||||
@RequiresDeviceMode(mode = CLOSED)
|
||||
@SdkSuppress(minSdkVersion = 35, codeName = "VanillaIceCream")
|
||||
@Test
|
||||
fun edgeToEdge_Foldable_api35_tallCutout() {
|
||||
onDevice().setClosedMode()
|
||||
forceTallCutout()
|
||||
exitDemoMode()
|
||||
enableDemoMode() // Mode change resets demo mode!
|
||||
screenshotSystemBar("edgeToEdge_Foldable_closed_system_tallCutout_api35")
|
||||
|
||||
onDevice().setFlatMode()
|
||||
exitDemoMode()
|
||||
forceTallCutout()
|
||||
enableDemoMode() // Mode change resets demo mode!
|
||||
screenshotSystemBar("edgeToEdge_Foldable_flat_system_tallCutout_api35")
|
||||
|
||||
onDevice().setClosedMode()
|
||||
resetCutout()
|
||||
}
|
||||
|
||||
// Very flaky:DeviceControllerOperationException: Device could not be set to the
|
||||
// requested screen orientation.
|
||||
|
||||
@RequiresDeviceMode(mode = CLOSED)
|
||||
@SdkSuppress(minSdkVersion = 35, codeName = "VanillaIceCream")
|
||||
@Test
|
||||
fun edgeToEdge_Foldable_api35_landscape() {
|
||||
onDevice().setClosedMode()
|
||||
onDevice().setScreenOrientation(LANDSCAPE)
|
||||
forceThreeButtonNavigation()
|
||||
exitDemoMode()
|
||||
enableDemoMode() // This fixes the satellite icon showing up uninvited.
|
||||
screenshotSideNavigationBar("edgeToEdge_Foldable_landscape_sideNav3button_35")
|
||||
onDevice().setScreenOrientation(PORTRAIT)
|
||||
}
|
||||
|
||||
private fun runFoldableTests(apiName: String) {
|
||||
resetCutout()
|
||||
onDevice().setClosedMode()
|
||||
screenshotSystemBar("edgeToEdge_Foldable_closed_system_$apiName")
|
||||
forceThreeButtonNavigation()
|
||||
screenshotNavigationBar("edgeToEdge_Foldable_closed_nav3button_$apiName")
|
||||
forceGestureNavigation()
|
||||
screenshotNavigationBar("edgeToEdge_Foldable_closed_navGesture_$apiName")
|
||||
|
||||
onDevice().setFlatMode()
|
||||
exitDemoMode()
|
||||
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")
|
||||
onDevice().setClosedMode()
|
||||
}
|
||||
|
||||
private fun screenshotSystemBar(screenshotFileName: String) {
|
||||
waitForWindowUpdate()
|
||||
|
||||
// Crop the top, adding extra pixels to check continuity
|
||||
val bitmap = takeScreenshot().let {
|
||||
val newHeight = 200
|
||||
Bitmap.createBitmap(it, 0, 0, it.width, newHeight)
|
||||
}
|
||||
assertSnapshot(bitmap, screenshotFileName)
|
||||
}
|
||||
|
||||
private fun screenshotNavigationBar(screenshotFileName: String) {
|
||||
waitForWindowUpdate()
|
||||
|
||||
// Crop the bottom, adding extra pixels to check continuity
|
||||
val bitmap = takeScreenshot().let {
|
||||
val newHeight = 200
|
||||
Bitmap.createBitmap(it, 0, it.height - newHeight, it.width, newHeight)
|
||||
}
|
||||
assertSnapshot(bitmap, screenshotFileName)
|
||||
}
|
||||
|
||||
private fun screenshotSideNavigationBar(screenshotFileName: String) {
|
||||
waitForWindowUpdate()
|
||||
|
||||
// Crop the top, adding extra pixels to check continuity
|
||||
val bitmap = takeScreenshot().let {
|
||||
Bitmap.createBitmap(it, it.width - 250, 0, 250, it.height)
|
||||
}
|
||||
assertSnapshot(bitmap, screenshotFileName)
|
||||
}
|
||||
|
||||
private fun forceThreeButtonNavigation() {
|
||||
UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).apply {
|
||||
executeShellCommand(
|
||||
"cmd overlay enable-exclusive " +
|
||||
"com.android.internal.systemui.navbar.threebutton",
|
||||
)
|
||||
}
|
||||
waitForWindowUpdate()
|
||||
}
|
||||
|
||||
private fun forceGestureNavigation() {
|
||||
UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).apply {
|
||||
executeShellCommand(
|
||||
"cmd overlay enable-exclusive " +
|
||||
"com.android.internal.systemui.navbar.gestural",
|
||||
)
|
||||
}
|
||||
waitForWindowUpdate()
|
||||
}
|
||||
|
||||
private fun forceTallCutout() {
|
||||
UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).apply {
|
||||
executeShellCommand(
|
||||
"cmd overlay enable-exclusive " +
|
||||
"--category com.android.internal.display.cutout.emulation.tall ",
|
||||
)
|
||||
}
|
||||
waitForWindowUpdate()
|
||||
}
|
||||
private fun resetCutout() {
|
||||
UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).apply {
|
||||
executeShellCommand(
|
||||
"cmd overlay enable-exclusive " +
|
||||
"--category com.android.internal.display.cutout.emulation.noCutout ",
|
||||
)
|
||||
}
|
||||
waitForWindowUpdate()
|
||||
}
|
||||
|
||||
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,
|
||||
8000,
|
||||
)
|
||||
}
|
||||
|
||||
private fun assertSnapshot(
|
||||
bitmap: Bitmap,
|
||||
name: String,
|
||||
filePath: String? = null,
|
||||
) {
|
||||
// Try to assert 3 times
|
||||
var count = 2
|
||||
while (true) {
|
||||
try {
|
||||
dropshots.assertSnapshot(bitmap, name, filePath)
|
||||
} catch (e: AssertionError) {
|
||||
if (count == 0) throw e
|
||||
count -= 1
|
||||
Log.i("EdgeToEdgeTest", "Test failed, retrying (count=$count)")
|
||||
waitForWindowUpdate()
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private fun executeShellCommand(command: String) {
|
||||
runOnUiThread {
|
||||
UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).executeShellCommand(command)
|
||||
}
|
||||
// ADB commands are not synchronized. This sleep was found empirically.
|
||||
Thread.sleep(20)
|
||||
}
|
||||
}
|
@ -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
|
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 33 KiB |
After Width: | Height: | Size: 51 KiB |
After Width: | Height: | Size: 9.9 KiB |
After Width: | Height: | Size: 20 KiB |
@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
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
|
||||
|
||||
http://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.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||
|
||||
<!-- 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>
|