[#439] Take light/dark screenshots

Co-authored-by: Honza <rychnovsky.honza@gmail.com>
This commit is contained in:
Carter Jernigan 2022-06-08 10:22:09 -04:00 committed by GitHub
parent b4bad94068
commit 020192b01e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 301 additions and 130 deletions

View File

@ -83,6 +83,10 @@ android {
} }
buildTypes { buildTypes {
getByName("debug").apply {
// Note that the build-conventions defines the res configs
isPseudoLocalesEnabled = true
}
getByName("release").apply { getByName("release").apply {
isMinifyEnabled = project.property("IS_MINIFY_ENABLED").toString().toBoolean() isMinifyEnabled = project.property("IS_MINIFY_ENABLED").toString().toBoolean()
isShrinkResources = project.property("IS_MINIFY_ENABLED").toString().toBoolean() isShrinkResources = project.property("IS_MINIFY_ENABLED").toString().toBoolean()
@ -143,14 +147,14 @@ dependencies {
implementation(projects.spackleAndroidLib) implementation(projects.spackleAndroidLib)
implementation(projects.uiLib) implementation(projects.uiLib)
androidTestImplementation(projects.testLib)
androidTestImplementation(libs.androidx.compose.test.junit) androidTestImplementation(libs.androidx.compose.test.junit)
androidTestImplementation(libs.androidx.navigation.compose) androidTestImplementation(libs.androidx.navigation.compose)
androidTestImplementation(libs.androidx.uiAutomator) androidTestImplementation(libs.androidx.uiAutomator)
androidTestImplementation(libs.bundles.androidx.test) androidTestImplementation(libs.bundles.androidx.test)
androidTestImplementation(projects.sdkExtLib) androidTestImplementation(projects.sdkExtLib)
androidTestImplementation(projects.spackleLib)
androidTestImplementation(projects.sdkExtUiLib) androidTestImplementation(projects.sdkExtUiLib)
androidTestImplementation(projects.spackleLib)
androidTestImplementation(projects.testLib)
if (isOrchestratorEnabled) { if (isOrchestratorEnabled) {
androidTestUtil(libs.androidx.test.orchestrator) { androidTestUtil(libs.androidx.test.orchestrator) {

View File

@ -1,5 +1,8 @@
package co.electriccoin.zcash.app package co.electriccoin.zcash.app
import android.content.Context
import android.os.Build
import android.os.LocaleList
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
@ -20,15 +23,18 @@ import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.matcher.ViewMatchers.isRoot import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.screenshot.captureToBitmap import androidx.test.espresso.screenshot.captureToBitmap
import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.filters.SmallTest import androidx.test.filters.LargeTest
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
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.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
import co.electriccoin.zcash.ui.MainActivity import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.design.component.ConfigurationOverride
import co.electriccoin.zcash.ui.design.component.UiMode
import co.electriccoin.zcash.ui.screen.backup.BackupTag import co.electriccoin.zcash.ui.screen.backup.BackupTag
import co.electriccoin.zcash.ui.screen.home.viewmodel.SecretState import co.electriccoin.zcash.ui.screen.home.viewmodel.SecretState
import co.electriccoin.zcash.ui.screen.restore.RestoreTag import co.electriccoin.zcash.ui.screen.restore.RestoreTag
@ -38,13 +44,34 @@ import kotlinx.coroutines.withContext
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
/*
* This screenshot implementation does not change the system-wide configuration, but rather
* injects a Context with a modified Configuration to change the uiMode and Locale.
*
* This works by:
* 1. Main Activity wraps the Composable with an Override
* 2. Main Activity exposes a Flow where a ConfigurationOverride can be set
* 3. We use an altered resContext in the tests instead of Application Context in order to load
* the right resources for comparison.
*
* Benefits of this implementation are that we do not modify system-wide values and don't require
* additional permissions to run these tests.
*
* Limitations of this implementation are that any views outside of Compose will not be updated, which
* can include the on-screen keyboard, system dialogs (like permissions), or other UI elements.
*
* An alternative implementation would be to use AppCompatActivity as the parent class for MainActivity,
* then rely on the AppCompat APIs for changing uiMode and Locale. This doesn't bring much benefit over
* our approach (it still has the problem with system dialogs and the keyboard), and it requires that
* we pull in the appcompat library.
*/
class ScreenshotTest : UiTestPrerequisites() { class ScreenshotTest : UiTestPrerequisites() {
companion object { companion object {
fun takeScreenshot(screenshotName: String) { fun takeScreenshot(tag: String, screenshotName: String) {
onView(isRoot()) onView(isRoot())
.captureToBitmap() .captureToBitmap()
.writeToTestStorage(screenshotName) .writeToTestStorage("$screenshotName - $tag")
} }
} }
@ -57,9 +84,52 @@ class ScreenshotTest : UiTestPrerequisites() {
} }
} }
private fun runWith(uiMode: UiMode, locale: String, action: (Context, String) -> Unit) {
val configurationOverride = ConfigurationOverride(uiMode, LocaleList.forLanguageTags(locale))
composeTestRule.activity.configurationOverrideFlow.value = configurationOverride
val applicationContext = ApplicationProvider.getApplicationContext<Context>()
val configuration = configurationOverride.newConfiguration(applicationContext.resources.configuration)
val resContext = applicationContext.createConfigurationContext(configuration)
action(resContext, "$uiMode-$locale")
}
@Test @Test
@SmallTest @MediumTest
fun take_screenshots_for_restore_wallet() { fun take_screenshots_for_restore_wallet_light_en_XA() {
runWith(UiMode.Light, "en-XA") { context, tag ->
take_screenshots_for_restore_wallet(context, tag)
}
}
@Test
@MediumTest
fun take_screenshots_for_restore_wallet_light_ar_XB() {
runWith(UiMode.Light, "ar-XB") { context, tag ->
take_screenshots_for_restore_wallet(context, tag)
}
}
@Test
@MediumTest
fun take_screenshots_for_restore_wallet_light_en_US() {
runWith(UiMode.Light, "en-US") { context, tag ->
take_screenshots_for_restore_wallet(context, tag)
}
}
// Dark mode was introduced in Android Q
@Test
@MediumTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
fun take_screenshots_for_restore_wallet_dark_en_US() {
runWith(UiMode.Dark, "en-US") { context, tag ->
take_screenshots_for_restore_wallet(context, tag)
}
}
private fun take_screenshots_for_restore_wallet(resContext: Context, tag: String) {
// TODO [#286]: Screenshot tests fail on Firebase Test Lab // TODO [#286]: Screenshot tests fail on Firebase Test Lab
if (FirebaseTestLabUtil.isFirebaseTestLab(ApplicationProvider.getApplicationContext())) { if (FirebaseTestLabUtil.isFirebaseTestLab(ApplicationProvider.getApplicationContext())) {
return return
@ -67,25 +137,25 @@ class ScreenshotTest : UiTestPrerequisites() {
composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.secretState.value is SecretState.None } composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.secretState.value is SecretState.None }
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_1_header)).also { composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_1_header)).also {
it.assertExists() it.assertExists()
} }
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_skip)).also { composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_skip)).also {
it.assertExists() it.assertExists()
it.performClick() it.performClick()
} }
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_4_import_existing_wallet)).also { composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_4_import_existing_wallet)).also {
it.assertExists() it.assertExists()
it.performClick() it.performClick()
} }
composeTestRule.onNodeWithText(getStringResource(R.string.restore_header)).also { composeTestRule.onNodeWithText(resContext.getString(R.string.restore_header)).also {
it.assertExists() it.assertExists()
} }
takeScreenshot("Import 1") takeScreenshot(tag, "Import 1")
val seedPhraseSplitLength = SeedPhraseFixture.new().split.size val seedPhraseSplitLength = SeedPhraseFixture.new().split.size
SeedPhraseFixture.new().split.forEachIndexed { index, string -> SeedPhraseFixture.new().split.forEachIndexed { index, string ->
@ -94,199 +164,231 @@ class ScreenshotTest : UiTestPrerequisites() {
// Take a screenshot half-way through filling in the seed phrase // Take a screenshot half-way through filling in the seed phrase
if (index == seedPhraseSplitLength / 2) { if (index == seedPhraseSplitLength / 2) {
takeScreenshot("Import 2") takeScreenshot(tag, "Import 2")
} }
} }
} }
composeTestRule.onNodeWithText(getStringResource(R.string.restore_complete_header)).also { composeTestRule.onNodeWithText(resContext.getString(R.string.restore_complete_header)).also {
it.assertExists() it.assertExists()
} }
takeScreenshot("Import 3") takeScreenshot(tag, "Import 3")
} }
@Test @Test
@SmallTest @LargeTest
fun take_screenshots_for_new_wallet_and_rest_of_app() { fun take_screenshots_for_new_wallet_and_rest_of_app_light_en_XA() {
runWith(UiMode.Light, "en-XA") { context, tag ->
take_screenshots_for_new_wallet_and_rest_of_app(context, tag)
}
}
@Test
@LargeTest
fun take_screenshots_for_new_wallet_and_rest_of_app_light_ar_XB() {
runWith(UiMode.Light, "ar-XB") { context, tag ->
take_screenshots_for_new_wallet_and_rest_of_app(context, tag)
}
}
@Test
@LargeTest
fun take_screenshots_for_new_wallet_and_rest_of_app_light_en_US() {
runWith(UiMode.Light, "en-US") { context, tag ->
take_screenshots_for_new_wallet_and_rest_of_app(context, tag)
}
}
// Dark mode was introduced in Android Q
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
fun take_screenshots_for_new_wallet_and_rest_of_app_dark_en_US() {
runWith(UiMode.Dark, "en-US") { context, tag ->
take_screenshots_for_new_wallet_and_rest_of_app(context, tag)
}
}
private fun take_screenshots_for_new_wallet_and_rest_of_app(resContext: Context, tag: String) {
// TODO [#286]: Screenshot tests fail on Firebase Test Lab // TODO [#286]: Screenshot tests fail on Firebase Test Lab
if (FirebaseTestLabUtil.isFirebaseTestLab(ApplicationProvider.getApplicationContext())) { if (FirebaseTestLabUtil.isFirebaseTestLab(ApplicationProvider.getApplicationContext())) {
return return
} }
onboardingScreenshots(composeTestRule) onboardingScreenshots(resContext, tag, composeTestRule)
backupScreenshots(composeTestRule) backupScreenshots(resContext, tag, composeTestRule)
homeScreenshots(composeTestRule) homeScreenshots(resContext, tag, composeTestRule)
// Profile screen // Profile screen
// navigateTo(MainActivity.NAV_PROFILE) // navigateTo(MainActivity.NAV_PROFILE)
composeTestRule.onNode(hasContentDescription(getStringResource(R.string.home_profile_content_description))).also { composeTestRule.onNode(hasContentDescription(resContext.getString(R.string.home_profile_content_description))).also {
it.assertExists() it.assertExists()
it.performClick() it.performClick()
} }
profileScreenshots(composeTestRule) profileScreenshots(resContext, tag, composeTestRule)
// Settings is a subscreen of profile // Settings is a subscreen of profile
composeTestRule.onNode(hasText(getStringResource(R.string.profile_settings))).also { composeTestRule.onNode(hasText(resContext.getString(R.string.profile_settings))).also {
it.assertExists() it.assertExists()
it.performClick() it.performClick()
} }
settingsScreenshots(composeTestRule) settingsScreenshots(resContext, tag, composeTestRule)
// Back to profile // Back to profile
composeTestRule.onNode(hasContentDescription(getStringResource(R.string.settings_back_content_description))).also { composeTestRule.onNode(hasContentDescription(resContext.getString(R.string.settings_back_content_description))).also {
it.assertExists() it.assertExists()
it.performClick() it.performClick()
} }
// Address Details is a subscreen of profile // Address Details is a subscreen of profile
composeTestRule.onNode(hasText(getStringResource(R.string.profile_see_address_details))).also { composeTestRule.onNode(hasText(resContext.getString(R.string.profile_see_address_details))).also {
it.assertExists() it.assertExists()
it.performClick() it.performClick()
} }
addressDetailsScreenshots(composeTestRule) addressDetailsScreenshots(resContext, tag, composeTestRule)
// Back to profile // Back to profile
composeTestRule.onNode(hasContentDescription(getStringResource(R.string.wallet_address_back_content_description))).also { composeTestRule.onNode(hasContentDescription(resContext.getString(R.string.wallet_address_back_content_description))).also {
it.assertExists() it.assertExists()
it.performClick() it.performClick()
} }
// Contact Support is a subscreen of profile // Contact Support is a subscreen of profile
composeTestRule.onNode(hasText(getStringResource(R.string.profile_support))).also { composeTestRule.onNode(hasText(resContext.getString(R.string.profile_support))).also {
it.performScrollTo() it.performScrollTo()
it.assertExists() it.assertExists()
it.performClick() it.performClick()
} }
supportScreenshots(composeTestRule) supportScreenshots(resContext, tag, composeTestRule)
// Back to profile // Back to profile
composeTestRule.onNode(hasContentDescription(getStringResource(R.string.support_back_content_description))).also { composeTestRule.onNode(hasContentDescription(resContext.getString(R.string.support_back_content_description))).also {
it.assertExists() it.assertExists()
it.performClick() it.performClick()
} }
composeTestRule.onNode(hasText(getStringResource(R.string.profile_title))).also { composeTestRule.onNode(hasText(resContext.getString(R.string.profile_title))).also {
it.assertExists() it.assertExists()
it.performClick() it.performClick()
} }
// About is a subscreen of profile // About is a subscreen of profile
composeTestRule.onNode(hasText(getStringResource(R.string.profile_about))).also { composeTestRule.onNode(hasText(resContext.getString(R.string.profile_about))).also {
it.performScrollTo() it.performScrollTo()
it.assertExists() it.assertExists()
it.performClick() it.performClick()
} }
aboutScreenshots(composeTestRule) aboutScreenshots(resContext, tag, composeTestRule)
// Back to profile // Back to profile
composeTestRule.onNode(hasContentDescription(getStringResource(R.string.about_back_content_description))).also { composeTestRule.onNode(hasContentDescription(resContext.getString(R.string.about_back_content_description))).also {
it.assertExists() it.assertExists()
it.performClick() it.performClick()
} }
// Back to home // Back to home
composeTestRule.onNode(hasContentDescription(getStringResource(R.string.settings_back_content_description))).also { composeTestRule.onNode(hasContentDescription(resContext.getString(R.string.settings_back_content_description))).also {
it.assertExists() it.assertExists()
it.performClick() it.performClick()
} }
composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.secretState.value is SecretState.Ready } composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.secretState.value is SecretState.Ready }
composeTestRule.onNode(hasText(getStringResource(R.string.home_button_request))).also { composeTestRule.onNode(hasText(resContext.getString(R.string.home_button_request))).also {
it.assertExists() it.assertExists()
it.performClick() it.performClick()
} }
composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.walletSnapshot.value != null } composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.walletSnapshot.value != null }
requestZecScreenshots(composeTestRule) requestZecScreenshots(resContext, tag, composeTestRule)
navigateTo(MainActivity.NAV_HOME) navigateTo(MainActivity.NAV_HOME)
composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.secretState.value is SecretState.Ready } composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.secretState.value is SecretState.Ready }
composeTestRule.onNode(hasText(getStringResource(R.string.home_button_send))).also { composeTestRule.onNode(hasText(resContext.getString(R.string.home_button_send))).also {
it.assertExists() it.assertExists()
it.performClick() it.performClick()
} }
composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.synchronizer.value != null } composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.synchronizer.value != null }
composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.spendingKey.value != null } composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.spendingKey.value != null }
composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.walletSnapshot.value != null } composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.walletSnapshot.value != null }
sendZecScreenshots(composeTestRule) sendZecScreenshots(resContext, tag, composeTestRule)
navigateTo(MainActivity.NAV_HOME) navigateTo(MainActivity.NAV_HOME)
} }
} }
private fun onboardingScreenshots(composeTestRule: AndroidComposeTestRule<ActivityScenarioRule<MainActivity>, MainActivity>) { private fun onboardingScreenshots(resContext: Context, tag: String, composeTestRule: AndroidComposeTestRule<ActivityScenarioRule<MainActivity>, MainActivity>) {
composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.secretState.value is SecretState.None } composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.secretState.value is SecretState.None }
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_1_header)).also { composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_1_header)).also {
it.assertExists() it.assertExists()
} }
ScreenshotTest.takeScreenshot("Onboarding 1") ScreenshotTest.takeScreenshot(tag, "Onboarding 1")
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_next)).also { composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_next)).also {
it.performClick() it.performClick()
} }
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_2_header)).also { composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_2_header)).also {
it.assertExists() it.assertExists()
ScreenshotTest.takeScreenshot("Onboarding 2") ScreenshotTest.takeScreenshot(tag, "Onboarding 2")
} }
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_next)).also { composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_next)).also {
it.performClick() it.performClick()
} }
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_3_header)).also { composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_3_header)).also {
it.assertExists() it.assertExists()
ScreenshotTest.takeScreenshot("Onboarding 3") ScreenshotTest.takeScreenshot(tag, "Onboarding 3")
} }
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_next)).also { composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_next)).also {
it.performClick() it.performClick()
} }
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_4_header)).also { composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_4_header)).also {
it.assertExists() it.assertExists()
ScreenshotTest.takeScreenshot("Onboarding 4") ScreenshotTest.takeScreenshot(tag, "Onboarding 4")
} }
composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_4_create_new_wallet)).also { composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_4_create_new_wallet)).also {
it.performClick() it.performClick()
} }
} }
private fun backupScreenshots(composeTestRule: AndroidComposeTestRule<ActivityScenarioRule<MainActivity>, MainActivity>) { private fun backupScreenshots(resContext: Context, tag: String, composeTestRule: AndroidComposeTestRule<ActivityScenarioRule<MainActivity>, MainActivity>) {
composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.secretState.value is SecretState.NeedsBackup } composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.secretState.value is SecretState.NeedsBackup }
composeTestRule.onNodeWithText(getStringResource(R.string.new_wallet_1_header)).also { composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_1_header)).also {
it.assertExists() it.assertExists()
} }
ScreenshotTest.takeScreenshot("Backup 1") ScreenshotTest.takeScreenshot(tag, "Backup 1")
composeTestRule.onNodeWithText(getStringResource(R.string.new_wallet_1_button)).also { composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_1_button)).also {
it.performClick() it.performClick()
} }
composeTestRule.onNodeWithText(getStringResource(R.string.new_wallet_2_header)).also { composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_2_header)).also {
it.assertExists() it.assertExists()
} }
ScreenshotTest.takeScreenshot("Backup 2") ScreenshotTest.takeScreenshot(tag, "Backup 2")
composeTestRule.onNodeWithText(getStringResource(R.string.new_wallet_2_button)).also { composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_2_button)).also {
it.performClick() it.performClick()
} }
composeTestRule.onNodeWithText(getStringResource(R.string.new_wallet_3_header)).also { composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_3_header)).also {
it.assertExists() it.assertExists()
} }
ScreenshotTest.takeScreenshot("Backup 3") ScreenshotTest.takeScreenshot(tag, "Backup 3")
composeTestRule.onNodeWithText(getStringResource(R.string.new_wallet_3_button_finished)).also { composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_3_button_finished)).also {
it.performClick() it.performClick()
} }
composeTestRule.onNodeWithText(getStringResource(R.string.new_wallet_4_header_verify)).also { composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_4_header_verify)).also {
it.assertExists() it.assertExists()
} }
ScreenshotTest.takeScreenshot("Backup 4") ScreenshotTest.takeScreenshot(tag, "Backup 4")
// Fail test first // Fail test first
composeTestRule.onAllNodesWithTag(BackupTag.DROPDOWN_CHIP).also { composeTestRule.onAllNodesWithTag(BackupTag.DROPDOWN_CHIP).also {
@ -302,21 +404,21 @@ private fun backupScreenshots(composeTestRule: AndroidComposeTestRule<ActivitySc
it[3].performClick() it[3].performClick()
composeTestRule.onNode(hasTestTag(BackupTag.DROPDOWN_MENU)).onChildren()[3].performClick() composeTestRule.onNode(hasTestTag(BackupTag.DROPDOWN_MENU)).onChildren()[3].performClick()
} }
composeTestRule.onNodeWithText(getStringResource(R.string.new_wallet_4_header_ouch)).also { composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_4_header_ouch)).also {
it.assertExists() it.assertExists()
ScreenshotTest.takeScreenshot("Backup Fail") ScreenshotTest.takeScreenshot(tag, "Backup Fail")
} }
composeTestRule.onNode(hasText(getStringResource(R.string.new_wallet_4_button_retry))).performClick() composeTestRule.onNode(hasText(resContext.getString(R.string.new_wallet_4_button_retry))).performClick()
composeTestRule.onNodeWithText(getStringResource(R.string.new_wallet_3_header)).also { composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_3_header)).also {
it.assertExists() it.assertExists()
} }
composeTestRule.onNodeWithText(getStringResource(R.string.new_wallet_3_button_finished)).also { composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_3_button_finished)).also {
it.performClick() it.performClick()
} }
composeTestRule.onNodeWithText(getStringResource(R.string.new_wallet_4_header_verify)).also { composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_4_header_verify)).also {
it.assertExists() it.assertExists()
} }
@ -336,99 +438,99 @@ private fun backupScreenshots(composeTestRule: AndroidComposeTestRule<ActivitySc
composeTestRule.onNode(hasTestTag(BackupTag.DROPDOWN_MENU)).onChildren()[2].performClick() composeTestRule.onNode(hasTestTag(BackupTag.DROPDOWN_MENU)).onChildren()[2].performClick()
} }
composeTestRule.onNode(hasText(getStringResource(R.string.new_wallet_5_body))).also { composeTestRule.onNode(hasText(resContext.getString(R.string.new_wallet_5_body))).also {
it.assertExists() it.assertExists()
ScreenshotTest.takeScreenshot("Backup 5") ScreenshotTest.takeScreenshot(tag, "Backup 5")
} }
composeTestRule.onNode(hasText(getStringResource(R.string.new_wallet_5_button_finished))).also { composeTestRule.onNode(hasText(resContext.getString(R.string.new_wallet_5_button_finished))).also {
it.assertExists() it.assertExists()
it.performClick() it.performClick()
} }
} }
private fun homeScreenshots(composeTestRule: AndroidComposeTestRule<ActivityScenarioRule<MainActivity>, MainActivity>) { private fun homeScreenshots(resContext: Context, tag: String, composeTestRule: AndroidComposeTestRule<ActivityScenarioRule<MainActivity>, MainActivity>) {
composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.secretState.value is SecretState.Ready } composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.secretState.value is SecretState.Ready }
composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.walletSnapshot.value != null } composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.walletSnapshot.value != null }
composeTestRule.onNode(hasText(getStringResource(R.string.home_button_send))).also { composeTestRule.onNode(hasText(resContext.getString(R.string.home_button_send))).also {
it.assertExists() it.assertExists()
ScreenshotTest.takeScreenshot("Home 1") ScreenshotTest.takeScreenshot(tag, "Home 1")
} }
} }
private fun profileScreenshots(composeTestRule: AndroidComposeTestRule<ActivityScenarioRule<MainActivity>, MainActivity>) { private fun profileScreenshots(resContext: Context, tag: String, composeTestRule: AndroidComposeTestRule<ActivityScenarioRule<MainActivity>, MainActivity>) {
composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.addresses.value != null } composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.addresses.value != null }
composeTestRule.onNode(hasText(getStringResource(R.string.profile_title))).also { composeTestRule.onNode(hasText(resContext.getString(R.string.profile_title))).also {
it.assertExists() it.assertExists()
} }
ScreenshotTest.takeScreenshot("Profile 1") ScreenshotTest.takeScreenshot(tag, "Profile 1")
} }
private fun settingsScreenshots(composeTestRule: ComposeTestRule) { private fun settingsScreenshots(resContext: Context, tag: String, composeTestRule: ComposeTestRule) {
composeTestRule.onNode(hasText(getStringResource(R.string.settings_header))).also { composeTestRule.onNode(hasText(resContext.getString(R.string.settings_header))).also {
it.assertExists() it.assertExists()
} }
ScreenshotTest.takeScreenshot("Settings 1") ScreenshotTest.takeScreenshot(tag, "Settings 1")
} }
private fun addressDetailsScreenshots(composeTestRule: ComposeTestRule) { private fun addressDetailsScreenshots(resContext: Context, tag: String, composeTestRule: ComposeTestRule) {
composeTestRule.onNode(hasText(getStringResource(R.string.wallet_address_title))).also { composeTestRule.onNode(hasText(resContext.getString(R.string.wallet_address_title))).also {
it.assertExists() it.assertExists()
} }
ScreenshotTest.takeScreenshot("Addresses 1") ScreenshotTest.takeScreenshot(tag, "Addresses 1")
} }
private fun requestZecScreenshots(composeTestRule: ComposeTestRule) { private fun requestZecScreenshots(resContext: Context, tag: String, composeTestRule: ComposeTestRule) {
composeTestRule.onNode(hasText(getStringResource(R.string.request_title))).also { composeTestRule.onNode(hasText(resContext.getString(R.string.request_title))).also {
it.assertExists() it.assertExists()
} }
ScreenshotTest.takeScreenshot("Request 1") ScreenshotTest.takeScreenshot(tag, "Request 1")
} }
private fun sendZecScreenshots(composeTestRule: ComposeTestRule) { private fun sendZecScreenshots(resContext: Context, tag: String, composeTestRule: ComposeTestRule) {
composeTestRule.onNode(hasText(getStringResource(R.string.send_title))).also { composeTestRule.onNode(hasText(resContext.getString(R.string.send_title))).also {
it.assertExists() it.assertExists()
} }
ScreenshotTest.takeScreenshot("Send 1") ScreenshotTest.takeScreenshot(tag, "Send 1")
composeTestRule.onNodeWithText(getStringResource(R.string.send_amount)).also { composeTestRule.onNodeWithText(resContext.getString(R.string.send_amount)).also {
val separators = MonetarySeparators.current() val separators = MonetarySeparators.current()
it.performTextInput("{${separators.decimal}}123") it.performTextInput("{${separators.decimal}}123")
} }
composeTestRule.onNodeWithText(getStringResource(R.string.send_to)).also { composeTestRule.onNodeWithText(resContext.getString(R.string.send_to)).also {
it.performTextInput(WalletAddressFixture.UNIFIED_ADDRESS_STRING) it.performTextInput(WalletAddressFixture.UNIFIED_ADDRESS_STRING)
} }
composeTestRule.onNodeWithText(getStringResource(R.string.send_create)).also { composeTestRule.onNodeWithText(resContext.getString(R.string.send_create)).also {
it.performClick() it.performClick()
} }
composeTestRule.waitForIdle() composeTestRule.waitForIdle()
ScreenshotTest.takeScreenshot("Send 2") ScreenshotTest.takeScreenshot(tag, "Send 2")
} }
private fun supportScreenshots(composeTestRule: ComposeTestRule) { private fun supportScreenshots(resContext: Context, tag: String, composeTestRule: ComposeTestRule) {
composeTestRule.onNode(hasText(getStringResource(R.string.support_header))).also { composeTestRule.onNode(hasText(resContext.getString(R.string.support_header))).also {
it.assertExists() it.assertExists()
} }
ScreenshotTest.takeScreenshot("Support 1") ScreenshotTest.takeScreenshot(tag, "Support 1")
} }
private fun aboutScreenshots(composeTestRule: ComposeTestRule) { private fun aboutScreenshots(resContext: Context, tag: String, composeTestRule: ComposeTestRule) {
composeTestRule.onNode(hasText(getStringResource(R.string.about_title))).also { composeTestRule.onNode(hasText(resContext.getString(R.string.about_title))).also {
it.assertExists() it.assertExists()
} }
ScreenshotTest.takeScreenshot("About 1") ScreenshotTest.takeScreenshot(tag, "About 1")
} }

