Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-06 10:37:44 +02:00
parent 8b677a8e6b
commit c176dc37c4
13 changed files with 362 additions and 19 deletions

View file

@ -4,6 +4,8 @@ import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
fun getClipboardText(context: Context): String? { fun getClipboardText(context: Context): String? {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
@ -27,12 +29,15 @@ fun clearClipboard(
clipboard.clearPrimaryClip() clipboard.clearPrimaryClip()
} }
// Kotlin's assert() is a no-op on device (JVM assertions disabled) — use JUnit asserts here.
fun assertClipboardTextEquals( fun assertClipboardTextEquals(
expected: String, expected: String,
context: Context = ApplicationProvider.getApplicationContext() context: Context = ApplicationProvider.getApplicationContext()
) { ) {
assertEquals("Clipboard text mismatch", expected, getClipboardText(context))
}
fun assertClipboardIsEmpty(context: Context = ApplicationProvider.getApplicationContext()) {
val actual = getClipboardText(context) val actual = getClipboardText(context)
assert(actual == expected) { assertTrue("Expected empty clipboard but was: '$actual'", actual.isNullOrEmpty())
"Clipboard text mismatch.\nExpected: '$expected'\nActual: '$actual'"
}
} }

View file

@ -4,6 +4,7 @@ import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
import com.tangem.common.extensions.clickWithAssertion import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.extensions.performTextInputInChunks import com.tangem.common.extensions.performTextInputInChunks
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.screens.accounts.onAccountDetailsScreen import com.tangem.screens.accounts.onAccountDetailsScreen
import com.tangem.screens.onAddCustomTokenScreen import com.tangem.screens.onAddCustomTokenScreen
import com.tangem.screens.onDetailsScreen import com.tangem.screens.onDetailsScreen
@ -12,6 +13,7 @@ import com.tangem.screens.onManageTokensScreen
import com.tangem.screens.onWalletSettingsScreen import com.tangem.screens.onWalletSettingsScreen
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
import kotlinx.coroutines.runBlocking
import com.tangem.core.res.R as CoreResR import com.tangem.core.res.R as CoreResR
private fun mainAccountName(): String = getResourceString(CoreResR.string.account_main_account_title) private fun mainAccountName(): String = getResourceString(CoreResR.string.account_main_account_title)
@ -66,7 +68,19 @@ fun BaseTestCase.addCustomTokenWithCustomDerivation(network: String, contract: S
navigateBackToMainFromManageTokens() navigateBackToMainFromManageTokens()
} }
fun BaseTestCase.navigateBackToMainFromManageTokens() { private const val MAX_BACKS_TO_MANAGE_TOKENS = 5
// The device-backs to close the sheet vary — press back until Manage Tokens is shown, then fail loudly.
private fun BaseTestCase.closeAddCustomTokenSheet() {
repeat(MAX_BACKS_TO_MANAGE_TOKENS) {
if (runCatching { onManageTokensScreen { searchField.assertIsDisplayed() } }.isSuccess) return
device.uiDevice.pressBack()
waitForIdle()
}
onManageTokensScreen { searchField.assertIsDisplayed() }
}
private fun BaseTestCase.navigateBackToWalletSettings() {
step("Click on 'Manage tokens' screen 'Back' button") { step("Click on 'Manage tokens' screen 'Back' button") {
waitForIdle() waitForIdle()
onManageTokensScreen { topAppBarBackButton.performClick() } onManageTokensScreen { topAppBarBackButton.performClick() }
@ -75,6 +89,60 @@ fun BaseTestCase.navigateBackToMainFromManageTokens() {
waitForIdle() waitForIdle()
onAccountDetailsScreen { topAppBarBackButton.performClick() } onAccountDetailsScreen { topAppBarBackButton.performClick() }
} }
}
fun BaseTestCase.forgetCurrentWalletAndReArmForNextScan() {
step("Close 'Add custom token' bottom sheet") { closeAddCustomTokenSheet() }
navigateBackToWalletSettings()
step("Click on 'Forget wallet' button") {
waitForIdle()
onWalletSettingsScreen {
scrollToForgetWallet()
forgetWalletButton.clickWithAssertion()
}
}
step("Confirm forgetting the wallet") {
awaitSuccess { onDialog { forgetButton.assertIsDisplayed() } }
onDialog { forgetButton.clickWithAssertion() }
}
// Markets tooltip is a one-time app-level flag; re-arm it so the next scan's openMainScreen can dismiss it.
step("Re-arm the markets tooltip for the next scan") {
runBlocking {
appPreferencesStore.editData { it.set(PreferencesKeys.SHOULD_SHOW_MARKETS_TOOLTIP_KEY, value = true) }
}
}
}
fun BaseTestCase.assertDerivationPathsInSelector(networkToOpen: String, vararg expectedPaths: Pair<String, String>) {
step("Click on network: '$networkToOpen'") {
awaitSuccess { onAddCustomTokenScreen { scrollToNetwork(networkToOpen) } }
onAddCustomTokenScreen { networkRow(networkToOpen).performClick() }
}
step("Click on 'Derivation path' field") {
awaitSuccess { onAddCustomTokenScreen { derivationSelectorField.assertExists() } }
onAddCustomTokenScreen { derivationSelectorField.performClick() }
}
expectedPaths.forEach { (networkId, path) ->
step("Assert '$networkId' derivation path is '$path'") {
awaitSuccess { onAddCustomTokenScreen { scrollToDerivationRow(networkId) } }
onAddCustomTokenScreen { derivationPath(networkId, path).assertIsDisplayed() }
}
}
}
fun BaseTestCase.toggleTokenNetworkInManageTokens(tokenTitle: String, networkTitle: String) {
step("Click on token: '$tokenTitle'") {
awaitSuccess { onManageTokensScreen { tokenItem(tokenTitle).assertIsDisplayed() } }
onManageTokensScreen { tokenItem(tokenTitle).performClick() }
}
step("Click on '$networkTitle' switch") {
awaitSuccess { onManageTokensScreen { networkSwitch(networkTitle).assertIsDisplayed() } }
onManageTokensScreen { networkSwitch(networkTitle).performClick() }
}
}
fun BaseTestCase.navigateBackToMainFromManageTokens() {
navigateBackToWalletSettings()
step("Click on 'Wallet settings' screen 'Back' button") { step("Click on 'Wallet settings' screen 'Back' button") {
waitForIdle() waitForIdle()
onWalletSettingsScreen { topAppBarBackButton.performClick() } onWalletSettingsScreen { topAppBarBackButton.performClick() }

View file

@ -11,12 +11,13 @@ import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onCompose
import io.github.kakaocup.compose.node.element.KNode import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString import io.github.kakaocup.kakao.common.utilities.getResourceString
import androidx.compose.ui.test.hasTestTag as withTestTag import androidx.compose.ui.test.hasTestTag as withTestTag
import androidx.compose.ui.test.hasText as withText
import com.tangem.core.res.R as CoreResR import com.tangem.core.res.R as CoreResR
class AddCustomTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : class AddCustomTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<AddCustomTokenPageObject>(semanticsProvider = semanticsProvider) { ComposeScreen<AddCustomTokenPageObject>(semanticsProvider = semanticsProvider) {
private val selectorList: KNode = child { val selectorList: KNode = child {
hasTestTag(AddCustomTokenScreenTestTags.SELECTOR_LIST) hasTestTag(AddCustomTokenScreenTestTags.SELECTOR_LIST)
} }
@ -29,6 +30,17 @@ class AddCustomTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvi
performScrollToNode(withTestTag(AddCustomTokenScreenTestTags.networkRow(networkName))) performScrollToNode(withTestTag(AddCustomTokenScreenTestTags.networkRow(networkName)))
} }
@OptIn(ExperimentalTestApi::class)
fun scrollToDerivationRow(networkId: String) = selectorList {
performScrollToNode(withTestTag(AddCustomTokenScreenTestTags.derivationRow(networkId)))
}
fun derivationPath(networkId: String, path: String): KNode = child {
useUnmergedTree = true
hasTestTag(AddCustomTokenScreenTestTags.derivationRow(networkId))
hasAnyDescendant(withText(path))
}
val contractAddressField: KNode = child { val contractAddressField: KNode = child {
hasTestTag(AddCustomTokenScreenTestTags.CONTRACT_ADDRESS_FIELD) hasTestTag(AddCustomTokenScreenTestTags.CONTRACT_ADDRESS_FIELD)
} }

View file

@ -2,6 +2,7 @@ package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase import com.tangem.common.BaseTestCase
import com.tangem.core.res.R as CoreResR
import com.tangem.core.ui.R import com.tangem.core.ui.R
import com.tangem.core.ui.test.BaseButtonTestTags import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.test.BaseDialogTestTags import com.tangem.core.ui.test.BaseDialogTestTags
@ -94,6 +95,11 @@ class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
hasText(getResourceString(R.string.common_ok)) hasText(getResourceString(R.string.common_ok))
} }
val forgetButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(CoreResR.string.common_forget))
}
val changeButton: KNode = child { val changeButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON) hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.common_change)) hasText(getResourceString(R.string.common_change))

