[#653] Refactor Backup flow screens

* [#653] Move copy to buffer action

- Trigger this action after seed phrase panel click and confirm via dialog window
- Added basic ui tests
- Added also dialog integration test
- Added related strings

* Revert "[#653] Move copy to buffer action"

This reverts commit 813eab00b747a779be5ef652745002f65c04572c.

* [#150] Refactoring the Backup flow to use Compose Scaffold

* Fix Backup flow screenshot test

- Removed scroll actions above nodes, which are no longer part of scroll behaviour - bottom navigation buttons are now part of Compose Scaffold component.

* Added scroll actions in screenshot test of Profile screen

- After tested the whole app with screenshot test on smaller screen device 4WVGA Nexus S

* Remove unnecessary screenshot test click action

- This click action on the Profile screen title seems to be unnecessary for the test and creates confusion

* ScreenshotTest module auto components init

- Changed the way we auto-initialize components in ScreenshotTest module
- Now we use androidx-startup library for it
- And we disabled the default way

* Add system back button navigation support

* Enable scrolling for Backup Test screen

* Fix Screenshot test on small screen in landscape

- Tested and fixed cases in which our screenshot test wasn't successful
- Tested on 4 WVGA Nexus S in landscpae mode

* Code clean

* Address review comments + stages refactoring

- Flattened BackupStage sealed class. Test and Failure are now regular stages.
- Introduced CheckSeed stage, instead of reusing Seed phrase for re-viewing.
- Simplified BackupView and custom Saver implementations.
- List of possible screen state stages is now lazy loaded value, instead of method.
- Some of the stages now override stage moving methods to enhance Backup screen state machine, as it's not linear.
- Existing tests updated to align with the new implementation of stages.

* Remove `run` block

* Rename CheckSeed -> ReviewSeed

Check might imply to another reader of the code that there is some going back to the test.  I went with the word Review which seems to better convey how that screen is passive for the user.

* Simplify list construction

This should have better performance.

* Crash instead of allowing back navigation

* Add documentation

* Fix initialization error

* Add non-localized string tag

Co-authored-by: Carter Jernigan <git@carterjernigan.com>
This commit is contained in:
Honza Rychnovsky 2022-11-23 08:17:06 +01:00 committed by GitHub
parent 00513a18da
commit 381af575ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 513 additions and 246 deletions

View File

@ -130,6 +130,7 @@ ANDROIDX_TEST_ORCHESTRATOR_VERSION=1.4.2
ANDROIDX_TEST_CORE_VERSION=1.5.0
ANDROIDX_TEST_MACROBENCHMARK_VERSION=1.2.0-alpha07
ANDROIDX_TEST_RUNNER_VERSION=1.5.1
ANDROIDX_STARTUP_VERSION=1.1.1
ANDROIDX_UI_AUTOMATOR_VERSION=2.2.0-alpha1
ANDROIDX_WORK_MANAGER_VERSION=2.7.1
CORE_LIBRARY_DESUGARING_VERSION=1.1.6

View File

@ -127,6 +127,7 @@ dependencyResolutionManagement {
val androidxProfileInstallerVersion = extra["ANDROIDX_PROFILE_INSTALLER_VERSION"].toString()
val androidxSecurityCryptoVersion = extra["ANDROIDX_SECURITY_CRYPTO_VERSION"].toString()
val androidxSplashScreenVersion = extra["ANDROIDX_SPLASH_SCREEN_VERSION"].toString()
val androidxStartupVersion = extra["ANDROIDX_STARTUP_VERSION"].toString()
val androidxTestCoreVersion = extra["ANDROIDX_TEST_CORE_VERSION"].toString()
val androidxTestJunitVersion = extra["ANDROIDX_TEST_JUNIT_VERSION"].toString()
val androidxTestMacrobenchmarkVersion = extra["ANDROIDX_TEST_MACROBENCHMARK_VERSION"].toString()
@ -176,6 +177,7 @@ dependencyResolutionManagement {
library("androidx-profileinstaller", "androidx.profileinstaller:profileinstaller:$androidxProfileInstallerVersion")
library("androidx-security-crypto", "androidx.security:security-crypto-ktx:$androidxSecurityCryptoVersion")
library("androidx-splash", "androidx.core:core-splashscreen:$androidxSplashScreenVersion")
library("androidx-startup", "androidx.startup:startup-runtime:$androidxStartupVersion")
library("androidx-viewmodel-compose", "androidx.lifecycle:lifecycle-viewmodel-compose:$androidxLifecycleVersion")
library("androidx-workmanager", "androidx.work:work-runtime-ktx:$androidxWorkManagerVersion")
library("desugaring", "com.android.tools:desugar_jdk_libs:$coreLibraryDesugaringVersion")

View File

@ -2,7 +2,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="zcash-ui-integration-test">
android:label="zcash-ui-integration-test"
android:icon="@mipmap/ic_launcher_square"
android:roundIcon="@mipmap/ic_launcher_round">
<activity
android:name="co.electriccoin.zcash.ui.integration.test.common.IntegrationTestingActivity"
android:exported="false" />

View File

@ -1,6 +1,9 @@
package co.electriccoin.zcash.ui.screen.backup.integration
import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.StateRestorationTester
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onAllNodesWithTag
@ -15,9 +18,11 @@ import co.electriccoin.zcash.ui.screen.backup.BackupTag
import co.electriccoin.zcash.ui.screen.backup.model.BackupStage
import co.electriccoin.zcash.ui.screen.backup.view.BackupTestSetup
import co.electriccoin.zcash.ui.test.getStringResource
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals
import kotlin.test.assertNotNull
class BackupIntegrationTest : UiTestPrerequisites() {
@ -40,7 +45,7 @@ class BackupIntegrationTest : UiTestPrerequisites() {
*/
@Test
@MediumTest
fun current_stage_restoration() {
fun backup_state_education_restoration() {
val restorationTester = StateRestorationTester(composeTestRule)
val testSetup = newTestSetup(BackupStage.EducationOverview)
@ -63,7 +68,7 @@ class BackupIntegrationTest : UiTestPrerequisites() {
@Test
@MediumTest
fun selected_choices_restoration() {
fun backup_state_test_running_restoration() {
val restorationTester = StateRestorationTester(composeTestRule)
val testSetup = newTestSetup(BackupStage.Test)
@ -72,6 +77,34 @@ class BackupIntegrationTest : UiTestPrerequisites() {
}
assertEquals(BackupStage.Test, testSetup.getStage())
val chipText = composeTestRule.getDropdownChipSelectedText(0, 0)
assertNotNull(chipText)
assertNotEquals("Chip text shouldn't be empty.", chipText, "")
assertEquals(BackupStage.Test, testSetup.getStage())
restorationTester.emulateSavedInstanceStateRestore()
assertEquals(BackupStage.Test, testSetup.getStage())
composeTestRule.onNodeWithText(chipText).also {
it.assertExists()
it.assertIsDisplayed()
}
}
@Test
@MediumTest
fun selected_choices_restoration() {
val restorationTester = StateRestorationTester(composeTestRule)
val testSetup = newTestSetup(BackupStage.Test)
restorationTester.setContent {
testSetup.getDefaultContent()
}
assertEquals(0, testSetup.getOnChoicesCallbackCount())
assertEquals(TestChoicesFixture.INITIAL_CHOICES.size, testSetup.getSelectedChoicesCount())
@ -82,15 +115,20 @@ class BackupIntegrationTest : UiTestPrerequisites() {
composeTestRule.onNode(hasTestTag(BackupTag.DROPDOWN_MENU)).onChildren()[1].performClick()
}
assertEquals(BackupStage.Test, testSetup.getStage())
assertEquals(2, testSetup.getOnChoicesCallbackCount())
assertEquals(4, testSetup.getSelectedChoicesCount())
restorationTester.emulateSavedInstanceStateRestore()
// we test here that the stage and the selected choices count remained unchanged after restore
assertEquals(BackupStage.Test, testSetup.getStage())
assertEquals(2, testSetup.getOnChoicesCallbackCount())
assertEquals(4, testSetup.getSelectedChoicesCount())
}
}
fun ComposeContentTestRule.getDropdownChipSelectedText(chipIndex: Int, selectionIndex: Int): String {
return onAllNodesWithTag(BackupTag.DROPDOWN_CHIP)[chipIndex].let { chip ->
chip.performClick()
onNode(hasTestTag(BackupTag.DROPDOWN_MENU)).onChildren()[selectionIndex].performClick()
chip.fetchSemanticsNode().config[SemanticsProperties.Text][selectionIndex].text
}
}

View File

@ -11,25 +11,25 @@ class BackupStageTest {
@Test
@SmallTest
fun getProgress_first() {
val progress = BackupStage.values().first().getProgress()
val progress = BackupStage.values.first().getProgress()
assertEquals(0, progress.current.value)
assertEquals(4, progress.last.value)
assertEquals(6, progress.last.value)
}
@Test
@SmallTest
fun getProgress_last() {
val progress = BackupStage.values().last().getProgress()
val progress = BackupStage.values.last().getProgress()
assertEquals(4, progress.current.value)
assertEquals(4, progress.last.value)
assertEquals(6, progress.current.value)
assertEquals(6, progress.last.value)
}
@Test
@SmallTest
fun hasNext_boundary() {
val last = BackupStage.values().last()
val last = BackupStage.values.last()
assertFalse(last.hasNext())
}
@ -37,7 +37,7 @@ class BackupStageTest {
@Test
@SmallTest
fun hasPrevious_boundary() {
val last = BackupStage.values().first()
val last = BackupStage.values.first()
assertFalse(last.hasPrevious())
}
@ -45,7 +45,7 @@ class BackupStageTest {
@Test
@SmallTest
fun getNext_from_first() {
val first = BackupStage.values().first()
val first = BackupStage.values.first()
val next = first.getNext()
assertNotEquals(first, next)
@ -55,7 +55,7 @@ class BackupStageTest {
@Test
@SmallTest
fun getNext_boundary() {
val last = BackupStage.values().last()
val last = BackupStage.values.last()
assertEquals(last, last.getNext())
}
@ -63,17 +63,17 @@ class BackupStageTest {
@Test
@SmallTest
fun getPrevious_from_last() {
val last = BackupStage.values().last()
val last = BackupStage.values.last()
val previous = last.getPrevious()
assertNotEquals(last, previous)
assertEquals(BackupStage.Test, previous)
assertEquals(BackupStage.Complete, previous)
}
@Test
@SmallTest
fun getPrevious_boundary() {
val first = BackupStage.values().first()
val first = BackupStage.values.first()
assertEquals(first, first.getPrevious())
}

View File

@ -3,12 +3,13 @@ package co.electriccoin.zcash.ui.screen.backup.view
import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onAllNodesWithTag
import androidx.compose.ui.test.onChildren
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import androidx.test.filters.MediumTest
import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.R
@ -56,10 +57,7 @@ class BackupViewTest : UiTestPrerequisites() {
fun copy_to_clipboard() {
val testSetup = newTestSetup(BackupStage.Seed)
composeTestRule.onNodeWithText(getStringResource(R.string.new_wallet_3_button_copy)).also {
it.performScrollTo()
it.performClick()
}
composeTestRule.clickCopyToBuffer()
assertEquals(1, testSetup.getOnCopyToClipboardCount())
}
@ -109,19 +107,17 @@ class BackupViewTest : UiTestPrerequisites() {
composeTestRule.onNode(hasTestTag(BackupTag.DROPDOWN_MENU)).onChildren()[3].performClick()
}
assertEquals(BackupStage.Test, testSetup.getStage())
assertEquals(BackupStage.Failure, testSetup.getStage())
composeTestRule.onNode(hasText(getStringResource(R.string.new_wallet_4_header_ouch)))
composeTestRule.onNode(hasText(getStringResource(R.string.new_wallet_4_button_retry))).also {
it.performScrollTo()
it.performClick()
}
assertEquals(BackupStage.Seed, testSetup.getStage())
composeTestRule.onNode(hasText(getStringResource(R.string.new_wallet_3_button_finished))).also {
it.performScrollTo()
it.performClick()
}
@ -129,7 +125,7 @@ class BackupViewTest : UiTestPrerequisites() {
// These verify that the test itself is re-displayed
composeTestRule.onNode(hasText(getStringResource(R.string.new_wallet_4_header_verify))).also {
composeTestRule.onNode(hasText(getStringResource(R.string.new_wallet_4_header))).also {
it.assertExists()
}
@ -152,18 +148,17 @@ class BackupViewTest : UiTestPrerequisites() {
@Test
@MediumTest
fun last_stage_click_back_to_seed() {
fun complete_stage_click_back_to_seed() {
val testSetup = newTestSetup(BackupStage.Complete)
val newWalletButton = composeTestRule.onNodeWithText(getStringResource(R.string.new_wallet_5_button_back))
newWalletButton.also {
it.performScrollTo()
it.performClick()
}
assertEquals(0, testSetup.getOnCopyToClipboardCount())
assertEquals(0, testSetup.getOnCompleteCallbackCount())
assertEquals(BackupStage.Seed, testSetup.getStage())
assertEquals(BackupStage.ReviewSeed, testSetup.getStage())
}
@Test
@ -172,28 +167,22 @@ class BackupViewTest : UiTestPrerequisites() {
val testSetup = newTestSetup(BackupStage.EducationOverview)
composeTestRule.onNodeWithText(getStringResource(R.string.new_wallet_1_button)).also {
it.performScrollTo()
it.performClick()
}
assertEquals(BackupStage.EducationRecoveryPhrase, testSetup.getStage())
composeTestRule.onNodeWithText(getStringResource(R.string.new_wallet_2_button)).also {
it.performScrollTo()
it.performClick()
}
assertEquals(BackupStage.Seed, testSetup.getStage())
composeTestRule.onNodeWithText(getStringResource(R.string.new_wallet_3_button_copy)).also {
it.performScrollTo()
it.performClick()
}
composeTestRule.clickCopyToBuffer()
assertEquals(1, testSetup.getOnCopyToClipboardCount())
composeTestRule.onNodeWithText(getStringResource(R.string.new_wallet_3_button_finished)).also {
it.performScrollTo()
it.performClick()
}
@ -222,3 +211,18 @@ class BackupViewTest : UiTestPrerequisites() {
assertEquals(1, testSetup.getOnCompleteCallbackCount())
}
}
fun ComposeContentTestRule.clickCopyToBuffer() {
// open menu
onNodeWithContentDescription(
getStringResource(R.string.new_wallet_toolbar_more_button_content_description)
).also { moreMenu ->
moreMenu.performClick()
// click menu button
onNodeWithText(
getStringResource(R.string.new_wallet_3_button_copy)
).also { menuButton ->
menuButton.performClick()
}
}
}

View File

@ -4,7 +4,6 @@ import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import androidx.activity.ComponentActivity
import androidx.activity.viewModels
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -16,7 +15,6 @@ import co.electriccoin.zcash.ui.screen.backup.ext.Saver
import co.electriccoin.zcash.ui.screen.backup.state.BackupState
import co.electriccoin.zcash.ui.screen.backup.state.TestChoices
import co.electriccoin.zcash.ui.screen.backup.view.BackupWallet
import co.electriccoin.zcash.ui.screen.backup.viewmodel.BackupViewModel
@Composable
internal fun MainActivity.WrapBackup(
@ -33,26 +31,22 @@ internal fun WrapBackup(
persistableWallet: PersistableWallet,
onBackupComplete: () -> Unit
) {
val backupViewModel by activity.viewModels<BackupViewModel>()
WrapBackup(
persistableWallet,
backupViewModel.backupState,
onCopyToClipboard = { copyToClipboard(activity.applicationContext, persistableWallet) },
onBackupComplete = onBackupComplete
)
}
// This extra layer of indirection allows unit tests to validate the testChoices state retention.
// If backupViewModel goes away eventually, then backupState retention could be tested as well.
// This extra layer of indirection allows unit tests to validate the screen state retention.
@Composable
internal fun WrapBackup(
persistableWallet: PersistableWallet,
backupState: BackupState,
onCopyToClipboard: () -> Unit,
onBackupComplete: () -> Unit
) {
val testChoices by rememberSaveable(stateSaver = TestChoices.Saver) { mutableStateOf(TestChoices()) }
val backupState by rememberSaveable(stateSaver = BackupState.Saver) { mutableStateOf(BackupState()) }
BackupWallet(
persistableWallet,

View File

@ -0,0 +1,25 @@
package co.electriccoin.zcash.ui.screen.backup.ext
import androidx.compose.runtime.saveable.mapSaver
import co.electriccoin.zcash.ui.screen.backup.model.BackupStage
import co.electriccoin.zcash.ui.screen.backup.model.values
import co.electriccoin.zcash.ui.screen.backup.state.BackupState
private const val KEY_STAGE = "stage" // $NON-NLS
internal val BackupState.Companion.Saver
get() = mapSaver(
save = { it.toSaverMap() },
restore = {
if (it.isEmpty()) {
BackupState()
} else {
val stage = BackupStage.values[it[KEY_STAGE] as Int]
BackupState(stage)
}
}
)
private fun BackupState.toSaverMap() = buildMap {
put(KEY_STAGE, current.value.order)
}

View File

@ -4,7 +4,7 @@ import androidx.compose.runtime.saveable.mapSaver
import co.electriccoin.zcash.spackle.model.Index
import co.electriccoin.zcash.ui.screen.backup.state.TestChoices
private const val KEY_TEST_CHOICES = "test_choices"
private const val KEY_TEST_CHOICES = "test_choices" // $NON-NLS
// Using a custom saver instead of Parcelize, to avoid adding an Android-specific API to
// the TestChoices and Index class

View File

@ -3,37 +3,99 @@ package co.electriccoin.zcash.ui.screen.backup.model
import co.electriccoin.zcash.spackle.model.Index
import co.electriccoin.zcash.spackle.model.Progress
enum class BackupStage {
// Note: the ordinal order is used to manage progression through each stage
// so be careful if reordering these
EducationOverview,
EducationRecoveryPhrase,
Seed,
Test,
Complete;
sealed class BackupStage(internal val order: Int) {
/*
* While the backup/restore UX is mostly linear, there are a few branches such as during the
* test and at the very end.
*
* We do not allow the user to back after completing the onboarding, because we don't want to
* give users the option to delete their seed from Google Credential Manager (once that feature
* is added). Instead, users should go into the app settings to delete a vaulted credential.
*
* We made the final seed review screen separate from the original seed screen. Although some
* code is duplicated, it makes each screen much easier to individually build/test and reduces
* risk of bugs being introduced.
*/
companion object
object EducationOverview : BackupStage(EDUCATION_OVERVIEW_ORDER)
object EducationRecoveryPhrase : BackupStage(EDUCATION_RECOVERY_PHRASE_ORDER)
object Seed : BackupStage(SEED_ORDER)
object Test : BackupStage(TEST_ORDER) {
// To bypass the Failure state
override fun getNext(): BackupStage {
return values[COMPLETE_ORDER]
}
}
object Failure : BackupStage(FAILURE_ORDER) {
// To let user preview the seed again after test failure
override fun getPrevious(): BackupStage {
return values[SEED_ORDER]
}
}
object Complete : BackupStage(COMPLETE_ORDER) {
// To disable back navigation after successful test
override fun hasPrevious(): Boolean {
return false
}
override fun getPrevious(): BackupStage {
error("Cannot go back once the onboarding is complete") // $NON-NLS-1$
}
}
object ReviewSeed : BackupStage(REVIEW_SEED_ORDER)
/**
* @see getPrevious
*/
fun hasPrevious() = ordinal > 0
open fun hasPrevious() = order > 0
/**
* @see getNext
*/
fun hasNext() = ordinal < values().size - 1
open fun hasNext() = order < values.size - 1
/**
* @return Previous item in ordinal order. Returns the first item when it cannot go further back.
*/
fun getPrevious() = values()[maxOf(0, ordinal - 1)]
open fun getPrevious() = values[maxOf(0, order - 1)]
/**
* @return Last item in ordinal order. Returns the last item when it cannot go further forward.
*/
fun getNext() = values()[minOf(values().size - 1, ordinal + 1)]
open fun getNext() = values[minOf(values.size - 1, order + 1)]
/**
* @return Last item in ordinal order. Returns the last item when it cannot go further forward.
* @return Returns current progression through stages.
*/
fun getProgress() = Progress(Index(ordinal), Index(values().size - 1))
fun getProgress() = Progress(Index(order), Index(values.size - 1))
}
// Note: the indexes are used to manage progression through each stage
// so be careful if changing these
private const val EDUCATION_OVERVIEW_ORDER = 0
private const val EDUCATION_RECOVERY_PHRASE_ORDER = 1
private const val SEED_ORDER = 2
private const val TEST_ORDER = 3
private const val FAILURE_ORDER = 4
private const val COMPLETE_ORDER = 5
private const val REVIEW_SEED_ORDER = 6
private val sealedClassValues: List<BackupStage> = buildList<BackupStage>() {
add(EDUCATION_OVERVIEW_ORDER, BackupStage.EducationOverview)
add(EDUCATION_RECOVERY_PHRASE_ORDER, BackupStage.EducationRecoveryPhrase)
add(SEED_ORDER, BackupStage.Seed)
add(TEST_ORDER, BackupStage.Test)
add(FAILURE_ORDER, BackupStage.Failure)
add(COMPLETE_ORDER, BackupStage.Complete)
add(REVIEW_SEED_ORDER, BackupStage.ReviewSeed)
}
// https://youtrack.jetbrains.com/issue/KT-8970/Object-is-uninitialized-null-when-accessed-from-static-context-ex.-companion-object-with-initialization-loop
val BackupStage.Companion.values: List<BackupStage>
get() = sealedClassValues

View File

@ -1,6 +1,7 @@
package co.electriccoin.zcash.ui.screen.backup.state
import co.electriccoin.zcash.ui.screen.backup.model.BackupStage
import co.electriccoin.zcash.ui.screen.backup.model.values
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -9,7 +10,7 @@ import kotlinx.coroutines.flow.StateFlow
* primarily useful on Android, for automated tests, and for iterative debugging with the Compose
* layout preview. The default constructor argument is generally fine for other platforms.
*/
class BackupState(initialState: BackupStage = BackupStage.values().first()) {
class BackupState(initialState: BackupStage = BackupStage.values.first()) {
private val mutableState = MutableStateFlow(initialState)
@ -25,11 +26,9 @@ class BackupState(initialState: BackupStage = BackupStage.values().first()) {
mutableState.value = current.value.getPrevious()
}
fun goToBeginning() {
mutableState.value = BackupStage.values().first()
fun goToStage(newStage: BackupStage) {
mutableState.value = newStage
}
fun goToSeed() {
mutableState.value = BackupStage.Seed
}
companion object
}

View File

@ -1,19 +1,37 @@
@file:Suppress("TooManyFunctions")
package co.electriccoin.zcash.ui.screen.backup.view
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.Card
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
@ -31,8 +49,6 @@ import co.electriccoin.zcash.ui.design.component.CHIP_GRID_ROW_SIZE
import co.electriccoin.zcash.ui.design.component.Chip
import co.electriccoin.zcash.ui.design.component.ChipGrid
import co.electriccoin.zcash.ui.design.component.GradientSurface
import co.electriccoin.zcash.ui.design.component.Header
import co.electriccoin.zcash.ui.design.component.NavigationButton
import co.electriccoin.zcash.ui.design.component.PrimaryButton
import co.electriccoin.zcash.ui.design.component.TertiaryButton
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
@ -62,6 +78,7 @@ fun ComposablePreview() {
/**
* @param onComplete Callback when the user has completed the backup test.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@Suppress("LongParameterList")
fun BackupWallet(
@ -72,32 +89,78 @@ fun BackupWallet(
onComplete: () -> Unit,
onChoicesChanged: ((choicesCount: Int) -> Unit)?
) {
when (backupState.current.collectAsState().value) {
BackupStage.EducationOverview -> EducationOverview(onNext = backupState::goNext)
BackupStage.EducationRecoveryPhrase -> EducationRecoveryPhrase(onNext = backupState::goNext)
BackupStage.Seed -> SeedPhrase(
wallet,
onNext = backupState::goNext,
onCopyToClipboard = onCopyToClipboard
)
BackupStage.Test -> Test(
wallet,
choices,
onBack = backupState::goPrevious,
onNext = backupState::goNext,
val currentBackupStage = backupState.current.collectAsState().value
Scaffold(
topBar = {
BackupTopAppBar(
backupStage = currentBackupStage,
onCopyToClipboard = onCopyToClipboard,
onBack = backupState::goPrevious,
selectedTestChoices = choices
)
},
bottomBar = {
BackupBottomNav(
backupStage = currentBackupStage,
onNext = backupState::goNext,
onBack = backupState::goPrevious,
selectedTestChoices = choices,
onComplete = onComplete,
onBackToSeedPhrase = {
backupState.goToStage(BackupStage.ReviewSeed)
}
)
}
) { paddingValues ->
BackupMainContent(
paddingValues = paddingValues,
backupState = backupState,
wallet = wallet,
choices = choices,
onChoicesChanged = onChoicesChanged
)
BackupStage.Complete -> Complete(
onComplete = onComplete,
onBackToSeedPhrase = backupState::goToSeed
)
}
}
@Composable
private fun EducationOverview(onNext: () -> Unit) {
Column(modifier = Modifier.verticalScroll(state = rememberScrollState())) {
Header(stringResource(R.string.new_wallet_1_header))
fun BackupMainContent(
paddingValues: PaddingValues,
backupState: BackupState,
wallet: PersistableWallet,
choices: TestChoices,
onChoicesChanged: ((choicesCount: Int) -> Unit)?
) {
Column(
Modifier
.padding(
top = paddingValues.calculateTopPadding(),
bottom = paddingValues.calculateBottomPadding()
)
) {
when (backupState.current.collectAsState().value) {
is BackupStage.EducationOverview -> EducationOverview()
is BackupStage.EducationRecoveryPhrase -> EducationRecoveryPhrase()
is BackupStage.Seed -> SeedPhrase(wallet)
is BackupStage.Test -> TestInProgress(
selectedTestChoices = choices,
onChoicesChanged = onChoicesChanged,
splitSeedPhrase = wallet.seedPhrase.split,
backupState = backupState
)
is BackupStage.Failure -> TestFailure()
is BackupStage.Complete -> TestComplete()
is BackupStage.ReviewSeed -> SeedPhrase(wallet)
}
}
}
@Composable
private fun EducationOverview() {
Column(
Modifier
.verticalScroll(rememberScrollState())
) {
Body(stringResource(R.string.new_wallet_1_body_1))
Image(
painter = painterResource(id = R.drawable.backup_1),
@ -109,15 +172,15 @@ private fun EducationOverview(onNext: () -> Unit) {
.weight(MINIMAL_WEIGHT, true)
)
Body(stringResource(R.string.new_wallet_1_body_2))
PrimaryButton(onClick = onNext, text = stringResource(R.string.new_wallet_1_button))
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun EducationRecoveryPhrase(onNext: () -> Unit) {
Column(modifier = Modifier.verticalScroll(state = rememberScrollState())) {
Header(stringResource(R.string.new_wallet_2_header))
private fun EducationRecoveryPhrase() {
Column(
Modifier
.verticalScroll(rememberScrollState())
) {
Body(stringResource(R.string.new_wallet_2_body_1))
Image(
painter = painterResource(id = R.drawable.backup_2),
@ -127,21 +190,19 @@ private fun EducationRecoveryPhrase(onNext: () -> Unit) {
Card {
Body(stringResource(R.string.new_wallet_2_body_3))
}
PrimaryButton(onClick = onNext, text = stringResource(R.string.new_wallet_2_button))
}
}
@Composable
private fun SeedPhrase(persistableWallet: PersistableWallet, onNext: () -> Unit, onCopyToClipboard: () -> Unit) {
private fun SeedPhrase(persistableWallet: PersistableWallet) {
SecureScreen()
Column(Modifier.verticalScroll(rememberScrollState())) {
Header(stringResource(R.string.new_wallet_3_header))
Column(
Modifier
.verticalScroll(rememberScrollState())
.padding(vertical = ZcashTheme.paddings.padding)
) {
Body(stringResource(R.string.new_wallet_3_body_1))
ChipGrid(persistableWallet.seedPhrase.split)
PrimaryButton(onClick = onNext, text = stringResource(R.string.new_wallet_3_button_finished))
TertiaryButton(onClick = onCopyToClipboard, text = stringResource(R.string.new_wallet_3_button_copy))
}
}
@ -150,37 +211,6 @@ private val testIndices = listOf(Index(4), Index(9), Index(16), Index(20))
private data class TestChoice(val originalIndex: Index, val word: String)
@Composable
private fun Test(
wallet: PersistableWallet,
selectedTestChoices: TestChoices,
onBack: () -> Unit,
onNext: () -> Unit,
onChoicesChanged: ((choicesCount: Int) -> Unit)?
) {
SecureScreen()
val splitSeedPhrase = wallet.seedPhrase.split
val currentSelectedTestChoice = selectedTestChoices.current.collectAsState().value
when {
currentSelectedTestChoice.size != testIndices.size -> {
TestInProgress(splitSeedPhrase, selectedTestChoices, onBack, onChoicesChanged)
}
currentSelectedTestChoice.all { splitSeedPhrase[it.key.value] == it.value } -> {
// The user got the test correct
onNext()
}
currentSelectedTestChoice.none { null == it.value } -> {
TestFailure {
// Clear the user's prior test inputs for the retest
selectedTestChoices.set(emptyMap())
onBack()
}
}
}
}
/*
* A few implementation notes on the test:
* - It is possible for the same word to appear twice in the word choices
@ -190,9 +220,11 @@ private fun Test(
private fun TestInProgress(
splitSeedPhrase: List<String>,
selectedTestChoices: TestChoices,
onBack: () -> Unit,
onChoicesChanged: ((choicesCount: Int) -> Unit)?
onChoicesChanged: ((choicesCount: Int) -> Unit)?,
backupState: BackupState
) {
SecureScreen()
val testChoices = splitSeedPhrase
.mapIndexed { index, word -> TestChoice(Index(index), word) }
.filter { testIndices.contains(it.originalIndex) }
@ -202,45 +234,49 @@ private fun TestInProgress(
listOf(it[1], it[0], it[3], it[2])
}
val currentSelectedTestChoice = selectedTestChoices.current.collectAsState().value
Column {
// This button doesn't match the design; just providing the navigation hook for now
NavigationButton(onClick = onBack, text = stringResource(R.string.new_wallet_4_button_back))
if (currentSelectedTestChoice.size == testIndices.size) {
if (currentSelectedTestChoice.all { splitSeedPhrase[it.key.value] == it.value }) {
// the user got the test correct
backupState.goNext()
} else {
backupState.goToStage(BackupStage.Failure)
}
}
Column(
Modifier
.verticalScroll(rememberScrollState())
.padding(vertical = ZcashTheme.paddings.padding)
) {
splitSeedPhrase.chunked(CHIP_GRID_ROW_SIZE).forEachIndexed { chunkIndex, chunk ->
Row(Modifier.fillMaxWidth()) {
chunk.forEachIndexed { subIndex, word ->
val currentIndex = Index(chunkIndex * CHIP_GRID_ROW_SIZE + subIndex)
Header(stringResource(R.string.new_wallet_4_header_verify))
// Body(stringResource(R.string.new_wallet_4_body_verify))
Column {
splitSeedPhrase.chunked(CHIP_GRID_ROW_SIZE).forEachIndexed { chunkIndex, chunk ->
Row(Modifier.fillMaxWidth()) {
chunk.forEachIndexed { subIndex, word ->
val currentIndex = Index(chunkIndex * CHIP_GRID_ROW_SIZE + subIndex)
if (testIndices.contains(currentIndex)) {
ChipDropDown(
currentIndex,
dropdownText = currentSelectedTestChoice[currentIndex]
?: "",
choices = testChoices.map { it.word },
modifier = Modifier
.weight(MINIMAL_WEIGHT)
.testTag(BackupTag.DROPDOWN_CHIP)
) {
selectedTestChoices.set(
HashMap(currentSelectedTestChoice).apply {
this[currentIndex] = testChoices[it.value].word
}
)
if (onChoicesChanged != null) {
onChoicesChanged(selectedTestChoices.current.value.size)
if (testIndices.contains(currentIndex)) {
ChipDropDown(
currentIndex,
dropdownText = currentSelectedTestChoice[currentIndex]
?: "",
choices = testChoices.map { it.word },
modifier = Modifier
.weight(MINIMAL_WEIGHT)
.testTag(BackupTag.DROPDOWN_CHIP)
) {
selectedTestChoices.set(
HashMap(currentSelectedTestChoice).apply {
this[currentIndex] = testChoices[it.value].word
}
}
} else {
Chip(
index = Index(chunkIndex * CHIP_GRID_ROW_SIZE + subIndex),
text = word,
modifier = Modifier.weight(MINIMAL_WEIGHT)
)
if (onChoicesChanged != null) {
onChoicesChanged(selectedTestChoices.current.value.size)
}
}
} else {
Chip(
index = Index(chunkIndex * CHIP_GRID_ROW_SIZE + subIndex),
text = word,
modifier = Modifier.weight(MINIMAL_WEIGHT)
)
}
}
}
@ -249,29 +285,26 @@ private fun TestInProgress(
}
@Composable
private fun TestFailure(onBackToSeedPhrase: () -> Unit) {
Column(modifier = Modifier.verticalScroll(state = rememberScrollState())) {
// This button doesn't match the design; just providing the navigation hook for now
NavigationButton(onClick = onBackToSeedPhrase, text = stringResource(R.string.new_wallet_4_button_back))
Header(stringResource(R.string.new_wallet_4_header_ouch))
private fun TestFailure() {
Column(
Modifier
.verticalScroll(rememberScrollState())
) {
Image(
painter = painterResource(id = R.drawable.backup_failure),
contentDescription = stringResource(id = R.string.backup_failure_content_description)
)
Box(Modifier.fillMaxHeight(MINIMAL_WEIGHT))
Body(stringResource(R.string.new_wallet_4_body_ouch_retry))
PrimaryButton(onClick = onBackToSeedPhrase, text = stringResource(R.string.new_wallet_4_button_retry))
}
}
@Composable
private fun Complete(onComplete: () -> Unit, onBackToSeedPhrase: () -> Unit) {
Column(modifier = Modifier.verticalScroll(state = rememberScrollState())) {
Header(stringResource(R.string.new_wallet_5_header))
private fun TestComplete() {
Column(
Modifier
.verticalScroll(rememberScrollState())
) {
Body(stringResource(R.string.new_wallet_5_body))
Image(
painter = painterResource(id = R.drawable.backup_success),
@ -283,7 +316,140 @@ private fun Complete(onComplete: () -> Unit, onBackToSeedPhrase: () -> Unit) {
.fillMaxWidth()
.weight(MINIMAL_WEIGHT, true)
)
PrimaryButton(onClick = onComplete, text = stringResource(R.string.new_wallet_5_button_finished))
TertiaryButton(onClick = onBackToSeedPhrase, text = stringResource(R.string.new_wallet_5_button_back))
}
}
@Composable
@OptIn(ExperimentalMaterial3Api::class)
private fun BackupTopAppBar(
backupStage: BackupStage,
onCopyToClipboard: () -> Unit,
onBack: () -> Unit,
selectedTestChoices: TestChoices
) {
var showCopySeedMenu = false
val screenTitleResId = when (backupStage) {
is BackupStage.EducationOverview -> {
R.string.new_wallet_1_header
}
is BackupStage.EducationRecoveryPhrase -> {
R.string.new_wallet_2_header
}
is BackupStage.Seed -> {
showCopySeedMenu = true
R.string.new_wallet_3_header
}
is BackupStage.Test -> {
R.string.new_wallet_4_header
}
is BackupStage.Failure -> {
R.string.new_wallet_4_header_ouch
}
is BackupStage.Complete -> {
R.string.new_wallet_5_header
}
is BackupStage.ReviewSeed -> {
showCopySeedMenu = true
R.string.new_wallet_3_header
}
}
TopAppBar(
title = { Text(text = stringResource(id = screenTitleResId)) },
navigationIcon = {
// hide back navigation button for the first and Complete stages
if (backupStage.hasPrevious() && backupStage != BackupStage.Complete) {
val onBackClickListener = {
if (backupStage is BackupStage.Failure) {
// Clear the user's prior test inputs for the retest
selectedTestChoices.set(emptyMap())
}
onBack()
}
BackHandler(enabled = true) { onBackClickListener() }
IconButton(onBackClickListener) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = stringResource(
R.string.new_wallet_navigation_back_button_content_description
)
)
}
}
},
actions = {
if (showCopySeedMenu) {
CopySeedMenu(onCopyToClipboard)
}
}
)
}
@Composable
private fun CopySeedMenu(onCopyToClipboard: () -> Unit) {
var expanded by rememberSaveable { mutableStateOf(false) }
IconButton(onClick = { expanded = true }) {
Icon(
Icons.Default.MoreVert,
contentDescription = stringResource(R.string.new_wallet_toolbar_more_button_content_description)
)
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
DropdownMenuItem(
text = { Text(stringResource(R.string.new_wallet_3_button_copy)) },
onClick = {
expanded = false
onCopyToClipboard()
}
)
}
}
@Suppress("LongParameterList")
@Composable
private fun BackupBottomNav(
backupStage: BackupStage,
onNext: () -> Unit,
onBack: () -> Unit,
selectedTestChoices: TestChoices,
onComplete: () -> Unit,
onBackToSeedPhrase: () -> Unit
) {
Column {
when (backupStage) {
is BackupStage.EducationOverview -> {
PrimaryButton(onClick = onNext, text = stringResource(R.string.new_wallet_1_button))
}
is BackupStage.EducationRecoveryPhrase -> {
PrimaryButton(onClick = onNext, text = stringResource(R.string.new_wallet_2_button))
}
is BackupStage.Seed -> {
PrimaryButton(onClick = onNext, text = stringResource(R.string.new_wallet_3_button_finished))
}
is BackupStage.Test -> {
// no bottom navigation button placed
}
is BackupStage.Failure -> {
PrimaryButton(
onClick = {
// Clear the user's prior test inputs for the retest
selectedTestChoices.set(emptyMap())
onBack()
},
text = stringResource(R.string.new_wallet_4_button_retry)
)
}
is BackupStage.Complete -> {
PrimaryButton(onClick = onComplete, text = stringResource(R.string.new_wallet_5_button_finished))
TertiaryButton(onClick = onBackToSeedPhrase, text = stringResource(R.string.new_wallet_5_button_back))
}
is BackupStage.ReviewSeed -> {
PrimaryButton(onClick = onBack, text = stringResource(R.string.new_wallet_3_button_finished))
}
}
}
}

View File

@ -1,37 +0,0 @@
package co.electriccoin.zcash.ui.screen.backup.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import cash.z.ecc.android.sdk.ext.collectWith
import co.electriccoin.zcash.ui.screen.backup.model.BackupStage
import co.electriccoin.zcash.ui.screen.backup.state.BackupState
class BackupViewModel(application: Application, savedStateHandle: SavedStateHandle) : AndroidViewModel(application) {
val backupState: BackupState = run {
val initialValue = if (savedStateHandle.contains(KEY_STAGE)) {
savedStateHandle.get<BackupStage>(KEY_STAGE)
} else {
null
}
if (null == initialValue) {
BackupState()
} else {
BackupState(initialValue)
}
}
init {
// viewModelScope is constructed with Dispatchers.Main.immediate, so this will
// update the save state as soon as a change occurs.
backupState.current.collectWith(viewModelScope) {
savedStateHandle.set(KEY_STAGE, it)
}
}
companion object {
private const val KEY_STAGE = "stage" // $NON-NLS
}
}

View File

@ -11,7 +11,6 @@ import co.electriccoin.zcash.spackle.EmulatorWtfUtil
import co.electriccoin.zcash.spackle.FirebaseTestLabUtil
import co.electriccoin.zcash.ui.BuildConfig
import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.screen.backup.viewmodel.BackupViewModel
import co.electriccoin.zcash.ui.screen.home.view.Home
import co.electriccoin.zcash.ui.screen.home.viewmodel.CheckUpdateViewModel
import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel
@ -87,9 +86,6 @@ internal fun WrapHome(
val onboardingViewModel by activity.viewModels<OnboardingViewModel>()
onboardingViewModel.onboardingState.goToBeginning()
onboardingViewModel.isImporting.value = false
val backupViewModel by activity.viewModels<BackupViewModel>()
backupViewModel.backupState.goToBeginning()
},
updateAvailable = updateAvailable
)

View File

@ -32,7 +32,7 @@ enum class OnboardingStage {
fun getNext() = values()[minOf(values().size - 1, ordinal + 1)]
/**
* @return Last item in ordinal order. Returns the last item when it cannot go further forward.
* @return Returns current progression through stages.
*/
fun getProgress() = Progress(Index(ordinal), Index(values().size - 1))
}

View File

@ -7,9 +7,9 @@ import cash.z.ecc.sdk.model.WalletAddress
import cash.z.ecc.sdk.model.ZecSend
import kotlinx.coroutines.runBlocking
private const val KEY_ADDRESS = "address"
private const val KEY_AMOUNT = "amount"
private const val KEY_MEMO = "memo"
private const val KEY_ADDRESS = "address" // $NON-NLS
private const val KEY_AMOUNT = "amount" // $NON-NLS
private const val KEY_MEMO = "memo" // $NON-NLS
// Using a custom saver instead of Parcelize, to avoid adding an Android-specific API to
// the ZecSend class

View File

@ -7,7 +7,6 @@ import androidx.activity.viewModels
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.screen.backup.viewmodel.BackupViewModel
import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.screen.onboarding.viewmodel.OnboardingViewModel
import co.electriccoin.zcash.ui.screen.settings.view.Settings
@ -48,9 +47,6 @@ private fun WrapSettings(
val onboardingViewModel by activity.viewModels<OnboardingViewModel>()
onboardingViewModel.onboardingState.goToBeginning()
onboardingViewModel.isImporting.value = false
val backupViewModel by activity.viewModels<BackupViewModel>()
backupViewModel.backupState.goToBeginning()
}
)
}

View File

@ -1,6 +1,9 @@
<resources>
<string name="new_wallet_clipboard_tag">Zcash Seed Phrase</string>
<string name="new_wallet_navigation_back_button_content_description">Back</string>
<string name="new_wallet_toolbar_more_button_content_description">More</string>
<string name="new_wallet_1_header">First things first</string>
<string name="new_wallet_1_body_1">It is important to understand that you are in charge here. Great, right? YOU get to be the bank!</string>
<string name="new_wallet_1_body_2">But it also means that YOU are the customer, and you need to be self-reliant.\n\nSo how do you recover funds that youve hidden on a complete decentralized and private block-chain?</string>
@ -17,13 +20,12 @@
<string name="new_wallet_3_button_finished">Finished!</string>
<string name="new_wallet_3_button_copy">Copy to buffer</string>
<string name="new_wallet_4_header_verify">Verify Your Backup</string>
<string name="new_wallet_4_header">Verify Your Backup</string>
<string name="new_wallet_4_header_ouch">Ouch, sorry, no.</string>
<string name="new_wallet_4_body_verify">Drag the words below to match your backed-up copy.</string>
<string name="new_wallet_4_body_ouch">Your placed words did not match your secret recovery phrase.</string>
<string name="new_wallet_4_body_ouch_retry">Your placed words did not match your secret recovery phrase.\n\nRemember, you cant recover your funds if you lose (or incorrectly save) these 24 words.</string>
<string name="new_wallet_4_button_retry">Im ready to try again</string>
<string name="new_wallet_4_button_back">Back</string>
<string name="new_wallet_5_header">Success!</string>
<string name="new_wallet_5_body">Place that backup somewhere safe and venture forth in security.</string>

View File

@ -67,6 +67,7 @@ dependencies {
implementation(libs.androidx.compose.test.junit)
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.startup)
implementation(libs.androidx.uiAutomator)
if (isOrchestratorEnabled) {

View File

@ -1,4 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:icon="@mipmap/ic_launcher_square"
android:roundIcon="@mipmap/ic_launcher_round">
<!-- Now we use androidx-startup for components auto-initialization -->
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="remove" />
</application>
</manifest>

View File

@ -233,6 +233,7 @@ class ScreenshotTest : UiTestPrerequisites() {
// Settings is a subscreen of profile
composeTestRule.onNode(hasText(resContext.getString(R.string.profile_settings))).also {
it.performScrollTo()
it.assertExists()
it.performClick()
}
@ -246,6 +247,7 @@ class ScreenshotTest : UiTestPrerequisites() {
// Address Details is a subscreen of profile
composeTestRule.onNode(hasText(resContext.getString(R.string.profile_see_address_details))).also {
it.performScrollTo()
it.assertExists()
it.performClick()
}
@ -273,7 +275,6 @@ class ScreenshotTest : UiTestPrerequisites() {
composeTestRule.onNode(hasText(resContext.getString(R.string.profile_title))).also {
it.assertExists()
it.performClick()
}
// About is a subscreen of profile
@ -309,6 +310,7 @@ class ScreenshotTest : UiTestPrerequisites() {
composeTestRule.onNode(hasText(resContext.getString(R.string.home_button_send))).also {
it.assertExists()
it.performScrollTo()
it.performClick()
}
composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.synchronizer.value != null }
@ -367,7 +369,6 @@ private fun backupScreenshots(resContext: Context, tag: String, composeTestRule:
ScreenshotTest.takeScreenshot(tag, "Backup 1")
composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_1_button)).also {
it.performScrollTo()
it.performClick()
}
@ -377,7 +378,6 @@ private fun backupScreenshots(resContext: Context, tag: String, composeTestRule:
ScreenshotTest.takeScreenshot(tag, "Backup 2")
composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_2_button)).also {
it.performScrollTo()
it.performClick()
}
@ -387,26 +387,29 @@ private fun backupScreenshots(resContext: Context, tag: String, composeTestRule:
ScreenshotTest.takeScreenshot(tag, "Backup 3")
composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_3_button_finished)).also {
it.performScrollTo()
it.performClick()
}
composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_4_header_verify)).also {
composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_4_header)).also {
it.assertExists()
}
ScreenshotTest.takeScreenshot(tag, "Backup 4")
// Fail test first
composeTestRule.onAllNodesWithTag(BackupTag.DROPDOWN_CHIP).also {
it[0].performScrollTo()
it[0].performClick()
composeTestRule.onNode(hasTestTag(BackupTag.DROPDOWN_MENU)).onChildren()[0].performClick()
it[1].performScrollTo()
it[1].performClick()
composeTestRule.onNode(hasTestTag(BackupTag.DROPDOWN_MENU)).onChildren()[1].performClick()
it[2].performScrollTo()
it[2].performClick()
composeTestRule.onNode(hasTestTag(BackupTag.DROPDOWN_MENU)).onChildren()[2].performClick()
it[3].performScrollTo()
it[3].performClick()
composeTestRule.onNode(hasTestTag(BackupTag.DROPDOWN_MENU)).onChildren()[3].performClick()
}
@ -416,7 +419,6 @@ private fun backupScreenshots(resContext: Context, tag: String, composeTestRule:
}
composeTestRule.onNode(hasText(resContext.getString(R.string.new_wallet_4_button_retry))).also {
it.performScrollTo()
it.performClick()
}
@ -424,26 +426,29 @@ private fun backupScreenshots(resContext: Context, tag: String, composeTestRule:
it.assertExists()
}
composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_3_button_finished)).also {
it.performScrollTo()
it.performClick()
}
composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_4_header_verify)).also {
composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_4_header)).also {
it.assertExists()
}
composeTestRule.onAllNodesWithTag(BackupTag.DROPDOWN_CHIP).also {
it.assertCountEquals(4)
it[0].performScrollTo()
it[0].performClick()
composeTestRule.onNode(hasTestTag(BackupTag.DROPDOWN_MENU)).onChildren()[1].performClick()
it[1].performScrollTo()
it[1].performClick()
composeTestRule.onNode(hasTestTag(BackupTag.DROPDOWN_MENU)).onChildren()[0].performClick()
it[2].performScrollTo()
it[2].performClick()
composeTestRule.onNode(hasTestTag(BackupTag.DROPDOWN_MENU)).onChildren()[3].performClick()
it[3].performScrollTo()
it[3].performClick()
composeTestRule.onNode(hasTestTag(BackupTag.DROPDOWN_MENU)).onChildren()[2].performClick()
}