From 7615190021198741baf270ea53f02853f8978edf Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 10 Jun 2026 15:52:22 +0200 Subject: [PATCH 1/3] Updated on 2026-08-14 --- .../com/tangem/scenarios/SendScenarios.kt | 87 ++++++++++++++++++ .../TokenDetailsScreenActionButtonsTest.kt | 58 ++++++++++++ .../confirmScreen/SendConfirmScreenTest.kt | 39 ++++++++ .../tests/send/feeScreen/SendFeeScreenTest.kt | 27 ++++++ .../tests/send/feeScreen/SendTokenFeeTest.kt | 92 +++++++++++++++++++ 5 files changed, 303 insertions(+) create mode 100644 app/src/androidTest/kotlin/com/tangem/tests/send/feeScreen/SendTokenFeeTest.kt diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/SendScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/SendScenarios.kt index 591528e289..5517764ac8 100644 --- a/app/src/androidTest/kotlin/com/tangem/scenarios/SendScenarios.kt +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/SendScenarios.kt @@ -7,9 +7,13 @@ import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG import com.tangem.common.extensions.clickWithAssertion +import com.tangem.common.extensions.extractText import com.tangem.common.utils.setWireMockScenarioState +import com.tangem.core.ui.R +import com.kaspersky.kaspresso.testcases.core.testcontext.TestContext import com.tangem.screens.* import com.tangem.tap.domain.sdk.mocks.MockContent +import io.github.kakaocup.kakao.common.utilities.getResourceString import io.qameta.allure.kotlin.Allure.step fun BaseTestCase.openSendScreen( @@ -248,6 +252,89 @@ fun BaseTestCase.checkSendViaSwapSuccessScreen() { } } +/** From the token details screen, open the transfer bottom sheet and reach the send amount input. */ +fun BaseTestCase.openSendFromTokenDetails() { + step("Click on 'Transfer' button") { + onTokenDetailsScreen { transferButton.clickWithAssertion() } + } + step("Click on 'Send' button in bottom sheet") { + onTransferBottomSheet { sendButton.clickWithAssertion() } + } +} + +/** Open an existing hot wallet and reach the send amount input for [tokenName]. */ +fun BaseTestCase.openSendScreenWithHotWallet(seedPhrase: String, tokenName: String) { + step("Open 'Main' screen with existing hot wallet") { + openMainScreenWithExistingHotWallet(seedPhrase) + } + step("Click on token with name: '$tokenName'") { + onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() } + } + openSendFromTokenDetails() +} + +fun BaseTestCase.getNetworkFeeAmount(): String { + var fee = "" + step("Read current network fee amount") { + onSendConfirmScreen { fee = feeAmount.extractText() } + } + return fee +} + +fun BaseTestCase.switchFeeToFastAndApply() { + val fastOption = getResourceString(R.string.common_fee_selector_option_fast) + step("Click on fee selector icon") { + onSendConfirmScreen { feeSelectorIcon.performClick() } + } + // Selecting a non-custom speed auto-applies and closes the fee selector — no 'Done' step. + step("Click on '$fastOption' fee option") { + onSendSelectNetworkFeeBottomSheet { regularFeeSelectorItem(fastOption).performClick() } + } +} + +fun BaseTestCase.assertNetworkFeeChanged(previousFee: String) { + step("Assert network fee changed from '$previousFee'") { + composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) { + runCatching { + var current = previousFee + onSendConfirmScreen { current = feeAmount.extractText() } + current != previousFee + }.getOrDefault(false) + } + } +} + +/** Reads the network fee amount on the 'Send confirm' screen (empty while it shows a loading shimmer). */ +fun BaseTestCase.readNetworkFeeAmount(): String { + var fee = "" + onSendConfirmScreen { fee = feeAmount.extractText() } + return fee +} + +/** + * Wait until the network fee value stops changing across two checks — the send button is disabled + * (and the hold-to-confirm gesture swallowed) until the fee finishes loading. + */ +fun TestContext.waitUntilNetworkFeeIsStable(readFee: () -> String) { + step("Wait for the network fee to finish loading") { + var previousFee: String? = null + flakySafely(timeoutMs = WAIT_UNTIL_TIMEOUT_LONG, intervalMs = FEE_STABILITY_INTERVAL_MS) { + val currentFee = readFee() + val isStable = currentFee.isNotEmpty() && currentFee == previousFee + previousFee = currentFee + if (!isStable) throw AssertionError("Network fee is still settling (current='$currentFee')") + } + } +} + +private const val FEE_STABILITY_INTERVAL_MS = 750L + +fun BaseTestCase.assertNetworkFeeContains(currencySymbol: String) { + step("Assert network fee contains '$currencySymbol'") { + onSendConfirmScreen { feeAmount.assertTextContains(currencySymbol, substring = true) } + } +} + fun BaseTestCase.selectTokenToSendViaSwap( swapTokenName: String, networkName: String, diff --git a/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/TokenDetailsScreenActionButtonsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/TokenDetailsScreenActionButtonsTest.kt index 99e629f6e9..2cf1653be7 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/TokenDetailsScreenActionButtonsTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/TokenDetailsScreenActionButtonsTest.kt @@ -3,12 +3,16 @@ package com.tangem.tests.actionButtons import com.tangem.common.BaseTestCase import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT import com.tangem.common.extensions.clickWithAssertion +import com.tangem.common.utils.resetWireMockScenarioState +import com.tangem.common.utils.setWireMockScenarioState import com.tangem.scenarios.checkQrCodeBottomSheetScenario import com.tangem.scenarios.goToQrCodeBottomSheet import com.tangem.scenarios.openMainScreen +import com.tangem.scenarios.openSendFromTokenDetails import com.tangem.scenarios.synchronizeAddresses import com.tangem.screens.onAddFundsBottomSheet import com.tangem.screens.onMainScreen +import com.tangem.screens.onSendScreen import com.tangem.screens.onSwapStoriesScreen import com.tangem.screens.onSwapTokenScreen import com.tangem.screens.onTokenDetailsScreen @@ -199,4 +203,58 @@ class TokenDetailsScreenActionButtonsTest : BaseTestCase() { } } } + + @AllureId("591") + @DisplayName("Action buttons (token details screen): send available for funded token, unavailable for empty token") + @Test + fun checkSendAvailabilityForFundedAndEmptyTokenTest() { + val emptyTokenTitle = "Polygon" + val fundedTokenTitle = "Ethereum" + val polygonBalanceScenarioName = "polygon_coin_balance" + val polygonBalanceScenarioState = "ZeroBalance" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(polygonBalanceScenarioName) + } + ).run { + step("Set WireMock scenario: '$polygonBalanceScenarioName' to state: '$polygonBalanceScenarioState'") { + setWireMockScenarioState(polygonBalanceScenarioName, polygonBalanceScenarioState) + } + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Click on token with name: '$emptyTokenTitle'") { + waitForIdle() + onMainScreen { tokenWithTitleAndAddress(emptyTokenTitle).clickWithAssertion() } + } + step("Assert 'Token details' screen is displayed") { + onTokenDetailsScreen { screenContainer.assertIsDisplayed() } + } + step("Assert 'Transfer' button is not displayed for the empty token") { + onTokenDetailsScreen { transferButton.assertIsNotDisplayed() } + } + step("Go back to 'Main Screen'") { + device.uiDevice.pressBack() + } + step("Assert 'Main Screen' is displayed") { + onMainScreen { screenContainer.assertIsDisplayed() } + } + step("Click on token with name: '$fundedTokenTitle'") { + onMainScreen { tokenWithTitleAndAddress(fundedTokenTitle).clickWithAssertion() } + } + step("Assert 'Transfer' button is displayed for the funded token") { + onTokenDetailsScreen { transferButton.assertIsDisplayed() } + } + step("Open the send flow from token details") { + openSendFromTokenDetails() + } + step("Assert 'Send' screen is displayed") { + onSendScreen { amountInputTextField.assertIsDisplayed() } + } + } + } } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/confirmScreen/SendConfirmScreenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/confirmScreen/SendConfirmScreenTest.kt index 4a8f00b331..b7fe82c3f4 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/send/confirmScreen/SendConfirmScreenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/send/confirmScreen/SendConfirmScreenTest.kt @@ -4,6 +4,7 @@ import com.tangem.common.BaseTestCase import com.tangem.common.constants.TestConstants.ETHEREUM_RECIPIENT_ADDRESS import com.tangem.common.constants.TestConstants.POLKADOT_RECIPIENT_ADDRESS import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO +import com.tangem.common.constants.TestConstants.SVS_SEED_PHRASE_12 import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG @@ -326,4 +327,42 @@ class SendConfirmScreenTest : BaseTestCase() { } } } + + @AllureId("557") + @DisplayName("Send (Confirm screen): send a second transaction while the first is still pending") + @Test + fun sendSecondTransactionWhileFirstActiveTest() { + val tokenName = "Ethereum" + val inputAmount = "0.001" + + setupHooks().run { + step("Open the send flow for '$tokenName' on an existing hot wallet") { + openSendScreenWithHotWallet(seedPhrase = SVS_SEED_PHRASE_12, tokenName = tokenName) + } + step("Enter amount '$inputAmount' and open the 'Send confirm' screen") { + enterAmountAndOpenSendConfirm(amount = inputAmount, recipientAddress = ETHEREUM_RECIPIENT_ADDRESS) + } + // Hold-to-confirm is swallowed while the fee is still settling — wait for it to load first. + waitUntilNetworkFeeIsStable { readNetworkFeeAmount() } + step("Sign, send and open the 'Transaction sent' screen") { + openSendSuccessScreenViaLongClickOnSendButton() + } + step("Click on 'Close' button") { + onSendSuccessScreen { closeButton.clickWithAssertion() } + } + step("Assert 'Token details' screen is displayed") { + onTokenDetailsScreen { screenContainer.assertIsDisplayed() } + } + step("Open the send flow again from token details") { + openSendFromTokenDetails() + } + step("Enter amount '$inputAmount' and open the 'Send confirm' screen") { + enterAmountAndOpenSendConfirm(amount = inputAmount, recipientAddress = ETHEREUM_RECIPIENT_ADDRESS) + } + waitUntilNetworkFeeIsStable { readNetworkFeeAmount() } + step("Sign, send and open the 'Transaction sent' screen") { + openSendSuccessScreenViaLongClickOnSendButton() + } + } + } } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/feeScreen/SendFeeScreenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/feeScreen/SendFeeScreenTest.kt index d67314ba1c..6689770f70 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/send/feeScreen/SendFeeScreenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/send/feeScreen/SendFeeScreenTest.kt @@ -5,6 +5,7 @@ import com.tangem.common.constants.TestConstants.BITCOIN_RECIPIENT_ADDRESS import com.tangem.common.constants.TestConstants.ETHEREUM_RECIPIENT_ADDRESS import com.tangem.common.constants.TestConstants.POLKADOT_RECIPIENT_ADDRESS import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO +import com.tangem.common.constants.TestConstants.SVS_SEED_PHRASE_12 import com.tangem.common.constants.TestConstants.TERRA_RECIPIENT_ADDRESS import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG @@ -443,4 +444,30 @@ class SendFeeScreenTest : BaseTestCase() { } } } + + @AllureId("547") + @DisplayName("Send (Fee screen): network fee recalculates on speed switch and sends") + @Test + fun recalculateFeeOnSpeedSwitchAndSendTest() { + val tokenName = "Ethereum" + val inputAmount = "0.8" + + setupHooks().run { + step("Open the send flow for '$tokenName' on an existing hot wallet") { + openSendScreenWithHotWallet(seedPhrase = SVS_SEED_PHRASE_12, tokenName = tokenName) + } + step("Enter amount '$inputAmount' and open the 'Send confirm' screen") { + enterAmountAndOpenSendConfirm(amount = inputAmount, recipientAddress = ETHEREUM_RECIPIENT_ADDRESS) + } + val marketFee = getNetworkFeeAmount() + step("Switch the network fee to 'Fast'") { + switchFeeToFastAndApply() + } + assertNetworkFeeChanged(marketFee) + waitUntilNetworkFeeIsStable { readNetworkFeeAmount() } + step("Sign, send and open the 'Transaction sent' screen") { + openSendSuccessScreenViaLongClickOnSendButton() + } + } + } } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/feeScreen/SendTokenFeeTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/feeScreen/SendTokenFeeTest.kt new file mode 100644 index 0000000000..d12751c12d --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/send/feeScreen/SendTokenFeeTest.kt @@ -0,0 +1,92 @@ +package com.tangem.tests.send.feeScreen + +import com.tangem.common.BaseTestCase +import com.tangem.common.constants.TestConstants.ETHEREUM_RECIPIENT_ADDRESS +import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO +import com.tangem.common.constants.TestConstants.SVS_SEED_PHRASE_12 +import com.tangem.common.constants.TestConstants.TERRA_RECIPIENT_ADDRESS +import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO +import com.tangem.common.utils.resetWireMockScenarioState +import com.tangem.common.utils.setWireMockScenarioState +import com.tangem.scenarios.* +import com.tangem.screens.* +import dagger.hilt.android.testing.HiltAndroidTest +import io.qameta.allure.kotlin.AllureId +import io.qameta.allure.kotlin.junit4.DisplayName +import org.junit.Test + +/** + * Completing a send paid with a fee in the token itself (no native fee coin), on a hot wallet: + * VeChain's VeThor and Terra Classic's TerraClassicUSD. + */ +@HiltAndroidTest +class SendTokenFeeTest : BaseTestCase() { + + private val tokenAmount = "1" + + @AllureId("4907") + @DisplayName("Send (Fee in token): send VeThor and complete the transaction") + @Test + fun sendVeThorWithFeeInTokenTest() { + val tokenName = "VeThor" + val scenarioState = "Vechain" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(USER_TOKENS_API_SCENARIO) + resetWireMockScenarioState(QUOTES_API_SCENARIO) + } + ).run { + step("Set WireMock scenario '$USER_TOKENS_API_SCENARIO' to '$scenarioState'") { + setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = scenarioState) + } + step("Set WireMock scenario '$QUOTES_API_SCENARIO' to '$scenarioState'") { + setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = scenarioState) + } + step("Open the send flow for '$tokenName' on an existing hot wallet") { + openSendScreenWithHotWallet(seedPhrase = SVS_SEED_PHRASE_12, tokenName = tokenName) + } + step("Enter amount '$tokenAmount' and open the 'Send confirm' screen") { + enterAmountAndOpenSendConfirm(amount = tokenAmount, recipientAddress = ETHEREUM_RECIPIENT_ADDRESS) + } + assertNetworkFeeContains("$") + waitUntilNetworkFeeIsStable { readNetworkFeeAmount() } + step("Sign, send and open the 'Transaction sent' screen") { + openSendSuccessScreenViaLongClickOnSendButton() + } + } + } + + @AllureId("4908") + @DisplayName("Send (Fee in token): send TerraClassicUSD and complete the transaction") + @Test + fun sendTerraClassicUsdWithFeeInTokenTest() { + val tokenName = "TerraClassicUSD" + val scenarioState = "Terra" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(USER_TOKENS_API_SCENARIO) + resetWireMockScenarioState(QUOTES_API_SCENARIO) + } + ).run { + step("Set WireMock scenario '$USER_TOKENS_API_SCENARIO' to '$scenarioState'") { + setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = scenarioState) + } + step("Set WireMock scenario '$QUOTES_API_SCENARIO' to '$scenarioState'") { + setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = scenarioState) + } + step("Open the send flow for '$tokenName' on an existing hot wallet") { + openSendScreenWithHotWallet(seedPhrase = SVS_SEED_PHRASE_12, tokenName = tokenName) + } + step("Enter amount '$tokenAmount' and open the 'Send confirm' screen") { + enterAmountAndOpenSendConfirm(amount = tokenAmount, recipientAddress = TERRA_RECIPIENT_ADDRESS) + } + assertNetworkFeeContains("$") + waitUntilNetworkFeeIsStable { readNetworkFeeAmount() } + step("Sign, send and open the 'Transaction sent' screen") { + openSendSuccessScreenViaLongClickOnSendButton() + } + } + } +} \ No newline at end of file From d0a713d0ddd4b77e55915941dce8b3799a0c7027 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 10 Jun 2026 16:20:30 +0200 Subject: [PATCH 2/3] Updated on 2026-08-14 --- .../com/tangem/tests/send/feeScreen/SendFeeScreenTest.kt | 2 +- .../com/tangem/tests/send/feeScreen/SendTokenFeeTest.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/feeScreen/SendFeeScreenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/feeScreen/SendFeeScreenTest.kt index 6689770f70..4fb3a7e707 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/send/feeScreen/SendFeeScreenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/send/feeScreen/SendFeeScreenTest.kt @@ -459,12 +459,12 @@ class SendFeeScreenTest : BaseTestCase() { step("Enter amount '$inputAmount' and open the 'Send confirm' screen") { enterAmountAndOpenSendConfirm(amount = inputAmount, recipientAddress = ETHEREUM_RECIPIENT_ADDRESS) } + waitUntilNetworkFeeIsStable { readNetworkFeeAmount() } val marketFee = getNetworkFeeAmount() step("Switch the network fee to 'Fast'") { switchFeeToFastAndApply() } assertNetworkFeeChanged(marketFee) - waitUntilNetworkFeeIsStable { readNetworkFeeAmount() } step("Sign, send and open the 'Transaction sent' screen") { openSendSuccessScreenViaLongClickOnSendButton() } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/feeScreen/SendTokenFeeTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/feeScreen/SendTokenFeeTest.kt index d12751c12d..8c50048abb 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/send/feeScreen/SendTokenFeeTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/send/feeScreen/SendTokenFeeTest.kt @@ -49,8 +49,8 @@ class SendTokenFeeTest : BaseTestCase() { step("Enter amount '$tokenAmount' and open the 'Send confirm' screen") { enterAmountAndOpenSendConfirm(amount = tokenAmount, recipientAddress = ETHEREUM_RECIPIENT_ADDRESS) } - assertNetworkFeeContains("$") waitUntilNetworkFeeIsStable { readNetworkFeeAmount() } + assertNetworkFeeContains("\$") step("Sign, send and open the 'Transaction sent' screen") { openSendSuccessScreenViaLongClickOnSendButton() } @@ -82,8 +82,8 @@ class SendTokenFeeTest : BaseTestCase() { step("Enter amount '$tokenAmount' and open the 'Send confirm' screen") { enterAmountAndOpenSendConfirm(amount = tokenAmount, recipientAddress = TERRA_RECIPIENT_ADDRESS) } - assertNetworkFeeContains("$") waitUntilNetworkFeeIsStable { readNetworkFeeAmount() } + assertNetworkFeeContains("\$") step("Sign, send and open the 'Transaction sent' screen") { openSendSuccessScreenViaLongClickOnSendButton() } From 234b5f5dbf766c8ab53ac4ce0ac7147f8d101b57 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 10 Jun 2026 20:22:55 +0200 Subject: [PATCH 3/3] Updated on 2026-08-14 --- .../androidTest/kotlin/com/tangem/scenarios/SendScenarios.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/SendScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/SendScenarios.kt index 5517764ac8..c2298b7eb2 100644 --- a/app/src/androidTest/kotlin/com/tangem/scenarios/SendScenarios.kt +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/SendScenarios.kt @@ -312,8 +312,9 @@ fun BaseTestCase.readNetworkFeeAmount(): String { } /** - * Wait until the network fee value stops changing across two checks — the send button is disabled - * (and the hold-to-confirm gesture swallowed) until the fee finishes loading. + * Wait until the network fee value stops changing across two checks — the send button stays disabled + * (and the hold-to-confirm gesture is swallowed) until the fee re-fetch settles. The hold button has + * no enabled/disabled semantics, so waiting on the fee value is the only reliable readiness signal. */ fun TestContext.waitUntilNetworkFeeIsStable(readFee: () -> String) { step("Wait for the network fee to finish loading") {