Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-14 12:12:44 +03:00
parent 39d4997aa7
commit e0feda9a00
7 changed files with 471 additions and 9 deletions

View file

@ -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<String> {
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 }
}

View file

@ -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<String> {
var tokens: List<String> = 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<String>) {
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() }
}
}

View file

@ -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) }

View file

@ -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<String> =
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) =

View file

@ -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<OrganizeTokensPageObject>(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<String> =
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) =

View file

@ -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())
}
}
}
}
}

View file

@ -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,
)
}
}
}
}