diff --git a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt index 455df5348b..e113abffd9 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt @@ -24,6 +24,7 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.walletmanager.WalletManagersStore import com.tangem.datasource.utils.WireMockRedirectInterceptor +import com.tangem.domain.account.supplier.SingleAccountListSupplier import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.tap.MainActivity @@ -63,6 +64,9 @@ abstract class BaseTestCase : TestCase( @Inject lateinit var getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase + @Inject + lateinit var singleAccountListSupplier: SingleAccountListSupplier + private val hiltRule = HiltAndroidRule(this) private val apiEnvironmentRule = ApiEnvironmentRule() private val permissionRule = GrantPermissionRule.grant( diff --git a/app/src/androidTest/kotlin/com/tangem/common/extensions/UiDeviceExt.kt b/app/src/androidTest/kotlin/com/tangem/common/extensions/UiDeviceExt.kt index b9121a87c6..00a0858c12 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/extensions/UiDeviceExt.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/extensions/UiDeviceExt.kt @@ -4,6 +4,8 @@ import androidx.test.uiautomator.By import androidx.test.uiautomator.Until import com.tangem.common.BaseTestCase import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG +import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_SHORT + fun BaseTestCase.swipeVertical( direction: SwipeDirection, @@ -96,6 +98,12 @@ fun BaseTestCase.restartApp(packageName: String) { waitForIdle() } +fun BaseTestCase.clickOnSystemButton(buttonName: String) { + device.uiDevice.wait(Until.hasObject(By.text(buttonName)), WAIT_UNTIL_TIMEOUT_SHORT) + device.uiDevice.findObject(By.text(buttonName))?.click() + ?: throw AssertionError("System '$buttonName' button not found") +} + enum class SwipeDirection { UP, DOWN } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/common/utils/DerivationPathHelper.kt b/app/src/androidTest/kotlin/com/tangem/common/utils/DerivationPathHelper.kt new file mode 100644 index 0000000000..3e06a795f9 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/common/utils/DerivationPathHelper.kt @@ -0,0 +1,20 @@ +package com.tangem.common.utils + +/** + * Helper for inspecting individual nodes of a BIP-44-style derivation path string + * (e.g. one read from `Network.derivationPath` of a token in the domain account model). + */ +object DerivationPathHelper { + + /** + * Returns the [index1Based]-th node of a derivation path, ignoring the leading `m`. + * For "m/44'/0'/1'/0/0": node 1 = "44'", node 3 = "1'", node 5 = "0". + */ + fun nodeAt(derivationPath: String, index1Based: Int): String { + val nodes = derivationPath.removePrefix("m/").split("/") + require(index1Based in 1..nodes.size) { + "Node #$index1Based is out of range for path '$derivationPath' (${nodes.size} nodes)" + } + return nodes[index1Based - 1] + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/AccountsScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/AccountsScenarios.kt index 7320642036..c8ecf3a61f 100644 --- a/app/src/androidTest/kotlin/com/tangem/scenarios/AccountsScenarios.kt +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/AccountsScenarios.kt @@ -1,14 +1,25 @@ package com.tangem.scenarios import com.tangem.common.BaseTestCase +import com.tangem.common.R +import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_VERY_LONG import com.tangem.common.extensions.clickWithAssertion +import com.tangem.domain.models.account.Account import com.tangem.screens.accounts.onAccountDetailsScreen +import com.tangem.screens.accounts.onAccountInfoEditorScreen import com.tangem.screens.accounts.onArchivedAccountsScreen import com.tangem.screens.onDetailsScreen import com.tangem.screens.onDialog import com.tangem.screens.onMainScreenTopBar import com.tangem.screens.onWalletSettingsScreen +import com.tangem.utils.logging.TangemLogger +import io.github.kakaocup.kakao.common.utilities.getResourceString import io.qameta.allure.kotlin.Allure.step +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout + +private const val ACCOUNT_POLL_INTERVAL_MS = 500L fun BaseTestCase.openWalletSettingsScreen() { step("Open 'Wallet details' screen") { @@ -19,6 +30,15 @@ fun BaseTestCase.openWalletSettingsScreen() { } } +fun BaseTestCase.startAccountCreation() { + step("Click on 'Add account' button") { + onWalletSettingsScreen { addAccountButton.clickWithAssertion() } + } + step("Assert 'Account info editor' screen is displayed") { + onAccountInfoEditorScreen { screenContainer.assertIsDisplayed() } + } +} + fun BaseTestCase.openAccountDetails(accountName: String) { step("Click on account: '$accountName'") { onWalletSettingsScreen { accountItem(accountName).clickWithAssertion() } @@ -28,6 +48,46 @@ fun BaseTestCase.openAccountDetails(accountName: String) { } } +fun BaseTestCase.checkUnsavedChangesCreationModal() { + step("Assert 'Unsaved changes' alert is displayed") { + onDialog { dialogContainer.assertIsDisplayed() } + } + step("Assert 'Unsaved changes' alert has proper title") { + onDialog { title.assertTextContains(getResourceString(R.string.account_unsaved_dialog_title)) } + } + step("Assert 'Unsaved changes' alert has proper description for account creation") { + onDialog { + text.assertTextContains(getResourceString(R.string.account_unsaved_dialog_message_create)) + } + } + step("Assert 'Keep editing' button is displayed in alert with proper text") { + onDialog { keepEditButton.assertIsDisplayed() } + } + step("Assert 'Discard' button is displayed in alert") { + onDialog { discardButton.assertIsDisplayed() } + } +} + +fun BaseTestCase.assertUnsavedChangesEditionModal() { + step("Assert 'Unsaved changes' alert is displayed") { + onDialog { dialogContainer.assertIsDisplayed() } + } + step("Assert 'Unsaved changes' alert has proper title") { + onDialog { title.assertTextContains(getResourceString(R.string.account_unsaved_dialog_title)) } + } + step("Assert 'Unsaved changes' alert has proper description for account creation") { + onDialog { + text.assertTextContains(getResourceString(R.string.account_unsaved_dialog_message_edit)) + } + } + step("Assert 'Keep editing' button is displayed in alert with proper text") { + onDialog { keepEditButton.assertIsDisplayed() } + } + step("Assert 'Discard' button is displayed in alert") { + onDialog { discardButton.assertIsDisplayed() } + } +} + fun BaseTestCase.archiveAccount() { step("Assert 'Archive' button is displayed") { onAccountDetailsScreen { archiveAccountButton.assertIsDisplayed() } @@ -99,4 +159,53 @@ fun BaseTestCase.restoreArchivedAccount(accountName: String) { .restoreButton.clickWithAssertion() } } -} \ No newline at end of file +} + +/** + * Polls [singleAccountListSupplier] for the selected wallet until a [Account.CryptoPortfolio] with the given + * [derivationIndex] appears with a non-empty token list, then returns it. + * + * Per-account token derivation paths live in the domain account model + * ([Account.CryptoPortfolio.cryptoCurrencies] → [com.tangem.domain.models.network.Network.derivationPath]), + * not in the tester-menu "Addresses info" (which reads from the account-agnostic wallet managers store and + * only ever shows main/base derivations). Reading the model directly is the reliable source for asserting + * per-account derivations. + */ +fun BaseTestCase.awaitCryptoPortfolioAccount(derivationIndex: Int): Account.CryptoPortfolio { + val walletId = getSelectedWalletSyncUseCase().getOrNull()?.walletId + ?: error("No selected wallet found") + + var account: Account.CryptoPortfolio? = null + runBlocking { + withTimeout(WAIT_UNTIL_TIMEOUT_VERY_LONG) { + while (true) { + val candidate = singleAccountListSupplier.getSyncOrNull(walletId) + ?.accounts + ?.filterIsInstance() + ?.firstOrNull { it.derivationIndex.value == derivationIndex } + + if (candidate != null && candidate.cryptoCurrencies.isNotEmpty()) { + TangemLogger.i( + "Account with derivation index $derivationIndex resolved: " + + "${candidate.cryptoCurrencies.size} token(s)", + ) + account = candidate + return@withTimeout + } + + delay(ACCOUNT_POLL_INTERVAL_MS) + } + } + } + + return requireNotNull(account) { + "Account with derivation index $derivationIndex was not found for wallet $walletId" + } +} + +/** + * Returns all derivation paths of tokens whose name equals [tokenName] (case-insensitive) within this account. + */ +fun Account.CryptoPortfolio.derivationPathsForToken(tokenName: String): List = cryptoCurrencies + .filter { it.name.equals(tokenName, ignoreCase = true) } + .mapNotNull { it.network.derivationPath.value } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt index b8a3422e11..b3efafdd81 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt @@ -32,6 +32,11 @@ class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : useUnmergedTree = true } + val gotItButton: KNode = child { + hasTestTag(BaseButtonTestTags.BUTTON) + hasText(getResourceString(R.string.common_got_it)) + } + val cancelButton: KNode = child { hasTestTag(BaseButtonTestTags.BUTTON) hasText(getResourceString(R.string.common_cancel)) @@ -52,6 +57,16 @@ class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : hasText(getResourceString(R.string.account_details_archive_action)) } + val discardButton: KNode = child { + hasTestTag(BaseButtonTestTags.BUTTON) + hasText(getResourceString(R.string.account_unsaved_dialog_action_second)) + } + + val keepEditButton: KNode = child { + hasTestTag(BaseButtonTestTags.BUTTON) + hasText(getResourceString(R.string.account_unsaved_dialog_action_first)) + } + val continueButton: KNode = child { hasTestTag(BaseButtonTestTags.BUTTON) hasText(getResourceString(R.string.common_continue)) diff --git a/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt index 5a8d22ddcb..5868e2121f 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt @@ -256,6 +256,22 @@ class MainScreenPageObject(private val semanticsProvider: SemanticsNodeInteracti useUnmergedTree = true } + /** + * Empty-tokens placeholder shown under an expanded account that has no tokens. + */ + val emptyAccountTokensPlaceholder: KNode = child { + hasTestTag(MainScreenTestTags.EMPTY_TOKENS_PLACEHOLDER) + useUnmergedTree = true + } + + /** + * 'Add tokens' button inside the empty-account placeholder. Click opens manage tokens for that account. + */ + val emptyAccountAddTokensButton: KNode = child { + hasTestTag(MainScreenTestTags.EMPTY_TOKENS_ADD_BUTTON) + useUnmergedTree = true + } + /** * Main account header on the main screen. Click to expand/collapse its tokens list. */ @@ -358,6 +374,46 @@ class MainScreenPageObject(private val semanticsProvider: SemanticsNodeInteracti } } + /** + * Account row on the main screen. Tappable — click to expand/collapse its tokens. + */ + @OptIn(ExperimentalTestApi::class) + fun findAccountSectionByName(accountName: String): KNode { + return lazyList.child { + hasTestTag(MainScreenTestTags.ACCOUNT_LIST_ITEM) + hasAnyDescendant(withText(accountName)) + useUnmergedTree = true + } + } + + /** + * Scrolls the account row into view and collapses the top bar so the account's tokens (or the + * empty placeholder) land within screen bounds after expansion. Click via [findAccountSectionByName]. + */ + @OptIn(ExperimentalTestApi::class) + fun scrollToAccountSection(accountName: String) { + collapseHeader() + lazyList.childWith { + hasTestTag(MainScreenTestTags.ACCOUNT_LIST_ITEM) + hasAnyDescendant(withText(accountName)) + useUnmergedTree = true + } + } + + /** + * Find a token row on the main screen by token name. Tokens belonging to collapsed accounts + * are hidden from the semantics tree, so expanding a single account before calling this + * effectively scopes the lookup to that account's tokens. + */ + @OptIn(ExperimentalTestApi::class) + fun findTokenInAnyAccountByName(tokenName: String): KNode { + return lazyList.child { + hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM) + hasAnyDescendant(withText(tokenName)) + useUnmergedTree = true + } + } + fun KNode.assertIsUnreachable() { this { hasAnyAncestor(withText(getResourceString(R.string.common_unreachable))) @@ -370,16 +426,11 @@ class MainScreenPageObject(private val semanticsProvider: SemanticsNodeInteracti * Tests will fail if assertIsNotDisplayed() or assertDoesNotExist() are used instead. */ fun assertTokenDoesNotExist(tokenTitle: String) { - try { - tokenWithTitleAndAddress(tokenTitle).assertExists() - throw AssertionError("Token with title '$tokenTitle' should not exist but was found") - } catch (e: AssertionError) { - if (e.message?.contains("No node found") == true) { - return - } else { - throw e - } - } + lazyList.child { + hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM) + hasAnyDescendant(withText(tokenTitle)) + useUnmergedTree = true + }.assertDoesNotExist() } fun assertTokensCount(expectedCount: Int) { diff --git a/app/src/androidTest/kotlin/com/tangem/screens/ManageTokensPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/ManageTokensPageObject.kt index 2d9882fde8..c91348bcb6 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/ManageTokensPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/ManageTokensPageObject.kt @@ -7,6 +7,7 @@ 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.SwitchTestTags +import com.tangem.core.ui.test.TopAppBarTestTags 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 @@ -20,6 +21,16 @@ import androidx.compose.ui.test.hasAnyAncestor as withAnyAncestor class ManageTokensPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : ComposeScreen(semanticsProvider = semanticsProvider) { + val topAppBarBackButton: KNode = child { + hasTestTag(TopAppBarTestTags.CLOSE_BUTTON) + } + + val topAppBarTitle: KNode = child { + hasTestTag(TopAppBarTestTags.TITLE) + hasText(getResourceString(com.tangem.core.ui.R.string.add_tokens_title)) + useUnmergedTree = true + } + val searchField: KNode = child { hasTestTag(BaseSearchBarTestTags.SEARCH_BAR) } diff --git a/app/src/androidTest/kotlin/com/tangem/screens/WalletSettingsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/WalletSettingsPageObject.kt index d1df56061b..43e4d9058a 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/WalletSettingsPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/WalletSettingsPageObject.kt @@ -16,6 +16,10 @@ import androidx.compose.ui.test.hasText as withText class WalletSettingsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : ComposeScreen(semanticsProvider = semanticsProvider) { + val screenContainer: KNode = child { + hasTestTag(WalletSettingsScreenTestTags.SCREEN_CONTAINER) + } + val topAppBarBackButton: KNode = child { hasTestTag(TopAppBarTestTags.CLOSE_BUTTON) } diff --git a/app/src/androidTest/kotlin/com/tangem/screens/accounts/AccountInfoEditorPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/accounts/AccountInfoEditorPageObject.kt new file mode 100644 index 0000000000..5250db790d --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/accounts/AccountInfoEditorPageObject.kt @@ -0,0 +1,45 @@ +package com.tangem.screens.accounts + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.core.ui.test.TopAppBarTestTags +import com.tangem.core.ui.test.accounts.AccountInfoEditScreenTestTags +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 + +class AccountInfoEditPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val screenContainer: KNode = child { + hasTestTag(AccountInfoEditScreenTestTags.ACCOUNT_DETAILS_CONTAINER) + } + + val accountNameField: KNode = child { + hasTestTag(AccountInfoEditScreenTestTags.NAME_FIELD) + } + + val accountCurrentIcon: KNode = child { + hasTestTag(AccountInfoEditScreenTestTags.SELECTED_ICON) + } + + val accountColorOption: KNode = child { + hasTestTag(AccountInfoEditScreenTestTags.COLOR_OPTION) + } + + val accountTypeOption: KNode = child { + hasTestTag(AccountInfoEditScreenTestTags.TYPE_OPTION) + } + + val saveAccountButton: KNode = child { + hasTestTag(AccountInfoEditScreenTestTags.SAVE_ACCOUNT_BUTTON) + } + + val crossButton: KNode = child { + hasTestTag(TopAppBarTestTags.CLOSE_BUTTON) + } + +} + +internal fun BaseTestCase.onAccountInfoEditorScreen(function: AccountInfoEditPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/accounts/AccountArchivationsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/accounts/AccountArchivationsTest.kt index 70e4d5d02a..a6b7e599c7 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/accounts/AccountArchivationsTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/accounts/AccountArchivationsTest.kt @@ -3,6 +3,7 @@ package com.tangem.tests.accounts import com.tangem.common.BaseTestCase import com.tangem.common.constants.TestConstants.REFERRAL_API_SCENARIO import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO +import com.tangem.common.extensions.clickAndWaitFor import com.tangem.common.extensions.clickWithAssertion import com.tangem.common.utils.resetWireMockScenarioState import com.tangem.common.utils.setWireMockScenarioState @@ -10,7 +11,9 @@ import com.tangem.core.ui.R import com.tangem.scenarios.* import com.tangem.screens.accounts.onAccountDetailsScreen import com.tangem.screens.accounts.onArchivedAccountsScreen +import com.tangem.screens.onDetailsScreen import com.tangem.screens.onDialog +import com.tangem.screens.onMainScreen import com.tangem.screens.onWalletSettingsScreen import dagger.hilt.android.testing.HiltAndroidTest import io.github.kakaocup.kakao.common.utilities.getResourceString @@ -164,8 +167,8 @@ class AccountArchivationsTest : BaseTestCase() { @Test @AllureId("5976") - @DisplayName("Accounts: restore an archived account") - fun restoreArchivedAccountTest() { + @DisplayName("Accounts: restore a simple archived account") + fun restoreSimpleArchivedAccountTest() { val archivedAccountName = "Account 3" val userAccountsInitialState = "TwoAccountsWithArchivedAccounts" val userAccountsAfterArchivationState = "ReadyToRestore" @@ -202,6 +205,108 @@ class AccountArchivationsTest : BaseTestCase() { } } + @Test + @AllureId("5980") + @DisplayName("Accounts: restore archived account with custom token transfer") + fun restoreArchivedAccountWithCustomTokensTest() { + val mainAccountName = "Main account" + val archivedAccountName = "Account 2" + val customTokenName = "Ethereum" + val expectedArchivedTokensInfo = "1 token" + val userAccountsInitialState = "OneAccountWithArchivedCustomToken" + val userAccountsReadyToRestoreState = "ReadyToRestoreCustomToken" + + setupHooks( + additionalBeforeSection = { + setWireMockScenarioState(userTokensScenario, userAccountsInitialState) + }, + additionalAfterSection = { + resetWireMockScenarioState(userTokensScenario) + }, + ).run { + step("Open 'Main Screen'") { openMainScreen() } + step("Synchronize addresses") { synchronizeAddresses() } + step("Open wallet settings") { openWalletSettingsScreen() } + step("Open 'Archived accounts' screen") { openArchivedAccountsScreen() } + + step("Verify archived account '$archivedAccountName' shows '$expectedArchivedTokensInfo'") { + onArchivedAccountsScreen { + val row = findArchivedAccountItemByName(archivedAccountName) + row.container.assertIsDisplayed() + row.subtitle.assertTextContains(expectedArchivedTokensInfo, substring = true) + } + } + step("Switch WireMock to '$userAccountsReadyToRestoreState'") { + setWireMockScenarioState(userTokensScenario, userAccountsReadyToRestoreState) + } + step("Click restore button for '$archivedAccountName'") { + onArchivedAccountsScreen { + findArchivedAccountItemByName(archivedAccountName) + .restoreButton.clickWithAssertion() + } + } + + step("Assert custom token migration dialog is displayed") { + onDialog { dialogContainer.assertIsDisplayed() } + } + step("Assert dialog text mentions main account '$mainAccountName'") { + onDialog { text.assertTextContains(mainAccountName, substring = true) } + } + step("Assert dialog text mentions restoring account '$archivedAccountName'") { + onDialog { text.assertTextContains(archivedAccountName, substring = true) } + } + step("Confirm migration in dialog") { + onDialog { gotItButton.clickWithAssertion() } + } + + step("Assert 'Wallet settings' screen is displayed") { + onWalletSettingsScreen { addAccountButton.assertIsDisplayed() } + } + step("Assert restored account '$archivedAccountName' is in active accounts list") { + onWalletSettingsScreen { accountItem(archivedAccountName).assertIsDisplayed() } + } + step("Navigate back to wallet details") { + onWalletSettingsScreen { topAppBarBackButton.clickWithAssertion() } + } + step("Navigate back to main screen") { + onDetailsScreen { topAppBarBackButton.clickWithAssertion() } + } + + step("Assert main account '$mainAccountName' is visible on main screen") { + onMainScreen { findAccountSectionByName(mainAccountName).assertIsDisplayed() } + } + step("Assert restored account '$archivedAccountName' is visible on main screen") { + onMainScreen { findAccountSectionByName(archivedAccountName).assertIsDisplayed() } + } + + step("Expand main account '$mainAccountName'") { + onMainScreen { findAccountSectionByName(mainAccountName).clickWithAssertion() } + } + step("Assert '$customTokenName' is NOT displayed under main account") { + onMainScreen { assertTokenDoesNotExist(customTokenName) } + } + step("Expand main account '$mainAccountName'") { + onMainScreen { findAccountSectionByName(mainAccountName).clickWithAssertion() } + } + step("Assert '$customTokenName' is NOT displayed under main account") { + onMainScreen { + assertTokenDoesNotExist(customTokenName) + } + } + + step("Expand restored account '$archivedAccountName' and assert '$customTokenName' is displayed") { + onMainScreen { + findAccountSectionByName(archivedAccountName).clickAndWaitFor( + rule = composeTestRule, + expectedCondition = { + onMainScreen { findTokenInAnyAccountByName(customTokenName).assertIsDisplayed() } + }, + ) + } + } + } + } + @Test @AllureId("7962") @DisplayName("Accounts: restore archived account error") @@ -250,4 +355,5 @@ class AccountArchivationsTest : BaseTestCase() { } } } + } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/accounts/AccountCreationTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/accounts/AccountCreationTest.kt new file mode 100644 index 0000000000..3793e13e4b --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/accounts/AccountCreationTest.kt @@ -0,0 +1,492 @@ +package com.tangem.tests.accounts + +import androidx.compose.ui.test.longClick +import com.tangem.common.BaseTestCase +import com.tangem.common.R +import com.tangem.common.extensions.clickAndWaitFor +import com.tangem.common.extensions.clickOnSystemButton +import com.tangem.common.extensions.clickWithAssertion +import com.tangem.common.utils.DerivationPathHelper +import com.tangem.common.utils.resetWireMockScenarioState +import com.tangem.common.utils.setClipboardText +import com.tangem.common.utils.setWireMockScenarioState +import com.tangem.scenarios.* +import com.tangem.screens.* +import com.tangem.screens.accounts.onAccountInfoEditorScreen +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.Assert.assertTrue +import org.junit.Test + +@HiltAndroidTest +class AccountCreationTest : BaseTestCase() { + + private val userTokensScenario = "user_tokens_api" + + @Test + @AllureId("5504") + @DisplayName("Accounts: account creation network error handling") + fun accountCreationErrorTest() { + val accountName = "Account 2" + val userAccountsGetErrorState = "AccountsGetError" + val userAccountsPutErrorState = "AccountsPutError" + val userAccountsBeforeCreationState = "AccountReadyToCreate" + + setupHooks( + additionalBeforeSection = { + setWireMockScenarioState(userTokensScenario, userAccountsGetErrorState) + }, + additionalAfterSection = { + resetWireMockScenarioState(userTokensScenario) + }, + ).run { + + step("Open 'Main Screen'") { openMainScreen() } + step("Synchronize addresses") { synchronizeAddresses() } + step("Open wallet settings") { openWalletSettingsScreen() } + step("Start account creation") { startAccountCreation() } + + step("Enter account name: '$accountName'") { + onAccountInfoEditorScreen { + accountNameField.performClick() + accountNameField.performTextInput(accountName) + } + } + step("Click 'Add account' button (GET accounts is blocked)") { + onAccountInfoEditorScreen { + saveAccountButton.clickAndWaitFor( + rule = composeTestRule, + expectedCondition = { + onDialog { dialogContainer.assertIsDisplayed() } + }, + ) + } + } + step("Assert error dialog details") { + assertErrorDialog( + expectedTitle = getResourceString(R.string.common_something_went_wrong), + expectedMessage = getResourceString(com.tangem.core.ui.R.string.account_generic_error_dialog_message), + ) + } + step("Dismiss error dialog") { dismissErrorDialog() } + step("Assert still on account creation screen") { + onAccountInfoEditorScreen { screenContainer.assertIsDisplayed() } + } + + step("Unblock GET accounts, block PUT accounts") { + setWireMockScenarioState(userTokensScenario, userAccountsPutErrorState) + } + step("Click 'Add account' button again (PUT accounts is blocked)") { + onAccountInfoEditorScreen { + saveAccountButton.clickAndWaitFor( + rule = composeTestRule, + expectedCondition = { + onDialog { dialogContainer.assertIsDisplayed() } + }, + ) + } + } + step("Assert still on account creation screen") { + assertErrorDialog( + expectedTitle = getResourceString(R.string.common_something_went_wrong), + expectedMessage = getResourceString(R.string.account_generic_error_dialog_message), + ) + } + + step("Unblock both 'accounts' requests") { + setWireMockScenarioState(userTokensScenario, userAccountsBeforeCreationState) + } + + step("Dismiss error dialog") { dismissErrorDialog() } + step("Assert still on account creation screen") { + onAccountInfoEditorScreen { screenContainer.assertIsDisplayed() } + } + + step("Click 'Add account' button again (both requests unblocked)") { + onAccountInfoEditorScreen { + saveAccountButton.clickAndWaitFor( + rule = composeTestRule, + expectedCondition = { + onManageTokensScreen { topAppBarTitle.assertIsDisplayed() } + }, + ) + } + } + step("Assert 'Manage Tokens' title is displayed") { + onManageTokensScreen { topAppBarTitle.assertIsDisplayed() } + } + step("Close 'Manage Tokens' screen") { + onManageTokensScreen { topAppBarBackButton.clickWithAssertion() } + } + step("Assert 'Wallet settings' screen is displayed") { + onWalletSettingsScreen { addAccountButton.assertIsDisplayed() } + } + step("Assert new account '$accountName' appears in accounts list") { + onWalletSettingsScreen { accountItem(accountName).assertIsDisplayed() } + } + } + } + + @Test + @AllureId("5507") + @DisplayName("Accounts: name field verifications") + fun accountsCreationNameFieldValidationTest() { + val accountName = "TestAccount12" + val longName = "A".repeat(21) + val emptyPlaceholderValue = "New account" + val editedName = "Edited" + val context = device.context + val pasteButtonName = "Paste" + + setupHooks().run { + step("Set clipboard text '$longName'") { + setClipboardText(context,longName) + } + step("Open 'Main Screen'") { openMainScreen() } + step("Synchronize addresses") { synchronizeAddresses() } + step("Open 'Wallet settings' screen") { openWalletSettingsScreen() } + step("Click on 'Add account' button") { + onWalletSettingsScreen { addAccountButton.clickWithAssertion() } + } + step("Assert 'Edit account details' dialog screen appears") { + onAccountInfoEditorScreen { screenContainer.assertIsDisplayed() } + } + + step("Enter account name manually: '$accountName'") { + onAccountInfoEditorScreen { + accountNameField.performClick() + accountNameField.performTextInput(accountName) + } + } + step("Assert name input is stable (keyboard doesn't flicker)") { + onAccountInfoEditorScreen { + accountNameField.assertTextContains(accountName) + } + } + step("Assert 'Add account' button is enabled") { + onAccountInfoEditorScreen { + saveAccountButton.assertIsEnabled() + } + } + + step("Clear the 'Edit name' field") { + onAccountInfoEditorScreen { + accountNameField.performTextClearance() + } + } + step("Assert 'Add account' button becomes inactive when field is empty") { + onAccountInfoEditorScreen { + saveAccountButton.assertIsNotEnabled() + } + } + step("Paste name from clipboard: '$accountName'") { + onAccountInfoEditorScreen { + accountNameField.performTextReplacement(accountName) + } + } + step("Assert pasted text is displayed in 'Account name' field") { + onAccountInfoEditorScreen { + accountNameField.assertTextContains(accountName) + } + } + step("Assert 'Add account' button is enabled") { + onAccountInfoEditorScreen { + saveAccountButton.assertIsEnabled() + } + } + step("Edit the entered name (clear and retype)") { + onAccountInfoEditorScreen { + accountNameField.performTextReplacement(editedName) + } + } + step("Assert edited name in 'Account name' field is displayed") { + onAccountInfoEditorScreen { + accountNameField.assertTextContains(editedName) + } + } + step("Delete all text and leave 'Account name' field empty") { + onAccountInfoEditorScreen { + accountNameField.performTextClearance() + } + } + step("Assert 'Add account' button is inactive") { + onAccountInfoEditorScreen { + saveAccountButton.assertIsNotEnabled() + } + } + step("Type name with more than 20 symbols") { + onAccountInfoEditorScreen { + accountNameField.performTextReplacement(longName) + } + } + step("Assert text over 20 symbols was not pasted and placeholder remains empty") { + onAccountInfoEditorScreen { + accountNameField.assertTextContains(emptyPlaceholderValue, substring = true) + } + } + step("Assert 'Add account' button is inactive") { + onAccountInfoEditorScreen { + saveAccountButton.assertIsNotEnabled() + } + } + step("Clear text field") { + onAccountInfoEditorScreen { accountNameField.performTextClearance() } + } + step("Paste text longer than 20 characters to 'Account name' field") { + onAccountInfoEditorScreen { + accountNameField.performTouchInput { longClick(durationMillis = 2_000L) } + } + } + step("Click on system 'Paste' button to paste clipboard text") { + clickOnSystemButton(pasteButtonName) + } + step("Assert text over 20 symbols was not pasted and placeholder remains empty") { + onAccountInfoEditorScreen { + accountNameField.assertTextContains(emptyPlaceholderValue, substring = true) + } + } + step("Assert 'Add account' button is inactive") { + onAccountInfoEditorScreen { + saveAccountButton.assertIsNotEnabled() + } + } + } + } + + @Test + @AllureId("5505") + @DisplayName( + "Accounts: check unsaved changes notification " + + "after attempt to close edited account creation form" + ) + fun accountsCreationUnsavedChangesForNameFieldNotificationTest() { + val accountName = "Hikarik Test" + + setupHooks().run { + step("Open 'Main Screen'") { openMainScreen() } + step("Synchronize addresses") { synchronizeAddresses() } + step("Open 'Wallet settings' screen") { openWalletSettingsScreen() } + step("Click on 'Add account' button") { + onWalletSettingsScreen { addAccountButton.clickWithAssertion() } + } + step("Assert edit account details dialog screen appears") { + onAccountInfoEditorScreen { screenContainer.assertIsDisplayed() } + } + + step("Enter account name manually: '$accountName'") { + onAccountInfoEditorScreen { + accountNameField.performClick() + accountNameField.performTextInput(accountName) + } + } + step("Tap 'Cross' button to attempt closing the screen") { + onAccountInfoEditorScreen { + crossButton.clickWithAssertion() + } + } + step("Verify 'Unsaved changes' screen parts") { + checkUnsavedChangesCreationModal() + } + + step("Tap 'Keep Editing' button to stay on screen") { + onDialog { keepEditButton.clickWithAssertion() } + } + step("Assert app still on 'Create account' screen") { + onAccountInfoEditorScreen { screenContainer.assertIsDisplayed() } + } + step("Assert previously entered data is preserved") { + onAccountInfoEditorScreen { + accountNameField.assertTextContains(accountName) + } + } + + step("Tap 'Cross' button to attempt closing the screen") { + onAccountInfoEditorScreen { crossButton.clickWithAssertion() } + } + step("Assert 'Unsaved changes' alert is displayed again") { + onDialog { dialogContainer.assertIsDisplayed() } + } + step("Tap 'Discard' button to discard and close") { + onDialog { discardButton.clickWithAssertion() } + } + step("Assert 'Create account' screen is closed and 'Wallet settings' displayed again") { + onWalletSettingsScreen { + screenContainer.assertIsDisplayed() + } + } + step("Verify no new account has appeared in the list") { + onWalletSettingsScreen { + accountItem(accountName).assertDoesNotExist() + } + } + } + } + + @Test + @AllureId("5502") + @DisplayName("Accounts: account creation, accounts mode and per-account token derivation") + fun accountCreationAndDerivationTest() { + val createdAccountName = "Account 2" + val accountReadyState = "AccountReadyToCreateDerivation" + val accountIndex = "1" + val btcTokenName = "Bitcoin" + val ethTokenName = "Ethereum" + + setupHooks( + additionalBeforeSection = { + setWireMockScenarioState(userTokensScenario, accountReadyState) + }, + additionalAfterSection = { + resetWireMockScenarioState(userTokensScenario) + }, + ).run { + step("Open 'Main Screen'") { openMainScreen() } + step("Synchronize addresses") { synchronizeAddresses() } + step("Open wallet settings") { openWalletSettingsScreen() } + step("Start account creation") { startAccountCreation() } + + step("Enter account name: '$createdAccountName'") { + onAccountInfoEditorScreen { + accountNameField.performClick() + accountNameField.performTextInput(createdAccountName) + } + } + step("Assert account creation screen with derivation hint is displayed") { + onAccountInfoEditorScreen { screenContainer.assertIsDisplayed() } + } + + step("Click 'Add account' and wait for 'Manage Tokens'") { + onAccountInfoEditorScreen { + saveAccountButton.clickAndWaitFor( + rule = composeTestRule, + expectedCondition = { + onManageTokensScreen { topAppBarTitle.assertIsDisplayed() } + }, + ) + } + } + + step("Close 'Manage Tokens' screen") { + onManageTokensScreen { topAppBarBackButton.clickWithAssertion() } + } + step("Assert 'Wallet settings' screen is displayed") { + onWalletSettingsScreen { addAccountButton.assertIsDisplayed() } + } + step("Assert new account '$createdAccountName' appears (last) in accounts list") { + onWalletSettingsScreen { accountItem(createdAccountName).assertIsDisplayed() } + } + step("Navigate back to wallet details") { + onWalletSettingsScreen { topAppBarBackButton.clickWithAssertion() } + } + step("Navigate back to main screen") { + onDetailsScreen { topAppBarBackButton.clickWithAssertion() } + } + + step("Assert accounts mode is on main: account '$createdAccountName' section is visible") { + onMainScreen { findAccountSectionByName(createdAccountName).assertIsDisplayed() } + } + + step("Assert per-account token derivation paths from the domain account model") { + val account = awaitCryptoPortfolioAccount(derivationIndex = accountIndex.toInt()) + + val btcPaths = account.derivationPathsForToken(btcTokenName) + assertTrue( + "Expected a $btcTokenName derivation with 3rd node = $accountIndex' (account index). Paths: $btcPaths", + btcPaths.any { DerivationPathHelper.nodeAt(it, index1Based = 3) == "$accountIndex'" }, + ) + + val ethPaths = account.derivationPathsForToken(ethTokenName) + assertTrue( + "Expected an $ethTokenName derivation with 5th node = $accountIndex (account index). Paths: $ethPaths", + ethPaths.any { DerivationPathHelper.nodeAt(it, index1Based = 5) == accountIndex }, + ) + } + } + } + + @Test + @AllureId("8746") + @DisplayName("Accounts: empty account placeholder and 'Add tokens' entry to manage tokens") + fun emptyAccountPlaceholderTest() { + val createdAccountName = "Account 2" + val accountReadyState = "AccountReadyToCreateEmpty" + + setupHooks( + additionalBeforeSection = { + setWireMockScenarioState(userTokensScenario, accountReadyState) + }, + additionalAfterSection = { + resetWireMockScenarioState(userTokensScenario) + }, + ).run { + step("Open 'Main Screen'") { openMainScreen() } + step("Synchronize addresses") { synchronizeAddresses() } + step("Open wallet settings") { openWalletSettingsScreen() } + step("Start account creation") { startAccountCreation() } + + step("Enter account name: '$createdAccountName'") { + onAccountInfoEditorScreen { + accountNameField.performClick() + accountNameField.performTextInput(createdAccountName) + } + } + step("Click on 'Add account' and wait for 'Manage Tokens'") { + onAccountInfoEditorScreen { + saveAccountButton.clickAndWaitFor( + rule = composeTestRule, + expectedCondition = { + onManageTokensScreen { topAppBarTitle.assertIsDisplayed() } + }, + ) + } + } + step("Close 'Manage Tokens' without adding any token") { + onManageTokensScreen { topAppBarBackButton.clickWithAssertion() } + } + step("Assert 'Wallet settings' screen is displayed") { + onWalletSettingsScreen { addAccountButton.assertIsDisplayed() } + } + step("Assert new empty account '$createdAccountName' appears in accounts list") { + onWalletSettingsScreen { accountItem(createdAccountName).assertIsDisplayed() } + } + step("Navigate back to wallet details") { + onWalletSettingsScreen { topAppBarBackButton.clickWithAssertion() } + } + step("Navigate back to main screen") { + onDetailsScreen { topAppBarBackButton.clickWithAssertion() } + } + step("Expand empty account '$createdAccountName' section") { + onMainScreen { + scrollToAccountSection(createdAccountName) + findAccountSectionByName(createdAccountName).clickAndWaitFor( + rule = composeTestRule, + expectedCondition = { + onMainScreen { emptyAccountTokensPlaceholder.assertIsDisplayed() } + }, + ) + } + } + step("Assert empty tokens placeholder is displayed") { + onMainScreen { emptyAccountTokensPlaceholder.assertIsDisplayed() } + } + step("Assert 'Add tokens' button is displayed under the placeholder") { + onMainScreen { emptyAccountAddTokensButton.assertIsDisplayed() } + } + step("Click on 'Add tokens' button") { + onMainScreen { + emptyAccountAddTokensButton.clickAndWaitFor( + rule = composeTestRule, + expectedCondition = { + onManageTokensScreen { topAppBarTitle.assertIsDisplayed() } + }, + ) + } + } + step("Assert 'Manage Tokens' screen is opened for the account") { + onManageTokensScreen { topAppBarTitle.assertIsDisplayed() } + } + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/MainScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/MainScreenTestTags.kt index c12417b238..bd535e8260 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/MainScreenTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/MainScreenTestTags.kt @@ -6,8 +6,11 @@ object MainScreenTestTags { const val TOP_BAR = "MAIN_SCREEN_TOP_BAR" const val TOKEN_LIST_ITEM = "MAIN_SCREEN_TOKEN_LIST_ITEM" const val WALLET_LIST_ITEM = "MAIN_SCREEN_WALLET_LIST_ITEM" + const val ACCOUNT_LIST_ITEM = "MAIN_SCREEN_ACCOUNT_LIST_ITEM" const val ORGANIZE_TOKENS_BUTTON = "MAIN_SCREEN_ORGANIZE_TOKENS_BUTTON" const val ADD_AND_MANAGE_BUTTON = "MAIN_SCREEN_ADD_AND_MANAGE_BUTTON" + const val EMPTY_TOKENS_PLACEHOLDER = "MAIN_SCREEN_EMPTY_TOKENS_PLACEHOLDER" + const val EMPTY_TOKENS_ADD_BUTTON = "MAIN_SCREEN_EMPTY_TOKENS_ADD_BUTTON" const val CARD_TITLE = "MAIN_SCREEN_CARD_TITLE" const val CARD_IMAGE = "MAIN_SCREEN_CARD_IMAGE" const val DEVICES_COUNT = "MAIN_SCREEN_DEVICES_COUNT" diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/accounts/AccountInfoEditScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/accounts/AccountInfoEditScreenTestTags.kt index 940f3eebe1..e82eaf72e4 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/accounts/AccountInfoEditScreenTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/accounts/AccountInfoEditScreenTestTags.kt @@ -2,7 +2,7 @@ package com.tangem.core.ui.test.accounts object AccountInfoEditScreenTestTags { const val ACCOUNT_DETAILS_CONTAINER = "ACCOUNT_INFO_EDIT_SCREEN_ACCOUNT_DETAILS_CONTAINER" - const val ADD_ACCOUNT_BUTTON = "ACCOUNT_INFO_EDIT_SCREEN_ADD_ACCOUNT_BUTTON" + const val SAVE_ACCOUNT_BUTTON = "ACCOUNT_INFO_EDIT_SCREEN_SAVE_ACCOUNT_BUTTON" const val COLOR_OPTION = "ACCOUNT_INFO_EDIT_SCREEN_ACCOUNT_INFO_COLOR_OPTION" const val TYPE_OPTION = "ACCOUNT_INFO_EDIT_SCREEN_ACCOUNT_INFO_TYPE_OPTION" const val SELECTED_ICON = "ACCOUNT_INFO_EDIT_SCREEN_SELECTED_ICON" diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/accounts/ArchivedAccountsScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/accounts/ArchivedAccountsScreenTestTags.kt index 9bb6ef093f..e072a4a1c7 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/accounts/ArchivedAccountsScreenTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/accounts/ArchivedAccountsScreenTestTags.kt @@ -1,7 +1,6 @@ package com.tangem.core.ui.test.accounts object ArchivedAccountsScreenTestTags { - const val ARCHIVED_ACCOUNTS_SCREEN_CONTAINER = "ARCHIVED_ACCOUNTS_LIST_CONTAINER" const val ARCHIVED_ACCOUNT_ITEM = "ARCHIVED_ACCOUNTS_LIST_ARCHIVED_ACCOUNT_ITEM" const val RESTORE_BUTTON = "ARCHIVED_ACCOUNTS_LIST_RESTORE_BUTTON" diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt index 1dc4a29253..299a2d500e 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt @@ -73,7 +73,8 @@ internal fun AccountCreateEditContent( .nestedScroll(nestedScrollConnection) .verticalScroll(rememberScrollState()) .padding(horizontal = 16.dp) - .weight(1f), + .weight(1f) + .testTag(AccountInfoEditScreenTestTags.ACCOUNT_DETAILS_CONTAINER), ) { AccountSummary(state.account, isCreateMode) SpacerH24() @@ -87,7 +88,8 @@ internal fun AccountCreateEditContent( PrimaryButton( modifier = Modifier .fillMaxWidth() - .padding(16.dp), + .padding(16.dp) + .testTag(AccountInfoEditScreenTestTags.SAVE_ACCOUNT_BUTTON), enabled = state.buttonState.isButtonEnabled, showProgress = state.buttonState.shouldShowProgress, text = state.buttonState.text.resolveReference(), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt index 169dd4b6d5..f0fa031f17 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt @@ -289,7 +289,7 @@ private fun LazyListScope.accountItem( val portfolioModifier = modifier .padding(top = if (index != 0) TangemTheme.dimens2.x2 else TangemTheme.dimens2.x3) - .testTag(MainScreenTestTags.TOKEN_LIST_ITEM) + .testTag(MainScreenTestTags.ACCOUNT_LIST_ITEM) .semantics { lazyListItemPosition = index } .roundedShapeItemDecoration( currentIndex = 0, @@ -522,7 +522,7 @@ private fun LazyListScope.nonContentAccountItem(listItem: TokensListItemUM2.Port @Composable internal fun NonContentItemContentV2(textColor: Color, modifier: Modifier = Modifier, onClick: () -> Unit) { Column( - modifier = modifier, + modifier = modifier.testTag(MainScreenTestTags.EMPTY_TOKENS_PLACEHOLDER), horizontalAlignment = Alignment.CenterHorizontally, ) { Icon( @@ -545,7 +545,7 @@ internal fun NonContentItemContentV2(textColor: Color, modifier: Modifier = Modi onClick = onClick, size = TangemButtonSize.X8, shape = TangemButtonShape.Rounded, - modifier = Modifier, + modifier = Modifier.testTag(MainScreenTestTags.EMPTY_TOKENS_ADD_BUTTON), ) } } \ No newline at end of file