[#792] Add Sending Screens
* [#804][Design system] Paddings - Dimens - Initial commit to ensure it meets our requirements - This solution is now in parallel with a simpler existing Paddings solution - Four spacing groups defined - Provides a way to define default and custom spacing values - Distinguished by screen size, but other metrics also available (e.g. orientation, aspect ratio, layout direction or screen shape) * Fix spacing value * Move spacings change logic to comment - We've decided to have only one regular spacing group for now, which is suitable for most of current phone devices * Move Dimens out of internal package * Link TODO for later Paddings remove * Link issue of Use Dimens across the app to TODO * [#792] Add sending screens - This extends the Send screen logic with several subscreens - A transaction submission connected to the Synchronizer - Some logic moved to Android specific class - System back navigation handler implemented - Styling with dimes introduced to all Send screens - New UI, Android specific, integration and screenshot tests added - Additionally we could consider implementing a proper SendState mechanism, as we do in e.g. BackupState * File issue of problematic pseudolocales texts inserted in Send.Form screen * Add scrolling to send view * Switch SendViewIntegrationTest away from TestSetup * Provide MockSynchronizer to integration test --------- Co-authored-by: Carter Jernigan <git@carterjernigan.com>
This commit is contained in:
parent
42e640b874
commit
543d5f2b59
|
@ -16,6 +16,6 @@ object ZecSendFixture {
|
|||
suspend fun new(
|
||||
address: String = ADDRESS,
|
||||
amount: Zatoshi = AMOUNT,
|
||||
message: Memo = MEMO
|
||||
) = ZecSend(WalletAddress.Unified.new(address), amount, message)
|
||||
memo: Memo = MEMO
|
||||
) = ZecSend(WalletAddress.Unified.new(address), amount, memo)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,168 @@
|
|||
package co.electriccoin.zcash.ui.fixture
|
||||
|
||||
import cash.z.ecc.android.sdk.CloseableSynchronizer
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
|
||||
import cash.z.ecc.android.sdk.model.Account
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.model.PendingTransaction
|
||||
import cash.z.ecc.android.sdk.model.Transaction
|
||||
import cash.z.ecc.android.sdk.model.TransactionOverview
|
||||
import cash.z.ecc.android.sdk.model.TransactionRecipient
|
||||
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
|
||||
import cash.z.ecc.android.sdk.model.WalletBalance
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.type.AddressType
|
||||
import cash.z.ecc.android.sdk.type.ConsensusMatchType
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
|
||||
/**
|
||||
* Mocked Synchronizer that can be used instead of the production SdkSynchronizer e.g. for tests.
|
||||
*/
|
||||
@Suppress("TooManyFunctions", "UNUSED_PARAMETER")
|
||||
internal class MockSynchronizer : CloseableSynchronizer {
|
||||
|
||||
override val clearedTransactions: Flow<List<TransactionOverview>>
|
||||
get() = error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
|
||||
|
||||
override val latestBirthdayHeight: BlockHeight
|
||||
get() = error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
|
||||
|
||||
override val latestHeight: BlockHeight
|
||||
get() = error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
|
||||
|
||||
override val network: ZcashNetwork
|
||||
get() = error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
|
||||
|
||||
override val networkHeight: StateFlow<BlockHeight?>
|
||||
get() = error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
|
||||
|
||||
override var onChainErrorHandler: ((BlockHeight, BlockHeight) -> Any)?
|
||||
get() = error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
|
||||
set(value) {}
|
||||
|
||||
override var onCriticalErrorHandler: ((Throwable?) -> Boolean)?
|
||||
get() = error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
|
||||
set(value) {}
|
||||
|
||||
override var onProcessorErrorHandler: ((Throwable?) -> Boolean)?
|
||||
get() = error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
|
||||
set(value) {}
|
||||
|
||||
override var onSetupErrorHandler: ((Throwable?) -> Boolean)?
|
||||
get() = error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
|
||||
set(value) {}
|
||||
|
||||
override var onSubmissionErrorHandler: ((Throwable?) -> Boolean)?
|
||||
get() = error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
|
||||
set(value) {}
|
||||
|
||||
override val orchardBalances: StateFlow<WalletBalance?>
|
||||
get() = error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
|
||||
|
||||
override val pendingTransactions: Flow<List<PendingTransaction>>
|
||||
get() = error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
|
||||
|
||||
override val processorInfo: Flow<CompactBlockProcessor.ProcessorInfo>
|
||||
get() = error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
|
||||
|
||||
override val progress: Flow<Int>
|
||||
get() = error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
|
||||
|
||||
override val receivedTransactions: Flow<List<Transaction.Received>>
|
||||
get() = error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
|
||||
|
||||
override val saplingBalances: StateFlow<WalletBalance?>
|
||||
get() = error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
|
||||
|
||||
override val sentTransactions: Flow<List<Transaction.Sent>>
|
||||
get() = error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
|
||||
|
||||
override val status: Flow<Synchronizer.Status>
|
||||
get() = error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
|
||||
|
||||
override val transparentBalances: StateFlow<WalletBalance?>
|
||||
get() = error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
|
||||
|
||||
override fun close() {
|
||||
error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
|
||||
}
|
||||
|
||||
override fun getMemos(transactionOverview: TransactionOverview): Flow<String> {
|
||||
error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
|
||||
}
|
||||
|
||||
override suspend fun getNearestRewindHeight(height: BlockHeight): BlockHeight {
|
||||
error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
|
||||
}
|
||||
|
||||
override fun getRecipients(transactionOverview: TransactionOverview): Flow<TransactionRecipient> {
|
||||
error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
|
||||
}
|
||||
|
||||
override suspend fun getSaplingAddress(account: Account): String {
|
||||
error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
|
||||
}
|
||||
|
||||
override suspend fun getTransparentAddress(account: Account): String {
|
||||
error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
|
||||
}
|
||||
|
||||
override suspend fun getTransparentBalance(tAddr: String): WalletBalance {
|
||||
error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
|
||||
}
|
||||
|
||||
override suspend fun getUnifiedAddress(account: Account): String {
|
||||
error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
|
||||
}
|
||||
|
||||
override suspend fun isValidShieldedAddr(address: String): Boolean {
|
||||
error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
|
||||
}
|
||||
|
||||
override suspend fun isValidTransparentAddr(address: String): Boolean {
|
||||
error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
|
||||
}
|
||||
|
||||
override suspend fun isValidUnifiedAddr(address: String): Boolean {
|
||||
error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
|
||||
}
|
||||
|
||||
override suspend fun quickRewind() {
|
||||
error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
|
||||
}
|
||||
|
||||
override suspend fun refreshUtxos(account: Account, since: BlockHeight): Int? {
|
||||
error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
|
||||
}
|
||||
|
||||
override suspend fun rewindToNearestHeight(height: BlockHeight, alsoClearBlockCache: Boolean) {
|
||||
error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
|
||||
}
|
||||
|
||||
/**
|
||||
* This method intentionally returns empty flow, as the PendingTransaction is only an SDK internal class.
|
||||
*/
|
||||
override fun sendToAddress(usk: UnifiedSpendingKey, amount: Zatoshi, toAddress: String, memo: String): Flow<PendingTransaction> {
|
||||
return emptyFlow()
|
||||
}
|
||||
|
||||
override fun shieldFunds(usk: UnifiedSpendingKey, memo: String): Flow<PendingTransaction> {
|
||||
error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
|
||||
}
|
||||
|
||||
override suspend fun validateAddress(address: String): AddressType {
|
||||
error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
|
||||
}
|
||||
|
||||
override suspend fun validateConsensusBranch(): ConsensusMatchType {
|
||||
error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun new() = MockSynchronizer()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
package co.electriccoin.zcash.ui.screen.send
|
||||
|
||||
import androidx.compose.ui.test.assertIsEnabled
|
||||
import androidx.compose.ui.test.assertIsNotEnabled
|
||||
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performTextClearance
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
import cash.z.ecc.android.sdk.fixture.WalletAddressFixture
|
||||
import cash.z.ecc.android.sdk.model.MonetarySeparators
|
||||
import cash.z.ecc.sdk.fixture.MemoFixture
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.test.getStringResource
|
||||
|
||||
internal fun ComposeContentTestRule.clickBack() {
|
||||
onNodeWithContentDescription(getStringResource(R.string.send_back_content_description)).also {
|
||||
it.performClick()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ComposeContentTestRule.setValidAmount() {
|
||||
onNodeWithText(getStringResource(R.string.send_amount)).also {
|
||||
val separators = MonetarySeparators.current()
|
||||
it.performTextClearance()
|
||||
it.performTextInput("123${separators.decimal}456")
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ComposeContentTestRule.setAmount(amount: String) {
|
||||
onNodeWithText(getStringResource(R.string.send_amount)).also {
|
||||
it.performTextClearance()
|
||||
it.performTextInput(amount)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ComposeContentTestRule.setValidAddress() {
|
||||
onNodeWithText(getStringResource(R.string.send_to)).also {
|
||||
it.performTextClearance()
|
||||
it.performTextInput(WalletAddressFixture.UNIFIED_ADDRESS_STRING)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ComposeContentTestRule.setAddress(address: String) {
|
||||
onNodeWithText(getStringResource(R.string.send_to)).also {
|
||||
it.performTextClearance()
|
||||
it.performTextInput(address)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ComposeContentTestRule.setValidMemo() {
|
||||
onNodeWithText(getStringResource(R.string.send_memo)).also {
|
||||
it.performTextClearance()
|
||||
it.performTextInput(MemoFixture.MEMO_STRING)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ComposeContentTestRule.setMemo(memo: String) {
|
||||
onNodeWithText(getStringResource(R.string.send_memo)).also {
|
||||
it.performTextClearance()
|
||||
it.performTextInput(memo)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ComposeContentTestRule.clickCreateAndSend() {
|
||||
onNodeWithText(getStringResource(R.string.send_create)).also {
|
||||
it.performClick()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ComposeContentTestRule.clickConfirmation() {
|
||||
onNodeWithText(getStringResource(R.string.send_confirmation_button)).also {
|
||||
it.performClick()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ComposeContentTestRule.assertOnForm() {
|
||||
onNodeWithText(getStringResource(R.string.send_create)).also {
|
||||
it.assertExists()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ComposeContentTestRule.assertOnConfirmation() {
|
||||
onNodeWithText(getStringResource(R.string.send_confirmation_button)).also {
|
||||
it.assertExists()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ComposeContentTestRule.assertOnSending() {
|
||||
onNodeWithText(getStringResource(R.string.send_in_progress_wait)).also {
|
||||
it.assertExists()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ComposeContentTestRule.assertOnSendSuccessful() {
|
||||
onNodeWithText(getStringResource(R.string.send_successful_title)).also {
|
||||
it.assertExists()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ComposeContentTestRule.assertOnSendFailure() {
|
||||
onNodeWithText(getStringResource(R.string.send_failure_title)).also {
|
||||
it.assertExists()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ComposeContentTestRule.assertSendEnabled() {
|
||||
onNodeWithText(getStringResource(R.string.send_create)).also {
|
||||
it.assertIsEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ComposeContentTestRule.assertSendDisabled() {
|
||||
onNodeWithText(getStringResource(R.string.send_create)).also {
|
||||
it.assertIsNotEnabled()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
package co.electriccoin.zcash.ui.screen.send
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
||||
import cash.z.ecc.android.sdk.model.ZecSend
|
||||
import cash.z.ecc.sdk.fixture.ZatoshiFixture
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.screen.send.ext.Saver
|
||||
import co.electriccoin.zcash.ui.screen.send.model.SendStage
|
||||
import co.electriccoin.zcash.ui.screen.send.view.Send
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
class SendViewTestSetup(
|
||||
private val composeTestRule: ComposeContentTestRule,
|
||||
private val initialState: SendStage,
|
||||
private val initialZecSend: ZecSend?
|
||||
) {
|
||||
private val onBackCount = AtomicInteger(0)
|
||||
private val onCreateCount = AtomicInteger(0)
|
||||
val mutableActionExecuted = MutableStateFlow(false)
|
||||
|
||||
@Volatile
|
||||
private var lastSendStage: SendStage? = null
|
||||
|
||||
@Volatile
|
||||
private var lastZecSend: ZecSend? = null
|
||||
|
||||
fun getOnBackCount(): Int {
|
||||
composeTestRule.waitForIdle()
|
||||
return onBackCount.get()
|
||||
}
|
||||
|
||||
fun getOnCreateCount(): Int {
|
||||
composeTestRule.waitForIdle()
|
||||
return onCreateCount.get()
|
||||
}
|
||||
|
||||
fun getLastZecSend(): ZecSend? {
|
||||
composeTestRule.waitForIdle()
|
||||
return lastZecSend
|
||||
}
|
||||
|
||||
fun getLastSendStage(): SendStage? {
|
||||
composeTestRule.waitForIdle()
|
||||
return lastSendStage
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Suppress("TestFunctionName")
|
||||
fun DefaultContent() {
|
||||
val (sendStage, setSendStage) =
|
||||
rememberSaveable { mutableStateOf(initialState) }
|
||||
|
||||
lastSendStage = sendStage
|
||||
|
||||
val onBackAction = {
|
||||
onBackCount.incrementAndGet()
|
||||
when (sendStage) {
|
||||
SendStage.Form -> {}
|
||||
SendStage.Confirmation -> setSendStage(SendStage.Form)
|
||||
SendStage.Sending -> {}
|
||||
SendStage.SendFailure -> setSendStage(SendStage.Form)
|
||||
SendStage.SendSuccessful -> {}
|
||||
}
|
||||
}
|
||||
|
||||
BackHandler {
|
||||
onBackAction()
|
||||
}
|
||||
|
||||
val (zecSend, setZecSend) =
|
||||
rememberSaveable(stateSaver = ZecSend.Saver) { mutableStateOf(initialZecSend) }
|
||||
|
||||
lastZecSend = zecSend
|
||||
|
||||
ZcashTheme {
|
||||
Send(
|
||||
mySpendableBalance = ZatoshiFixture.new(),
|
||||
sendStage = sendStage,
|
||||
onSendStageChange = setSendStage,
|
||||
zecSend = zecSend,
|
||||
onZecSendChange = setZecSend,
|
||||
onBack = onBackAction,
|
||||
onCreateAndSend = {
|
||||
onCreateCount.incrementAndGet()
|
||||
lastZecSend = it
|
||||
mutableActionExecuted.update { true }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun setDefaultContent() {
|
||||
composeTestRule.setContent {
|
||||
DefaultContent()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package co.electriccoin.zcash.ui.screen.send.ext
|
||||
|
||||
import androidx.test.filters.SmallTest
|
||||
import cash.z.ecc.sdk.fixture.MemoFixture
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.test.getAppContext
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class MemoExtTest {
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun value_or_empty_char_test_empty_input() {
|
||||
val actual = MemoFixture.new(memoString = "").valueOrEmptyChar(getAppContext())
|
||||
|
||||
val expected = getAppContext().getString(R.string.empty_char)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun value_or_empty_char_test_non_empty_input() {
|
||||
val actual = MemoFixture.new(memoString = MemoFixture.MEMO_STRING).valueOrEmptyChar(getAppContext())
|
||||
|
||||
val expected = MemoFixture.MEMO_STRING
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
package co.electriccoin.zcash.ui.screen.send.integration
|
||||
|
||||
import androidx.compose.ui.test.assertTextContains
|
||||
import androidx.compose.ui.test.junit4.StateRestorationTester
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.test.filters.MediumTest
|
||||
import cash.z.ecc.android.sdk.fixture.WalletFixture
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.sdk.fixture.ZatoshiFixture
|
||||
import cash.z.ecc.sdk.fixture.ZecSendFixture
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.fixture.MockSynchronizer
|
||||
import co.electriccoin.zcash.ui.screen.send.WrapSend
|
||||
import co.electriccoin.zcash.ui.screen.send.assertOnConfirmation
|
||||
import co.electriccoin.zcash.ui.screen.send.assertOnForm
|
||||
import co.electriccoin.zcash.ui.screen.send.clickBack
|
||||
import co.electriccoin.zcash.ui.screen.send.clickCreateAndSend
|
||||
import co.electriccoin.zcash.ui.screen.send.setAddress
|
||||
import co.electriccoin.zcash.ui.screen.send.setAmount
|
||||
import co.electriccoin.zcash.ui.screen.send.setMemo
|
||||
import co.electriccoin.zcash.ui.test.getStringResource
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class SendViewIntegrationTest {
|
||||
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
private val wallet = WalletFixture.Alice
|
||||
private val network = ZcashNetwork.Testnet
|
||||
private val spendingKey = runBlocking {
|
||||
WalletFixture.Alice.getUnifiedSpendingKey(
|
||||
seed = wallet.seedPhrase,
|
||||
network = network
|
||||
)
|
||||
}
|
||||
private val synchronizer = MockSynchronizer.new()
|
||||
private val balance = ZatoshiFixture.new()
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun send_screens_values_state_restoration() {
|
||||
val restorationTester = StateRestorationTester(composeTestRule)
|
||||
|
||||
val expectedAmount = ZecSendFixture.AMOUNT.value
|
||||
val expectedAddress = ZecSendFixture.ADDRESS
|
||||
val expectedMemo = ZecSendFixture.MEMO.value
|
||||
|
||||
restorationTester.setContent {
|
||||
WrapSend(
|
||||
synchronizer = synchronizer,
|
||||
spendableBalance = balance,
|
||||
spendingKey = spendingKey,
|
||||
goBack = {}
|
||||
)
|
||||
}
|
||||
|
||||
// Fill form
|
||||
composeTestRule.assertOnForm()
|
||||
composeTestRule.setAddress(expectedAddress)
|
||||
composeTestRule.setAmount(expectedAmount.toString())
|
||||
composeTestRule.setMemo(expectedMemo)
|
||||
|
||||
// Move to confirmation
|
||||
composeTestRule.clickCreateAndSend()
|
||||
composeTestRule.assertOnConfirmation()
|
||||
|
||||
restorationTester.emulateSavedInstanceStateRestore()
|
||||
|
||||
// Check if stage recreated correctly
|
||||
composeTestRule.assertOnConfirmation()
|
||||
|
||||
// Move back to form
|
||||
composeTestRule.clickBack()
|
||||
composeTestRule.assertOnForm()
|
||||
|
||||
// And check recreated form values too
|
||||
// We use that the assertTextContains searches in SemanticsProperties.EditableText too
|
||||
// Note also that we don't check the amount field value, as it's changed by validation mechanisms
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.send_to)).also {
|
||||
it.assertTextContains(ZecSendFixture.ADDRESS)
|
||||
}
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.send_memo)).also {
|
||||
it.assertTextContains(ZecSendFixture.MEMO.value)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
package co.electriccoin.zcash.ui.screen.send.view
|
||||
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.test.espresso.Espresso
|
||||
import androidx.test.filters.MediumTest
|
||||
import cash.z.ecc.android.sdk.model.ZecSend
|
||||
import cash.z.ecc.sdk.fixture.ZecSendFixture
|
||||
import co.electriccoin.zcash.test.UiTestPrerequisites
|
||||
import co.electriccoin.zcash.ui.screen.send.SendViewTestSetup
|
||||
import co.electriccoin.zcash.ui.screen.send.assertOnConfirmation
|
||||
import co.electriccoin.zcash.ui.screen.send.assertOnForm
|
||||
import co.electriccoin.zcash.ui.screen.send.assertOnSendFailure
|
||||
import co.electriccoin.zcash.ui.screen.send.assertOnSendSuccessful
|
||||
import co.electriccoin.zcash.ui.screen.send.assertOnSending
|
||||
import co.electriccoin.zcash.ui.screen.send.clickConfirmation
|
||||
import co.electriccoin.zcash.ui.screen.send.model.SendStage
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
// Non-multiplatform tests that require interacting with the Android system (e.g. system back navigation)
|
||||
// These don't have persistent state, so they are still unit tests.
|
||||
class SendViewAndroidTest : UiTestPrerequisites() {
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
private fun newTestSetup(
|
||||
sendStage: SendStage = SendStage.Form,
|
||||
zecSend: ZecSend? = null
|
||||
) = SendViewTestSetup(
|
||||
composeTestRule,
|
||||
sendStage,
|
||||
zecSend
|
||||
).apply {
|
||||
setDefaultContent()
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun back_on_sending_with_system_navigation_disabled_check() {
|
||||
val testSetup = newTestSetup(
|
||||
SendStage.Confirmation,
|
||||
runBlocking { ZecSendFixture.new() }
|
||||
)
|
||||
|
||||
assertEquals(0, testSetup.getOnBackCount())
|
||||
|
||||
composeTestRule.assertOnConfirmation()
|
||||
composeTestRule.clickConfirmation()
|
||||
composeTestRule.assertOnSending()
|
||||
|
||||
Espresso.pressBack()
|
||||
|
||||
composeTestRule.assertOnSending()
|
||||
|
||||
assertEquals(1, testSetup.getOnBackCount())
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun back_on_send_successful_with_system_navigation() {
|
||||
val testSetup = newTestSetup(
|
||||
SendStage.SendSuccessful,
|
||||
runBlocking { ZecSendFixture.new() }
|
||||
)
|
||||
|
||||
assertEquals(0, testSetup.getOnBackCount())
|
||||
|
||||
composeTestRule.assertOnSendSuccessful()
|
||||
|
||||
Espresso.pressBack()
|
||||
|
||||
assertEquals(1, testSetup.getOnBackCount())
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun back_on_send_failure_with_system_navigation() {
|
||||
val testSetup = newTestSetup(
|
||||
SendStage.SendFailure,
|
||||
runBlocking { ZecSendFixture.new() }
|
||||
)
|
||||
|
||||
assertEquals(0, testSetup.getOnBackCount())
|
||||
|
||||
composeTestRule.assertOnSendFailure()
|
||||
|
||||
Espresso.pressBack()
|
||||
|
||||
composeTestRule.assertOnForm()
|
||||
|
||||
assertEquals(1, testSetup.getOnBackCount())
|
||||
}
|
||||
}
|
|
@ -1,14 +1,10 @@
|
|||
package co.electriccoin.zcash.ui.screen.send.view
|
||||
|
||||
import androidx.compose.ui.test.assertIsEnabled
|
||||
import androidx.compose.ui.test.assertIsNotEnabled
|
||||
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performTextClearance
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
import androidx.test.filters.MediumTest
|
||||
import cash.z.ecc.android.sdk.ext.collectWith
|
||||
import cash.z.ecc.android.sdk.fixture.WalletAddressFixture
|
||||
|
@ -16,22 +12,35 @@ import cash.z.ecc.android.sdk.model.Memo
|
|||
import cash.z.ecc.android.sdk.model.MonetarySeparators
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.android.sdk.model.ZecSend
|
||||
import cash.z.ecc.sdk.fixture.MemoFixture
|
||||
import cash.z.ecc.sdk.fixture.ZatoshiFixture
|
||||
import cash.z.ecc.sdk.fixture.ZecRequestFixture
|
||||
import cash.z.ecc.sdk.fixture.ZecSendFixture
|
||||
import co.electriccoin.zcash.test.UiTestPrerequisites
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.screen.send.SendViewTestSetup
|
||||
import co.electriccoin.zcash.ui.screen.send.assertOnConfirmation
|
||||
import co.electriccoin.zcash.ui.screen.send.assertOnForm
|
||||
import co.electriccoin.zcash.ui.screen.send.assertOnSendFailure
|
||||
import co.electriccoin.zcash.ui.screen.send.assertOnSendSuccessful
|
||||
import co.electriccoin.zcash.ui.screen.send.assertOnSending
|
||||
import co.electriccoin.zcash.ui.screen.send.assertSendDisabled
|
||||
import co.electriccoin.zcash.ui.screen.send.assertSendEnabled
|
||||
import co.electriccoin.zcash.ui.screen.send.clickBack
|
||||
import co.electriccoin.zcash.ui.screen.send.clickConfirmation
|
||||
import co.electriccoin.zcash.ui.screen.send.clickCreateAndSend
|
||||
import co.electriccoin.zcash.ui.screen.send.model.SendStage
|
||||
import co.electriccoin.zcash.ui.screen.send.setAmount
|
||||
import co.electriccoin.zcash.ui.screen.send.setMemo
|
||||
import co.electriccoin.zcash.ui.screen.send.setValidAddress
|
||||
import co.electriccoin.zcash.ui.screen.send.setValidAmount
|
||||
import co.electriccoin.zcash.ui.screen.send.setValidMemo
|
||||
import co.electriccoin.zcash.ui.test.getStringResource
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
@ -40,11 +49,22 @@ class SendViewTest : UiTestPrerequisites() {
|
|||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
private fun newTestSetup(
|
||||
sendStage: SendStage = SendStage.Form,
|
||||
zecSend: ZecSend? = null
|
||||
) = SendViewTestSetup(
|
||||
composeTestRule,
|
||||
sendStage,
|
||||
zecSend
|
||||
).apply {
|
||||
setDefaultContent()
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun create_button_disabled() {
|
||||
@Suppress("UNUSED_VARIABLE")
|
||||
val testSetup = TestSetup(composeTestRule)
|
||||
val testSetup = newTestSetup()
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.send_create)).also {
|
||||
it.assertExists()
|
||||
|
@ -56,10 +76,10 @@ class SendViewTest : UiTestPrerequisites() {
|
|||
@MediumTest
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun create_request_no_memo() = runTest {
|
||||
val testSetup = TestSetup(composeTestRule)
|
||||
val testSetup = newTestSetup()
|
||||
|
||||
assertEquals(0, testSetup.getOnCreateCount())
|
||||
assertEquals(null, testSetup.getLastSend())
|
||||
assertEquals(null, testSetup.getLastZecSend())
|
||||
|
||||
composeTestRule.setValidAmount()
|
||||
composeTestRule.setValidAddress()
|
||||
|
@ -74,7 +94,7 @@ class SendViewTest : UiTestPrerequisites() {
|
|||
assertEquals(1, testSetup.getOnCreateCount())
|
||||
|
||||
launch {
|
||||
testSetup.getLastSend().also {
|
||||
testSetup.getLastZecSend().also {
|
||||
assertNotNull(it)
|
||||
assertEquals(WalletAddressFixture.unified(), it.destination)
|
||||
assertEquals(Zatoshi(12345678900000), it.amount)
|
||||
|
@ -90,10 +110,10 @@ class SendViewTest : UiTestPrerequisites() {
|
|||
@MediumTest
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun create_request_with_memo() = runTest {
|
||||
val testSetup = TestSetup(composeTestRule)
|
||||
val testSetup = newTestSetup()
|
||||
|
||||
assertEquals(0, testSetup.getOnCreateCount())
|
||||
assertEquals(null, testSetup.getLastSend())
|
||||
assertEquals(null, testSetup.getLastZecSend())
|
||||
|
||||
composeTestRule.setValidAmount()
|
||||
composeTestRule.setValidAddress()
|
||||
|
@ -110,7 +130,7 @@ class SendViewTest : UiTestPrerequisites() {
|
|||
assertEquals(1, testSetup.getOnCreateCount())
|
||||
|
||||
launch {
|
||||
testSetup.getLastSend().also {
|
||||
testSetup.getLastZecSend().also {
|
||||
assertNotNull(it)
|
||||
assertEquals(WalletAddressFixture.unified(), it.destination)
|
||||
assertEquals(Zatoshi(12345678900000), it.amount)
|
||||
|
@ -126,11 +146,11 @@ class SendViewTest : UiTestPrerequisites() {
|
|||
@MediumTest
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun check_regex_functionality_valid_inputs() = runTest {
|
||||
val testSetup = TestSetup(composeTestRule)
|
||||
val testSetup = newTestSetup()
|
||||
val separators = MonetarySeparators.current()
|
||||
|
||||
assertEquals(0, testSetup.getOnCreateCount())
|
||||
assertEquals(null, testSetup.getLastSend())
|
||||
assertEquals(null, testSetup.getLastZecSend())
|
||||
composeTestRule.assertSendDisabled()
|
||||
|
||||
composeTestRule.setValidAmount()
|
||||
|
@ -168,7 +188,7 @@ class SendViewTest : UiTestPrerequisites() {
|
|||
assertEquals(1, testSetup.getOnCreateCount())
|
||||
|
||||
launch {
|
||||
testSetup.getLastSend().also {
|
||||
testSetup.getLastZecSend().also {
|
||||
assertNotNull(it)
|
||||
assertEquals(WalletAddressFixture.unified(), it.destination)
|
||||
assertEquals(Zatoshi(12345678900000), it.amount)
|
||||
|
@ -184,11 +204,11 @@ class SendViewTest : UiTestPrerequisites() {
|
|||
@MediumTest
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun check_regex_functionality_invalid_inputs() = runTest {
|
||||
val testSetup = TestSetup(composeTestRule)
|
||||
val testSetup = newTestSetup()
|
||||
val separators = MonetarySeparators.current()
|
||||
|
||||
assertEquals(0, testSetup.getOnCreateCount())
|
||||
assertEquals(null, testSetup.getLastSend())
|
||||
assertEquals(null, testSetup.getLastZecSend())
|
||||
composeTestRule.assertSendDisabled()
|
||||
|
||||
composeTestRule.setAmount("aaa")
|
||||
|
@ -214,7 +234,7 @@ class SendViewTest : UiTestPrerequisites() {
|
|||
composeTestRule.assertSendDisabled()
|
||||
|
||||
assertEquals(0, testSetup.getOnCreateCount())
|
||||
assertEquals(null, testSetup.getLastSend())
|
||||
assertEquals(null, testSetup.getLastZecSend())
|
||||
composeTestRule.assertSendDisabled()
|
||||
}
|
||||
|
||||
|
@ -222,7 +242,7 @@ class SendViewTest : UiTestPrerequisites() {
|
|||
@MediumTest
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun max_memo_length() = runTest {
|
||||
val testSetup = TestSetup(composeTestRule)
|
||||
val testSetup = newTestSetup()
|
||||
|
||||
composeTestRule.setValidAmount()
|
||||
composeTestRule.setValidAddress()
|
||||
|
@ -246,7 +266,7 @@ class SendViewTest : UiTestPrerequisites() {
|
|||
assertEquals(1, testSetup.getOnCreateCount())
|
||||
|
||||
launch {
|
||||
testSetup.getLastSend().also {
|
||||
testSetup.getLastZecSend().also {
|
||||
assertNotNull(it)
|
||||
assertEquals(WalletAddressFixture.unified(), it.destination)
|
||||
assertEquals(Zatoshi(12345600000), it.amount)
|
||||
|
@ -261,7 +281,7 @@ class SendViewTest : UiTestPrerequisites() {
|
|||
@Test
|
||||
@MediumTest
|
||||
fun back_on_form() {
|
||||
val testSetup = TestSetup(composeTestRule)
|
||||
val testSetup = newTestSetup()
|
||||
|
||||
assertEquals(0, testSetup.getOnBackCount())
|
||||
|
||||
|
@ -273,7 +293,7 @@ class SendViewTest : UiTestPrerequisites() {
|
|||
@Test
|
||||
@MediumTest
|
||||
fun back_on_confirmation() {
|
||||
val testSetup = TestSetup(composeTestRule)
|
||||
val testSetup = newTestSetup()
|
||||
|
||||
assertEquals(0, testSetup.getOnBackCount())
|
||||
|
||||
|
@ -284,127 +304,95 @@ class SendViewTest : UiTestPrerequisites() {
|
|||
composeTestRule.clickBack()
|
||||
composeTestRule.assertOnForm()
|
||||
|
||||
assertEquals(1, testSetup.getOnBackCount())
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun back_on_sending_disabled_check() {
|
||||
newTestSetup(
|
||||
SendStage.Confirmation,
|
||||
runBlocking { ZecSendFixture.new() }
|
||||
)
|
||||
|
||||
composeTestRule.assertOnConfirmation()
|
||||
composeTestRule.clickConfirmation()
|
||||
composeTestRule.assertOnSending()
|
||||
|
||||
composeTestRule.onNodeWithContentDescription(getStringResource(R.string.send_back_content_description)).also {
|
||||
it.assertDoesNotExist()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun back_on_send_successful() {
|
||||
val testSetup = newTestSetup(
|
||||
SendStage.SendSuccessful,
|
||||
runBlocking { ZecSendFixture.new() }
|
||||
)
|
||||
|
||||
assertEquals(0, testSetup.getOnBackCount())
|
||||
|
||||
composeTestRule.assertOnSendSuccessful()
|
||||
composeTestRule.clickBack()
|
||||
|
||||
assertEquals(1, testSetup.getOnBackCount())
|
||||
}
|
||||
|
||||
private class TestSetup(private val composeTestRule: ComposeContentTestRule) {
|
||||
@Test
|
||||
@MediumTest
|
||||
fun close_on_send_successful() {
|
||||
val testSetup = newTestSetup(
|
||||
SendStage.SendSuccessful,
|
||||
runBlocking { ZecSendFixture.new() }
|
||||
)
|
||||
|
||||
private val onBackCount = AtomicInteger(0)
|
||||
private val onCreateCount = AtomicInteger(0)
|
||||
val mutableActionExecuted = MutableStateFlow(false)
|
||||
assertEquals(0, testSetup.getOnBackCount())
|
||||
|
||||
@Volatile
|
||||
private var onSendZecRequest: ZecSend? = null
|
||||
|
||||
fun getOnBackCount(): Int {
|
||||
composeTestRule.waitForIdle()
|
||||
return onBackCount.get()
|
||||
composeTestRule.assertOnSendSuccessful()
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.send_successful_button)).also {
|
||||
it.assertExists()
|
||||
it.performClick()
|
||||
}
|
||||
|
||||
fun getOnCreateCount(): Int {
|
||||
composeTestRule.waitForIdle()
|
||||
return onCreateCount.get()
|
||||
assertEquals(1, testSetup.getOnBackCount())
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun back_on_send_failure() {
|
||||
val testSetup = newTestSetup(
|
||||
SendStage.SendFailure,
|
||||
runBlocking { ZecSendFixture.new() }
|
||||
)
|
||||
|
||||
assertEquals(0, testSetup.getOnBackCount())
|
||||
|
||||
composeTestRule.assertOnSendFailure()
|
||||
composeTestRule.clickBack()
|
||||
composeTestRule.assertOnForm()
|
||||
|
||||
assertEquals(1, testSetup.getOnBackCount())
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun close_on_send_failure() {
|
||||
val testSetup = newTestSetup(
|
||||
SendStage.SendFailure,
|
||||
runBlocking { ZecSendFixture.new() }
|
||||
)
|
||||
|
||||
assertEquals(0, testSetup.getOnBackCount())
|
||||
|
||||
composeTestRule.assertOnSendFailure()
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.send_failure_button)).also {
|
||||
it.assertExists()
|
||||
it.performClick()
|
||||
}
|
||||
composeTestRule.assertOnForm()
|
||||
|
||||
fun getLastSend(): ZecSend? {
|
||||
composeTestRule.waitForIdle()
|
||||
return onSendZecRequest
|
||||
}
|
||||
|
||||
init {
|
||||
composeTestRule.setContent {
|
||||
ZcashTheme {
|
||||
Send(
|
||||
mySpendableBalance = ZatoshiFixture.new(),
|
||||
goBack = {
|
||||
onBackCount.incrementAndGet()
|
||||
},
|
||||
onCreateAndSend = {
|
||||
onCreateCount.incrementAndGet()
|
||||
onSendZecRequest = it
|
||||
mutableActionExecuted.update { true }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun ComposeContentTestRule.clickBack() {
|
||||
onNodeWithContentDescription(getStringResource(R.string.send_back_content_description)).also {
|
||||
it.performClick()
|
||||
}
|
||||
}
|
||||
|
||||
private fun ComposeContentTestRule.setValidAmount() {
|
||||
onNodeWithText(getStringResource(R.string.send_amount)).also {
|
||||
val separators = MonetarySeparators.current()
|
||||
it.performTextClearance()
|
||||
it.performTextInput("123${separators.decimal}456")
|
||||
}
|
||||
}
|
||||
|
||||
private fun ComposeContentTestRule.setAmount(amount: String) {
|
||||
onNodeWithText(getStringResource(R.string.send_amount)).also {
|
||||
it.performTextClearance()
|
||||
it.performTextInput(amount)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ComposeContentTestRule.setValidAddress() {
|
||||
onNodeWithText(getStringResource(R.string.send_to)).also {
|
||||
it.performTextClearance()
|
||||
it.performTextInput(WalletAddressFixture.UNIFIED_ADDRESS_STRING)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ComposeContentTestRule.setValidMemo() {
|
||||
onNodeWithText(getStringResource(R.string.send_memo)).also {
|
||||
it.performTextClearance()
|
||||
it.performTextInput(MemoFixture.MEMO_STRING)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ComposeContentTestRule.setMemo(memo: String) {
|
||||
onNodeWithText(getStringResource(R.string.send_memo)).also {
|
||||
it.performTextClearance()
|
||||
it.performTextInput(memo)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ComposeContentTestRule.clickCreateAndSend() {
|
||||
onNodeWithText(getStringResource(R.string.send_create)).also {
|
||||
it.performClick()
|
||||
}
|
||||
}
|
||||
|
||||
private fun ComposeContentTestRule.clickConfirmation() {
|
||||
onNodeWithText(getStringResource(R.string.send_confirm)).also {
|
||||
it.performClick()
|
||||
}
|
||||
}
|
||||
|
||||
private fun ComposeContentTestRule.assertOnForm() {
|
||||
onNodeWithText(getStringResource(R.string.send_create)).also {
|
||||
it.assertExists()
|
||||
}
|
||||
}
|
||||
|
||||
private fun ComposeContentTestRule.assertOnConfirmation() {
|
||||
onNodeWithText(getStringResource(R.string.send_confirm)).also {
|
||||
it.assertExists()
|
||||
}
|
||||
}
|
||||
|
||||
private fun ComposeContentTestRule.assertSendEnabled() {
|
||||
onNodeWithText(getStringResource(R.string.send_create)).also {
|
||||
it.assertIsEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
private fun ComposeContentTestRule.assertSendDisabled() {
|
||||
onNodeWithText(getStringResource(R.string.send_create)).also {
|
||||
it.assertIsNotEnabled()
|
||||
assertEquals(1, testSetup.getOnBackCount())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
@file:Suppress("ktlint:filename")
|
||||
|
||||
package co.electriccoin.zcash.ui.screen.scan
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
|
|
|
@ -3,14 +3,27 @@
|
|||
package co.electriccoin.zcash.ui.screen.send
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.android.sdk.model.ZecSend
|
||||
import cash.z.ecc.android.sdk.model.isFailedSubmit
|
||||
import cash.z.ecc.android.sdk.model.isSubmitSuccess
|
||||
import cash.z.ecc.sdk.send
|
||||
import co.electriccoin.zcash.spackle.Twig
|
||||
import co.electriccoin.zcash.ui.MainActivity
|
||||
import co.electriccoin.zcash.ui.screen.home.model.spendableBalance
|
||||
import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel
|
||||
import co.electriccoin.zcash.ui.screen.send.ext.Saver
|
||||
import co.electriccoin.zcash.ui.screen.send.model.SendStage
|
||||
import co.electriccoin.zcash.ui.screen.send.view.Send
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
@ -27,23 +40,76 @@ private fun WrapSend(
|
|||
goBack: () -> Unit
|
||||
) {
|
||||
val walletViewModel by activity.viewModels<WalletViewModel>()
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value
|
||||
|
||||
val spendableBalance = walletViewModel.walletSnapshot.collectAsStateWithLifecycle().value?.spendableBalance()
|
||||
|
||||
val spendingKey = walletViewModel.spendingKey.collectAsStateWithLifecycle().value
|
||||
|
||||
WrapSend(synchronizer, spendableBalance, spendingKey, goBack)
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@Composable
|
||||
internal fun WrapSend(
|
||||
synchronizer: Synchronizer?,
|
||||
spendableBalance: Zatoshi?,
|
||||
spendingKey: UnifiedSpendingKey?,
|
||||
goBack: () -> Unit
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
// For now, we're avoiding sub-navigation to keep the navigation logic simple. But this might
|
||||
// change once deep-linking support is added. It depends on whether deep linking should do one of:
|
||||
// 1. Use a different UI flow entirely
|
||||
// 2. Show a pre-filled Send form
|
||||
// 3. Go directly to the Confirmation screen
|
||||
val (sendStage, setSendStage) = rememberSaveable { mutableStateOf(SendStage.Form) }
|
||||
|
||||
val (zecSend, setZecSend) = rememberSaveable(stateSaver = ZecSend.Saver) { mutableStateOf(null) }
|
||||
|
||||
val onBackAction = {
|
||||
when (sendStage) {
|
||||
SendStage.Form -> goBack()
|
||||
SendStage.Confirmation -> setSendStage(SendStage.Form)
|
||||
SendStage.Sending -> { /* no action - wait until done */ }
|
||||
SendStage.SendFailure -> setSendStage(SendStage.Form)
|
||||
SendStage.SendSuccessful -> goBack()
|
||||
}
|
||||
}
|
||||
|
||||
BackHandler {
|
||||
onBackAction()
|
||||
}
|
||||
|
||||
if (null == synchronizer || null == spendableBalance || null == spendingKey) {
|
||||
// Display loading indicator
|
||||
} else {
|
||||
Send(
|
||||
mySpendableBalance = spendableBalance,
|
||||
goBack = goBack,
|
||||
sendStage = sendStage,
|
||||
onSendStageChange = setSendStage,
|
||||
zecSend = zecSend,
|
||||
onZecSendChange = setZecSend,
|
||||
onBack = onBackAction,
|
||||
onCreateAndSend = {
|
||||
scope.launch {
|
||||
synchronizer.send(spendingKey, it)
|
||||
goBack()
|
||||
synchronizer.send(spendingKey, it).collect {
|
||||
Twig.debug { "Sending transaction id: ${it.id}" }
|
||||
|
||||
if (it.isSubmitSuccess()) {
|
||||
setSendStage(SendStage.SendSuccessful)
|
||||
Twig.debug {
|
||||
"Transaction id:${it.id} submitted successfully at ${it.createTime} with " +
|
||||
"${it.submitAttempts} attempts."
|
||||
}
|
||||
} else if (it.isFailedSubmit()) {
|
||||
Twig.debug { "Transaction id:${it.id} submission failed with: ${it.errorMessage}." }
|
||||
setSendStage(SendStage.SendFailure)
|
||||
}
|
||||
// All other states of Pending transaction mean waiting for one of the states above
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
package co.electriccoin.zcash.ui.screen.send.ext
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import cash.z.ecc.android.sdk.model.Memo
|
||||
import co.electriccoin.zcash.ui.R
|
||||
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
internal fun Memo.valueOrEmptyChar(): String {
|
||||
LocalConfiguration.current
|
||||
return valueOrEmptyChar(LocalContext.current)
|
||||
}
|
||||
|
||||
internal fun Memo.valueOrEmptyChar(context: Context): String {
|
||||
return value.ifEmpty { context.getString(R.string.empty_char) }
|
||||
}
|
|
@ -26,5 +26,5 @@ internal fun WalletAddress.abbreviated(context: Context): String {
|
|||
val firstFive = address.substring(0, ABBREVIATION_INDEX)
|
||||
val lastFive = address.substring(address.length - ABBREVIATION_INDEX, address.length)
|
||||
|
||||
return context.getString(R.string.send_abbreviated_address_format, firstFive, lastFive)
|
||||
return context.getString(R.string.send_confirmation_abbreviated_address_format, firstFive, lastFive)
|
||||
}
|
||||
|
|
|
@ -2,5 +2,8 @@ package co.electriccoin.zcash.ui.screen.send.model
|
|||
|
||||
enum class SendStage {
|
||||
Form,
|
||||
Confirmation
|
||||
Confirmation,
|
||||
Sending,
|
||||
SendFailure,
|
||||
SendSuccessful
|
||||
}
|
||||
|
|
|
@ -48,8 +48,8 @@ import co.electriccoin.zcash.ui.design.component.PrimaryButton
|
|||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme.dimens
|
||||
import co.electriccoin.zcash.ui.screen.send.ext.ABBREVIATION_INDEX
|
||||
import co.electriccoin.zcash.ui.screen.send.ext.Saver
|
||||
import co.electriccoin.zcash.ui.screen.send.ext.abbreviated
|
||||
import co.electriccoin.zcash.ui.screen.send.ext.valueOrEmptyChar
|
||||
import co.electriccoin.zcash.ui.screen.send.model.SendStage
|
||||
|
||||
@Composable
|
||||
|
@ -59,47 +59,47 @@ fun PreviewSend() {
|
|||
GradientSurface {
|
||||
Send(
|
||||
mySpendableBalance = ZatoshiFixture.new(),
|
||||
goBack = {},
|
||||
onCreateAndSend = {}
|
||||
sendStage = SendStage.Form,
|
||||
onSendStageChange = {},
|
||||
zecSend = null,
|
||||
onZecSendChange = {},
|
||||
onCreateAndSend = {},
|
||||
onBack = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun Send(
|
||||
mySpendableBalance: Zatoshi,
|
||||
goBack: () -> Unit,
|
||||
sendStage: SendStage,
|
||||
onSendStageChange: (SendStage) -> Unit,
|
||||
zecSend: ZecSend?,
|
||||
onZecSendChange: (ZecSend) -> Unit,
|
||||
onBack: () -> Unit,
|
||||
onCreateAndSend: (ZecSend) -> Unit
|
||||
) {
|
||||
// For now, we're avoiding sub-navigation to keep the navigation logic simple. But this might
|
||||
// change once deep-linking support is added. It depends on whether deep linking should do one of:
|
||||
// 1. Use a different UI flow entirely
|
||||
// 2. Show a pre-filled Send form
|
||||
// 3. Go directly to the press-and-hold confirmation
|
||||
val (sendStage, setSendStage) = rememberSaveable { mutableStateOf(SendStage.Form) }
|
||||
|
||||
Scaffold(topBar = {
|
||||
SendTopAppBar(onBack = {
|
||||
when (sendStage) {
|
||||
SendStage.Form -> goBack()
|
||||
SendStage.Confirmation -> setSendStage(SendStage.Form)
|
||||
}
|
||||
})
|
||||
SendTopAppBar(
|
||||
onBack = onBack,
|
||||
showBackNavigationButton = sendStage != SendStage.Sending
|
||||
)
|
||||
}) { paddingValues ->
|
||||
SendMainContent(
|
||||
myBalance = mySpendableBalance,
|
||||
onBack = onBack,
|
||||
sendStage = sendStage,
|
||||
setSendStage = setSendStage,
|
||||
onCreateAndSend = onCreateAndSend,
|
||||
onSendStageChange = onSendStageChange,
|
||||
zecSend = zecSend,
|
||||
onZecSendChange = onZecSendChange,
|
||||
onSendSubmit = onCreateAndSend,
|
||||
modifier = Modifier
|
||||
.verticalScroll(
|
||||
rememberScrollState()
|
||||
)
|
||||
.padding(
|
||||
top = paddingValues.calculateTopPadding() + dimens.spacingDefault,
|
||||
bottom = dimens.spacingDefault,
|
||||
bottom = paddingValues.calculateBottomPadding() + dimens.spacingDefault,
|
||||
start = dimens.spacingDefault,
|
||||
end = dimens.spacingDefault
|
||||
)
|
||||
|
@ -109,51 +109,83 @@ fun Send(
|
|||
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
private fun SendTopAppBar(onBack: () -> Unit) {
|
||||
TopAppBar(
|
||||
title = { Text(text = stringResource(id = R.string.send_title)) },
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = onBack
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ArrowBack,
|
||||
contentDescription = stringResource(R.string.send_back_content_description)
|
||||
)
|
||||
private fun SendTopAppBar(
|
||||
onBack: () -> Unit,
|
||||
showBackNavigationButton: Boolean = true
|
||||
) {
|
||||
if (showBackNavigationButton) {
|
||||
TopAppBar(
|
||||
title = { Text(text = stringResource(id = R.string.send_title)) },
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = onBack
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ArrowBack,
|
||||
contentDescription = stringResource(R.string.send_back_content_description)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
} else {
|
||||
TopAppBar(title = { Text(text = stringResource(id = R.string.send_title)) })
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
private fun SendMainContent(
|
||||
myBalance: Zatoshi,
|
||||
zecSend: ZecSend?,
|
||||
onZecSendChange: (ZecSend) -> Unit,
|
||||
onBack: () -> Unit,
|
||||
sendStage: SendStage,
|
||||
setSendStage: (SendStage) -> Unit,
|
||||
onCreateAndSend: (ZecSend) -> Unit,
|
||||
onSendStageChange: (SendStage) -> Unit,
|
||||
onSendSubmit: (ZecSend) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val (zecSend, setZecSend) = rememberSaveable(stateSaver = ZecSend.Saver) { mutableStateOf(null) }
|
||||
|
||||
if (sendStage == SendStage.Form || null == zecSend) {
|
||||
SendForm(
|
||||
myBalance = myBalance,
|
||||
previousZecSend = zecSend,
|
||||
onCreateAndSend = {
|
||||
setSendStage(SendStage.Confirmation)
|
||||
setZecSend(it)
|
||||
},
|
||||
modifier = modifier
|
||||
)
|
||||
} else {
|
||||
Confirmation(
|
||||
zecSend = zecSend,
|
||||
onConfirmation = {
|
||||
onCreateAndSend(zecSend)
|
||||
},
|
||||
modifier = modifier
|
||||
)
|
||||
when {
|
||||
(sendStage == SendStage.Form || null == zecSend) -> {
|
||||
SendForm(
|
||||
myBalance = myBalance,
|
||||
previousZecSend = zecSend,
|
||||
onCreateZecSend = {
|
||||
onSendStageChange(SendStage.Confirmation)
|
||||
onZecSendChange(it)
|
||||
},
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
(sendStage == SendStage.Confirmation) -> {
|
||||
Confirmation(
|
||||
zecSend = zecSend,
|
||||
onConfirmation = {
|
||||
onSendStageChange(SendStage.Sending)
|
||||
onSendSubmit(zecSend)
|
||||
},
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
(sendStage == SendStage.Sending) -> {
|
||||
Sending(
|
||||
zecSend = zecSend,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
(sendStage == SendStage.SendSuccessful) -> {
|
||||
SendSuccessful(
|
||||
zecSend = zecSend,
|
||||
modifier = modifier,
|
||||
onDone = onBack
|
||||
)
|
||||
}
|
||||
(sendStage == SendStage.SendFailure) -> {
|
||||
SendFailure(
|
||||
zecSend = zecSend,
|
||||
modifier = modifier,
|
||||
onDone = onBack
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -162,11 +194,10 @@ private fun SendMainContent(
|
|||
// TODO [#294]: DetektAll failed LongMethod
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
private fun SendForm(
|
||||
myBalance: Zatoshi,
|
||||
previousZecSend: ZecSend?,
|
||||
onCreateAndSend: (ZecSend) -> Unit,
|
||||
onCreateZecSend: (ZecSend) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
@ -190,6 +221,7 @@ private fun SendForm(
|
|||
Column(
|
||||
modifier
|
||||
.fillMaxHeight()
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Header(
|
||||
text = stringResource(id = R.string.send_balance, myBalance.toZecString()),
|
||||
|
@ -268,7 +300,7 @@ private fun SendForm(
|
|||
)
|
||||
|
||||
when (zecSendValidation) {
|
||||
is ZecSendExt.ZecSendValidation.Valid -> onCreateAndSend(zecSendValidation.zecSend)
|
||||
is ZecSendExt.ZecSendValidation.Valid -> onCreateZecSend(zecSendValidation.zecSend)
|
||||
is ZecSendExt.ZecSendValidation.Invalid -> validation = zecSendValidation.validationErrors
|
||||
}
|
||||
},
|
||||
|
@ -286,18 +318,193 @@ private fun Confirmation(
|
|||
onConfirmation: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(modifier) {
|
||||
Text(
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxHeight()
|
||||
.verticalScroll(
|
||||
rememberScrollState()
|
||||
)
|
||||
.then(modifier)
|
||||
) {
|
||||
Body(
|
||||
stringResource(
|
||||
R.string.send_amount_and_address_format,
|
||||
R.string.send_confirmation_amount_and_address_format,
|
||||
zecSend.amount.toZecString(),
|
||||
zecSend.destination.abbreviated()
|
||||
)
|
||||
)
|
||||
if (zecSend.memo.value.isNotEmpty()) {
|
||||
Body(
|
||||
stringResource(
|
||||
R.string.send_confirmation_memo_format,
|
||||
zecSend.memo.value
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.weight(MINIMAL_WEIGHT)
|
||||
)
|
||||
|
||||
PrimaryButton(
|
||||
modifier = Modifier.padding(top = dimens.spacingSmall),
|
||||
onClick = onConfirmation,
|
||||
text = stringResource(id = R.string.send_confirm)
|
||||
text = stringResource(id = R.string.send_confirmation_button)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Sending(
|
||||
zecSend: ZecSend,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxHeight()
|
||||
.verticalScroll(
|
||||
rememberScrollState()
|
||||
)
|
||||
.then(modifier)
|
||||
) {
|
||||
Header(
|
||||
text = stringResource(
|
||||
R.string.send_in_progress_amount_format,
|
||||
zecSend.amount.toZecString()
|
||||
),
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Body(
|
||||
text = zecSend.destination.abbreviated(),
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
if (zecSend.memo.value.isNotEmpty()) {
|
||||
Body(
|
||||
stringResource(
|
||||
R.string.send_in_progress_memo_format,
|
||||
zecSend.memo.value
|
||||
),
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.weight(MINIMAL_WEIGHT)
|
||||
)
|
||||
|
||||
Body(
|
||||
modifier = Modifier
|
||||
.padding(vertical = dimens.spacingSmall)
|
||||
.fillMaxWidth(),
|
||||
text = stringResource(R.string.send_in_progress_wait),
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SendSuccessful(
|
||||
zecSend: ZecSend,
|
||||
onDone: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxHeight()
|
||||
.verticalScroll(
|
||||
rememberScrollState()
|
||||
)
|
||||
.then(modifier)
|
||||
) {
|
||||
Header(
|
||||
text = stringResource(R.string.send_successful_title),
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(dimens.spacingDefault)
|
||||
)
|
||||
|
||||
Body(
|
||||
stringResource(
|
||||
R.string.send_successful_amount_address_memo,
|
||||
zecSend.amount.toZecString(),
|
||||
zecSend.destination.abbreviated(),
|
||||
zecSend.memo.valueOrEmptyChar()
|
||||
)
|
||||
)
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.weight(MINIMAL_WEIGHT)
|
||||
)
|
||||
|
||||
PrimaryButton(
|
||||
modifier = Modifier.padding(top = dimens.spacingSmall),
|
||||
text = stringResource(R.string.send_successful_button),
|
||||
onClick = onDone
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SendFailure(
|
||||
zecSend: ZecSend,
|
||||
onDone: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxHeight()
|
||||
.verticalScroll(
|
||||
rememberScrollState()
|
||||
)
|
||||
.then(modifier)
|
||||
) {
|
||||
Header(
|
||||
text = stringResource(R.string.send_failure_title),
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(dimens.spacingDefault)
|
||||
)
|
||||
|
||||
Body(
|
||||
stringResource(
|
||||
R.string.send_failure_amount_address_memo,
|
||||
zecSend.amount.toZecString(),
|
||||
zecSend.destination.abbreviated(),
|
||||
zecSend.memo.valueOrEmptyChar()
|
||||
)
|
||||
)
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.weight(MINIMAL_WEIGHT)
|
||||
)
|
||||
|
||||
PrimaryButton(
|
||||
modifier = Modifier.padding(top = dimens.spacingSmall),
|
||||
text = stringResource(R.string.send_failure_button),
|
||||
onClick = onDone
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="fiat_currency_conversion_rate_unavailable">Unavailable</string>
|
||||
<string name="empty_char">-</string>
|
||||
</resources>
|
||||
|
|
|
@ -7,11 +7,23 @@
|
|||
<string name="send_to">Who would you like to send ZEC to?</string>
|
||||
<string name="send_amount">How much?</string>
|
||||
<string name="send_memo">Memo</string>
|
||||
|
||||
<string name="send_create">Send</string>
|
||||
|
||||
<string name="send_amount_and_address_format" formatted="true">Send <xliff:g id="amount" example="12.345">%1$s</xliff:g> ZEC to <xliff:g id="address" example="zs1g7cqw … mvyzgm">%2$s</xliff:g>?</string>
|
||||
<string name="send_abbreviated_address_format" formatted="true"><xliff:g id="first_five" example="zs1g7">%1$s</xliff:g>…<xliff:g id="last_five" example="mvyzg">%2$s</xliff:g></string>
|
||||
<string name="send_confirm">Press to send ZEC</string>
|
||||
<string name="send_confirmation_amount_and_address_format" formatted="true">Send <xliff:g id="amount" example="12.345">%1$s</xliff:g> ZEC to <xliff:g id="address" example="zs1g7cqw … mvyzgm">%2$s</xliff:g>?</string>
|
||||
<string name="send_confirmation_memo_format" formatted="true">Memo: <xliff:g id="memo" example="for Veronika">%1$s</xliff:g></string>
|
||||
<string name="send_confirmation_abbreviated_address_format" formatted="true"><xliff:g id="first_five" example="zs1g7">%1$s</xliff:g>…<xliff:g id="last_five" example="mvyzg">%2$s</xliff:g></string>
|
||||
<string name="send_confirmation_button">Press to send ZEC</string>
|
||||
|
||||
<string name="send_in_progress_amount_format" formatted="true">Sending <xliff:g id="amount" example="12.345">%1$s</xliff:g> ZEC to</string>
|
||||
<string name="send_in_progress_memo_format" formatted="true">with a memo: <xliff:g id="memo" example="for Veronika">%1$s</xliff:g></string>
|
||||
<string name="send_in_progress_wait">Please wait</string>
|
||||
|
||||
<string name="send_failure_title">Sending failure</string>
|
||||
<string name="send_failure_amount_address_memo" formatted="true">Sending failed for: <xliff:g id="amount" example="12.345">%1$s</xliff:g> ZEC to <xliff:g id="address" example="zs1g7cqw … mvyzgm">%2$s</xliff:g> with a memo: <xliff:g id="memo" example="for Veronika">%3$s</xliff:g></string>
|
||||
<string name="send_failure_button">Back</string>
|
||||
|
||||
<string name="send_successful_title">Sending successful</string>
|
||||
<string name="send_successful_amount_address_memo" formatted="true">Sending succeeded for: <xliff:g id="amount" example="12.345">%1$s</xliff:g> ZEC to <xliff:g id="address" example="zs1g7cqw … mvyzgm">%2$s</xliff:g> with a memo: <xliff:g id="memo" example="for Veronika">%3$s</xliff:g></string>
|
||||
<string name="send_successful_button">Close</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -23,6 +23,7 @@ import androidx.compose.ui.test.performScrollTo
|
|||
import androidx.compose.ui.test.performTextInput
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.core.graphics.writeToTestStorage
|
||||
import androidx.test.espresso.Espresso
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isRoot
|
||||
import androidx.test.espresso.screenshot.captureToBitmap
|
||||
|
@ -33,6 +34,7 @@ import androidx.test.filters.SdkSuppress
|
|||
import cash.z.ecc.android.sdk.fixture.WalletAddressFixture
|
||||
import cash.z.ecc.android.sdk.model.MonetarySeparators
|
||||
import cash.z.ecc.android.sdk.model.SeedPhrase
|
||||
import cash.z.ecc.sdk.fixture.MemoFixture
|
||||
import cash.z.ecc.sdk.fixture.SeedPhraseFixture
|
||||
import co.electriccoin.zcash.configuration.model.map.StringConfiguration
|
||||
import co.electriccoin.zcash.spackle.FirebaseTestLabUtil
|
||||
|
@ -532,25 +534,56 @@ private fun sendZecScreenshots(resContext: Context, tag: String, composeTestRule
|
|||
it.assertExists()
|
||||
}
|
||||
|
||||
// Screenshot: Empty form
|
||||
ScreenshotTest.takeScreenshot(tag, "Send 1")
|
||||
|
||||
composeTestRule.onNodeWithText(resContext.getString(R.string.send_amount)).also {
|
||||
val separators = MonetarySeparators.current()
|
||||
|
||||
it.performTextInput("{${separators.decimal}}123")
|
||||
it.performTextInput("0${separators.decimal}123")
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(resContext.getString(R.string.send_to)).also {
|
||||
it.performTextInput(WalletAddressFixture.UNIFIED_ADDRESS_STRING)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(resContext.getString(R.string.send_memo)).also {
|
||||
it.performTextInput(MemoFixture.MEMO_STRING)
|
||||
}
|
||||
|
||||
// To close soft keyboard to reveal the send button
|
||||
Espresso.closeSoftKeyboard()
|
||||
|
||||
// Screenshot: Fulfilled form
|
||||
ScreenshotTest.takeScreenshot(tag, "Send 2")
|
||||
|
||||
composeTestRule.onNodeWithText(resContext.getString(R.string.send_create)).also {
|
||||
it.performClick()
|
||||
}
|
||||
|
||||
composeTestRule.waitForIdle()
|
||||
/*
|
||||
TODO [#817]: Screenshot test on Send with pseudolocales problem
|
||||
TODO [#817]: https://github.com/zcash/secant-android-wallet/issues/817
|
||||
// Screenshot: Confirmation
|
||||
ScreenshotTest.takeScreenshot(tag, "Send 3")
|
||||
|
||||
ScreenshotTest.takeScreenshot(tag, "Send 2")
|
||||
composeTestRule.onNodeWithText(resContext.getString(R.string.send_confirmation_button)).also {
|
||||
it.performClick()
|
||||
}
|
||||
|
||||
// Screenshot: Sending
|
||||
ScreenshotTest.takeScreenshot(tag, "Send 4")
|
||||
|
||||
// Note: this is potentially a long running waiting for the transaction submit result
|
||||
// Remove this last section of taking screenshot if it turns out to be problematic
|
||||
composeTestRule.waitUntil(DEFAULT_TIMEOUT_MILLISECONDS) {
|
||||
composeTestRule.onAllNodesWithText(resContext.getString(R.string.send_in_progress_wait))
|
||||
.fetchSemanticsNodes().isEmpty()
|
||||
}
|
||||
|
||||
// Screenshot: Result
|
||||
ScreenshotTest.takeScreenshot(tag, "Send 5")
|
||||
*/
|
||||
}
|
||||
|
||||
private fun supportScreenshots(resContext: Context, tag: String, composeTestRule: ComposeTestRule) {
|
||||
|
|
Loading…
Reference in New Issue