Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-30 11:36:06 +03:00
parent d76fcd143c
commit d197fae012
9 changed files with 459 additions and 11 deletions

View file

@ -0,0 +1,61 @@
package com.tangem.scenarios
import com.tangem.common.BaseTestCase
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.screens.*
import com.tangem.screens.onMainScreen
import com.tangem.screens.onSendConfirmScreen
import com.tangem.screens.onSendScreen
import com.tangem.screens.onTokenDetailsScreen
import io.qameta.allure.kotlin.Allure.step
fun BaseTestCase.checkNetworkFeeBlock(currentFeeAmount: String, withFeeSelector: Boolean) {
step("Assert fee selector block is displayed") {
onSendConfirmScreen { feeSelectorBlock.assertIsDisplayed() }
}
step("Assert fee selector icon is displayed") {
onSendConfirmScreen { feeSelectorIcon.assertIsDisplayed() }
}
step("Assert fee selector title is displayed") {
onSendConfirmScreen { feeSelectorTitle.assertIsDisplayed() }
}
step("Assert fee selector tooltip icon is displayed") {
onSendConfirmScreen { feeSelectorTooltipIcon.assertIsDisplayed() }
}
step("Assert fee amount = '$currentFeeAmount'") {
onSendConfirmScreen { feeAmount.assertTextContains(currentFeeAmount) }
}
if (withFeeSelector) {
step("Assert select fee icon is displayed") {
onSendConfirmScreen { selectFeeIcon.assertIsDisplayed() }
}
}
}
fun BaseTestCase.openSendConfirmScreen(
tokenName: String,
inputAmount: String,
recipientAddress: String
) {
step("Click on token with name: '$tokenName'") {
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
}
step("Click on 'Send' button") {
onTokenDetailsScreen { sendButton().performClick() }
}
step("Type '$inputAmount' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(inputAmount)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type recipient address") {
onSendAddressScreen { addressTextField.performTextReplacement(recipientAddress) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
}

View file

@ -9,6 +9,8 @@ import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onCompose
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
import androidx.compose.ui.test.hasText as withText
import androidx.compose.ui.test.hasTestTag as withTestTag
import com.tangem.core.ui.R as CoreUiR
class SendConfirmPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<SendConfirmPageObject>(semanticsProvider = semanticsProvider) {
@ -29,6 +31,11 @@ class SendConfirmPageObject(semanticsProvider: SemanticsNodeInteractionsProvider
useUnmergedTree = true
}
val secondaryAmount: KNode = child {
hasTestTag(BaseAmountBlockTestTags.SECONDARY_AMOUNT)
useUnmergedTree = true
}
fun leaveDepositButton(amount: String): KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasAnyDescendant(withText(getResourceString(R.string.send_notification_leave_button, amount)))
@ -66,6 +73,18 @@ class SendConfirmPageObject(semanticsProvider: SemanticsNodeInteractionsProvider
useUnmergedTree = true
}
fun warningMessage(messageResId: Int): KNode = child {
hasTestTag(NotificationTestTags.MESSAGE)
hasText(getResourceString(messageResId))
useUnmergedTree = true
}
fun warningIcon(messageResId: Int): KNode = child {
hasTestTag(NotificationTestTags.ICON)
hasAnySibling(withText(getResourceString(messageResId)))
useUnmergedTree = true
}
fun recipientAddress(recipientAddress: String): KNode = child {
hasTestTag(SendConfirmScreenTestTags.RECIPIENT_ADDRESS)
hasText(recipientAddress)
@ -76,6 +95,40 @@ class SendConfirmPageObject(semanticsProvider: SemanticsNodeInteractionsProvider
hasTestTag(SendConfirmScreenTestTags.BLOCKCHAIN_ADDRESS)
useUnmergedTree = true
}
val feeSelectorBlock: KNode = child {
hasTestTag(FeeSelectorBlockTestTags.SELECTOR_BLOCK)
}
val feeSelectorIcon: KNode = child {
hasTestTag(FeeSelectorBlockTestTags.ICON)
useUnmergedTree = true
}
val feeSelectorTitle: KNode = child {
hasTestTag(FeeSelectorBlockTestTags.TITLE)
useUnmergedTree = true
}
val feeSelectorTooltipIcon: KNode = child {
hasTestTag(FeeSelectorBlockTestTags.TOOLTIP_ICON)
useUnmergedTree = true
}
val selectFeeIcon: KNode = child {
hasTestTag(FeeSelectorBlockTestTags.SELECT_FEE_ICON)
useUnmergedTree = true
}
val feeAmount: KNode = child {
hasParent(withTestTag(FeeSelectorBlockTestTags.FEE_AMOUNT))
useUnmergedTree = true
}
val refreshButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(CoreUiR.string.warning_button_refresh))
}
}
internal fun BaseTestCase.onSendConfirmScreen(function: SendConfirmPageObject.() -> Unit) =

View file

@ -38,6 +38,16 @@ class SendPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
useUnmergedTree = true
}
val equivalentInputAmount: KNode = child {
hasTestTag(SendScreenTestTags.EQUIVALENT_INPUT_AMOUNT)
useUnmergedTree = true
}
val exchangeIcon: KNode = child {
hasTestTag(SendScreenTestTags.EXCHANGE_ICON)
useUnmergedTree = true
}
val tokenName: KNode = child {
hasTestTag(SendScreenTestTags.TOKEN_NAME)
useUnmergedTree = true

View file

@ -9,10 +9,7 @@ import com.tangem.common.constants.TestConstants.ETHEREUM_RECIPIENT_ADDRESS
import com.tangem.common.constants.TestConstants.ETHEREUM_RECIPIENT_SHORTENED_ADDRESS
import com.tangem.common.constants.TestConstants.XRP_RECIPIENT_ADDRESS
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.clearClipboard
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setClipboardText
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.common.utils.*
import com.tangem.core.ui.R
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses

View file

@ -0,0 +1,299 @@
package com.tangem.tests.send.confirmScreen
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.USER_TOKENS_API_SCENARIO
import com.tangem.common.extensions.*
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.core.ui.R
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
@HiltAndroidTest
class SendConfirmScreenTest : BaseTestCase() {
@AllureId("4003")
@DisplayName("Send (Confirm screen): change sending amount")
@Test
fun changeSendingAmountTest() {
val tokenName = "Ethereum"
val inputAmount = "1"
val newInputAmount = "0.9"
val ethereumAmount = "1.00"
val fiatAmount = "$2,535.63"
val newFiatAmount = "$2,282.07"
val newEthereumAmount = "0.90"
val recipientAddress = ETHEREUM_RECIPIENT_ADDRESS
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Click on token with name: '$tokenName'") {
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
}
step("Click on 'Send' button") {
onTokenDetailsScreen { sendButton().performClick() }
}
step("Type '$inputAmount' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(inputAmount)
}
}
step("Assert fiat amount = '$fiatAmount'") {
onSendScreen { equivalentInputAmount.assertTextContains(fiatAmount) }
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type recipient address") {
onSendAddressScreen { addressTextField.performTextReplacement(recipientAddress) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert token amount = '$ethereumAmount'") {
onSendConfirmScreen { primaryAmount.assertTextContains(ethereumAmount) }
}
step("Assert fiat amount = '$fiatAmount'") {
onSendConfirmScreen { secondaryAmount.assertTextContains(fiatAmount) }
}
step("Click on token amount") {
onSendConfirmScreen { primaryAmount.performClick() }
}
step("Clear input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextClearance()
}
}
step("Type '$newInputAmount' in input text field") {
onSendScreen {
amountInputTextField.performTextReplacement(newInputAmount)
}
}
step("Assert fiat amount = '$newFiatAmount'") {
onSendScreen { equivalentInputAmount.assertTextContains(newFiatAmount) }
}
step("Click on 'Continue' button") {
onSendScreen { continueButton.clickWithAssertion() }
}
step("Assert token amount = '$newEthereumAmount'") {
onSendConfirmScreen { primaryAmount.assertTextContains(newEthereumAmount) }
}
step("Assert fiat amount = '$newFiatAmount'") {
onSendConfirmScreen { secondaryAmount.assertTextContains(newFiatAmount) }
}
}
}
@AllureId("552")
@DisplayName("Send (Confirm screen): switch to equivalent")
@Test
fun switchToEquivalentTest() {
val tokenName = "Ethereum"
val inputAmount = "1"
val tokenAmount = "1.00"
val ethereumAmount = "ETH 1.00"
val fiatAmount = "$2,535.63"
val recipientAddress = ETHEREUM_RECIPIENT_ADDRESS
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Click on token with name: '$tokenName'") {
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
}
step("Click on 'Send' button") {
onTokenDetailsScreen { sendButton().performClick() }
}
step("Type '$inputAmount' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(inputAmount)
}
}
step("Assert fiat amount = '$fiatAmount'") {
onSendScreen { equivalentInputAmount.assertTextContains(fiatAmount) }
}
step("Click on exchange icon") {
onSendScreen { exchangeIcon.performClick() }
}
step("Assert token amount = '$ethereumAmount'") {
onSendScreen { equivalentInputAmount.assertTextContains(ethereumAmount) }
}
step("Click on exchange icon") {
onSendScreen { exchangeIcon.performClick() }
}
step("Assert fiat amount = '$fiatAmount'") {
onSendScreen { equivalentInputAmount.assertTextContains(fiatAmount) }
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type recipient address") {
onSendAddressScreen { addressTextField.performTextReplacement(recipientAddress) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert primary amount = '$tokenAmount'") {
onSendConfirmScreen { primaryAmount.assertTextContains(tokenAmount) }
}
step("Assert secondary amount = '$fiatAmount'") {
onSendConfirmScreen { secondaryAmount.assertTextContains(fiatAmount) }
}
}
}
@AllureId("553")
@DisplayName("Send (Confirm screen): check fee for blockchain with unknown fee")
@Test
fun checkFeeForBlockchainWithUnknownFeeTest() {
val tokenName = "Polygon"
val inputAmount = "1"
val recipientAddress = ETHEREUM_RECIPIENT_ADDRESS
val currentFeeAmount = "<$0.01"
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Swipe up to token") {
swipeVertical(SwipeDirection.UP)
}
step("Open 'Send Confirm' screen for token '$tokenName'") {
openSendConfirmScreen(
tokenName = tokenName,
inputAmount = inputAmount,
recipientAddress = recipientAddress
)
}
step("Check network fee block") {
checkNetworkFeeBlock(currentFeeAmount = currentFeeAmount, withFeeSelector = true)
}
}
}
@AllureId("4565")
@DisplayName("Send (Confirm screen): check fee for token with unknown fee")
@Test
fun checkFeeForTokenWithUnknownFeeTest() {
val tokenName = "POL (ex-MATIC)"
val inputAmount = "1"
val recipientAddress = ETHEREUM_RECIPIENT_ADDRESS
val currentFeeAmount = "<$0.01"
val scenarioName = "eth_estimate_gas"
val scenarioState = "UnknownFee"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(scenarioName)
}
).run {
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Swipe up to token") {
swipeVertical(SwipeDirection.UP)
}
step("Open 'Send Confirm' screen for token '$tokenName'") {
openSendConfirmScreen(
tokenName = tokenName,
inputAmount = inputAmount,
recipientAddress = recipientAddress
)
}
step("Check network fee block") {
checkNetworkFeeBlock(currentFeeAmount = currentFeeAmount, withFeeSelector = true)
}
}
}
@AllureId("554")
@DisplayName("Send (Confirm screen): check fee warning")
@Test
fun checkFeeWarningTest() {
val tokenName = "Polkadot"
val tokenAmount = "0.1"
val warningTitleResId = R.string.send_fee_unreachable_error_title
val warningMessageResId = R.string.send_fee_unreachable_error_text
val currentFeeAmount = "$0.05"
setupHooks(
additionalAfterSection = {
enableWiFi()
enableMobileData()
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Open 'Send Screen' with token: $tokenName") {
openSendScreen(tokenName)
}
step("Type '$tokenAmount' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(tokenAmount)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(POLKADOT_RECIPIENT_ADDRESS) }
}
step("Turn off internet") {
disableWiFi()
disableMobileData()
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Turn on internet") {
enableWiFi()
enableMobileData()
}
step("Assert 'Network fee info unreachable' warning title is displayed") {
onSendConfirmScreen { warningTitle(warningTitleResId).assertIsDisplayed() }
}
step("Assert 'Check your internet connection' warning message is displayed") {
onSendConfirmScreen { warningMessage(warningMessageResId).assertIsDisplayed() }
}
step("Assert warning icon is displayed") {
onSendConfirmScreen { warningIcon(warningTitleResId).assertIsDisplayed() }
}
step("Click on 'Refresh' button") {
waitForIdle()
onSendConfirmScreen { refreshButton.clickWithAssertion() }
}
step("Check network fee block") {
checkNetworkFeeBlock(currentFeeAmount = currentFeeAmount, withFeeSelector = false)
}
}
}
}

