Updated on 2026-08-14
This commit is contained in:
parent
8b677a8e6b
commit
c176dc37c4
13 changed files with 362 additions and 19 deletions
|
|
@ -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'"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -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() }
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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() } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 },
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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()}"
|
||||||
}
|
}
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue