Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-29 09:42:11 +02:00
parent 3472f48de7
commit 9b6ed88d78
8 changed files with 581 additions and 1 deletions

View file

@ -0,0 +1,156 @@
package com.tangem.scenarios
import androidx.compose.ui.semantics.SemanticsActions
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.hasAnyDescendant
import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performSemanticsAction
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.swipeLeft
import androidx.compose.ui.test.swipeRight
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_VERY_LONG
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.extensions.isDisplayedSafely
import com.tangem.core.ui.test.BuyTokenScreenTestTags
import com.tangem.core.ui.test.DetailsScreenTestTags
import com.tangem.core.ui.test.MainScreenTestTags
import com.tangem.screens.*
import com.tangem.tap.domain.sdk.mocks.MockContent
import com.tangem.tap.domain.sdk.mocks.MockProvider
import io.qameta.allure.kotlin.Allure.step
// The 'Add Wallet' button sits in a nested LazyColumn and rejects touch injection, so invoke OnClick directly.
private fun BaseTestCase.clickViaSemantics(matcher: SemanticsMatcher, useUnmergedTree: Boolean = false) {
composeTestRule.onNode(matcher, useUnmergedTree).performSemanticsAction(SemanticsActions.OnClick)
}
// Both pager pages stay mounted; the off-screen one ignores swipes, so always swipe the on-screen wallet card.
private fun BaseTestCase.swipeDisplayedWallet(toPrevious: Boolean) {
val nodes = composeTestRule.onAllNodes(hasTestTag(MainScreenTestTags.WALLET_LIST_ITEM))
for (i in 0 until nodes.fetchSemanticsNodes().size) {
val swiped = runCatching {
nodes[i].assertIsDisplayed()
nodes[i].performTouchInput { if (toPrevious) swipeRight() else swipeLeft() }
}.isSuccess
if (swiped) break
}
waitForIdle()
}
/**
* Adds a second hardware (card) wallet from a wallet-restricted state (a hot wallet already exists):
* 'Add Wallet' scans a card immediately there is no wallet-type chooser so the scan mock must be
* set before the click. After saving, the app returns to Main; the added card lands as an off-screen
* pager page, so this swipes to it and syncs its missing addresses before returning.
*/
fun BaseTestCase.addNewCardWallet(mockContent: MockContent) {
step("Click 'More' button on TopBar") {
onMainScreenTopBar { moreButton.clickWithAssertion() }
}
MockProvider.setMocks(mockContent)
step("Click on 'Add Wallet' button (scans a new hardware wallet)") {
clickViaSemantics(hasTestTag(DetailsScreenTestTags.ADD_WALLET_BUTTON))
}
// Details pops back to Main only after the freshly scanned wallet finishes its initial load over many (some failing) RPCs.
// Gate on the top-bar More button, not the screen container — the bottom Markets sheet can leave the container un-"displayed".
step("Assert 'Main' screen is displayed with the new wallet") {
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_VERY_LONG) {
runCatching { onMainScreenTopBar { moreButton.assertIsDisplayed() } }.isSuccess
}
}
// The added card is the newest pager page; its "Synchronize addresses" prompt is off-screen until swiped to.
step("Synchronize the new card wallet's addresses") {
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) {
var shown = false
onMainScreen { shown = synchronizeAddressesButton.isDisplayedSafely() }
if (!shown) swipeDisplayedWallet(toPrevious = false)
shown
}
// Let the pager fling settle before clicking — the wait above releases as soon as the prompt has bounds, and a
// click landing mid-animation is eaten by the button's clickableSingle debounce, so the prompt never clears.
waitForIdle()
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
// The prompt clears once the card's addresses are derived (re-scan + reload over many, some failing, RPCs).
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_VERY_LONG) {
var generated = false
onMainScreen { generated = !synchronizeAddressesButton.isDisplayedSafely() }
generated
}
}
}
/** Taps the on-screen token row by [tokenName]; the wallet pager keeps both pages mounted, so click the displayed copy. */
fun BaseTestCase.clickDisplayedTokenOnMain(tokenName: String) {
val matcher = hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM) and hasAnyDescendant(hasText(tokenName))
step("Click on token '$tokenName' on the visible wallet") {
// Both pager pages mount the same token; click the one currently on-screen once the swipe has settled.
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) {
val nodes = composeTestRule.onAllNodes(matcher, useUnmergedTree = true)
(0 until nodes.fetchSemanticsNodes().size).any { i ->
runCatching {
nodes[i].assertIsDisplayed()
nodes[i].performClick()
}.isSuccess
}
}
}
}
/** Swipes the wallet card to the previous wallet (the one added before the current page). */
fun BaseTestCase.switchToPreviousWallet() {
step("Swipe wallet card to the previous wallet") {
swipeDisplayedWallet(toPrevious = true)
}
}
/** Opens the receive selector and picks identical [token] located on a different wallet [walletName] via its wallet tab. */
fun BaseTestCase.selectReceiveTokenOnWallet(token: String, walletName: String) {
step("Click on 'Choose token' button") {
onSwapTokenScreen { chooseTokenButton.performClick() }
}
// The wallet tab is a custom clickable Row that Kakao's performClick can't hit (autoscroll fails); invoke OnClick directly.
step("Select wallet tab '$walletName'") {
clickViaSemantics(
hasTestTag(BuyTokenScreenTestTags.WALLET_TAB) and hasAnyDescendant(hasText(walletName)),
useUnmergedTree = true,
)
}
step("Click on token with name '$token'") {
clickViaSemantics(
hasTestTag(BuyTokenScreenTestTags.LAZY_LIST_ITEM) and hasAnyDescendant(hasText(token)),
useUnmergedTree = true,
)
}
}
/** Searches the receive selector for [token], adds it to recipient wallet [recipientWalletName] via the market result. */
fun BaseTestCase.addMissingReceiveTokenToWallet(token: String, recipientWalletName: String) {
step("Click on 'Choose token' button") {
onSwapTokenScreen { chooseTokenButton.performClick() }
}
step("Type '$token' in search field") {
onSwapSelectTokenScreen {
searchBarBlock.performClick()
searchBarBlock.performTextInput(token)
}
}
step("Click on market token '$token'") {
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) {
runCatching { onSwapSelectTokenScreen { marketsTokenWithName(token).performClick() } }.isSuccess
}
}
// The 'Add token' sheet pre-selects the recipient (the only wallet missing the token, since the source already holds it).
step("Assert recipient wallet '$recipientWalletName' is selected") {
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) {
runCatching { onAddToPortfolioScreen { walletName(recipientWalletName).assertIsDisplayed() } }.isSuccess
}
}
step("Click on 'Add' button") {
onAddToPortfolioScreen { addButton.performClick() }
}
}