View file

@ -18,6 +18,7 @@ import androidx.compose.ui.test.hasText as withText
import androidx.compose.ui.test.hasAnySibling as withAnySibling import androidx.compose.ui.test.hasAnySibling as withAnySibling
import androidx.compose.ui.test.hasAnyDescendant as withAnyDescendant import androidx.compose.ui.test.hasAnyDescendant as withAnyDescendant
import androidx.compose.ui.test.hasAnyAncestor as withAnyAncestor import androidx.compose.ui.test.hasAnyAncestor as withAnyAncestor
import com.tangem.core.res.R as CoreResR
class ManageTokensPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : class ManageTokensPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<ManageTokensPageObject>(semanticsProvider = semanticsProvider) { ComposeScreen<ManageTokensPageObject>(semanticsProvider = semanticsProvider) {
@ -61,6 +62,18 @@ class ManageTokensPageObject(semanticsProvider: SemanticsNodeInteractionsProvide
) )
} }
fun networkName(networkName: String): KNode = child {
useUnmergedTree = true
addSemanticsMatcher(
withTestTag(ManageTokensScreenTestTags.NETWORK_NAME)
.and(withAnyDescendant(withText(networkName))),
)
}
val contractAddressCopiedMessage: KNode = child {
hasText(getResourceString(CoreResR.string.contract_address_copied_message))
}
fun networkSwitch(networkName: String): KNode = child { fun networkSwitch(networkName: String): KNode = child {
useUnmergedTree = true useUnmergedTree = true
addSemanticsMatcher( addSemanticsMatcher(

View file

@ -6,16 +6,22 @@ import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
import com.tangem.common.extensions.performTextInputInChunks import com.tangem.common.extensions.performTextInputInChunks
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.domain.models.scan.ProductType
import com.tangem.scenarios.addCustomTokenWithCustomDerivation import com.tangem.scenarios.addCustomTokenWithCustomDerivation
import com.tangem.scenarios.assertDerivationPathsInSelector
import com.tangem.scenarios.forgetCurrentWalletAndReArmForNextScan
import com.tangem.scenarios.navigateBackToMainFromManageTokens import com.tangem.scenarios.navigateBackToMainFromManageTokens
import com.tangem.scenarios.openAddCustomToken import com.tangem.scenarios.openAddCustomToken
import com.tangem.scenarios.openMainScreen import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.onAddCustomTokenScreen import com.tangem.screens.onAddCustomTokenScreen
import com.tangem.screens.onMainScreen import com.tangem.screens.onMainScreen
import com.tangem.tap.domain.sdk.mocks.content.Wallet1LegacyDerivationMockContent
import com.tangem.tap.domain.sdk.mocks.content.Wallet2NoEd25519Slip0010MockContent
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Assert.assertFalse
import org.junit.Test import org.junit.Test
@HiltAndroidTest @HiltAndroidTest
@ -25,6 +31,8 @@ class AddCustomTokenTest : BaseTestCase() {
private val tokenTitle = "Tether" private val tokenTitle = "Tether"
private val ethereumNetwork = "Ethereum" private val ethereumNetwork = "Ethereum"
private val solanaNetwork = "Solana" private val solanaNetwork = "Solana"
private val bitcoinNetworkId = "bitcoin"
private val ethereumClassicNetworkId = "ethereum-classic"
private val ethContract = "0xdac17f958d2ee523a2206206994597c13d831ec7" private val ethContract = "0xdac17f958d2ee523a2206206994597c13d831ec7"
private val solanaContract = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" private val solanaContract = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
private val customDerivationPath = "m/44'/60'/0'/0/1" private val customDerivationPath = "m/44'/60'/0'/0/1"
@ -173,4 +181,71 @@ class AddCustomTokenTest : BaseTestCase() {
} }
} }
} }
@AllureId("777")
@DisplayName("Add custom token: network with a missing curve is not offered")
@Test
fun missingCurveNetworkNotOfferedTest() {
val bitcoinNetwork = "Bitcoin"
setupHooks(
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
).run {
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
setWireMockScenarioState(COINS_API_SCENARIO, richState)
}
step("Open 'Main Screen'") { openMainScreen(mockContent = Wallet2NoEd25519Slip0010MockContent) }
step("Open 'Add custom token' screen") { openAddCustomToken() }
step("Assert supported network '$bitcoinNetwork' is displayed") {
flakySafely { onAddCustomTokenScreen { scrollToNetwork(bitcoinNetwork) } }
}
step("Assert '$solanaNetwork' network is not displayed for the missing-curve card") {
onAddCustomTokenScreen { selectorList.assertIsDisplayed() }
val solanaOffered = runCatching { onAddCustomTokenScreen { scrollToNetwork(solanaNetwork) } }.isSuccess
assertFalse("'$solanaNetwork' should not be offered for a missing-curve card", solanaOffered)
}
}
}
@AllureId("776")
@DisplayName("Add custom token: derivation paths match the card version (V1/V2/V3)")
@Test
fun derivationPathsMatchCardVersionTest() {
setupHooks(
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
).run {
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
setWireMockScenarioState(COINS_API_SCENARIO, richState)
}
step("Assert derivation paths for a legacy-batch V1 Wallet card") {
openMainScreen(mockContent = Wallet1LegacyDerivationMockContent)
openAddCustomToken()
assertDerivationPathsInSelector(
ethereumNetwork,
bitcoinNetworkId to "m/44'/0'/0'/0/0",
ethereumClassicNetworkId to "m/44'/61'/0'/0/0",
)
forgetCurrentWalletAndReArmForNextScan()
}
step("Assert derivation paths for a V2 Wallet card") {
openMainScreen()
openAddCustomToken()
assertDerivationPathsInSelector(
ethereumNetwork,
bitcoinNetworkId to "m/44'/0'/0'/0/0",
ethereumClassicNetworkId to "m/44'/60'/0'/0/0",
)
forgetCurrentWalletAndReArmForNextScan()
}
step("Assert derivation paths for a V3 Wallet 2 card") {
openMainScreen(productType = ProductType.Wallet2)
openAddCustomToken()
assertDerivationPathsInSelector(
ethereumNetwork,
bitcoinNetworkId to "m/84'/0'/0'/0/0",
ethereumClassicNetworkId to "m/44'/61'/0'/0/0",
)
}
}
}
} }

