Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-29 14:47:35 +02:00
parent de41c92d35
commit 392234221b
4 changed files with 46 additions and 55 deletions

View file

@ -1,47 +1,15 @@
package com.tangem.scenarios
import androidx.compose.ui.semantics.SemanticsActions
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.hasAnyDescendant
import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performSemanticsAction
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.swipeLeft
import androidx.compose.ui.test.swipeRight
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_VERY_LONG
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.extensions.isDisplayedSafely
import com.tangem.core.ui.test.BuyTokenScreenTestTags
import com.tangem.core.ui.test.DetailsScreenTestTags
import com.tangem.core.ui.test.MainScreenTestTags
import com.tangem.screens.*
import com.tangem.tap.domain.sdk.mocks.MockContent
import com.tangem.tap.domain.sdk.mocks.MockProvider
import io.qameta.allure.kotlin.Allure.step
// Custom/nested clickables (Add-Wallet row, wallet tab) reject touch injection — drive OnClick directly.
private fun BaseTestCase.clickViaSemantics(matcher: SemanticsMatcher, useUnmergedTree: Boolean = false) {
composeTestRule.onNode(matcher, useUnmergedTree).performSemanticsAction(SemanticsActions.OnClick)
}
// Both pager pages stay mounted; the off-screen one ignores swipes, so always swipe the on-screen wallet card.
private fun BaseTestCase.swipeDisplayedWallet(toPrevious: Boolean) {
val nodes = composeTestRule.onAllNodes(hasTestTag(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) break
}
waitForIdle()
}
/** 'Add Wallet' scans a card immediately (no type chooser), so [mockContent] must be set before the click. */
fun BaseTestCase.addNewCardWallet(mockContent: MockContent) {
step("Click 'More' button on TopBar") {
@ -49,7 +17,7 @@ fun BaseTestCase.addNewCardWallet(mockContent: MockContent) {
}
MockProvider.setMocks(mockContent)
step("Click on 'Add Wallet' button (scans a new hardware wallet)") {
clickViaSemantics(hasTestTag(DetailsScreenTestTags.ADD_WALLET_BUTTON))
onDetailsScreen { addWalletButton.clickWithAssertion() }
}
// Gate on the top-bar More button, not the container — the bottom Markets sheet can leave the container un-"displayed".
step("Assert 'Main' screen is displayed with the new wallet") {
@ -62,7 +30,7 @@ fun BaseTestCase.addNewCardWallet(mockContent: MockContent) {
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) {
var shown = false
onMainScreen { shown = synchronizeAddressesButton.isDisplayedSafely() }
if (!shown) swipeDisplayedWallet(toPrevious = false)
if (!shown) onMainScreen { swipeToAdjacentWallet(toPrevious = false) }
shown
}
// Let the pager fling settle — a click mid-animation is eaten by the button's clickableSingle debounce.
@ -77,25 +45,15 @@ fun BaseTestCase.addNewCardWallet(mockContent: MockContent) {
}
}
/** Both pager pages mount the same token; clicks the [tokenName] copy currently on-screen. */
fun BaseTestCase.clickDisplayedTokenOnMain(tokenName: String) {
val matcher = hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM) and hasAnyDescendant(hasText(tokenName))
step("Click on token '$tokenName' on the visible wallet") {
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) {
val nodes = composeTestRule.onAllNodes(matcher, useUnmergedTree = true)
(0 until nodes.fetchSemanticsNodes().size).any { i ->
runCatching {
nodes[i].assertIsDisplayed()
nodes[i].performClick()
}.isSuccess
}
}
onMainScreen { clickDisplayedToken(tokenName) }
}
}
fun BaseTestCase.switchToPreviousWallet() {
step("Swipe wallet card to the previous wallet") {
swipeDisplayedWallet(toPrevious = true)
onMainScreen { swipeToAdjacentWallet(toPrevious = true) }
}
}
@ -104,18 +62,11 @@ fun BaseTestCase.selectReceiveTokenOnWallet(token: String, walletName: String) {
step("Click on 'Choose token' button") {
onSwapTokenScreen { chooseTokenButton.performClick() }
}
// The wallet tab is a custom clickable Row that Kakao's performClick can't hit (autoscroll fails); invoke OnClick directly.
step("Select wallet tab '$walletName'") {
clickViaSemantics(
hasTestTag(BuyTokenScreenTestTags.WALLET_TAB) and hasAnyDescendant(hasText(walletName)),
useUnmergedTree = true,
)
onBuyTokenScreen { walletTab(walletName).performClick() }
}
step("Click on token with name '$token'") {
clickViaSemantics(
hasTestTag(BuyTokenScreenTestTags.LAZY_LIST_ITEM) and hasAnyDescendant(hasText(token)),
useUnmergedTree = true,
)
onBuyTokenScreen { tokenWithTitle(token).performClick() }
}
}

View file

@ -15,6 +15,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 androidx.compose.ui.test.hasText as withText
class BuyTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<BuyTokenPageObject>(semanticsProvider = semanticsProvider) {
@ -48,6 +49,19 @@ class BuyTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
useUnmergedTree = true
}
}
fun walletTab(walletName: String): KNode = child {
hasTestTag(BuyTokenScreenTestTags.WALLET_TAB)
hasAnyDescendant(withText(walletName))
useUnmergedTree = true
}
@OptIn(ExperimentalTestApi::class)
fun tokenWithTitle(tokenTitle: String): LazyListItemNode = lazyList.childWith<LazyListItemNode> {
hasTestTag(BuyTokenScreenTestTags.LAZY_LIST_ITEM)
hasText(tokenTitle)
useUnmergedTree = true
}
}
internal fun BaseTestCase.onBuyTokenScreen(function: BuyTokenPageObject.() -> Unit) =

View file

@ -28,6 +28,10 @@ class DetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
useUnmergedTree = true
}
val addWalletButton: KNode = child {
hasTestTag(DetailsScreenTestTags.ADD_WALLET_BUTTON)
}
val buyTangemButton: KNode = child {
hasTestTag(DetailsScreenTestTags.SCREEN_ITEM)
hasText(getResourceString(R.string.details_buy_wallet))

View file

@ -111,6 +111,28 @@ class MainScreenPageObject(private val semanticsProvider: SemanticsNodeInteracti
)
}
// Wallet pager keeps the adjacent page composed (beyondViewportPageCount=1), so the token is mounted on two pages — click the displayed copy.
fun clickDisplayedToken(tokenName: String) {
val matcher = withTestTag(MainScreenTestTags.TOKEN_LIST_ITEM) and hasAnyDescendant(withText(tokenName))
val nodes = semanticsProvider.onAllNodes(matcher, useUnmergedTree = true)
for (i in 0 until nodes.fetchSemanticsNodes().size) {
if (runCatching { nodes[i].assertIsDisplayed(); nodes[i].performClick() }.isSuccess) return
}
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.
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 restoringProgressText: KNode = child {
hasTestTag(MainScreenTestTags.SYNC_PROGRESS_TEXT)
useUnmergedTree = true