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

View file

@ -4,6 +4,7 @@ import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.extensions.performTextInputInChunks
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.screens.accounts.onAccountDetailsScreen
import com.tangem.screens.onAddCustomTokenScreen
import com.tangem.screens.onDetailsScreen
@ -12,6 +13,7 @@ import com.tangem.screens.onManageTokensScreen
import com.tangem.screens.onWalletSettingsScreen
import io.github.kakaocup.kakao.common.utilities.getResourceString
import io.qameta.allure.kotlin.Allure.step
import kotlinx.coroutines.runBlocking
import com.tangem.core.res.R as CoreResR
private fun mainAccountName(): String = getResourceString(CoreResR.string.account_main_account_title)
@ -66,7 +68,19 @@ fun BaseTestCase.addCustomTokenWithCustomDerivation(network: String, contract: S
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") {
waitForIdle()
onManageTokensScreen { topAppBarBackButton.performClick() }
@ -75,6 +89,60 @@ fun BaseTestCase.navigateBackToMainFromManageTokens() {
waitForIdle()
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") {
waitForIdle()
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.kakao.common.utilities.getResourceString
import androidx.compose.ui.test.hasTestTag as withTestTag
import androidx.compose.ui.test.hasText as withText
import com.tangem.core.res.R as CoreResR
class AddCustomTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<AddCustomTokenPageObject>(semanticsProvider = semanticsProvider) {
private val selectorList: KNode = child {
val selectorList: KNode = child {
hasTestTag(AddCustomTokenScreenTestTags.SELECTOR_LIST)
}
@ -29,6 +30,17 @@ class AddCustomTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvi
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 {
hasTestTag(AddCustomTokenScreenTestTags.CONTRACT_ADDRESS_FIELD)
}

View file

@ -2,6 +2,7 @@ package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.res.R as CoreResR
import com.tangem.core.ui.R
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.test.BaseDialogTestTags
@ -94,6 +95,11 @@ class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
hasText(getResourceString(R.string.common_ok))
}
val forgetButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(CoreResR.string.common_forget))
}
val changeButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
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.hasAnyDescendant as withAnyDescendant
import androidx.compose.ui.test.hasAnyAncestor as withAnyAncestor
import com.tangem.core.res.R as CoreResR
class ManageTokensPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
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 {
useUnmergedTree = true
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.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.domain.models.scan.ProductType
import com.tangem.scenarios.addCustomTokenWithCustomDerivation
import com.tangem.scenarios.assertDerivationPathsInSelector
import com.tangem.scenarios.forgetCurrentWalletAndReArmForNextScan
import com.tangem.scenarios.navigateBackToMainFromManageTokens
import com.tangem.scenarios.openAddCustomToken
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.onAddCustomTokenScreen
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 io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Assert.assertFalse
import org.junit.Test
@HiltAndroidTest
@ -25,6 +31,8 @@ class AddCustomTokenTest : BaseTestCase() {
private val tokenTitle = "Tether"
private val ethereumNetwork = "Ethereum"
private val solanaNetwork = "Solana"
private val bitcoinNetworkId = "bitcoin"
private val ethereumClassicNetworkId = "ethereum-classic"
private val ethContract = "0xdac17f958d2ee523a2206206994597c13d831ec7"
private val solanaContract = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
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
import androidx.compose.ui.test.longClick
import androidx.compose.ui.test.performTextInput
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.COINS_API_SCENARIO
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.setWireMockScenarioState
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.openManageTokens
import com.tangem.scenarios.toggleTokenNetworkInManageTokens
import com.tangem.screens.onDialog
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 io.github.kakaocup.kakao.common.utilities.getResourceString
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Test
import com.tangem.core.res.R as CoreResR
@HiltAndroidTest
class ManageTokensTest : BaseTestCase() {
private val richState = "ManageTokensRich"
private val solanaTokenTitle = "USD Coin"
private val solanaNetworkTitle = "SOLANA"
private val solanaName = "Solana"
@AllureId("765")
@DisplayName("Manage tokens: network standard labels are shown for token networks")
@Test
fun networkStandardLabelsTest() {
val tokenTitle = "Tether"
val ethereum = "Ethereum"
val bnbSmartChain = "BNB Smart Chain"
val tron = "Tron"
val erc20 = "ERC20"
val bep20 = "BEP20"
val trc20 = "TRC20"
setupHooks(
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
@ -38,21 +56,21 @@ class ManageTokensTest : BaseTestCase() {
flakySafely { onManageTokensScreen { tokenItem(tokenTitle).assertIsDisplayed() } }
onManageTokensScreen { tokenItem(tokenTitle).performClick() }
}
step("Assert 'Ethereum' network standard 'ERC20' is displayed") {
step("Assert '$ethereum' network standard '$erc20' is displayed") {
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 {
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 {
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")
@Test
fun solanaNetworkNoWarningTest() {
val tokenTitle = "USD Coin"
val networkTitle = "SOLANA"
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() }
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(
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
@ -124,13 +167,83 @@ class ManageTokensTest : BaseTestCase() {
flakySafely { onManageTokensScreen { tokenItem(tokenTitle).assertIsDisplayed() } }
onManageTokensScreen { tokenItem(tokenTitle).performClick() }
}
step("Click on '$networkTitle' switch") {
flakySafely { onManageTokensScreen { networkSwitch(networkTitle).assertIsDisplayed() } }
onManageTokensScreen { networkSwitch(networkTitle).performClick() }
step("Long click on '$tokenNetworkTitle' network row") {
flakySafely { onManageTokensScreen { networkName(tokenNetworkTitle).assertIsDisplayed() } }
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()
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("Ed25519 Curve") { EdCurveMockContent },
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("Dev Wallet") { DevWalletMockContent },
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"
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 -> {
DerivationPathItem(
modifier = itemModifier,
modifier = itemModifier.testTag(AddCustomTokenScreenTestTags.derivationRow(item.id)),
model = item,
)
}