View file

@ -1,30 +1,48 @@
package com.tangem.tests.main package com.tangem.tests.main
import androidx.compose.ui.test.longClick
import androidx.compose.ui.test.performTextInput import androidx.compose.ui.test.performTextInput
import com.tangem.common.BaseTestCase import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.COINS_API_SCENARIO import com.tangem.common.constants.TestConstants.COINS_API_SCENARIO
import com.tangem.common.extensions.clickWithAssertion import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.assertClipboardIsEmpty
import com.tangem.common.utils.assertClipboardTextEquals
import com.tangem.common.utils.clearClipboard
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.scenarios.openMainScreen import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.openManageTokens import com.tangem.scenarios.openManageTokens
import com.tangem.scenarios.toggleTokenNetworkInManageTokens
import com.tangem.screens.onDialog import com.tangem.screens.onDialog
import com.tangem.screens.onManageTokensScreen import com.tangem.screens.onManageTokensScreen
import com.tangem.tap.domain.sdk.mocks.content.Firmware451MockContent
import com.tangem.tap.domain.sdk.mocks.content.Wallet2NoEd25519Slip0010MockContent
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import io.github.kakaocup.kakao.common.utilities.getResourceString
import io.qameta.allure.kotlin.AllureId import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Test import org.junit.Test
import com.tangem.core.res.R as CoreResR
@HiltAndroidTest @HiltAndroidTest
class ManageTokensTest : BaseTestCase() { class ManageTokensTest : BaseTestCase() {
private val richState = "ManageTokensRich" private val richState = "ManageTokensRich"
private val solanaTokenTitle = "USD Coin"
private val solanaNetworkTitle = "SOLANA"
private val solanaName = "Solana"
@AllureId("765") @AllureId("765")
@DisplayName("Manage tokens: network standard labels are shown for token networks") @DisplayName("Manage tokens: network standard labels are shown for token networks")
@Test @Test
fun networkStandardLabelsTest() { fun networkStandardLabelsTest() {
val tokenTitle = "Tether" val tokenTitle = "Tether"
val ethereum = "Ethereum"
val bnbSmartChain = "BNB Smart Chain"
val tron = "Tron"
val erc20 = "ERC20"
val bep20 = "BEP20"
val trc20 = "TRC20"
setupHooks( setupHooks(
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) }, additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
@ -38,21 +56,21 @@ class ManageTokensTest : BaseTestCase() {
flakySafely { onManageTokensScreen { tokenItem(tokenTitle).assertIsDisplayed() } } flakySafely { onManageTokensScreen { tokenItem(tokenTitle).assertIsDisplayed() } }
onManageTokensScreen { tokenItem(tokenTitle).performClick() } onManageTokensScreen { tokenItem(tokenTitle).performClick() }
} }
step("Assert 'Ethereum' network standard 'ERC20' is displayed") { step("Assert '$ethereum' network standard '$erc20' is displayed") {
flakySafely { flakySafely {
onManageTokensScreen { networkStandard(networkName = "Ethereum", standard = "ERC20").assertIsDisplayed() } onManageTokensScreen { networkStandard(networkName = ethereum, standard = erc20).assertIsDisplayed() }
} }
} }
step("Assert 'BNB Smart Chain' network standard 'BEP20' is displayed") { step("Assert '$bnbSmartChain' network standard '$bep20' is displayed") {
flakySafely { flakySafely {
onManageTokensScreen { onManageTokensScreen {
networkStandard(networkName = "BNB Smart Chain", standard = "BEP20").assertIsDisplayed() networkStandard(networkName = bnbSmartChain, standard = bep20).assertIsDisplayed()
} }
} }
} }
step("Assert 'Tron' network standard 'TRC20' is displayed") { step("Assert '$tron' network standard '$trc20' is displayed") {
flakySafely { flakySafely {
onManageTokensScreen { networkStandard(networkName = "Tron", standard = "TRC20").assertIsDisplayed() } onManageTokensScreen { networkStandard(networkName = tron, standard = trc20).assertIsDisplayed() }
} }
} }
} }
@ -109,8 +127,33 @@ class ManageTokensTest : BaseTestCase() {
@DisplayName("Manage tokens: enabling Solana network on a modern card shows no warning") @DisplayName("Manage tokens: enabling Solana network on a modern card shows no warning")
@Test @Test
fun solanaNetworkNoWarningTest() { fun solanaNetworkNoWarningTest() {
val tokenTitle = "USD Coin" setupHooks(
val networkTitle = "SOLANA" additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
).run {
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
setWireMockScenarioState(COINS_API_SCENARIO, richState)
}
step("Open 'Main Screen'") { openMainScreen() }
step("Open 'Manage tokens' screen") { openManageTokens() }
step("Enable '$solanaNetworkTitle' network for token: '$solanaTokenTitle'") {
toggleTokenNetworkInManageTokens(tokenTitle = solanaTokenTitle, networkTitle = solanaNetworkTitle)
}
step("Assert hide-token alert is not displayed") {
waitForIdle()
onDialog { dialogContainer.assertDoesNotExist() }
}
}
}
@AllureId("719")
@DisplayName("Manage tokens: long tap on a token network copies the contract address, main network does not")
@Test
fun copyContractAddressOnNetworkLongTapTest() {
val tokenTitle = "Tether"
val tokenNetworkTitle = "ETHEREUM"
val coinTitle = "Bitcoin"
val coinNetworkTitle = "BITCOIN"
val contractAddress = "0xdac17f958d2ee523a2206206994597c13d831ec7"
setupHooks( setupHooks(
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) }, additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
@ -124,13 +167,83 @@ class ManageTokensTest : BaseTestCase() {
flakySafely { onManageTokensScreen { tokenItem(tokenTitle).assertIsDisplayed() } } flakySafely { onManageTokensScreen { tokenItem(tokenTitle).assertIsDisplayed() } }
onManageTokensScreen { tokenItem(tokenTitle).performClick() } onManageTokensScreen { tokenItem(tokenTitle).performClick() }
} }
step("Click on '$networkTitle' switch") { step("Long click on '$tokenNetworkTitle' network row") {
flakySafely { onManageTokensScreen { networkSwitch(networkTitle).assertIsDisplayed() } } flakySafely { onManageTokensScreen { networkName(tokenNetworkTitle).assertIsDisplayed() } }
onManageTokensScreen { networkSwitch(networkTitle).performClick() } onManageTokensScreen {
networkName(tokenNetworkTitle).performTouchInput {
longClick(position = center, durationMillis = 1000L)
}
}
} }
step("Assert hide-token alert is not displayed") { step("Assert contract-address-copied message is displayed") {
flakySafely { onManageTokensScreen { contractAddressCopiedMessage.assertIsDisplayed() } }
}
step("Assert clipboard contains '$tokenTitle' contract address") {
assertClipboardTextEquals(contractAddress)
}
step("Clear clipboard") { clearClipboard() }
step("Click on token: '$coinTitle'") {
flakySafely { onManageTokensScreen { tokenItem(coinTitle).assertIsDisplayed() } }
onManageTokensScreen { tokenItem(coinTitle).performClick() }
}
step("Long click on '$coinNetworkTitle' main network row") {
flakySafely { onManageTokensScreen { networkName(coinNetworkTitle).assertIsDisplayed() } }
onManageTokensScreen {
networkName(coinNetworkTitle).performTouchInput {
longClick(position = center, durationMillis = 1000L)
}
}
}
step("Assert clipboard is empty after long click on main network") {
waitForIdle() waitForIdle()
onDialog { dialogContainer.assertDoesNotExist() } assertClipboardIsEmpty()
}
}
}
@AllureId("737")
@DisplayName("Manage tokens: enabling Solana on an old-firmware card shows firmware-limitation warning")
@Test
fun solanaFirmwareLimitationWarningTest() {
val warningMessage = getResourceString(CoreResR.string.alert_manage_tokens_unsupported_message, solanaName)
setupHooks(
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
).run {
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
setWireMockScenarioState(COINS_API_SCENARIO, richState)
}
step("Open 'Main Screen'") { openMainScreen(mockContent = Firmware451MockContent) }
step("Open 'Manage tokens' screen") { openManageTokens() }
step("Enable '$solanaNetworkTitle' network for token: '$solanaTokenTitle'") {
toggleTokenNetworkInManageTokens(tokenTitle = solanaTokenTitle, networkTitle = solanaNetworkTitle)
}
step("Assert firmware-limitation warning is displayed") {
flakySafely { onDialog { containerWithText(warningMessage).assertIsDisplayed() } }
}
}
}
@AllureId("767")
@DisplayName("Manage tokens: enabling Solana on a card missing its curve shows unsupported-curve warning")
@Test
fun solanaUnsupportedCurveWarningTest() {
val warningMessage =
getResourceString(CoreResR.string.alert_manage_tokens_unsupported_curve_message, solanaName)
setupHooks(
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
).run {
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
setWireMockScenarioState(COINS_API_SCENARIO, richState)
}
step("Open 'Main Screen'") { openMainScreen(mockContent = Wallet2NoEd25519Slip0010MockContent) }
step("Open 'Manage tokens' screen") { openManageTokens() }
step("Enable '$solanaNetworkTitle' network for token: '$solanaTokenTitle'") {
toggleTokenNetworkInManageTokens(tokenTitle = solanaTokenTitle, networkTitle = solanaNetworkTitle)
}
step("Assert unsupported-curve warning is displayed") {
flakySafely { onDialog { containerWithText(warningMessage).assertIsDisplayed() } }
} }
} }
} }

