From 8ffbfc27e790f6b3f996658da82e5c8d9c5b063c Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 1 Jul 2026 09:30:03 +0200 Subject: [PATCH] Updated on 2026-08-14 --- .../tangem/common/constants/TestConstants.kt | 1 + .../tangem/scenarios/ManageTokensScenarios.kt | 86 +++++++++ .../screens/AddCustomTokenPageObject.kt | 55 ++++++ .../tangem/screens/ManageTokensPageObject.kt | 21 +++ .../tangem/tests/main/AddCustomTokenTest.kt | 176 ++++++++++++++++++ .../com/tangem/tests/main/ManageTokensTest.kt | 137 ++++++++++++++ .../ui/test/AddCustomTokenScreenTestTags.kt | 11 ++ .../managetokens/ui/CustomTokenFormContent.kt | 20 +- .../ui/CustomTokenSelectorContent.kt | 9 +- 9 files changed, 510 insertions(+), 6 deletions(-) create mode 100644 app/src/androidTest/kotlin/com/tangem/scenarios/ManageTokensScenarios.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/AddCustomTokenPageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/tests/main/AddCustomTokenTest.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/tests/main/ManageTokensTest.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/AddCustomTokenScreenTestTags.kt diff --git a/app/src/androidTest/kotlin/com/tangem/common/constants/TestConstants.kt b/app/src/androidTest/kotlin/com/tangem/common/constants/TestConstants.kt index e9cf254d43..7147ac9ef1 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/constants/TestConstants.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/constants/TestConstants.kt @@ -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" diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/ManageTokensScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/ManageTokensScenarios.kt new file mode 100644 index 0000000000..e04f963f0d --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/ManageTokensScenarios.kt @@ -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() } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/AddCustomTokenPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/AddCustomTokenPageObject.kt new file mode 100644 index 0000000000..346da3e80a --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/AddCustomTokenPageObject.kt @@ -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(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) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/ManageTokensPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/ManageTokensPageObject.kt index c91348bcb6..70beda1905 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/ManageTokensPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/ManageTokensPageObject.kt @@ -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( diff --git a/app/src/androidTest/kotlin/com/tangem/tests/main/AddCustomTokenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/main/AddCustomTokenTest.kt new file mode 100644 index 0000000000..a7fd3109a5 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/main/AddCustomTokenTest.kt @@ -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() } + } + } + } +} \ 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 new file mode 100644 index 0000000000..2d43da235a --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/main/ManageTokensTest.kt @@ -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() } + } + } + } +} \ 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 new file mode 100644 index 0000000000..ff4479c8f0 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/AddCustomTokenScreenTestTags.kt @@ -0,0 +1,11 @@ +package com.tangem.core.ui.test + +object AddCustomTokenScreenTestTags { + const val CONTRACT_ADDRESS_FIELD = "ADD_CUSTOM_TOKEN_CONTRACT_ADDRESS_FIELD" + const val DERIVATION_SELECTOR_FIELD = "ADD_CUSTOM_TOKEN_DERIVATION_SELECTOR_FIELD" + const val CUSTOM_DERIVATION_BUTTON = "ADD_CUSTOM_TOKEN_CUSTOM_DERIVATION_BUTTON" + const val WARNING_NOTIFICATION = "ADD_CUSTOM_TOKEN_WARNING_NOTIFICATION" + const val SELECTOR_LIST = "ADD_CUSTOM_TOKEN_SELECTOR_LIST" + + fun networkRow(networkName: String): String = "ADD_CUSTOM_TOKEN_NETWORK_ROW_${networkName.lowercase()}" +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenFormContent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenFormContent.kt index 65951a6c05..e69a9d6606 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenFormContent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenFormContent.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.testTag import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider @@ -35,6 +36,7 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.AddCustomTokenScreenTestTags import com.tangem.features.managetokens.component.preview.PreviewCustomTokenFormComponent import com.tangem.features.managetokens.entity.customtoken.ClickableFieldUM import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM @@ -122,11 +124,15 @@ private fun FormContent(model: CustomTokenFormUM, modifier: Modifier = Modifier) val derivationPath = model.derivationPath if (derivationPath != null) { - ClickableField(model = derivationPath) + ClickableField( + modifier = Modifier.testTag(AddCustomTokenScreenTestTags.DERIVATION_SELECTOR_FIELD), + model = derivationPath, + ) } model.notifications.fastForEach { notification -> Notification( + modifier = Modifier.testTag(AddCustomTokenScreenTestTags.WARNING_NOTIFICATION), config = notification.config, containerColor = TangemTheme.colors.button.disabled, ) @@ -143,14 +149,19 @@ private fun TokenForm(tokenForm: CustomTokenFormUM.TokenFormUM, modifier: Modifi shape = TangemTheme.shapes.roundedCornersXMedium, ), ) { - tokenForm.fields.values.forEach { field -> - TextField(model = field) + tokenForm.fields.forEach { (key, field) -> + TextField( + model = field, + fieldTestTag = AddCustomTokenScreenTestTags.CONTRACT_ADDRESS_FIELD.takeIf { + key == Field.CONTRACT_ADDRESS + }, + ) } } } @Composable -private fun TextField(model: TextInputFieldUM, modifier: Modifier = Modifier) { +private fun TextField(model: TextInputFieldUM, modifier: Modifier = Modifier, fieldTestTag: String? = null) { InformationBlock( modifier = modifier, title = { @@ -192,6 +203,7 @@ private fun TextField(model: TextInputFieldUM, modifier: Modifier = Modifier) { modifier = Modifier .padding(bottom = TangemTheme.dimens.spacing12) .fillMaxWidth() + .then(if (fieldTestTag != null) Modifier.testTag(fieldTestTag) else Modifier) .onFocusChanged { model.onFocusChange(it.isFocused) }, 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 4922a66af7..6f5a206399 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 @@ -16,6 +16,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -30,6 +31,7 @@ import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.AddCustomTokenScreenTestTags import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.managetokens.component.AddCustomTokenMode @@ -50,6 +52,7 @@ internal fun CustomTokenSelectorContent(model: CustomTokenSelectorUM, modifier: LazyColumn( modifier = modifier + .testTag(AddCustomTokenScreenTestTags.SELECTOR_LIST) .background(color = TangemTheme.colors.background.secondary) .bottomFade(), contentPadding = PaddingValues( @@ -92,7 +95,7 @@ internal fun CustomTokenSelectorContent(model: CustomTokenSelectorUM, modifier: when (item) { is CurrencyNetworkUM -> { NetworkItem( - modifier = itemModifier, + modifier = itemModifier.testTag(AddCustomTokenScreenTestTags.networkRow(item.name)), model = item, ) } @@ -112,7 +115,9 @@ private fun Header(header: CustomTokenSelectorUM.HeaderUM, modifier: Modifier = when (header) { is CustomTokenSelectorUM.HeaderUM.CustomDerivationButton -> { CustomDerivationButton( - modifier = modifier.padding(vertical = TangemTheme.dimens.spacing16), + modifier = modifier + .padding(vertical = TangemTheme.dimens.spacing16) + .testTag(AddCustomTokenScreenTestTags.CUSTOM_DERIVATION_BUTTON), enteredDerivationPath = header.value, isSelected = header.value != null, onClick = header.onClick,