View file

@ -25,6 +25,7 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDirection
@ -47,6 +48,7 @@ import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.SendScreenTestTags
import com.tangem.core.ui.utils.rememberDecimalFormat
import kotlinx.coroutines.delay
@ -191,7 +193,8 @@ private fun BoxScope.AmountFieldCurrencyInfo(amountUM: AmountState.Data, onCurre
.size(16.dp)
.graphicsLayer {
rotationY = iconRotateState
},
}
.testTag(SendScreenTestTags.EXCHANGE_ICON),
)
AmountFieldCurrencyIcon(
amountUM = amountUM,
@ -227,6 +230,7 @@ private fun AmountFieldCurrencyIcon(amountUM: AmountState.Data) {
style = TangemTheme.typography.caption2.copy(textDirection = TextDirection.ContentOrLtr),
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
modifier = Modifier.testTag(SendScreenTestTags.EQUIVALENT_INPUT_AMOUNT),
)
if (isFiatValue) {
CurrencyIcon(

View file

@ -0,0 +1,10 @@
package com.tangem.core.ui.test
object FeeSelectorBlockTestTags {
const val SELECTOR_BLOCK = "FEE_SELECTOR_BLOCK"
const val ICON = "FEE_SELECTOR_ICON"
const val TITLE = "FEE_SELECTOR_TITLE"
const val FEE_AMOUNT = "FEE_SELECTOR_FEE_AMOUNT"
const val TOOLTIP_ICON = "FEE_SELECTOR_TOOLTIP_ICON"
const val SELECT_FEE_ICON = "FEE_SELECTOR_SELECT_FEE_ICON"
}

View file

@ -5,6 +5,8 @@ object SendScreenTestTags {
const val AMOUNT_CONTAINER_TITLE = "SEND_SCREEN_AMOUNT_CONTAINER_TITLE"
const val INPUT_TEXT_FIELD = "SEND_SCREEN_INPUT_TEXT_FIELD"
const val EQUIVALENT_INPUT_AMOUNT = "SEND_SCREEN_EQUIVALENT_INPUT_AMOUNT"
const val EXCHANGE_ICON = "SEND_SCREEN_EXCHANGE_ICON"
const val TOKEN_NAME = "SEND_SCREEN_TOKEN_NAME"
const val PRIMARY_AMOUNT = "SEND_SCREEN_PRIMARY_AMOUNT"
const val SECONDARY_AMOUNT = "SEND_SCREEN_SECONDARY_AMOUNT"

View file

@ -11,6 +11,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.*
import androidx.compose.ui.text.style.TextAlign
@ -35,6 +36,7 @@ import com.tangem.core.ui.format.bigdecimal.fee
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.FeeSelectorBlockTestTags
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.features.send.v2.api.entity.*
@ -54,11 +56,14 @@ internal fun FeeSelectorBlockContent(
Row(
modifier = modifier
.background(TangemTheme.colors.background.action)
.padding(12.dp),
.padding(12.dp)
.testTag(FeeSelectorBlockTestTags.SELECTOR_BLOCK),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
modifier = Modifier.size(24.dp),
modifier = Modifier
.size(24.dp)
.testTag(FeeSelectorBlockTestTags.ICON),
painter = painterResource(R.drawable.ic_fee_new_24),
contentDescription = null,
tint = TangemTheme.colors.icon.accent,
@ -85,7 +90,8 @@ private fun FeeSelectorStaticPart(onReadMoreClick: () -> Unit, modifier: Modifie
Text(
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing4)
.weight(1f, fill = false),
.weight(1f, fill = false)
.testTag(FeeSelectorBlockTestTags.TITLE),
text = stringResourceSafe(R.string.common_network_fee_title),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
@ -123,7 +129,9 @@ private fun FeeSelectorStaticPart(onReadMoreClick: () -> Unit, modifier: Modifie
.clip(CircleShape),
content = { contentModifier ->
Icon(
modifier = contentModifier.size(TangemTheme.dimens.size16),
modifier = contentModifier
.size(TangemTheme.dimens.size16)
.testTag(FeeSelectorBlockTestTags.TOOLTIP_ICON),
painter = painterResource(id = R.drawable.ic_token_info_24),
contentDescription = null,
tint = TangemTheme.colors.icon.informative,
@ -174,11 +182,15 @@ private fun FeeContent(state: FeeSelectorUM.Content, modifier: Modifier = Modifi
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.End,
modifier = Modifier.padding(start = TangemTheme.dimens.spacing4),
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing4)
.testTag(FeeSelectorBlockTestTags.FEE_AMOUNT),
)
if (!state.feeItems.isSingleItem()) {
Icon(
modifier = Modifier.size(width = 18.dp, height = 24.dp),
modifier = Modifier
.size(width = 18.dp, height = 24.dp)
.testTag(FeeSelectorBlockTestTags.SELECT_FEE_ICON),
painter = painterResource(id = R.drawable.ic_select_18_24),
contentDescription = null,
tint = TangemTheme.colors.icon.informative,