View file

@ -33,6 +33,9 @@ object MockProvider {
MockOption("Shiba (No Backup, No Wallets)") { ShibaNoBackupNoWalletsMockContent }, MockOption("Shiba (No Backup, No Wallets)") { ShibaNoBackupNoWalletsMockContent },
MockOption("Ed25519 Curve") { EdCurveMockContent }, MockOption("Ed25519 Curve") { EdCurveMockContent },
MockOption("Secp256k1 Curve") { Secpk1CurveMockContent }, MockOption("Secp256k1 Curve") { Secpk1CurveMockContent },
MockOption("Wallet 2 (No ed25519_slip0010)") { Wallet2NoEd25519Slip0010MockContent },
MockOption("Wallet 1 (Legacy derivation)") { Wallet1LegacyDerivationMockContent },
MockOption("Firmware 4.51") { Firmware451MockContent },
MockOption("Backup Wallet") { BackupWalletMockContent }, MockOption("Backup Wallet") { BackupWalletMockContent },
MockOption("Dev Wallet") { DevWalletMockContent }, MockOption("Dev Wallet") { DevWalletMockContent },
MockOption("Firmware 4.12") { Firmware412MockContent }, MockOption("Firmware 4.12") { Firmware412MockContent },

View file

@ -0,0 +1,15 @@
package com.tangem.tap.domain.sdk.mocks.content
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.domain.sdk.mocks.MockContent
// Firmware 4.51: HD-capable (>= 4.39) but below SolanaTokensAvailable (4.52), so Solana tokens are firmware-limited.
object Firmware451MockContent : MockContent by WalletMockContent {
override val cardDto: CardDTO = WalletMockContent.cardDto.copy(
firmwareVersion = WalletMockContent.cardDto.firmwareVersion.copy(minor = 51),
)
override val scanResponse: ScanResponse = WalletMockContent.scanResponse.copy(card = cardDto)
}

View file

@ -0,0 +1,13 @@
package com.tangem.tap.domain.sdk.mocks.content
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.domain.sdk.mocks.MockContent
// Wallet1 on a first-batch id (AC01) — resolves to the V1 (legacy) derivation style.
object Wallet1LegacyDerivationMockContent : MockContent by WalletMockContent {
override val cardDto: CardDTO = WalletMockContent.cardDto.copy(batchId = "AC01")
override val scanResponse: ScanResponse = WalletMockContent.scanResponse.copy(card = cardDto)
}

View file

@ -0,0 +1,18 @@
package com.tangem.tap.domain.sdk.mocks.content
import com.tangem.common.card.EllipticCurve
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.domain.sdk.mocks.MockContent
// Wallet2 without its ed25519_slip0010 wallet, so adding Solana warns UnsupportedCurve (no wallet for its curve).
object Wallet2NoEd25519Slip0010MockContent : MockContent by Wallet2WithSeedPhraseMockContent {
override val cardDto: CardDTO = Wallet2WithSeedPhraseMockContent.cardDto.copy(
wallets = Wallet2WithSeedPhraseMockContent.cardDto.wallets.filterNot {
it.curve == EllipticCurve.Ed25519Slip0010
},
)
override val scanResponse: ScanResponse = Wallet2WithSeedPhraseMockContent.scanResponse.copy(card = cardDto)
}

View file

@ -8,4 +8,6 @@ object AddCustomTokenScreenTestTags {
const val SELECTOR_LIST = "ADD_CUSTOM_TOKEN_SELECTOR_LIST" const val SELECTOR_LIST = "ADD_CUSTOM_TOKEN_SELECTOR_LIST"
fun networkRow(networkName: String): String = "ADD_CUSTOM_TOKEN_NETWORK_ROW_${networkName.lowercase()}" fun networkRow(networkName: String): String = "ADD_CUSTOM_TOKEN_NETWORK_ROW_${networkName.lowercase()}"
fun derivationRow(networkId: String): String = "ADD_CUSTOM_TOKEN_DERIVATION_ROW_${networkId.lowercase()}"
} }

View file

@ -101,7 +101,7 @@ internal fun CustomTokenSelectorContent(model: CustomTokenSelectorUM, modifier:
} }
is DerivationPathUM -> { is DerivationPathUM -> {
DerivationPathItem( DerivationPathItem(
modifier = itemModifier, modifier = itemModifier.testTag(AddCustomTokenScreenTestTags.derivationRow(item.id)),
model = item, model = item,
) )
} }