View File

@ -1,7 +0,0 @@
package co.electriccoin.zcash.app.test
import android.content.Context
import androidx.annotation.StringRes
import androidx.test.core.app.ApplicationProvider
fun getStringResource(@StringRes resId: Int) = ApplicationProvider.getApplicationContext<Context>().getString(resId)

View File

@ -0,0 +1,64 @@
package co.electriccoin.zcash.ui.design.component
import android.content.res.Configuration
import android.os.LocaleList
import android.view.ContextThemeWrapper
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import kotlinx.coroutines.flow.StateFlow
/**
* Wrap a Composable with a way to override the Android Configuration. This is primarily useful
* for automated tests.
*/
@Composable
fun Override(configurationOverrideFlow: StateFlow<ConfigurationOverride?>, content: @Composable () -> Unit) {
val configurationOverride = configurationOverrideFlow.collectAsState().value
if (null == configurationOverride) {
content()
} else {
val configuration = configurationOverride.newConfiguration(LocalConfiguration.current)
val contextWrapper = run {
val context = LocalContext.current
object : ContextThemeWrapper() {
init {
attachBaseContext(context)
applyOverrideConfiguration(configuration)
}
}
}
CompositionLocalProvider(
LocalConfiguration provides configuration,
LocalContext provides contextWrapper
) {
content()
}
}
}
data class ConfigurationOverride(val uiMode: UiMode?, val locale: LocaleList?) {
fun newConfiguration(fromConfiguration: Configuration) = Configuration(fromConfiguration).apply {
this@ConfigurationOverride.uiMode?.let {
uiMode = (uiMode and Configuration.UI_MODE_NIGHT_MASK.inv()) or it.flag()
}
this@ConfigurationOverride.locale?.let {
setLocales(it)
}
}
}
enum class UiMode {
Light, Dark;
}
private fun UiMode.flag() = when (this) {
UiMode.Light -> Configuration.UI_MODE_NIGHT_NO
UiMode.Dark -> Configuration.UI_MODE_NIGHT_YES
}

