Updated on 2026-08-14
This commit is contained in:
commit
df1ddfaa36
7 changed files with 378 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?.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
|
package com.tangem.scenarios
|
||||||
|
|
||||||
import com.tangem.common.BaseTestCase
|
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.SwipeDirection
|
||||||
import com.tangem.common.extensions.clickWithAssertion
|
import com.tangem.common.extensions.clickWithAssertion
|
||||||
import com.tangem.common.extensions.swipeVertical
|
import com.tangem.common.extensions.swipeVertical
|
||||||
import com.tangem.screens.onAddAndManageBottomSheet
|
import com.tangem.screens.onAddAndManageBottomSheet
|
||||||
import com.tangem.screens.onMainScreen
|
import com.tangem.screens.onMainScreen
|
||||||
|
import com.tangem.screens.onOrganizeTokensScreen
|
||||||
import io.qameta.allure.kotlin.Allure.step
|
import io.qameta.allure.kotlin.Allure.step
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
|
||||||
fun BaseTestCase.openOrganizeTokensScreen() {
|
fun BaseTestCase.openOrganizeTokensScreen() {
|
||||||
step("Swipe to 'Add & Manage' button") {
|
step("Swipe to 'Add & Manage' button") {
|
||||||
|
|
@ -19,4 +22,30 @@ fun BaseTestCase.openOrganizeTokensScreen() {
|
||||||
step("Click on 'Organize tokens' button in bottom sheet") {
|
step("Click on 'Organize tokens' button in bottom sheet") {
|
||||||
onAddAndManageBottomSheet { organizeTokensButton.clickWithAssertion() }
|
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) {
|
fun BaseTestCase.clickDisplayedTokenOnMain(tokenName: String) {
|
||||||
step("Click on token '$tokenName' on the visible wallet") {
|
step("Click on token '$tokenName' on the visible wallet") {
|
||||||
onMainScreen { clickDisplayedToken(tokenName) }
|
onMainScreen { clickDisplayedToken(tokenName) }
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
package com.tangem.screens
|
package com.tangem.screens
|
||||||
|
|
||||||
|
import androidx.compose.ui.geometry.Rect
|
||||||
import androidx.compose.ui.semantics.SemanticsProperties
|
import androidx.compose.ui.semantics.SemanticsProperties
|
||||||
import androidx.compose.ui.test.*
|
import androidx.compose.ui.test.*
|
||||||
import com.tangem.common.BaseTestCase
|
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.getQuantityString
|
||||||
import com.tangem.common.extensions.hasLazyListItemPosition
|
import com.tangem.common.extensions.hasLazyListItemPosition
|
||||||
import com.tangem.common.utils.LazyListItemNode
|
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.KNode
|
||||||
import io.github.kakaocup.compose.node.element.lazylist.KLazyListNode
|
import io.github.kakaocup.compose.node.element.lazylist.KLazyListNode
|
||||||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
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.hasTestTag as withTestTag
|
||||||
import androidx.compose.ui.test.hasText as withText
|
import androidx.compose.ui.test.hasText as withText
|
||||||
import com.tangem.core.res.R as CoreResR
|
import com.tangem.core.res.R as CoreResR
|
||||||
|
|
@ -121,16 +125,75 @@ class MainScreenPageObject(private val semanticsProvider: SemanticsNodeInteracti
|
||||||
error("Token '$tokenName' is not displayed on the current wallet page")
|
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. A horizontal swipe is a silent no-op unless
|
||||||
|
* the collapsing balance header is fully expanded and pinned to the top, so we retry: swipe, and
|
||||||
|
* whenever the wallet identity doesn't change, expand the header and try again.
|
||||||
|
*/
|
||||||
fun swipeToAdjacentWallet(toPrevious: Boolean) {
|
fun swipeToAdjacentWallet(toPrevious: Boolean) {
|
||||||
val nodes = semanticsProvider.onAllNodes(withTestTag(MainScreenTestTags.WALLET_LIST_ITEM))
|
val before = displayedWalletIdentity()
|
||||||
for (i in 0 until nodes.fetchSemanticsNodes().size) {
|
repeat(times = WALLET_SWITCH_ATTEMPTS) {
|
||||||
val swiped = runCatching {
|
swipeCurrentPage(toPrevious)
|
||||||
nodes[i].assertIsDisplayed()
|
val now = displayedWalletIdentity()
|
||||||
nodes[i].performTouchInput { if (toPrevious) swipeRight() else swipeLeft() }
|
if (now != null && now != before) return
|
||||||
}.isSuccess
|
expandCollapsingHeader()
|
||||||
if (swiped) return
|
|
||||||
}
|
}
|
||||||
|
error("Wallet did not switch from '$before' after $WALLET_SWITCH_ATTEMPTS attempts")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expand only *after* a swipe that didn't page: if the header is already pinned, a swipe-down here
|
||||||
|
// would trigger pull-to-refresh and un-pin it, breaking the horizontal swipe.
|
||||||
|
private fun expandCollapsingHeader() {
|
||||||
|
onScreenPage()?.performTouchInput {
|
||||||
|
swipeDown(startY = visibleSize.height * 0.3f, endY = visibleSize.height * 0.8f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun swipeCurrentPage(toPrevious: Boolean) {
|
||||||
|
onScreenPage()?.performTouchInput { if (toPrevious) swipeRight() else swipeLeft() }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Identity = title + balance: a still-restoring wallet has no CARD_TITLE but always a WALLET_BALANCE.
|
||||||
|
private fun displayedWalletIdentity(): String? {
|
||||||
|
val title = onScreenPageChild(withTestTag(MainScreenTestTags.CARD_TITLE))?.firstText()
|
||||||
|
val balance = onScreenPageChild(withTestTag(MainScreenTestTags.WALLET_BALANCE))?.firstText()
|
||||||
|
return listOfNotNull(title, balance).joinToString(separator = "|").ifBlank { null }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The full-width pager-page container currently on-screen. The pager keeps adjacent pages composed
|
||||||
|
* off-screen at ±pageWidth (so [assertIsDisplayed] can't tell them apart), hence selection by
|
||||||
|
* geometry: the on-screen page is the only one whose left edge is within half a page of x=0.
|
||||||
|
*/
|
||||||
|
private fun onScreenPage(): SemanticsNodeInteraction? =
|
||||||
|
firstNodeMatching(withTestTag(MainScreenTestTags.SCREEN_CONTAINER), useUnmergedTree = false) {
|
||||||
|
abs(it.left) < it.width / 2f
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A node matching [matcher] whose centre lies within the on-screen page (skips zero-size off-screen copies). */
|
||||||
|
private fun onScreenPageChild(matcher: SemanticsMatcher): SemanticsNodeInteraction? {
|
||||||
|
val page = onScreenPage()?.fetchSemanticsNode()?.boundsInRoot ?: return null
|
||||||
|
return firstNodeMatching(matcher) {
|
||||||
|
it.width > 0f && it.height > 0f && it.center.x >= page.left && it.center.x < page.right
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun SemanticsNodeInteraction.firstText(): String? = fetchSemanticsNode().firstTextOrNull()
|
||||||
|
|
||||||
|
// Single geometry primitive behind the pager helpers: the first node matching [matcher] whose
|
||||||
|
// bounds satisfy [predicate]. Replaces the per-caller onAllNodes(...)[i] loops.
|
||||||
|
private fun firstNodeMatching(
|
||||||
|
matcher: SemanticsMatcher,
|
||||||
|
useUnmergedTree: Boolean = true,
|
||||||
|
predicate: (Rect) -> Boolean,
|
||||||
|
): SemanticsNodeInteraction? {
|
||||||
|
val nodes = semanticsProvider.onAllNodes(matcher, useUnmergedTree = useUnmergedTree)
|
||||||
|
repeat(times = nodes.fetchSemanticsNodes().size) { index ->
|
||||||
|
val node = nodes[index]
|
||||||
|
val matches = runCatching { predicate(node.fetchSemanticsNode().boundsInRoot) }.getOrDefault(false)
|
||||||
|
if (matches) return node
|
||||||
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
val restoringProgressText: KNode = child {
|
val restoringProgressText: KNode = child {
|
||||||
|
|
@ -380,6 +443,23 @@ class MainScreenPageObject(private val semanticsProvider: SemanticsNodeInteracti
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Collapses the header, scrolls to and clicks the 'Add & manage' button on the on-screen wallet page. */
|
||||||
|
@OptIn(ExperimentalTestApi::class)
|
||||||
|
fun clickDisplayedAddAndManageButton() {
|
||||||
|
val container = onScreenPage() ?: error("No on-screen wallet page found")
|
||||||
|
// Best-effort: the button is a footer outside the scrollable list, so performScrollToNode can
|
||||||
|
// throw when it's already visible — that 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 button = onScreenPageChild(withTestTag(MainScreenTestTags.ADD_AND_MANAGE_BUTTON))
|
||||||
|
?: error("'Add & manage' button is not displayed on the current wallet page")
|
||||||
|
button.performClick()
|
||||||
|
}
|
||||||
|
|
||||||
val searchThroughMarketPlaceholder: KNode = child {
|
val searchThroughMarketPlaceholder: KNode = child {
|
||||||
hasText(getResourceString(R.string.markets_search_title_placeholder))
|
hasText(getResourceString(R.string.markets_search_title_placeholder))
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
|
|
@ -485,6 +565,21 @@ class MainScreenPageObject(private val semanticsProvider: SemanticsNodeInteracti
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}.assertIsDisplayed()
|
}.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) =
|
internal fun BaseTestCase.onMainScreen(function: MainScreenPageObject.() -> Unit) =
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
package com.tangem.screens
|
package com.tangem.screens
|
||||||
|
|
||||||
|
import androidx.compose.ui.geometry.lerp
|
||||||
import androidx.compose.ui.test.SemanticsMatcher
|
import androidx.compose.ui.test.SemanticsMatcher
|
||||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
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.BaseTestCase
|
||||||
|
import com.tangem.common.extensions.displayedTextsInVisualOrder
|
||||||
import com.tangem.common.extensions.hasLazyListItemPosition
|
import com.tangem.common.extensions.hasLazyListItemPosition
|
||||||
import com.tangem.common.utils.LazyListItemNode
|
import com.tangem.common.utils.LazyListItemNode
|
||||||
import com.tangem.core.ui.test.OrganizeTokensScreenTestTags
|
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.KNode
|
||||||
import io.github.kakaocup.compose.node.element.lazylist.KLazyListNode
|
import io.github.kakaocup.compose.node.element.lazylist.KLazyListNode
|
||||||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
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.hasAnyChild as withAnyChild
|
||||||
import androidx.compose.ui.test.hasAnySibling as withAnySibling
|
import androidx.compose.ui.test.hasAnySibling as withAnySibling
|
||||||
import androidx.compose.ui.test.hasTestTag as withTestTag
|
import androidx.compose.ui.test.hasTestTag as withTestTag
|
||||||
import androidx.compose.ui.test.hasText as withText
|
import androidx.compose.ui.test.hasText as withText
|
||||||
|
|
||||||
class OrganizeTokensPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
class OrganizeTokensPageObject(private val semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
ComposeScreen<OrganizeTokensPageObject>(semanticsProvider = semanticsProvider) {
|
ComposeScreen<OrganizeTokensPageObject>(semanticsProvider = semanticsProvider) {
|
||||||
|
|
||||||
// region TopBar
|
// region TopBar
|
||||||
|
|
@ -93,6 +98,15 @@ class OrganizeTokensPageObject(semanticsProvider: SemanticsNodeInteractionsProvi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read TOKEN_TITLE nodes, not the outer TOKEN_LIST_ITEM: the item container here doesn't merge its
|
||||||
|
// descendants, so the nested title Text 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 {
|
fun tokenDraggableButton(tokenTitle: String): KNode {
|
||||||
return lazyList.child {
|
return lazyList.child {
|
||||||
hasTestTag(OrganizeTokensScreenTestTags.DRAGGABLE_IMAGE)
|
hasTestTag(OrganizeTokensScreenTestTags.DRAGGABLE_IMAGE)
|
||||||
|
|
@ -108,6 +122,47 @@ class OrganizeTokensPageObject(semanticsProvider: SemanticsNodeInteractionsProvi
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reorder by synthesizing a drag on the source row's handle: down, hold, step to the destination
|
||||||
|
// centre, lift — a single `swipe` won't engage the reorder detector. Only valid within one network
|
||||||
|
// group (isValidDropTarget), so source and destination must share a group.
|
||||||
|
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) =
|
internal fun BaseTestCase.onOrganizeTokensScreen(function: OrganizeTokensPageObject.() -> Unit) =
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
package com.tangem.tests.main
|
||||||
|
|
||||||
|
import com.tangem.common.BaseTestCase
|
||||||
|
import com.tangem.common.extensions.clickWithAssertion
|
||||||
|
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() {
|
||||||
|
setupHooks().run {
|
||||||
|
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()
|
||||||
|
require(before.size > sourceIndex && before.size > destinationIndex) {
|
||||||
|
"Expected at least ${maxOf(sourceIndex, destinationIndex) + 1} tokens to reorder, but got ${before.size}: $before"
|
||||||
|
}
|
||||||
|
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.constants.TestConstants.USER_TOKENS_API_SCENARIO
|
||||||
import com.tangem.common.utils.resetWireMockScenarioState
|
import com.tangem.common.utils.resetWireMockScenarioState
|
||||||
import com.tangem.common.utils.setWireMockScenarioState
|
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.openMainScreen
|
||||||
import com.tangem.scenarios.synchronizeAddresses
|
import com.tangem.scenarios.synchronizeAddresses
|
||||||
import com.tangem.screens.onMainScreen
|
import com.tangem.screens.onMainScreen
|
||||||
|
import com.tangem.tap.domain.sdk.mocks.content.Wallet2MockContent
|
||||||
import dagger.hilt.android.testing.HiltAndroidTest
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
import io.qameta.allure.kotlin.AllureId
|
import io.qameta.allure.kotlin.AllureId
|
||||||
import io.qameta.allure.kotlin.junit4.DisplayName
|
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||||
|
import org.junit.Assert.assertNotEquals
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
@HiltAndroidTest
|
@HiltAndroidTest
|
||||||
|
|
@ -48,4 +53,54 @@ class TokenListTest : BaseTestCase() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AllureId("177")
|
||||||
|
@DisplayName("Main: token list differs after switching to a second wallet")
|
||||||
|
@Test
|
||||||
|
fun tokenListChangedAfterSwitchingWalletTest() {
|
||||||
|
val userTokensState = "ReducedTokens"
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
}
|
||||||
|
).run {
|
||||||
|
step("Open 'Main Screen'") {
|
||||||
|
openMainScreen()
|
||||||
|
}
|
||||||
|
step("Synchronize addresses") {
|
||||||
|
synchronizeAddresses()
|
||||||
|
}
|
||||||
|
|
||||||
|
val firstWalletTokens = getMainScreenTokensOrder()
|
||||||
|
|
||||||
|
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$userTokensState'") {
|
||||||
|
setWireMockScenarioState(USER_TOKENS_API_SCENARIO, 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