Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-01 09:30:03 +02:00
parent e2af488b32
commit 8ffbfc27e7
9 changed files with 510 additions and 6 deletions

View file

@ -46,6 +46,7 @@ object TestConstants {
const val ALLURE_LABEL_VALUE = "Kaspresso"
const val USER_TOKENS_API_SCENARIO = "user_tokens_api"
const val COINS_API_SCENARIO = "coins_api"
const val REFERRAL_API_SCENARIO = "referral_api"
const val QUOTES_API_SCENARIO = "quotes_api"
const val CREATE_USER_WALLET_API_SCENARIO = "create_user_wallet_api"

View file

@ -0,0 +1,86 @@
package com.tangem.scenarios
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.screens.accounts.onAccountDetailsScreen
import com.tangem.screens.onAddCustomTokenScreen
import com.tangem.screens.onDetailsScreen
import com.tangem.screens.onDialog
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 com.tangem.core.res.R as CoreResR
private fun mainAccountName(): String = getResourceString(CoreResR.string.account_main_account_title)
// flakySafely is unavailable in BaseTestCase extensions — wait until the assertion/action stops throwing.
private fun BaseTestCase.awaitSuccess(block: () -> Unit) {
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT) { runCatching(block).isSuccess }
}
fun BaseTestCase.openManageTokens(accountName: String = mainAccountName()) {
openWalletSettingsScreen()
openAccountDetails(accountName)
step("Click on 'Manage tokens' button") {
onAccountDetailsScreen { manageTokensButton.clickWithAssertion() }
}
}
fun BaseTestCase.openAddCustomToken(accountName: String = mainAccountName()) {
openManageTokens(accountName)
step("Click on 'Add custom token' button") {
onManageTokensScreen { addCustomTokenButton.clickWithAssertion() }
}
}
fun BaseTestCase.addCustomTokenWithCustomDerivation(network: String, contract: String, derivationPath: String) {
step("Click on network: '$network'") {
awaitSuccess { onAddCustomTokenScreen { scrollToNetwork(network) } }
onAddCustomTokenScreen { networkRow(network).performClick() }
}
step("Enter contract address: '$contract'") {
awaitSuccess { onAddCustomTokenScreen { contractAddressField.assertExists() } }
onAddCustomTokenScreen { contractAddressField.performTextInputInChunks(contract) }
}
step("Click on 'Derivation path' field") {
awaitSuccess { onAddCustomTokenScreen { derivationSelectorField.assertExists() } }
onAddCustomTokenScreen { derivationSelectorField.performClick() }
}
step("Click on 'Custom derivation' button") {
awaitSuccess { onAddCustomTokenScreen { customDerivationButton.assertIsDisplayed() } }
onAddCustomTokenScreen { customDerivationButton.performClick() }
}
step("Enter custom derivation path: '$derivationPath'") {
awaitSuccess { onDialog { inputField.assertIsDisplayed() } }
onDialog { inputField.performTextInputInChunks(derivationPath) }
awaitSuccess { onDialog { okButton.assertIsEnabled() } }
onDialog { okButton.performClick() }
}
step("Click on 'Add token' button") {
awaitSuccess { onAddCustomTokenScreen { addTokenButton.assertIsEnabled() } }
onAddCustomTokenScreen { addTokenButton.performClick() }
}
navigateBackToMainFromManageTokens()
}
fun BaseTestCase.navigateBackToMainFromManageTokens() {
step("Click on 'Manage tokens' screen 'Back' button") {
waitForIdle()
onManageTokensScreen { topAppBarBackButton.performClick() }
}
step("Click on 'Account details' screen 'Back' button") {
waitForIdle()
onAccountDetailsScreen { topAppBarBackButton.performClick() }
}
step("Click on 'Wallet settings' screen 'Back' button") {
waitForIdle()
onWalletSettingsScreen { topAppBarBackButton.performClick() }
}
step("Click on 'Details' screen 'Back' button") {
waitForIdle()
onDetailsScreen { topAppBarBackButton.performClick() }
}
}

View file

@ -0,0 +1,55 @@
package com.tangem.screens
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import androidx.compose.ui.test.performScrollToNode
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.test.AddCustomTokenScreenTestTags
import com.tangem.core.ui.test.BaseButtonTestTags
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
import androidx.compose.ui.test.hasTestTag as withTestTag
import com.tangem.core.res.R as CoreResR
class AddCustomTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<AddCustomTokenPageObject>(semanticsProvider = semanticsProvider) {
private val selectorList: KNode = child {
hasTestTag(AddCustomTokenScreenTestTags.SELECTOR_LIST)
}
fun networkRow(networkName: String): KNode = child {
hasTestTag(AddCustomTokenScreenTestTags.networkRow(networkName))
}
@OptIn(ExperimentalTestApi::class)
fun scrollToNetwork(networkName: String) = selectorList {
performScrollToNode(withTestTag(AddCustomTokenScreenTestTags.networkRow(networkName)))
}
val contractAddressField: KNode = child {
hasTestTag(AddCustomTokenScreenTestTags.CONTRACT_ADDRESS_FIELD)
}
val derivationSelectorField: KNode = child {
hasTestTag(AddCustomTokenScreenTestTags.DERIVATION_SELECTOR_FIELD)
}
val customDerivationButton: KNode = child {
hasTestTag(AddCustomTokenScreenTestTags.CUSTOM_DERIVATION_BUTTON)
}
val warningNotification: KNode = child {
hasTestTag(AddCustomTokenScreenTestTags.WARNING_NOTIFICATION)
}
val addTokenButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(CoreResR.string.common_add_token))
}
}
internal fun BaseTestCase.onAddCustomTokenScreen(function: AddCustomTokenPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -6,6 +6,7 @@ import com.tangem.common.R
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.test.BaseSearchBarTestTags
import com.tangem.core.ui.test.ManageTokensScreenTestTags
import com.tangem.core.ui.test.SearchBarTestTags
import com.tangem.core.ui.test.SwitchTestTags
import com.tangem.core.ui.test.TopAppBarTestTags
import io.github.kakaocup.compose.node.element.ComposeScreen
@ -35,11 +36,31 @@ class ManageTokensPageObject(semanticsProvider: SemanticsNodeInteractionsProvide
hasTestTag(BaseSearchBarTestTags.SEARCH_BAR)
}
val searchClearButton: KNode = child {
hasTestTag(SearchBarTestTags.CLEAR_BUTTON)
}
val addCustomTokenButton: KNode = child {
hasTestTag(TopAppBarTestTags.MORE_BUTTON)
}
fun tokenItem(tokenName: String): KNode = child {
hasTestTag(ManageTokensScreenTestTags.TOKEN_ITEM)
hasText(tokenName)
}
fun networkStandard(networkName: String, standard: String): KNode = child {
useUnmergedTree = true
addSemanticsMatcher(
withText(standard).and(
withAnyAncestor(
withTestTag(ManageTokensScreenTestTags.NETWORK_NAME)
.and(withAnyDescendant(withText(networkName, ignoreCase = true))),
),
),
)
}
fun networkSwitch(networkName: String): KNode = child {
useUnmergedTree = true
addSemanticsMatcher(

View file

@ -0,0 +1,176 @@
package com.tangem.tests.main
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.COINS_API_SCENARIO
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.scenarios.addCustomTokenWithCustomDerivation
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 dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Test
@HiltAndroidTest
class AddCustomTokenTest : BaseTestCase() {
private val richState = "ManageTokensRich"
private val tokenTitle = "Tether"
private val ethereumNetwork = "Ethereum"
private val solanaNetwork = "Solana"
private val ethContract = "0xdac17f958d2ee523a2206206994597c13d831ec7"
private val solanaContract = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
private val customDerivationPath = "m/44'/60'/0'/0/1"
@AllureId("772")
@DisplayName("Add custom token: added token appears on Main")
@Test
fun addCustomTokenAppearsOnMainTest() {
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("Synchronize addresses") {
flakySafely { onMainScreen { synchronizeAddressesButton.assertIsDisplayed() } }
synchronizeAddresses(assertBalance = false)
}
step("Open 'Add custom token' screen") { openAddCustomToken() }
step("Click on network: '$ethereumNetwork'") {
flakySafely { onAddCustomTokenScreen { scrollToNetwork(ethereumNetwork) } }
onAddCustomTokenScreen { networkRow(ethereumNetwork).performClick() }
}
step("Enter contract address: '$ethContract'") {
flakySafely { onAddCustomTokenScreen { contractAddressField.assertExists() } }
// Chunked input spans several validation ticks — the model drops the first sampled form change.
onAddCustomTokenScreen { contractAddressField.performTextInputInChunks(ethContract) }
}
step("Click on 'Add token' button") {
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { onAddCustomTokenScreen { addTokenButton.assertIsEnabled() } }
onAddCustomTokenScreen { addTokenButton.performClick() }
}
step("Navigate back to 'Main Screen'") { navigateBackToMainFromManageTokens() }
step("Assert token: '$tokenTitle' is displayed on Main") {
flakySafely { onMainScreen { tokenWithTitleAndAddress(tokenTitle).assertIsDisplayed() } }
}
}
}
@AllureId("775")
@DisplayName("Add custom token: custom derivation path is accepted")
@Test
fun addCustomTokenWithCustomDerivationTest() {
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("Synchronize addresses") {
flakySafely { onMainScreen { synchronizeAddressesButton.assertIsDisplayed() } }
synchronizeAddresses(assertBalance = false)
}
step("Open 'Add custom token' screen") { openAddCustomToken() }
step("Add custom token with custom derivation on '$ethereumNetwork'") {
addCustomTokenWithCustomDerivation(
network = ethereumNetwork,
contract = ethContract,
derivationPath = customDerivationPath,
)
}
step("Assert token: '$tokenTitle' is displayed on Main") {
flakySafely { onMainScreen { tokenWithTitleAndAddress(tokenTitle).assertIsDisplayed() } }
}
}
}
@AllureId("771")
@DisplayName("Add custom token: custom derivation indicator is shown on Main")
@Test
fun customDerivationIndicatorOnMainTest() {
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("Synchronize addresses") {
flakySafely { onMainScreen { synchronizeAddressesButton.assertIsDisplayed() } }
synchronizeAddresses(assertBalance = false)
}
step("Open 'Add custom token' screen") { openAddCustomToken() }
step("Add custom token with custom derivation on '$ethereumNetwork'") {
addCustomTokenWithCustomDerivation(
network = ethereumNetwork,
contract = ethContract,
derivationPath = customDerivationPath,
)
}
step("Assert custom derivation indicator for '$tokenTitle' is displayed on Main") {
flakySafely { onMainScreen { tokenWithCustomDerivationIcon(tokenTitle).assertIsDisplayed() } }
}
}
}
@AllureId("770")
@DisplayName("Add custom token: derivation field is available for a non-EVM network")
@Test
fun derivationAvailableForNonEvmNetworkTest() {
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 'Add custom token' screen") { openAddCustomToken() }
step("Click on network: '$solanaNetwork'") {
flakySafely { onAddCustomTokenScreen { scrollToNetwork(solanaNetwork) } }
onAddCustomTokenScreen { networkRow(solanaNetwork).performClick() }
}
step("Assert 'Derivation path' field is available") {
flakySafely { onAddCustomTokenScreen { derivationSelectorField.assertExists() } }
}
}
}
@AllureId("769")
@DisplayName("Add custom token: Solana token on a modern card shows no unsupported warning")
@Test
fun solanaTokenModernCardNoWarningTest() {
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 'Add custom token' screen") { openAddCustomToken() }
step("Click on network: '$solanaNetwork'") {
flakySafely { onAddCustomTokenScreen { scrollToNetwork(solanaNetwork) } }
onAddCustomTokenScreen { networkRow(solanaNetwork).performClick() }
}
step("Enter contract address: '$solanaContract'") {
flakySafely { onAddCustomTokenScreen { contractAddressField.assertExists() } }
onAddCustomTokenScreen { contractAddressField.performTextInputInChunks(solanaContract) }
}
step("Assert 'Add token' button is enabled") {
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { onAddCustomTokenScreen { addTokenButton.assertIsEnabled() } }
}
step("Assert unsupported-token warning is not displayed") {
onAddCustomTokenScreen { warningNotification.assertDoesNotExist() }
}
}
}
}

View file

@ -0,0 +1,137 @@
package com.tangem.tests.main
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.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.openManageTokens
import com.tangem.screens.onDialog
import com.tangem.screens.onManageTokensScreen
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Test
@HiltAndroidTest
class ManageTokensTest : BaseTestCase() {
private val richState = "ManageTokensRich"
@AllureId("765")
@DisplayName("Manage tokens: network standard labels are shown for token networks")
@Test
fun networkStandardLabelsTest() {
val tokenTitle = "Tether"
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("Click on token: '$tokenTitle'") {
flakySafely { onManageTokensScreen { tokenItem(tokenTitle).assertIsDisplayed() } }
onManageTokensScreen { tokenItem(tokenTitle).performClick() }
}
step("Assert 'Ethereum' network standard 'ERC20' is displayed") {
flakySafely {
onManageTokensScreen { networkStandard(networkName = "Ethereum", standard = "ERC20").assertIsDisplayed() }
}
}
step("Assert 'BNB Smart Chain' network standard 'BEP20' is displayed") {
flakySafely {
onManageTokensScreen {
networkStandard(networkName = "BNB Smart Chain", standard = "BEP20").assertIsDisplayed()
}
}
}
step("Assert 'Tron' network standard 'TRC20' is displayed") {
flakySafely {
onManageTokensScreen { networkStandard(networkName = "Tron", standard = "TRC20").assertIsDisplayed() }
}
}
}
}
@AllureId("667")
@DisplayName("Manage tokens: search by name and ticker filters the list")
@Test
fun searchByNameAndTickerTest() {
val tokenTitle = "Tether"
val nameQuery = "Tether"
val tickerQuery = "USDT"
val emptyQuery = "Zzqnotoken"
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("Search by name: '$nameQuery'") {
onManageTokensScreen {
searchField.performClick()
searchField.performTextInput(nameQuery)
}
}
step("Assert token: '$tokenTitle' is displayed") {
flakySafely { onManageTokensScreen { tokenItem(tokenTitle).assertIsDisplayed() } }
}
step("Clear search field") {
onManageTokensScreen { searchClearButton.clickWithAssertion() }
}
step("Search by ticker: '$tickerQuery'") {
onManageTokensScreen { searchField.performTextInput(tickerQuery) }
}
step("Assert token: '$tokenTitle' is displayed") {
flakySafely { onManageTokensScreen { tokenItem(tokenTitle).assertIsDisplayed() } }
}
step("Clear search field") {
onManageTokensScreen { searchClearButton.clickWithAssertion() }
}
step("Search by unknown query: '$emptyQuery'") {
onManageTokensScreen { searchField.performTextInput(emptyQuery) }
}
step("Assert token: '$tokenTitle' is not displayed") {
flakySafely { onManageTokensScreen { tokenItem(tokenTitle).assertDoesNotExist() } }
}
}
}
@AllureId("763")
@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("Click on token: '$tokenTitle'") {
flakySafely { onManageTokensScreen { tokenItem(tokenTitle).assertIsDisplayed() } }
onManageTokensScreen { tokenItem(tokenTitle).performClick() }
}
step("Click on '$networkTitle' switch") {
flakySafely { onManageTokensScreen { networkSwitch(networkTitle).assertIsDisplayed() } }
onManageTokensScreen { networkSwitch(networkTitle).performClick() }
}
step("Assert hide-token alert is not displayed") {
waitForIdle()
onDialog { dialogContainer.assertDoesNotExist() }
}
}
}
}