View File

@ -81,7 +81,7 @@ dependencies {
implementation(projects.sdkExtLib) implementation(projects.sdkExtLib)
implementation(projects.sdkExtUiLib) implementation(projects.sdkExtUiLib)
implementation(projects.spackleAndroidLib) implementation(projects.spackleAndroidLib)
implementation(projects.uiDesignLib) api(projects.uiDesignLib)
androidTestImplementation(projects.testLib) androidTestImplementation(projects.testLib)
androidTestImplementation(libs.bundles.androidx.test) androidTestImplementation(libs.bundles.androidx.test)

View File

@ -33,7 +33,9 @@ import cash.z.ecc.sdk.type.fromResources
import co.electriccoin.zcash.spackle.EmulatorWtfUtil import co.electriccoin.zcash.spackle.EmulatorWtfUtil
import co.electriccoin.zcash.spackle.FirebaseTestLabUtil import co.electriccoin.zcash.spackle.FirebaseTestLabUtil
import co.electriccoin.zcash.ui.design.compat.FontCompat import co.electriccoin.zcash.ui.design.compat.FontCompat
import co.electriccoin.zcash.ui.design.component.ConfigurationOverride
import co.electriccoin.zcash.ui.design.component.GradientSurface import co.electriccoin.zcash.ui.design.component.GradientSurface
import co.electriccoin.zcash.ui.design.component.Override
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.about.WrapAbout import co.electriccoin.zcash.ui.screen.about.WrapAbout
import co.electriccoin.zcash.ui.screen.backup.WrapBackup import co.electriccoin.zcash.ui.screen.backup.WrapBackup
@ -59,6 +61,7 @@ import co.electriccoin.zcash.ui.screen.update.AppUpdateCheckerImp
import co.electriccoin.zcash.ui.screen.update.WrapUpdate import co.electriccoin.zcash.ui.screen.update.WrapUpdate
import co.electriccoin.zcash.ui.screen.update.model.UpdateState import co.electriccoin.zcash.ui.screen.update.model.UpdateState
import co.electriccoin.zcash.ui.screen.wallet_address.view.WalletAddresses import co.electriccoin.zcash.ui.screen.wallet_address.view.WalletAddresses
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlin.time.Duration import kotlin.time.Duration
@ -84,6 +87,9 @@ class MainActivity : ComponentActivity() {
@VisibleForTesting(otherwise = VisibleForTesting.NONE) @VisibleForTesting(otherwise = VisibleForTesting.NONE)
lateinit var navControllerForTesting: NavHostController lateinit var navControllerForTesting: NavHostController
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
val configurationOverrideFlow = MutableStateFlow<ConfigurationOverride?>(null)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -120,25 +126,27 @@ class MainActivity : ComponentActivity() {
private fun setupUiContent() { private fun setupUiContent() {
setContent { setContent {
ZcashTheme { Override(configurationOverrideFlow) {
GradientSurface( ZcashTheme {
Modifier GradientSurface(
.fillMaxWidth() Modifier
.fillMaxHeight() .fillMaxWidth()
) { .fillMaxHeight()
when (val secretState = walletViewModel.secretState.collectAsState().value) { ) {
SecretState.Loading -> { when (val secretState = walletViewModel.secretState.collectAsState().value) {
// For now, keep displaying splash screen using condition above. SecretState.Loading -> {
// In the future, we might consider displaying something different here. // For now, keep displaying splash screen using condition above.
// In the future, we might consider displaying something different here.
}
SecretState.None -> {
WrapOnboarding()
}
is SecretState.NeedsBackup -> WrapBackup(
secretState.persistableWallet,
onBackupComplete = { walletViewModel.persistBackupComplete() }
)
is SecretState.Ready -> Navigation()
} }
SecretState.None -> {
WrapOnboarding()
}
is SecretState.NeedsBackup -> WrapBackup(
secretState.persistableWallet,
onBackupComplete = { walletViewModel.persistBackupComplete() }
)
is SecretState.Ready -> Navigation()
} }
} }
} }

View File

@ -3,6 +3,6 @@
<style name="Theme.App.Starting" parent="Theme.SplashScreen"> <style name="Theme.App.Starting" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/splash_screen_background</item> <item name="windowSplashScreenBackground">@color/splash_screen_background</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_launcher_adaptive_foreground</item> <item name="windowSplashScreenAnimatedIcon">@drawable/ic_launcher_adaptive_foreground</item>
<item name="postSplashScreenTheme">@style/Theme.AppCompat.DayNight</item> <item name="postSplashScreenTheme">@style/Theme.AppCompat.DayNight.NoActionBar</item>
</style> </style>
</resources> </resources>