Updated on 2026-08-14
This commit is contained in:
parent
39d4997aa7
commit
e0feda9a00
7 changed files with 471 additions and 9 deletions
|
|
@ -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 }
|
||||
}
|
||||
|
|
@ -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() }
|
||||
}
|
||||
}
|
||||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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) =
|
||||
|
|
|
|||
|
|
@ -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) =
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue