From e0feda9a004c1c20fcd9e95324624514c21baaa8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 14 Jul 2026 12:12:44 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../extensions/SemanticsNodeExtensions.kt | 34 ++++ .../tangem/scenarios/MainScreenScenarios.kt | 29 +++ .../tangem/scenarios/MultiWalletScenarios.kt | 15 ++ .../tangem/screens/MainScreenPageObject.kt | 178 +++++++++++++++++- .../screens/OrganizeTokensPageObject.kt | 73 ++++++- .../tangem/tests/main/OrganizeTokensTest.kt | 95 ++++++++++ .../com/tangem/tests/main/TokenListTest.kt | 56 ++++++ 7 files changed, 471 insertions(+), 9 deletions(-) create mode 100644 app/src/androidTest/kotlin/com/tangem/common/extensions/SemanticsNodeExtensions.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/tests/main/OrganizeTokensTest.kt diff --git a/app/src/androidTest/kotlin/com/tangem/common/extensions/SemanticsNodeExtensions.kt b/app/src/androidTest/kotlin/com/tangem/common/extensions/SemanticsNodeExtensions.kt new file mode 100644 index 0000000000..2d83fbcfb5 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/common/extensions/SemanticsNodeExtensions.kt @@ -0,0 +1,34 @@ +package com.tangem.common.extensions + +import androidx.compose.ui.semantics.SemanticsNode +import androidx.compose.ui.semantics.SemanticsProperties +import androidx.compose.ui.semantics.getOrNull +import androidx.compose.ui.test.SemanticsNodeInteractionCollection +import androidx.compose.ui.test.assertIsDisplayed + +/** Returns the first non-blank text found in this node or its subtree (unmerged tree). */ +fun SemanticsNode.firstTextOrNull(): String? { + config.getOrNull(SemanticsProperties.Text) + ?.firstOrNull()?.text?.toString()?.takeIf { it.isNotBlank() } + ?.let { return it } + children.forEach { child -> child.firstTextOrNull()?.let { return it } } + return null +} + +/** + * Reads the first text of every currently displayed node in this collection, in visual order + * (top-to-bottom, then left-to-right). Non-displayed nodes and nodes without text are skipped. + */ +fun SemanticsNodeInteractionCollection.displayedTextsInVisualOrder(): List { + val count = fetchSemanticsNodes().size + return (0 until count) + .mapNotNull { index -> + val interaction = get(index) + if (runCatching { interaction.assertIsDisplayed() }.isFailure) return@mapNotNull null + val node = interaction.fetchSemanticsNode() + val text = node.firstTextOrNull() ?: return@mapNotNull null + node.boundsInRoot to text + } + .sortedWith(compareBy({ it.first.top }, { it.first.left })) + .map { it.second } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/MainScreenScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/MainScreenScenarios.kt index 9c9b025c84..fd075d8aa0 100644 --- a/app/src/androidTest/kotlin/com/tangem/scenarios/MainScreenScenarios.kt +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/MainScreenScenarios.kt @@ -1,12 +1,15 @@ package com.tangem.scenarios import com.tangem.common.BaseTestCase +import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG import com.tangem.common.extensions.SwipeDirection import com.tangem.common.extensions.clickWithAssertion import com.tangem.common.extensions.swipeVertical import com.tangem.screens.onAddAndManageBottomSheet import com.tangem.screens.onMainScreen +import com.tangem.screens.onOrganizeTokensScreen import io.qameta.allure.kotlin.Allure.step +import org.junit.Assert.assertEquals fun BaseTestCase.openOrganizeTokensScreen() { step("Swipe to 'Add & Manage' button") { @@ -19,4 +22,30 @@ fun BaseTestCase.openOrganizeTokensScreen() { step("Click on 'Organize tokens' button in bottom sheet") { onAddAndManageBottomSheet { organizeTokensButton.clickWithAssertion() } } +} + +fun BaseTestCase.getMainScreenTokensOrder(): List { + var tokens: List = emptyList() + step("Read displayed token titles from 'Main' screen") { + awaitSuccess(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) { + onMainScreen { tokens = getDisplayedTokenTitles() } + require(tokens.isNotEmpty()) { "No token titles found on the main screen" } + } + } + return tokens +} + +fun BaseTestCase.assertOrganizeTokensMatch(expectedTokens: List) { + step("Open 'Organize tokens' bottom-sheet") { + onMainScreen { clickDisplayedAddAndManageButton() } + onAddAndManageBottomSheet { organizeTokensButton.clickWithAssertion() } + } + step("Assert 'Organize tokens' list matches the main screen order") { + onOrganizeTokensScreen { + assertEquals(expectedTokens, getDisplayedTokenTitles()) + } + } + step("Return to 'Main' screen") { + onOrganizeTokensScreen { cancelButton.clickWithAssertion() } + } } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/MultiWalletScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/MultiWalletScenarios.kt index dc7275cb26..4507c6d1bb 100644 --- a/app/src/androidTest/kotlin/com/tangem/scenarios/MultiWalletScenarios.kt +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/MultiWalletScenarios.kt @@ -45,6 +45,21 @@ fun BaseTestCase.addNewCardWallet(mockContent: MockContent) { } } +fun BaseTestCase.addNewCardWalletWithoutSync(mockContent: MockContent) { + step("Click 'More' button on TopBar") { + onMainScreenTopBar { moreButton.clickWithAssertion() } + } + MockProvider.setMocks(mockContent) + step("Click on 'Add Wallet' button (scans a new hardware wallet)") { + onDetailsScreen { addWalletButton.clickWithAssertion() } + } + step("Assert 'Main' screen is displayed with the new wallet") { + composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_VERY_LONG) { + runCatching { onMainScreenTopBar { moreButton.assertIsDisplayed() } }.isSuccess + } + } +} + fun BaseTestCase.clickDisplayedTokenOnMain(tokenName: String) { step("Click on token '$tokenName' on the visible wallet") { onMainScreen { clickDisplayedToken(tokenName) } diff --git a/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt index 50e450451a..41de9330e3 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt @@ -1,8 +1,11 @@ package com.tangem.screens +import androidx.compose.ui.geometry.Rect import androidx.compose.ui.semantics.SemanticsProperties import androidx.compose.ui.test.* import com.tangem.common.BaseTestCase +import com.tangem.common.extensions.displayedTextsInVisualOrder +import com.tangem.common.extensions.firstTextOrNull import com.tangem.common.extensions.getQuantityString import com.tangem.common.extensions.hasLazyListItemPosition import com.tangem.common.utils.LazyListItemNode @@ -14,6 +17,7 @@ import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onCompose import io.github.kakaocup.compose.node.element.KNode import io.github.kakaocup.compose.node.element.lazylist.KLazyListNode import io.github.kakaocup.kakao.common.utilities.getResourceString +import kotlin.math.abs import androidx.compose.ui.test.hasTestTag as withTestTag import androidx.compose.ui.test.hasText as withText import com.tangem.core.res.R as CoreResR @@ -121,16 +125,114 @@ class MainScreenPageObject(private val semanticsProvider: SemanticsNodeInteracti error("Token '$tokenName' is not displayed on the current wallet page") } - // Adjacent pager pages stay mounted; swipe the wallet card that's actually on-screen. + /** + * Switches to the previous/next wallet in the pager. + * + * The pager only accepts horizontal swipes while the collapsing balance header is fully + * expanded and pinned to the top (`canPagerScroll = heightOffset == 0`). A *partially* collapsed + * header still shows the wallet card, yet the horizontal swipe is a silent no-op — so a visible + * card is not a reliable "ready to page" signal. We therefore try the swipe, and whenever the + * wallet identity doesn't change we expand the header and retry, so a no-op swipe can't pass + * unnoticed. Identity is title+balance rather than just the name: a still-restoring wallet (hot + * wallet) shows "Restoring…" instead of a title, but always shows a balance. + */ + @OptIn(ExperimentalTestApi::class) fun swipeToAdjacentWallet(toPrevious: Boolean) { - val nodes = semanticsProvider.onAllNodes(withTestTag(MainScreenTestTags.WALLET_LIST_ITEM)) - for (i in 0 until nodes.fetchSemanticsNodes().size) { - val swiped = runCatching { - nodes[i].assertIsDisplayed() - nodes[i].performTouchInput { if (toPrevious) swipeRight() else swipeLeft() } - }.isSuccess - if (swiped) return + val before = displayedWalletIdentity() + repeat(times = WALLET_SWITCH_ATTEMPTS) { + // Swipe first. The pager only pages while the header is pinned to the top; if it already + // is (the common case), a swipe-down here would instead trigger pull-to-refresh and + // un-pin it, breaking the horizontal swipe — so we only expand *after* a swipe that + // didn't page (i.e. the header was collapsed). + swipeCurrentPage(toPrevious) + val now = displayedWalletIdentity() + if (now != null && now != before) return + expandCollapsingHeader() } + error("Wallet did not switch from '$before' after $WALLET_SWITCH_ATTEMPTS attempts") + } + + /** Expands the collapsing balance header (pins the balance to the top) so the pager can page. */ + @OptIn(ExperimentalTestApi::class) + private fun expandCollapsingHeader() { + onScreenPageNode(withTestTag(MainScreenTestTags.SCREEN_CONTAINER))?.performTouchInput { + swipeDown(startY = visibleSize.height * 0.3f, endY = visibleSize.height * 0.8f) + } + } + + /** + * Pages to the previous/next wallet by swiping horizontally on the on-screen token list. + * The list is a vertical scroller, so it passes the horizontal drag up to the HorizontalPager + * instead of consuming it (a per-page balance card may consume horizontal gestures itself). + */ + @OptIn(ExperimentalTestApi::class) + private fun swipeCurrentPage(toPrevious: Boolean) { + onScreenPageNode(withTestTag(MainScreenTestTags.SCREEN_CONTAINER))?.performTouchInput { + if (toPrevious) swipeRight() else swipeLeft() + } + } + + /** + * A best-effort identity of the on-screen wallet: its title combined with its balance. Works in + * every sync state — a still-restoring wallet has no [MainScreenTestTags.CARD_TITLE] but always + * shows a [MainScreenTestTags.WALLET_BALANCE] — so the value reliably differs across a swap. + */ + private fun displayedWalletIdentity(): String? { + val page = onScreenPageRect() ?: return null + val title = firstTextInPage(page, MainScreenTestTags.CARD_TITLE) + val balance = firstTextInPage(page, MainScreenTestTags.WALLET_BALANCE) + return listOfNotNull(title, balance).joinToString(separator = "|").ifBlank { null } + } + + /** + * Horizontal bounds of the on-screen wallet page, taken from its token list. SCREEN_CONTAINER is + * reliably one-per-page and full-width (unlike WALLET_LIST_ITEM, which also tags a multi-page + * wrapper), so it is the trustworthy anchor for "which page is on-screen". + */ + private fun onScreenPageRect(): Rect? = + onScreenPageNode(withTestTag(MainScreenTestTags.SCREEN_CONTAINER))?.fetchSemanticsNode()?.boundsInRoot + + /** First text of the node tagged [tag] whose centre lies within the on-screen [page]. */ + private fun firstTextInPage(page: Rect, tag: String): String? { + val nodes = semanticsProvider.onAllNodes(withTestTag(tag), useUnmergedTree = true) + for (i in 0 until nodes.fetchSemanticsNodes().size) { + val text = runCatching { + val bounds = nodes[i].fetchSemanticsNode().boundsInRoot + // Skip zero-size nodes: off-screen pager pages collapse to bounds (0,0,0,0), whose + // centre (0,0) would otherwise pass the "centre within page" test (page.left is 0) + // and leak a stale wallet's text — the exact cause of undetected wallet switches. + if (bounds.width > 0f && bounds.height > 0f && + bounds.center.x >= page.left && bounds.center.x < page.right + ) { + nodes[i].fetchSemanticsNode().firstTextOrNull() + } else { + null + } + }.getOrNull() + if (text != null) return text + } + return null + } + + /** + * The full-width pager-page node matching [matcher] that is currently centred on-screen. + * + * The wallet pager keeps adjacent pages composed (beyondViewportPageCount=1) at ±pageWidth, so + * `onAllNodes` returns nodes from off-screen pages too. `assertIsDisplayed` is unreliable here + * (off-screen pages are placed, not clipped away), so we select by geometry: the on-screen page + * is the only full-width node whose left edge is within half a page of x=0. + */ + private fun onScreenPageNode(matcher: SemanticsMatcher): SemanticsNodeInteraction? { + val nodes = semanticsProvider.onAllNodes(matcher) + for (i in 0 until nodes.fetchSemanticsNodes().size) { + val node = nodes[i] + val onScreen = runCatching { + val bounds = node.fetchSemanticsNode().boundsInRoot + abs(bounds.left) < bounds.width / 2f + }.getOrDefault(false) + if (onScreen) return node + } + return null } val restoringProgressText: KNode = child { @@ -380,6 +482,51 @@ class MainScreenPageObject(private val semanticsProvider: SemanticsNodeInteracti useUnmergedTree = true } + /** + * Collapses the header, scrolls to and clicks the 'Add & manage' button on the wallet page + * that is actually on-screen. + * + * The wallet pager keeps the adjacent page composed (beyondViewportPageCount=1) at ±pageWidth, + * so both pages' MAIN_SCREEN_CONTAINER / button nodes are in the tree at once. We select the + * on-screen page by geometry (see [onScreenPageNode]) and click only the button within it. + */ + @OptIn(ExperimentalTestApi::class) + fun clickDisplayedAddAndManageButton() { + val container = onScreenPageNode(withTestTag(MainScreenTestTags.SCREEN_CONTAINER)) + ?: error("No on-screen wallet page found") + // Best-effort: collapse the header and scroll the button into view. The button is a footer + // outside the scrollable list, so performScrollToNode can throw — harmless when it's already + // visible, but it must not abort the click below. + runCatching { + container.performTouchInput { + swipeUp(startY = visibleSize.height * 0.6f, endY = visibleSize.height * 0.1f) + } + container.performScrollToNode(withTestTag(MainScreenTestTags.ADD_AND_MANAGE_BUTTON)) + } + + val page = container.fetchSemanticsNode().boundsInRoot + val buttons = semanticsProvider.onAllNodes( + withTestTag(MainScreenTestTags.ADD_AND_MANAGE_BUTTON), + useUnmergedTree = true, + ) + for (i in 0 until buttons.fetchSemanticsNodes().size) { + val clicked = runCatching { + val bounds = buttons[i].fetchSemanticsNode().boundsInRoot + // Skip zero-size (off-screen page) buttons and require the centre within the page. + if (bounds.width > 0f && bounds.height > 0f && + bounds.center.x >= page.left && bounds.center.x < page.right + ) { + buttons[i].performClick() + true + } else { + false + } + }.getOrDefault(false) + if (clicked) return + } + error("'Add & manage' button is not displayed on the current wallet page") + } + val searchThroughMarketPlaceholder: KNode = child { hasText(getResourceString(R.string.markets_search_title_placeholder)) useUnmergedTree = true @@ -485,6 +632,21 @@ class MainScreenPageObject(private val semanticsProvider: SemanticsNodeInteracti useUnmergedTree = true }.assertIsDisplayed() } + + /** + * Token titles displayed on the current wallet page, in visual order. Reads the + * [TokenElementsTestTags.TOKEN_TITLE] rows so network group headers (which share the + * TOKEN_LIST_ITEM tag) are excluded, keeping the result symmetric with the 'Organize tokens' reader. + */ + fun getDisplayedTokenTitles(): List = + semanticsProvider.onAllNodes( + withTestTag(TokenElementsTestTags.TOKEN_TITLE), + useUnmergedTree = true, + ).displayedTextsInVisualOrder() + + private companion object { + const val WALLET_SWITCH_ATTEMPTS = 4 + } } internal fun BaseTestCase.onMainScreen(function: MainScreenPageObject.() -> Unit) = diff --git a/app/src/androidTest/kotlin/com/tangem/screens/OrganizeTokensPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/OrganizeTokensPageObject.kt index 2ea02089e6..81d8df17dd 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/OrganizeTokensPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/OrganizeTokensPageObject.kt @@ -1,8 +1,12 @@ package com.tangem.screens +import androidx.compose.ui.geometry.lerp import androidx.compose.ui.test.SemanticsMatcher import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import androidx.compose.ui.test.hasAnyDescendant +import androidx.compose.ui.test.performTouchInput import com.tangem.common.BaseTestCase +import com.tangem.common.extensions.displayedTextsInVisualOrder import com.tangem.common.extensions.hasLazyListItemPosition import com.tangem.common.utils.LazyListItemNode import com.tangem.core.ui.test.OrganizeTokensScreenTestTags @@ -14,12 +18,13 @@ import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onCompose import io.github.kakaocup.compose.node.element.KNode import io.github.kakaocup.compose.node.element.lazylist.KLazyListNode import io.github.kakaocup.kakao.common.utilities.getResourceString +import androidx.compose.ui.test.hasAnyAncestor as withAnyAncestor import androidx.compose.ui.test.hasAnyChild as withAnyChild import androidx.compose.ui.test.hasAnySibling as withAnySibling import androidx.compose.ui.test.hasTestTag as withTestTag import androidx.compose.ui.test.hasText as withText -class OrganizeTokensPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : +class OrganizeTokensPageObject(private val semanticsProvider: SemanticsNodeInteractionsProvider) : ComposeScreen(semanticsProvider = semanticsProvider) { // region TopBar @@ -93,6 +98,22 @@ class OrganizeTokensPageObject(semanticsProvider: SemanticsNodeInteractionsProvi } } + /** + * Reads displayed token titles from the 'Organize tokens' screen in visual order + * (top-to-bottom, then left-to-right). Mirrors [MainScreenPageObject.getDisplayedTokenTitles] + * so the two lists can be compared directly. + * + * Titles are read from the [TokenElementsTestTags.TOKEN_TITLE] nodes rather than the outer + * TOKEN_LIST_ITEM: the item container here does not merge its descendants, so the title Text + * (nested inside the title Row) never surfaces on the item node's semantics. + */ + fun getDisplayedTokenTitles(): List = + semanticsProvider.onAllNodes( + withTestTag(TokenElementsTestTags.TOKEN_TITLE) and + withAnyAncestor(withTestTag(OrganizeTokensScreenTestTags.TOKENS_LAZY_LIST)), + useUnmergedTree = true, + ).displayedTextsInVisualOrder() + fun tokenDraggableButton(tokenTitle: String): KNode { return lazyList.child { hasTestTag(OrganizeTokensScreenTestTags.DRAGGABLE_IMAGE) @@ -108,6 +129,56 @@ class OrganizeTokensPageObject(semanticsProvider: SemanticsNodeInteractionsProvi ) } } + + /** + * Drag-and-drops the [source] token onto the [destination] token's slot to reorder the list, + * the Android analog of iOS `press(forDuration:thenDragTo:)`. + * + * The list is a reorderable LazyColumn (sh.calvin.reorderable) driven from each row's drag + * handle ([OrganizeTokensScreenTestTags.DRAGGABLE_IMAGE]). We synthesize the gesture manually: + * press down on the source handle, hold briefly, then move in small steps to the destination + * row's centre and lift — a single `swipe` won't engage the reorder detector. + * + * NOTE: reordering is only permitted **within the same network group** (`isValidDropTarget`), + * so both tokens must belong to the same group for the drop to take effect. + */ + fun dragToken(source: String, destination: String) { + val sourceHandle = semanticsProvider.onNode(dragHandleMatcher(source), useUnmergedTree = true) + val sourceNode = sourceHandle.fetchSemanticsNode() + val destinationNode = semanticsProvider + .onNode(tokenItemMatcher(destination), useUnmergedTree = true) + .fetchSemanticsNode() + + // performTouchInput coordinates are local to the source node; express both endpoints there. + val origin = sourceNode.positionInRoot + val start = sourceNode.boundsInRoot.center - origin + val end = destinationNode.boundsInRoot.center - origin + + sourceHandle.performTouchInput { + down(start) + advanceEventTime(DRAG_HOLD_MS) + repeat(times = DRAG_STEPS) { step -> + moveTo(lerp(start = start, stop = end, fraction = (step + 1).toFloat() / DRAG_STEPS)) + advanceEventTime(DRAG_STEP_MS) + } + up() + } + } + + // The drag handle sits under an untagged wrapper inside TOKEN_NON_FIAT_BLOCK, so anchor on the + // enclosing TOKEN_LIST_ITEM (one handle + one title per item) instead of the direct parent. + private fun dragHandleMatcher(tokenTitle: String): SemanticsMatcher = + withTestTag(OrganizeTokensScreenTestTags.DRAGGABLE_IMAGE) and + withAnyAncestor(tokenItemMatcher(tokenTitle)) + + private fun tokenItemMatcher(tokenTitle: String): SemanticsMatcher = + withTestTag(OrganizeTokensScreenTestTags.TOKEN_LIST_ITEM) and hasAnyDescendant(withText(tokenTitle)) + + private companion object { + const val DRAG_HOLD_MS = 200L + const val DRAG_STEPS = 16 + const val DRAG_STEP_MS = 16L + } } internal fun BaseTestCase.onOrganizeTokensScreen(function: OrganizeTokensPageObject.() -> Unit) = diff --git a/app/src/androidTest/kotlin/com/tangem/tests/main/OrganizeTokensTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/main/OrganizeTokensTest.kt new file mode 100644 index 0000000000..753d64202e --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/main/OrganizeTokensTest.kt @@ -0,0 +1,95 @@ +package com.tangem.tests.main + +import com.tangem.common.BaseTestCase +import com.tangem.common.extensions.clickWithAssertion +import com.tangem.common.utils.resetWireMockScenarioState +import com.tangem.common.utils.setWireMockScenarioState +import com.tangem.scenarios.addNewCardWalletWithoutSync +import com.tangem.scenarios.assertOrganizeTokensMatch +import com.tangem.scenarios.getMainScreenTokensOrder +import com.tangem.scenarios.openMainScreen +import com.tangem.scenarios.switchToPreviousWallet +import com.tangem.scenarios.synchronizeAddresses +import com.tangem.screens.onAddAndManageBottomSheet +import com.tangem.screens.onMainScreen +import com.tangem.screens.onOrganizeTokensScreen +import com.tangem.tap.domain.sdk.mocks.content.Wallet2MockContent +import dagger.hilt.android.testing.HiltAndroidTest +import io.qameta.allure.kotlin.AllureId +import io.qameta.allure.kotlin.junit4.DisplayName +import org.junit.Assert.assertEquals +import org.junit.Test + +@HiltAndroidTest +class OrganizeTokensTest : BaseTestCase() { + + @AllureId("71") + @DisplayName("Organize tokens: Correct tokens list displaying for current wallet") + @Test + fun organizeTokensCorrectTokensListDisplaying() { + val userTokensScenario = "user_tokens_api" + val userTokensState = "Wallet2MockTokensList" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(userTokensScenario) + } + ).run { + step("Set WireMock scenario: '$userTokensScenario' to state: '$userTokensState'") { + setWireMockScenarioState(userTokensScenario, userTokensState) + } + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Add a second card wallet") { + addNewCardWalletWithoutSync(Wallet2MockContent) + } + step("Switch to first wallet Main screen") { + switchToPreviousWallet() + } + + val firstWalletTokens = getMainScreenTokensOrder() + assertOrganizeTokensMatch(firstWalletTokens) + + step("Switch to second wallet Main screen") { + onMainScreen { swipeToAdjacentWallet(toPrevious = false) } + } + + val secondWalletTokens = getMainScreenTokensOrder() + assertOrganizeTokensMatch(secondWalletTokens) + } + } + + @AllureId("2753") + @DisplayName("Organize tokens: Tokens order changing") + @Test + fun organizeTokensOrderChanging() { + setupHooks().run{ + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Open 'Organize tokens' bottom-sheet") { + onMainScreen { clickDisplayedAddAndManageButton() } + onAddAndManageBottomSheet { organizeTokensButton.clickWithAssertion() } + } + step("Drag the 3rd token onto the 2nd position and assert the new order") { + onOrganizeTokensScreen { + val sourceIndex = 2 + val destinationIndex = 1 + val before = getDisplayedTokenTitles() + dragToken(source = before[sourceIndex], destination = before[destinationIndex]) + val expected = before.toMutableList().apply { + add(destinationIndex, removeAt(sourceIndex)) + } + assertEquals(expected, getDisplayedTokenTitles()) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/main/TokenListTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/main/TokenListTest.kt index b479c2aac6..8422b5cc66 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/main/TokenListTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/main/TokenListTest.kt @@ -4,12 +4,17 @@ import com.tangem.common.BaseTestCase import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO import com.tangem.common.utils.resetWireMockScenarioState import com.tangem.common.utils.setWireMockScenarioState +import com.tangem.scenarios.addNewCardWalletWithoutSync +import com.tangem.scenarios.getMainScreenTokensOrder import com.tangem.scenarios.openMainScreen import com.tangem.scenarios.synchronizeAddresses import com.tangem.screens.onMainScreen +import com.tangem.tap.domain.sdk.mocks.content.Wallet2MockContent import dagger.hilt.android.testing.HiltAndroidTest import io.qameta.allure.kotlin.AllureId import io.qameta.allure.kotlin.junit4.DisplayName +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertTrue import org.junit.Test @HiltAndroidTest @@ -48,4 +53,55 @@ class TokenListTest : BaseTestCase() { } } + @AllureId("177") + @DisplayName("Main: token list differs after switching to a second wallet") + @Test + fun tokenListChangedAfterSwitchingWalletTest() { + val userTokensScenario = "user_tokens_api" + val userTokensState = "ReducedTokens" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(userTokensScenario) + } + ).run { + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + + val firstWalletTokens = getMainScreenTokensOrder() + + step("Set WireMock scenario: '$userTokensScenario' to state: '$userTokensState'") { + setWireMockScenarioState(userTokensScenario, userTokensState) + } + step("Add a second card wallet") { + addNewCardWalletWithoutSync(Wallet2MockContent) + } + + val secondWalletTokens = getMainScreenTokensOrder() + + step("Assert both wallets exposed a non-empty token list") { + assertTrue( + "First wallet token list should not be empty", + firstWalletTokens.isNotEmpty(), + ) + assertTrue( + "Second wallet token list should not be empty", + secondWalletTokens.isNotEmpty(), + ) + } + step("Assert the two wallets show different token lists") { + assertNotEquals( + "Expected the second wallet's token list to differ from the first " + + "(first=$firstWalletTokens, second=$secondWalletTokens)", + firstWalletTokens, + secondWalletTokens, + ) + } + } + } + } \ No newline at end of file