View file

@ -0,0 +1,27 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import androidx.compose.ui.test.hasClickAction
import com.tangem.common.BaseTestCase
import com.tangem.wallet.R
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
class AddToPortfolioPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<AddToPortfolioPageObject>(semanticsProvider = semanticsProvider) {
fun walletName(walletName: String): KNode = child {
hasText(walletName)
useUnmergedTree = true
}
val addButton: KNode = child {
hasText(getResourceString(R.string.common_add))
hasClickAction()
}
}
internal fun BaseTestCase.onAddToPortfolioScreen(function: AddToPortfolioPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -5,14 +5,18 @@ import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.extensions.extractText import com.tangem.common.extensions.extractText
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO 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.SVS_SEED_PHRASE_12
import com.tangem.common.constants.TestConstants.TANGEM_PAY_ELIGIBILITY_SCENARIO
import com.tangem.common.constants.TestConstants.USER_TOKENS_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.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_VERY_LONG
import com.tangem.common.utils.resetWireMockScenarioState import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.core.ui.R as CoreUiR import com.tangem.core.ui.R as CoreUiR
import com.tangem.scenarios.* import com.tangem.scenarios.*
import com.tangem.screens.* import com.tangem.screens.*
import com.tangem.screens.tangempay.*
import com.tangem.tap.domain.sdk.mocks.content.Wallet2WithDerivationsMockContent import com.tangem.tap.domain.sdk.mocks.content.Wallet2WithDerivationsMockContent
import com.tangem.tap.domain.sdk.mocks.content.WalletMockContent
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import io.github.kakaocup.kakao.common.utilities.getResourceString import io.github.kakaocup.kakao.common.utilities.getResourceString
import io.qameta.allure.kotlin.Allure.step import io.qameta.allure.kotlin.Allure.step
@ -808,6 +812,334 @@ class AppTransfersTest : BaseTestCase() {
} }
} }
@AllureId("10005")
@DisplayName("App transfers: Tron network fee")
@Test
fun tronNetworkFeeTest() {
val token = "Tron"
val amount = "0.001"
val userTokensState = "TwoAccountsSameTron"
val networksProvidersScenario = "networks_providers"
val appTransfersNetworksState = "AppTransfersNetworks"
setupHooks(
additionalBeforeAppLaunchSection = {
setWireMockScenarioState(storiesScenario, storiesErrorState)
// networks_providers configures SDK RPC hosts at launch — must be set before the activity starts.
setWireMockScenarioState(networksProvidersScenario, appTransfersNetworksState)
},
additionalAfterSection = {
resetWireMockScenarioState(storiesScenario)
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(networksProvidersScenario)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$userTokensState'") {
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = userTokensState)
}
// Non-zero prices keep total fiat > 0 so the empty-wallet banner doesn't push the account list under the Markets sheet.
step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: '$appTransfersNetworksState'") {
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = appTransfersNetworksState)
}
step("Open Swap in Transfer mode for '$token'") { openSwapInTransferMode(token) }
step("Enter amount '$amount'") { inputAmount(amount) }
step("Assert Transfer mode is ready") { assertTransferReady() }
step("Assert network fee is displayed") { waitForFeeDisplayed() }
}
}
// [REDACTED_TASK_KEY]: blockchain SDK TonProvidersBuilder drops public providers, so TON has no provider in the mocked build.
@Ignore("[REDACTED_JIRA]")
@AllureId("10012")
@DisplayName("App transfers: TON network fee")
@Test
fun tonNetworkFeeTest() {
// The SDK names TON's coin "Gram" (Blockchain.TON.getCoinName), so the portfolio row shows "Gram", not "Toncoin".
val token = "Gram"
val amount = "0.001"
val userTokensState = "TwoAccountsSameTON"
val networksProvidersScenario = "networks_providers"
val appTransfersNetworksState = "AppTransfersNetworks"
setupHooks(
additionalBeforeAppLaunchSection = {
setWireMockScenarioState(storiesScenario, storiesErrorState)
// networks_providers configures SDK RPC hosts at launch — must be set before the activity starts.
setWireMockScenarioState(networksProvidersScenario, appTransfersNetworksState)
},
additionalAfterSection = {
resetWireMockScenarioState(storiesScenario)
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(networksProvidersScenario)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$userTokensState'") {
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = userTokensState)
}
// Non-zero prices keep total fiat > 0 so the empty-wallet banner doesn't push the account list under the Markets sheet.
step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: '$appTransfersNetworksState'") {
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = appTransfersNetworksState)
}
step("Open Swap in Transfer mode for '$token'") { openSwapInTransferMode(token) }
step("Enter amount '$amount'") { inputAmount(amount) }
step("Assert Transfer mode is ready") { assertTransferReady() }
step("Assert network fee is displayed") { waitForFeeDisplayed() }
}
}
@AllureId("10013")
@DisplayName("App transfers: Cosmos network fee")
@Test
fun cosmosNetworkFeeTest() {
val token = "Cosmos"
val amount = "0.001"
val userTokensState = "TwoAccountsSameCosmos"
val networksProvidersScenario = "networks_providers"
val appTransfersNetworksState = "AppTransfersNetworks"
setupHooks(
additionalBeforeAppLaunchSection = {
setWireMockScenarioState(storiesScenario, storiesErrorState)
// networks_providers configures SDK RPC hosts at launch — must be set before the activity starts.
setWireMockScenarioState(networksProvidersScenario, appTransfersNetworksState)
},
additionalAfterSection = {
resetWireMockScenarioState(storiesScenario)
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(networksProvidersScenario)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$userTokensState'") {
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = userTokensState)
}
// Non-zero prices keep total fiat > 0 so the empty-wallet banner doesn't push the account list under the Markets sheet.
step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: '$appTransfersNetworksState'") {
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = appTransfersNetworksState)
}
step("Open Swap in Transfer mode for '$token'") { openSwapInTransferMode(token) }
step("Enter amount '$amount'") { inputAmount(amount) }
step("Assert Transfer mode is ready") { assertTransferReady() }
step("Assert network fee is displayed") { waitForFeeDisplayed() }
}
}
@AllureId("10015")
@DisplayName("App transfers: Aptos network fee")
@Test
fun aptosNetworkFeeTest() {
val token = "Aptos"
val amount = "0.001"
val userTokensState = "TwoAccountsSameAptos"
val networksProvidersScenario = "networks_providers"
val appTransfersNetworksState = "AppTransfersNetworks"
setupHooks(
additionalBeforeAppLaunchSection = {
setWireMockScenarioState(storiesScenario, storiesErrorState)
// networks_providers configures SDK RPC hosts at launch — must be set before the activity starts.
setWireMockScenarioState(networksProvidersScenario, appTransfersNetworksState)
},
additionalAfterSection = {
resetWireMockScenarioState(storiesScenario)
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(networksProvidersScenario)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$userTokensState'") {
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = userTokensState)
}
// Non-zero prices keep total fiat > 0 so the empty-wallet banner doesn't push the account list under the Markets sheet.
step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: '$appTransfersNetworksState'") {
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = appTransfersNetworksState)
}
step("Open Swap in Transfer mode for '$token'") { openSwapInTransferMode(token) }
step("Enter amount '$amount'") { inputAmount(amount) }
step("Assert Transfer mode is ready") { assertTransferReady() }
step("Assert network fee is displayed") { waitForFeeDisplayed() }
}
}
@AllureId("9856")
@DisplayName("App transfers: Transfer mode is available from a Tangem Pay account")
@Test
fun transferModeAvailableFromTangemPayAccountTest() {
val token = "USDC"
val receiveAccountName = "Main account"
val eligibilityState = "PaeraCustomer"
val balanceScenario = "tangem_pay_balance_update"
val balanceInitialState = "InitialBalance"
val historyScenario = "tangem_pay_transaction_history"
val historyInitialState = "InitialEmpty"
val userTokensState = "TangemPayTransferUsdc"
setupHooks(
additionalBeforeAppLaunchSection = { setWireMockScenarioState(storiesScenario, storiesErrorState) },
additionalAfterSection = {
resetWireMockScenarioState(storiesScenario)
resetWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO)
resetWireMockScenarioState(balanceScenario)
resetWireMockScenarioState(historyScenario)
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
}
).run {
step("Set WireMock scenario: '$TANGEM_PAY_ELIGIBILITY_SCENARIO' to state: '$eligibilityState'") {
setWireMockScenarioState(scenarioName = TANGEM_PAY_ELIGIBILITY_SCENARIO, state = eligibilityState)
}
step("Set WireMock scenario: '$balanceScenario' to state: '$balanceInitialState'") {
setWireMockScenarioState(scenarioName = balanceScenario, state = balanceInitialState)
}
step("Set WireMock scenario: '$historyScenario' to state: '$historyInitialState'") {
setWireMockScenarioState(scenarioName = historyScenario, state = historyInitialState)
}
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$userTokensState'") {
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = userTokensState)
}
step("Open Tangem Pay") { openTangemPay() }
step("Click on 'Withdraw' button") {
onTangemPayMainScreen { withdrawButton.clickWithAssertion() }
}
step("Acknowledge withdrawal note sheet") {
onTangemPayWithdrawNoteSheet {
title.assertIsDisplayed()
gotItButton.clickWithAssertion()
}
}
step("Choose identical receive token '$token' from '$receiveAccountName'") {
chooseIdenticalReceiveToken(tokenName = token, receiveAccountName = receiveAccountName)
}
// Assert the top-bar 'Transfer' title (unambiguous transfer-mode signal) + no provider block; the
// withdraw-entry swap keeps recalculating, so use flakySafely rather than assertTransferReady's waitUntil.
step("Assert Transfer mode is ready") {
flakySafely(WAIT_UNTIL_TIMEOUT_VERY_LONG) {
onSwapTokenScreen { transferTitle.assertIsDisplayed() }
}
onSwapTokenScreen { providersBlock.assertIsNotDisplayed() }
}
}
}
@AllureId("9995")
@DisplayName("App transfers: transfer between different wallets reaches 'Transfer in progress' screen")
@Test
fun transferBetweenDifferentWalletsReachesFinishTest() {
val token = "Ethereum"
val amount = "0.001"
val secondWalletName = "Wallet 2"
val userTokensState = "EthereumWithSecondToken"
setupHooks(
additionalBeforeAppLaunchSection = { setWireMockScenarioState(storiesScenario, storiesErrorState) },
additionalAfterSection = {
resetWireMockScenarioState(storiesScenario)
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(ethCallScenario)
resetWireMockScenarioState(ethBalanceScenario)
}
).run {
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$userTokensState'") {
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = userTokensState)
}
step("Set WireMock scenario: '$ethCallScenario' to state: '$started'") {
setWireMockScenarioState(scenarioName = ethCallScenario, state = started)
}
step("Set WireMock scenario: '$ethBalanceScenario' to state: '$started'") {
setWireMockScenarioState(scenarioName = ethBalanceScenario, state = started)
}
step("Open 'Main' screen with existing hot wallet") {
openMainScreenWithExistingHotWallet(SVS_SEED_PHRASE_12)
}
step("Generate missing addresses") { generateMissingHotWalletAddresses() }
step("Wait for addresses to be generated") { waitForAddressesGenerated() }
step("Add a second card wallet '$secondWalletName'") {
addNewCardWallet(WalletMockContent)
}
step("Switch back to the hot wallet") { switchToPreviousWallet() }
step("Click on token with name: '$token'") { clickDisplayedTokenOnMain(token) }
step("Open 'Swap' screen") {
openSwapScreen(from = SwapEntryPoint.TokenDetails, storiesExist = false)
}
step("Select identical receive token '$token' on '$secondWalletName'") {
selectReceiveTokenOnWallet(token = token, walletName = secondWalletName)
}
step("Enter amount '$amount'") { inputAmount(amount) }
step("Assert Transfer mode is ready") { assertTransferReady() }
step("Assert network fee is displayed") { waitForFeeDisplayed() }
step("Hold to confirm the transfer") { holdToConfirmTransfer() }
step("Assert 'Transfer in progress' screen is displayed") {
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
onSwapSuccessScreen { transferInProgressTitle.assertIsDisplayed() }
}
}
}
}
@AllureId("9996")
@DisplayName("App transfers: adding a missing token to the recipient wallet enables Transfer")
@Test
fun addMissingTokenToRecipientWalletEnablesTransferTest() {
val token = "Ethereum"
val bitcoinToken = "Bitcoin"
val recipientWalletName = "Wallet"
val recipientWithoutEthereumState = "RecipientWithoutEthereum"
val ethereumWithSecondTokenState = "EthereumWithSecondToken"
setupHooks(
additionalBeforeAppLaunchSection = { setWireMockScenarioState(storiesScenario, storiesErrorState) },
additionalAfterSection = {
resetWireMockScenarioState(storiesScenario)
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(ethCallScenario)
resetWireMockScenarioState(ethBalanceScenario)
}
).run {
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$recipientWithoutEthereumState'") {
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = recipientWithoutEthereumState)
}
step("Set WireMock scenario: '$ethCallScenario' to state: '$started'") {
setWireMockScenarioState(scenarioName = ethCallScenario, state = started)
}
step("Set WireMock scenario: '$ethBalanceScenario' to state: '$started'") {
setWireMockScenarioState(scenarioName = ethBalanceScenario, state = started)
}
step("Open 'Main' screen with existing hot wallet") {
openMainScreenWithExistingHotWallet(SVS_SEED_PHRASE_12)
}
step("Generate missing addresses") { generateMissingHotWalletAddresses() }
step("Wait for addresses to be generated") { waitForAddressesGenerated() }
step("Assert token '$bitcoinToken' is displayed") {
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
onMainScreen { tokenWithTitleAndAddress(bitcoinToken).assertIsDisplayed() }
}
}
// Switch the user-tokens mock so the second wallet loads with Ethereum while the recipient stays Ethereum-less.
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$ethereumWithSecondTokenState'") {
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = ethereumWithSecondTokenState)
}
step("Add a second card wallet") {
addNewCardWallet(WalletMockContent)
}
step("Click on token with name: '$token'") { clickDisplayedTokenOnMain(token) }
step("Open 'Swap' screen") {
openSwapScreen(from = SwapEntryPoint.TokenDetails, storiesExist = false)
}
step("Add missing token '$token' to recipient wallet '$recipientWalletName'") {
addMissingReceiveTokenToWallet(token = token, recipientWalletName = recipientWalletName)
}
step("Assert Transfer mode is ready") { assertTransferReady() }
}
}
// [REDACTED_TASK_KEY]: transfer mode never runs tx validation, so the destination rent-exemption notification never shows. // [REDACTED_TASK_KEY]: transfer mode never runs tx validation, so the destination rent-exemption notification never shows.
@Ignore("[REDACTED_JIRA]") @Ignore("[REDACTED_JIRA]")
@AllureId("9852") @AllureId("9852")

