Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-11 10:52:45 +02:00
commit 52d1ed4312
5 changed files with 304 additions and 0 deletions

View file

@ -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,90 @@ 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 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<Unit>.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,

View file

@ -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() }
}
}
}
}

View file

@ -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()
}
}
}
}

View file

@ -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)
}
waitUntilNetworkFeeIsStable { readNetworkFeeAmount() }
val marketFee = getNetworkFeeAmount()
step("Switch the network fee to 'Fast'") {
switchFeeToFastAndApply()
}
assertNetworkFeeChanged(marketFee)
step("Sign, send and open the 'Transaction sent' screen") {
openSendSuccessScreenViaLongClickOnSendButton()
}
}
}
}

View file

@ -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)
}
waitUntilNetworkFeeIsStable { readNetworkFeeAmount() }
assertNetworkFeeContains("\$")
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)
}
waitUntilNetworkFeeIsStable { readNetworkFeeAmount() }
assertNetworkFeeContains("\$")
step("Sign, send and open the 'Transaction sent' screen") {
openSendSuccessScreenViaLongClickOnSendButton()
}
}
}
}