diff --git a/app/src/androidTest/kotlin/com/tangem/common/utils/ClipboardUtils.kt b/app/src/androidTest/kotlin/com/tangem/common/utils/ClipboardUtils.kt index c2e3981abc..9706e39a09 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/utils/ClipboardUtils.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/utils/ClipboardUtils.kt @@ -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() ) { + assertEquals("Clipboard text mismatch", expected, getClipboardText(context)) +} + +fun assertClipboardIsEmpty(context: Context = ApplicationProvider.getApplicationContext()) { val actual = getClipboardText(context) - assert(actual == expected) { - "Clipboard text mismatch.\nExpected: '$expected'\nActual: '$actual'" - } + assertTrue("Expected empty clipboard but was: '$actual'", actual.isNullOrEmpty()) } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/ManageTokensScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/ManageTokensScenarios.kt index e04f963f0d..4d04dde6ae 100644 --- a/app/src/androidTest/kotlin/com/tangem/scenarios/ManageTokensScenarios.kt +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/ManageTokensScenarios.kt @@ -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) { + 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() } diff --git a/app/src/androidTest/kotlin/com/tangem/screens/AddCustomTokenPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/AddCustomTokenPageObject.kt index 346da3e80a..050e140fe5 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/AddCustomTokenPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/AddCustomTokenPageObject.kt @@ -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(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) } diff --git a/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt index 5feacc5839..dfe62bef7a 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt @@ -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)) diff --git a/app/src/androidTest/kotlin/com/tangem/screens/ManageTokensPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/ManageTokensPageObject.kt index 70beda1905..1e4b297f82 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/ManageTokensPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/ManageTokensPageObject.kt @@ -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(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( diff --git a/app/src/androidTest/kotlin/com/tangem/tests/main/AddCustomTokenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/main/AddCustomTokenTest.kt index a7fd3109a5..6c61f76733 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/main/AddCustomTokenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/main/AddCustomTokenTest.kt @@ -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", + ) + } + } + } } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/main/ManageTokensTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/main/ManageTokensTest.kt index 2d43da235a..c61c73d7dd 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/main/ManageTokensTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/main/ManageTokensTest.kt @@ -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() } } } } } diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/MockProvider.kt b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/MockProvider.kt index ecdb384b6d..432627fb65 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/MockProvider.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/MockProvider.kt @@ -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 }, diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/Firmware451MockContent.kt b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/Firmware451MockContent.kt new file mode 100644 index 0000000000..f9e0faefdb --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/Firmware451MockContent.kt @@ -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) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/Wallet1LegacyDerivationMockContent.kt b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/Wallet1LegacyDerivationMockContent.kt new file mode 100644 index 0000000000..ebf81ffdb4 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/Wallet1LegacyDerivationMockContent.kt @@ -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) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/Wallet2NoEd25519Slip0010MockContent.kt b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/Wallet2NoEd25519Slip0010MockContent.kt new file mode 100644 index 0000000000..24df3a35d1 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/Wallet2NoEd25519Slip0010MockContent.kt @@ -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) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/AddCustomTokenScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/AddCustomTokenScreenTestTags.kt index ff4479c8f0..39987e1409 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/AddCustomTokenScreenTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/AddCustomTokenScreenTestTags.kt @@ -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()}" } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt index 6f5a206399..2219261da2 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt @@ -101,7 +101,7 @@ internal fun CustomTokenSelectorContent(model: CustomTokenSelectorUM, modifier: } is DerivationPathUM -> { DerivationPathItem( - modifier = itemModifier, + modifier = itemModifier.testTag(AddCustomTokenScreenTestTags.derivationRow(item.id)), model = item, ) }