View file

@ -132,6 +132,10 @@ object WalletMockContent : MockContent {
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62), publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41), chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
), ),
DerivationPath("m/44'/118'/0'/0/0") to ExtendedPublicKey( // Cosmos
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
),
DerivationPath("m/1852'/1815'/0'/0/0") to ExtendedPublicKey( DerivationPath("m/1852'/1815'/0'/0/0") to ExtendedPublicKey(
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62), publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41), chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
@ -181,6 +185,22 @@ object WalletMockContent : MockContent {
publicKey = byteArrayOf(-65, -53, -62, -12, -57, -32, -38, -9, -128, -52, -83, -61, 73, 39, 41, 15, -74, -97, 38, 52, -101, 63, 74, -56, -20, 15, 57, -127, 114, -93, -17, -109), publicKey = byteArrayOf(-65, -53, -62, -12, -57, -32, -38, -9, -128, -52, -83, -61, 73, 39, 41, 15, -74, -97, 38, 52, -101, 63, 74, -56, -20, 15, 57, -127, 114, -93, -17, -109),
chainCode = byteArrayOf(-81, 15, 125, 28, -115, -22, 87, 81, 87, -123, -25, -74, 86, 2, 1, 110, -115, 65, -110, 63, -64, 83, -93, -97, -104, 123, 12, -26, 94, 27, 84, -6), chainCode = byteArrayOf(-81, 15, 125, 28, -115, -22, 87, 81, 87, -123, -25, -74, 86, 2, 1, 110, -115, 65, -110, 63, -64, 83, -93, -97, -104, 123, 12, -26, 94, 27, 84, -6),
), ),
DerivationPath("m/44'/607'/0'/0/0") to ExtendedPublicKey( // TON (account 1)
publicKey = byteArrayOf(30, -109, -39, 33, -94, -73, 121, 50, -75, 86, 102, -2, -74, -23, 63, -3, 79, -82, -103, 106, 82, -86, -107, -63, -46, 104, 7, 18, -41, 15, -87, -43),
chainCode = byteArrayOf(15, 61, -29, 22, 30, 45, -51, -60, 5, 62, -87, -35, 54, -97, -5, -44, -54, -107, -14, -119, -3, 92, 91, 75, -66, 26, 112, 83, 122, -25, -64, 40),
),
DerivationPath("m/44'/607'/1'/0/0") to ExtendedPublicKey( // TON (account 2)
publicKey = byteArrayOf(-51, 62, 97, 25, 83, 75, -79, 23, 6, -42, -94, 45, 91, -66, 57, -80, -75, -39, 19, -88, 95, -124, 50, 39, 114, -118, 27, -122, 48, -69, 7, -111),
chainCode = byteArrayOf(77, 63, 69, -114, -25, 105, -123, -42, -87, 107, 86, -43, 46, 92, -78, -107, -72, -81, -102, 45, 75, 97, -120, -10, 118, 27, -34, -50, -92, 3, 47, -126),
),
DerivationPath("m/44'/637'/0'/0'/0'") to ExtendedPublicKey( // Aptos (account 1)
publicKey = byteArrayOf(-65, -53, -62, -12, -57, -32, -38, -9, -128, -52, -83, -61, 73, 39, 41, 15, -74, -97, 38, 52, -101, 63, 74, -56, -20, 15, 57, -127, 114, -93, -17, -109),
chainCode = byteArrayOf(-81, 15, 125, 28, -115, -22, 87, 81, 87, -123, -25, -74, 86, 2, 1, 110, -115, 65, -110, 63, -64, 83, -93, -97, -104, 123, 12, -26, 94, 27, 84, -6),
),
DerivationPath("m/44'/637'/1'/0'/0'") to ExtendedPublicKey( // Aptos (account 2)
publicKey = byteArrayOf(-65, -53, -62, -12, -57, -32, -38, -9, -128, -52, -83, -61, 73, 39, 41, 15, -74, -97, 38, 52, -101, 63, 74, -56, -20, 15, 57, -127, 114, -93, -17, -109),
chainCode = byteArrayOf(-81, 15, 125, 28, -115, -22, 87, 81, 87, -123, -25, -74, 86, 2, 1, 110, -115, 65, -110, 63, -64, 83, -93, -97, -104, 123, 12, -26, 94, 27, 84, -6),
),
), ),
extendedPublicKey = ExtendedPublicKey( extendedPublicKey = ExtendedPublicKey(
publicKey = byteArrayOf(-65, -53, -62, -12, -57, -32, -38, -9, -128, -52, -83, -61, 73, 39, 41, 15, -74, -97, 38, 52, -101, 63, 74, -56, -20, 15, 57, -127, 114, -93, -17, -109), publicKey = byteArrayOf(-65, -53, -62, -12, -57, -32, -38, -9, -128, -52, -83, -61, 73, 39, 41, 15, -74, -97, 38, 52, -101, 63, 74, -56, -20, 15, 57, -127, 114, -93, -17, -109),
@ -290,6 +310,20 @@ object WalletMockContent : MockContent {
parentFingerprint = byteArrayOf(0, 0, 0, 0), parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0, childNumber = 0,
), ),
DerivationPath("m/44'/118'/0'/0/0") to ExtendedPublicKey( // Cosmos (account 1)
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/118'/1'/0/0") to ExtendedPublicKey( // Cosmos (account 2)
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/144'/0'/0/0") to ExtendedPublicKey( // XRP DerivationPath("m/44'/144'/0'/0/0") to ExtendedPublicKey( // XRP
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62), publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41), chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
@ -424,6 +458,34 @@ object WalletMockContent : MockContent {
parentFingerprint = byteArrayOf(0, 0, 0, 0), parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0, childNumber = 0,
), ),
DerivationPath("m/44'/607'/0'/0/0") to ExtendedPublicKey( // TON (account 1)
publicKey = byteArrayOf(30, -109, -39, 33, -94, -73, 121, 50, -75, 86, 102, -2, -74, -23, 63, -3, 79, -82, -103, 106, 82, -86, -107, -63, -46, 104, 7, 18, -41, 15, -87, -43),
chainCode = byteArrayOf(15, 61, -29, 22, 30, 45, -51, -60, 5, 62, -87, -35, 54, -97, -5, -44, -54, -107, -14, -119, -3, 92, 91, 75, -66, 26, 112, 83, 122, -25, -64, 40),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/607'/1'/0/0") to ExtendedPublicKey( // TON (account 2)
publicKey = byteArrayOf(-51, 62, 97, 25, 83, 75, -79, 23, 6, -42, -94, 45, 91, -66, 57, -80, -75, -39, 19, -88, 95, -124, 50, 39, 114, -118, 27, -122, 48, -69, 7, -111),
chainCode = byteArrayOf(77, 63, 69, -114, -25, 105, -123, -42, -87, 107, 86, -43, 46, 92, -78, -107, -72, -81, -102, 45, 75, 97, -120, -10, 118, 27, -34, -50, -92, 3, 47, -126),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/637'/0'/0'/0'") to ExtendedPublicKey( // Aptos (account 1)
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/637'/1'/0'/0'") to ExtendedPublicKey( // Aptos (account 2)
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
), ),
), ),

