[#200][#285] Use Espresso screenshot API

- Also reduced liklihood of timeouts on Firebase test lab for robo tests
 - Fix emulatorwtf run configuration
 - Fix screenshots on older API levels
 - Fix minumum range for emulator.wtf
This commit is contained in:
Carter Jernigan 2022-06-02 11:09:02 -04:00 committed by GitHub
parent adc774a20d
commit 1880b2a43f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 38 additions and 145 deletions

View File

@ -387,7 +387,7 @@ jobs:
with: with:
name: Release binaries name: Release binaries
- name: Robo test - name: Robo test
timeout-minutes: 15 timeout-minutes: 20
env: env:
# Path depends on `release_build` job, plus path of `Download a single artifact` step # Path depends on `release_build` job, plus path of `Download a single artifact` step
BINARIES_ZIP_PATH: binaries.zip BINARIES_ZIP_PATH: binaries.zip

View File

@ -1,17 +1,15 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="testDebugWithEmulatorWtf" type="GradleRunConfiguration" factoryName="Gradle"> <configuration default="false" name="testDebugWithEmulatorWtf :app:testZcashmainnetDebugWithEmulatorWtf" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings> <ExternalSystemSettings>
<option name="executionName" /> <option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" /> <option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" /> <option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" /> <option name="scriptParameters" value="testDebugWithEmulatorWtf :app:testZcashmainnetDebugWithEmulatorWtf" />
<option name="taskDescriptions"> <option name="taskDescriptions">
<list /> <list />
</option> </option>
<option name="taskNames"> <option name="taskNames">
<list> <list />
<option value="testDebugWithEmulatorWtf" />
</list>
</option> </option>
<option name="vmOptions" /> <option name="vmOptions" />
</ExternalSystemSettings> </ExternalSystemSettings>

View File

@ -101,6 +101,7 @@ android {
listOf( listOf(
"**/*.kotlin_metadata", "**/*.kotlin_metadata",
".readme", ".readme",
"build-data.properties",
"META-INF/*.kotlin_module", "META-INF/*.kotlin_module",
"META-INF/android.arch**", "META-INF/android.arch**",
"META-INF/androidx**", "META-INF/androidx**",
@ -110,7 +111,6 @@ android {
"META-INF/services/org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor", "META-INF/services/org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor",
"META-INF/services/org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar", "META-INF/services/org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar",
"META-INF/services/org.jetbrains.kotlin.diagnostics.rendering.DefaultErrorMessages\$Extension", "META-INF/services/org.jetbrains.kotlin.diagnostics.rendering.DefaultErrorMessages\$Extension",
"build-data.properties",
"firebase-**.properties", "firebase-**.properties",
"kotlin/**", "kotlin/**",
"play-services-**.properties", "play-services-**.properties",
@ -206,27 +206,6 @@ if (googlePlayServiceKeyFilePath.isNotEmpty()) {
} }
} }
val reportsDirectory = "${buildDir}/reports/androidTests/connected"
// This is coordinated with `EccScreenCaptureProcessor`
val onDeviceScreenshotsDirectory = "/sdcard/Pictures/zcash_screenshots"
val clearScreenshotsTask = tasks.create<Exec>("clearScreenshots") {
executable = project.android.adbExecutable.absolutePath
args = listOf("shell", "rm", "-r", onDeviceScreenshotsDirectory)
}
val fetchScreenshotsTask = tasks.create<Exec>("fetchScreenshots") {
executable = project.android.adbExecutable.absolutePath
args = listOf("pull", onDeviceScreenshotsDirectory, reportsDirectory)
finalizedBy(clearScreenshotsTask)
}
tasks.whenTaskAdded {
if (name == "connectedZcashmainnetDebugAndroidTest") {
finalizedBy(fetchScreenshotsTask)
}
}
fladle { fladle {
// Firebase Test Lab has min and max values that might differ from our project's // Firebase Test Lab has min and max values that might differ from our project's
@ -265,7 +244,7 @@ fladle {
} }
) )
testTimeout.set("5m") testTimeout.set("3m")
devices.addAll( devices.addAll(
mapOf("model" to "Pixel2", "version" to minSdkVersion), mapOf("model" to "Pixel2", "version" to minSdkVersion),
@ -278,13 +257,19 @@ fladle {
} }
emulatorwtf { emulatorwtf {
// This path needs to be coordinated with the implementation in the app module's tests directoriesToPull.set(listOf("/sdcard/googletest/test_outputfiles"))
directoriesToPull.set(listOf("/sdcard/Pictures/zcash_screenshots"))
val appMinSdkVersion = run {
@Suppress("MagicNumber", "PropertyName", "VariableNaming")
val EMULATOR_WTF_MIN_SDK = 23
val buildMinSdk = project.properties["ANDROID_APP_MIN_SDK_VERSION"].toString().toInt()
buildMinSdk.coerceAtLeast(EMULATOR_WTF_MIN_SDK).toString()
}
devices.set( devices.set(
listOf( listOf(
// TODO [#285]: Our screenshot tests don't work on older devices mapOf("model" to "Pixel2", "version" to appMinSdkVersion),
// mapOf("model" to "Pixel2", "version" to minSdkVersion),
// TODO [#430]: App won't run on API 31 Intel emulators // TODO [#430]: App won't run on API 31 Intel emulators
@Suppress("MagicNumber") @Suppress("MagicNumber")
mapOf("model" to "Pixel2", "version" to 30) mapOf("model" to "Pixel2", "version" to 30)

View File

@ -1,10 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest <manifest package="co.electriccoin.zcash.app.test">
xmlns:android="http://schemas.android.com/apk/res/android"
package="co.electriccoin.zcash.test">
<!-- Required to write screenshots -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
</manifest> </manifest>

View File

@ -1,8 +1,5 @@
package co.electriccoin.zcash.app package co.electriccoin.zcash.app
import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import androidx.compose.ui.test.assertCountEquals import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.hasContentDescription import androidx.compose.ui.test.hasContentDescription
import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.hasTestTag
@ -18,16 +15,15 @@ import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo import androidx.compose.ui.test.performScrollTo
import androidx.compose.ui.test.performTextInput import androidx.compose.ui.test.performTextInput
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import androidx.test.core.graphics.writeToTestStorage
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.screenshot.captureToBitmap
import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import androidx.test.runner.screenshot.Screenshot
import cash.z.ecc.sdk.ext.ui.model.MonetarySeparators import cash.z.ecc.sdk.ext.ui.model.MonetarySeparators
import cash.z.ecc.sdk.fixture.SeedPhraseFixture import cash.z.ecc.sdk.fixture.SeedPhraseFixture
import cash.z.ecc.sdk.fixture.WalletAddressFixture import cash.z.ecc.sdk.fixture.WalletAddressFixture
import co.electriccoin.zcash.app.test.EccScreenCaptureProcessor
import co.electriccoin.zcash.app.test.getStringResource import co.electriccoin.zcash.app.test.getStringResource
import co.electriccoin.zcash.spackle.FirebaseTestLabUtil import co.electriccoin.zcash.spackle.FirebaseTestLabUtil
import co.electriccoin.zcash.test.UiTestPrerequisites import co.electriccoin.zcash.test.UiTestPrerequisites
@ -39,44 +35,21 @@ import co.electriccoin.zcash.ui.screen.restore.RestoreTag
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.junit.BeforeClass
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.RuleChain
// TODO [#285]: Screenshot tests fail on older devices due to issue granting external storage permission
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
class ScreenshotTest : UiTestPrerequisites() { class ScreenshotTest : UiTestPrerequisites() {
companion object { companion object {
@BeforeClass
@JvmStatic
fun setupPPlus() {
if (Build.VERSION_CODES.P <= Build.VERSION.SDK_INT) {
val instrumentation = InstrumentationRegistry.getInstrumentation()
if (PackageManager.PERMISSION_DENIED == instrumentation.context.checkCallingOrSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
instrumentation.uiAutomation.grantRuntimePermission(instrumentation.context.packageName, Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
}
}
fun takeScreenshot(screenshotName: String) { fun takeScreenshot(screenshotName: String) {
val screenshot = Screenshot.capture().apply { onView(isRoot())
name = screenshotName .captureToBitmap()
} .writeToTestStorage(screenshotName)
screenshot.process(setOf(EccScreenCaptureProcessor.new()))
} }
} }
private val composeTestRule = createAndroidComposeRule(MainActivity::class.java)
@get:Rule @get:Rule
val ruleChain = if (Build.VERSION_CODES.P <= Build.VERSION.SDK_INT) { val composeTestRule = createAndroidComposeRule(MainActivity::class.java)
composeTestRule
} else {
val runtimePermissionRule = GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE)
RuleChain.outerRule(runtimePermissionRule).around(composeTestRule)
}
private fun navigateTo(route: String) = runBlocking { private fun navigateTo(route: String) = runBlocking {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {

View File

@ -1,55 +0,0 @@
package co.electriccoin.zcash.app.test
import android.os.Build
import android.os.Environment
import androidx.test.runner.screenshot.ScreenCapture
import androidx.test.runner.screenshot.ScreenCaptureProcessor
import java.io.BufferedOutputStream
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.util.UUID
class EccScreenCaptureProcessor private constructor(private val screenshotDir: File) : ScreenCaptureProcessor {
@Throws(IOException::class)
override fun process(capture: ScreenCapture): String {
screenshotDir.checkDirectoryIsWriteable()
val filename = newFilename(
name = capture.name ?: "",
suffix = capture.format.toString().lowercase()
)
BufferedOutputStream(FileOutputStream(File(screenshotDir, filename))).use {
capture.bitmap.compress(capture.format, DEFAULT_QUALITY, it)
it.flush()
}
return filename
}
companion object {
const val DEFAULT_QUALITY = 100
fun new(): EccScreenCaptureProcessor {
// Screenshots need to be stored in a public directory so they won't get cleared by Test Orchestrator
// This path must be coordinated with the build.gradle.kts script which copies these off the device
@Suppress("DEPRECATION")
val screenshotsDirectory = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "zcash_screenshots").also {
it.mkdirs()
}
return EccScreenCaptureProcessor(screenshotsDirectory)
}
private fun newFilename(name: String, suffix: String) = "screenshot-$name-${Build.VERSION.SDK_INT}-${Build.DEVICE}-${UUID.randomUUID()}.$suffix"
}
}
private fun File.checkDirectoryIsWriteable() {
if (!isDirectory && !canWrite()) {
throw IOException("The directory $this does not exist or is not writable.")
}
}

View File

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="co.electriccoin.zcash"> package="co.electriccoin.zcash.app">
<application <application
android:name=".app.ZcashApplication" android:name="co.electriccoin.zcash.app.ZcashApplication"
android:allowBackup="false" android:allowBackup="false"
android:label="@string/app_name"> android:label="@string/app_name">
@ -11,7 +11,7 @@
Using an alias ensures we can refactor the actual Activity without breaking Using an alias ensures we can refactor the actual Activity without breaking
clients. --> clients. -->
<activity-alias <activity-alias
android:name=".LauncherActivity" android:name="co.electricoin.zcash.LauncherActivity"
android:exported="true" android:exported="true"
android:label="@string/app_name" android:label="@string/app_name"
android:targetActivity="co.electriccoin.zcash.ui.MainActivity"> android:targetActivity="co.electriccoin.zcash.ui.MainActivity">

View File

@ -1,7 +1,6 @@
package co.electriccoin.zcash.app package co.electriccoin.zcash.app
import android.app.Application import android.app.Application
import co.electriccoin.zcash.BuildConfig
import co.electriccoin.zcash.crash.android.CrashReporter import co.electriccoin.zcash.crash.android.CrashReporter
import co.electriccoin.zcash.spackle.StrictModeCompat import co.electriccoin.zcash.spackle.StrictModeCompat
import co.electriccoin.zcash.spackle.Twig import co.electriccoin.zcash.spackle.Twig

View File

@ -2,7 +2,7 @@
// These are determined by `ew-cli --models` // These are determined by `ew-cli --models`
@Suppress("MagicNumber", "PropertyName", "VariableNaming") @Suppress("MagicNumber", "PropertyName", "VariableNaming")
val EMULATOR_WTF_MIN_SDK = 24 val EMULATOR_WTF_MIN_SDK = 23
@Suppress("MagicNumber", "PropertyName", "VariableNaming") @Suppress("MagicNumber", "PropertyName", "VariableNaming")
val EMULATOR_WTF_MAX_SDK = 31 val EMULATOR_WTF_MAX_SDK = 31

View File

@ -97,12 +97,11 @@ fladle {
filesToDownload.set(listOf( filesToDownload.set(listOf(
"*/matrix_*/*test_results_merged\\.xml", "*/matrix_*/*test_results_merged\\.xml",
"*/matrix_*/*/artifacts/sdcard/Pictures/zcash_screenshots/*\\.png" "*/matrix_*/*/artifacts/sdcard/googletest/test_outputfiles/*\\.png"
)) ))
directoriesToPull.set(listOf( directoriesToPull.set(listOf(
// This path needs to be coordinated with the implementation in the app module's tests "/sdcard/googletest/test_outputfiles"
"/sdcard/Pictures/zcash_screenshots"
)) ))
} }

View File

@ -64,7 +64,7 @@ IS_SDK_INCLUDED_BUILD=false
# A lower version on the libraries helps to ensure some degree of backwards compatiblity, for the project # A lower version on the libraries helps to ensure some degree of backwards compatiblity, for the project
# as a whole. But a higher version on the app ensures that we aren't directly supporting users # as a whole. But a higher version on the app ensures that we aren't directly supporting users
# with old devices. # with old devices.
ANDROID_LIB_MIN_SDK_VERSION=23 ANDROID_LIB_MIN_SDK_VERSION=24
ANDROID_APP_MIN_SDK_VERSION=27 ANDROID_APP_MIN_SDK_VERSION=27
ANDROID_TARGET_SDK_VERSION=31 ANDROID_TARGET_SDK_VERSION=31
ANDROID_COMPILE_SDK_VERSION=31 ANDROID_COMPILE_SDK_VERSION=31
@ -91,14 +91,14 @@ ANDROIDX_COMPOSE_MATERIAL3_VERSION=1.0.0-alpha09
ANDROIDX_COMPOSE_VERSION=1.1.1 ANDROIDX_COMPOSE_VERSION=1.1.1
ANDROIDX_CONSTRAINTLAYOUT_VERSION=1.0.0 ANDROIDX_CONSTRAINTLAYOUT_VERSION=1.0.0
ANDROIDX_CORE_VERSION=1.7.0 ANDROIDX_CORE_VERSION=1.7.0
ANDROIDX_ESPRESSO_VERSION=3.4.0 ANDROIDX_ESPRESSO_VERSION=3.5.0-alpha07
ANDROIDX_LIFECYCLE_VERSION=2.4.1 ANDROIDX_LIFECYCLE_VERSION=2.4.1
ANDROIDX_NAVIGATION_COMPOSE_VERSION=2.4.2 ANDROIDX_NAVIGATION_COMPOSE_VERSION=2.4.2
ANDROIDX_SECURITY_CRYPTO_VERSION=1.1.0-alpha03 ANDROIDX_SECURITY_CRYPTO_VERSION=1.1.0-alpha03
ANDROIDX_SPLASH_SCREEN_VERSION=1.0.0-rc01 ANDROIDX_SPLASH_SCREEN_VERSION=1.0.0-rc01
ANDROIDX_TEST_JUNIT_VERSION=1.1.3 ANDROIDX_TEST_JUNIT_VERSION=1.1.4-alpha07
ANDROIDX_TEST_ORCHESTRATOR_VERSION=1.4.1 ANDROIDX_TEST_ORCHESTRATOR_VERSION=1.4.2-alpha03
ANDROIDX_TEST_CORE_VERSION=1.4.1-alpha06 ANDROIDX_TEST_CORE_VERSION=1.4.1-alpha07
ANDROIDX_TEST_RUNNER_VERSION=1.5.0-alpha03 ANDROIDX_TEST_RUNNER_VERSION=1.5.0-alpha03
ANDROIDX_UI_AUTOMATOR_VERSION=2.2.0-alpha1 ANDROIDX_UI_AUTOMATOR_VERSION=2.2.0-alpha1
ANDROIDX_WORK_MANAGER_VERSION=2.7.1 ANDROIDX_WORK_MANAGER_VERSION=2.7.1

View File

@ -199,8 +199,8 @@ dependencyResolutionManagement {
//alias("androidx-espresso-contrib", "androidx.test.espresso:espresso-contrib:$androidxEspressoVersion") //alias("androidx-espresso-contrib", "androidx.test.espresso:espresso-contrib:$androidxEspressoVersion")
library("androidx-espresso-core", "androidx.test.espresso:espresso-core:$androidxEspressoVersion") library("androidx-espresso-core", "androidx.test.espresso:espresso-core:$androidxEspressoVersion")
library("androidx-espresso-intents", "androidx.test.espresso:espresso-intents:$androidxEspressoVersion") library("androidx-espresso-intents", "androidx.test.espresso:espresso-intents:$androidxEspressoVersion")
library("androidx-test-core", "androidx.test:core:$androidxTestCoreVersion") library("androidx-test-core", "androidx.test:core-ktx:$androidxTestCoreVersion")
library("androidx-test-junit", "androidx.test.ext:junit:$androidxTestJunitVersion") library("androidx-test-junit", "androidx.test.ext:junit-ktx:$androidxTestJunitVersion")
library("androidx-test-orchestrator", "androidx.test:orchestrator:$androidxTestOrchestratorVersion") library("androidx-test-orchestrator", "androidx.test:orchestrator:$androidxTestOrchestratorVersion")
library("androidx-test-runner", "androidx.test:runner:$androidxTestRunnerVersion") library("androidx-test-runner", "androidx.test:runner:$androidxTestRunnerVersion")
library("androidx-uiAutomator", "androidx.test.uiautomator:uiautomator-v18:$androidxUiAutomatorVersion") library("androidx-uiAutomator", "androidx.test.uiautomator:uiautomator-v18:$androidxUiAutomatorVersion")