View file

@ -3,4 +3,5 @@ package com.tangem.core.ui.test
object BuyTokenScreenTestTags { object BuyTokenScreenTestTags {
const val LAZY_LIST = "BUY_TOKEN_SCREEN_LAZY_LIST" const val LAZY_LIST = "BUY_TOKEN_SCREEN_LAZY_LIST"
const val LAZY_LIST_ITEM = "BUY_TOKEN_SCREEN_LAZY_LIST_ITEM" const val LAZY_LIST_ITEM = "BUY_TOKEN_SCREEN_LAZY_LIST_ITEM"
const val WALLET_TAB = "BUY_TOKEN_SCREEN_WALLET_TAB"
} }

View file

@ -5,4 +5,5 @@ object DetailsScreenTestTags {
const val SCREEN_ITEM = "DETAILS_SCREEN_ITEM" const val SCREEN_ITEM = "DETAILS_SCREEN_ITEM"
const val VERSION_NAME = "DETAILS_SCREEN_VERSION_NAME" const val VERSION_NAME = "DETAILS_SCREEN_VERSION_NAME"
const val USER_WALLET_ITEM = "DETAILS_SCREEN_USER_WALLET_ITEM" const val USER_WALLET_ITEM = "DETAILS_SCREEN_USER_WALLET_ITEM"
const val ADD_WALLET_BUTTON = "DETAILS_SCREEN_ADD_WALLET_BUTTON"
} }

View file

@ -252,6 +252,7 @@ private fun WalletTabItem(state: WalletTabUM, modifier: Modifier = Modifier) {
.clip(RoundedCornerShape(percent = 50)) .clip(RoundedCornerShape(percent = 50))
.background(backgroundColor) .background(backgroundColor)
.clickable(onClick = state.onClick) .clickable(onClick = state.onClick)
.testTag(BuyTokenScreenTestTags.WALLET_TAB)
.padding(horizontal = 16.dp, vertical = 8.dp), .padding(horizontal = 16.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,

View file

@ -130,7 +130,7 @@ private fun AddWalletButton(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
BlockCard( BlockCard(
modifier = modifier, modifier = modifier.testTag(DetailsScreenTestTags.ADD_WALLET_BUTTON),
onClick = onClick, onClick = onClick,
enabled = !isInProgress, enabled = !isInProgress,
) { ) {