Updated on 2026-08-14

This commit is contained in:
Tangem 2025-07-22 11:54:49 +03:00
parent b0b6081040
commit e40706d5c6
11 changed files with 428 additions and 24 deletions

View file

@ -0,0 +1,9 @@
package com.tangem.common.extensions
import androidx.compose.ui.test.SemanticsMatcher
import com.tangem.core.ui.utils.LazyListItemPositionSemantics
import io.github.kakaocup.compose.node.builder.ViewBuilder
fun ViewBuilder.hasLazyListItemPosition(position: Int) = apply {
addSemanticsMatcher(SemanticsMatcher.expectValue(LazyListItemPositionSemantics, position))
}

View file

@ -4,7 +4,9 @@ import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.common.extensions.hasLazyListItemPosition
import com.tangem.common.utils.LazyListItemNode
import com.tangem.core.ui.test.TokenElementsTestTags
import com.tangem.core.ui.test.MainScreenTestTags
import com.tangem.core.ui.utils.LazyListItemPositionSemantics
import com.tangem.feature.wallet.impl.R
@ -14,6 +16,7 @@ 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.hasTestTag as withTestTag
import androidx.compose.ui.test.hasText as withText
class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<MainScreenPageObject>(semanticsProvider = semanticsProvider) {
@ -45,7 +48,7 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
hasText(tokenTitle)
}.child<KNode> {
hasTestTag(MainScreenTestTags.TOKEN_FIAT_AMOUNT)
hasTestTag(TokenElementsTestTags.TOKEN_FIAT_AMOUNT)
useUnmergedTree = true
}
}
@ -63,6 +66,37 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
}
}
@OptIn(ExperimentalTestApi::class)
fun organizeTokensButton(): KNode {
return lazyList.childWith<LazyListItemNode> {
hasTestTag(MainScreenTestTags.ORGANIZE_TOKENS_BUTTON)
}.child<KNode> {
hasText(getResourceString(R.string.organize_tokens_title))
useUnmergedTree = true
}
}
fun tokenNetworkGroupTitle(tokenNetwork: String): KNode {
return lazyList.child {
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
hasAnyChild(withText(tokenNetwork))
useUnmergedTree = true
}
}
@OptIn(ExperimentalTestApi::class)
fun tokenWithTitleAndPosition(tokenTitle: String, index: Int): KNode {
return lazyList.childWith<LazyListItemNode> {
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
hasText(tokenTitle)
hasLazyListItemPosition(index)
}.child<KNode> {
hasTestTag(TokenElementsTestTags.TOKEN_TITLE)
useUnmergedTree = true
}
}
/**
* This assertion is required to properly verify the token's absence in the semantic tree.
* Tests will fail if assertIsNotDisplayed() or assertDoesNotExist() are used instead.

View file

@ -0,0 +1,113 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.common.extensions.hasLazyListItemPosition
import com.tangem.common.utils.LazyListItemNode
import com.tangem.core.ui.test.TokenElementsTestTags
import com.tangem.core.ui.test.OrganizeTokensScreenTestTags
import com.tangem.core.ui.utils.LazyListItemPositionSemantics
import com.tangem.feature.wallet.impl.R
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
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
import androidx.compose.ui.test.hasTestTag as withTestTag
import androidx.compose.ui.test.hasAnySibling as withAnySibling
import androidx.compose.ui.test.hasAnyChild as withAnyChild
class OrganizeTokensPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<OrganizeTokensPageObject>(semanticsProvider = semanticsProvider) {
// region TopBar
val title: KNode = child {
hasText(getResourceString(R.string.organize_tokens_title))
}
private val topBarGroupButton: KNode = child {
hasTestTag(OrganizeTokensScreenTestTags.GROUP_BUTTON)
}
val groupButton: KNode = topBarGroupButton.child {
hasText(getResourceString(R.string.organize_tokens_group))
useUnmergedTree = true
}
val ungroupButton: KNode = topBarGroupButton.child {
hasText(getResourceString(R.string.organize_tokens_ungroup))
useUnmergedTree = true
}
val sortByBalanceButton: KNode = child {
hasTestTag(OrganizeTokensScreenTestTags.SORT_BY_BALANCE_BUTTON)
useUnmergedTree = true
}
// endregion TopBar
private val lazyList = KLazyListNode(
semanticsProvider = semanticsProvider,
viewBuilderAction = { hasTestTag(OrganizeTokensScreenTestTags.TOKENS_LAZY_LIST) },
itemTypeBuilder = {
itemType(::LazyListItemNode)
},
positionMatcher = { position ->
SemanticsMatcher.expectValue(
LazyListItemPositionSemantics,
position
)
}
)
val applyButton: KNode = child {
hasTestTag(OrganizeTokensScreenTestTags.APPLY_BUTTON)
useUnmergedTree = true
}
val cancelButton: KNode = child {
hasTestTag(OrganizeTokensScreenTestTags.CANCEL_BUTTON)
useUnmergedTree = true
}
fun tokenWithTitle(tokenTitle: String): KNode {
return lazyList.child {
hasTestTag(OrganizeTokensScreenTestTags.TOKEN_LIST_ITEM)
hasAnyDescendant(withText(tokenTitle))
useUnmergedTree = true
}
}
fun tokenNetworkGroupTitle(tokenNetwork: String): KNode {
return lazyList.child {
hasTestTag(OrganizeTokensScreenTestTags.GROUP_TITLE_ITEM)
hasAnyChild(withText(tokenNetwork))
useUnmergedTree = true
}
}
fun tokenWithTitleAndPosition(tokenTitle: String, index: Int): KNode {
return lazyList.child {
hasLazyListItemPosition(index)
hasTestTag(OrganizeTokensScreenTestTags.TOKEN_LIST_ITEM)
hasAnyDescendant(withText(tokenTitle))
useUnmergedTree = true
}
}
fun tokenDraggableButton(tokenTitle: String): KNode {
return lazyList.child {
hasTestTag(OrganizeTokensScreenTestTags.DRAGGABLE_IMAGE)
useUnmergedTree = true
hasParent(withTestTag(TokenElementsTestTags.TOKEN_NON_FIAT_BLOCK)
.and(withAnySibling(withTestTag(TokenElementsTestTags.TOKEN_TITLE)
.and(withAnyChild(withText(tokenTitle))))
)
)
}
}
}
internal fun BaseTestCase.onOrganizeTokensScreen(function: OrganizeTokensPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,206 @@
package com.tangem.tests
import androidx.compose.ui.test.onAllNodesWithText
import com.tangem.common.BaseTestCase
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.scenarios.OpenMainScreenScenario
import com.tangem.screens.onMainScreen
import com.tangem.screens.onOrganizeTokensScreen
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Test
@HiltAndroidTest
class OrganizeTokensTest : BaseTestCase() {
@AllureId("2755")
@DisplayName("Organize tokens: group tokens")
@Test
fun groupTokensTest() {
setupHooks().run {
val tokenTitle = "Ethereum"
val tokenNetwork = "Ethereum network"
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
}
step("Click on 'Synchronize addresses' button" ) {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Click 'Organize tokens' button") {
onMainScreen { organizeTokensButton().clickWithAssertion() }
}
step("Assert 'Organize tokens' screen is opened") {
onOrganizeTokensScreen {
title.assertIsDisplayed()
tokenWithTitle(tokenTitle).assertIsDisplayed()
}
}
step("Click 'Group' button") {
onOrganizeTokensScreen { groupButton.clickWithAssertion() }
}
step("Assert tokens were grouped on 'Organize tokens' screen") {
onOrganizeTokensScreen { tokenNetworkGroupTitle(tokenNetwork).assertIsDisplayed() }
}
step("Click 'Apply' button") {
onOrganizeTokensScreen { applyButton.clickWithAssertion() }
}
step("Assert tokens were grouped on 'Main screen'") {
onMainScreen { tokenNetworkGroupTitle(tokenNetwork).assertIsDisplayed() }
}
step("Click 'Organize tokens' button") {
onMainScreen { organizeTokensButton().clickWithAssertion() }
}
step("Assert 'Organize tokens' screen is opened") {
onOrganizeTokensScreen {
title.assertIsDisplayed()
tokenWithTitle(tokenTitle).assertIsDisplayed()
}
}
step("Click 'Ungroup' button") {
onOrganizeTokensScreen { ungroupButton.clickWithAssertion() }
}
step("Assert tokens were ungrouped on 'Organize tokens' screen") {
onOrganizeTokensScreen { tokenNetworkGroupTitle(tokenNetwork).assertIsNotDisplayed() }
}
step("Click 'Apply' button") {
onOrganizeTokensScreen { applyButton.clickWithAssertion() }
}
step("Assert tokens were ungrouped on 'Main screen'") {
onMainScreen { tokenNetworkGroupTitle(tokenNetwork).assertIsNotDisplayed() }
}
}
}
@AllureId("2752")
@DisplayName("Organize tokens: check position of tokens")
@Test
fun checkPositionOfTokensTest() {
setupHooks().run {
val ethereumTitle = "Ethereum"
val bitcoinTitle = "Bitcoin"
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
}
step("Click on 'Synchronize addresses' button" ) {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Check positions of tokens on 'Main Screen'") {
onMainScreen {
tokenWithTitleAndPosition(bitcoinTitle, 0).assertIsDisplayed()
tokenWithTitleAndPosition(ethereumTitle, 1).assertIsDisplayed()
}
}
step("Click 'Organize tokens' button") {
onMainScreen { organizeTokensButton().clickWithAssertion() }
}
step("Check positions of tokens on 'Organize tokens' screen") {
onOrganizeTokensScreen {
tokenWithTitleAndPosition(bitcoinTitle, 1).assertIsDisplayed()
tokenWithTitleAndPosition(ethereumTitle, 2).assertIsDisplayed()
}
}
}
}
@AllureId("2753")
@DisplayName("Organize tokens: check position of tokens")
// toDo: on test build there is not ability to drag element
// @Test
fun checkCustomTokensOrderTest() {
setupHooks().run {
val ethereumTitle = "Ethereum"
val bitcoinTitle = "Bitcoin"
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
}
step("Click on 'Synchronize addresses' button" ) {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Check positions of tokens on 'Main Screen'") {
onMainScreen {
tokenWithTitleAndPosition(bitcoinTitle, 0).assertIsDisplayed()
tokenWithTitleAndPosition(ethereumTitle, 1).assertIsDisplayed()
}
}
step("Click 'Organize tokens' button") {
onMainScreen { organizeTokensButton().clickWithAssertion() }
}
step("Drag $bitcoinTitle down on 'Organize tokens' screen") {
composeTestRule.waitUntil(timeoutMillis = 100_000) {
composeTestRule.onAllNodesWithText("Data loaded").fetchSemanticsNodes().isNotEmpty()
}
onOrganizeTokensScreen {
tokenWithTitleAndPosition(bitcoinTitle, 2).assertIsDisplayed()
}
}
step("Click 'Apply' button") {
onOrganizeTokensScreen { applyButton.clickWithAssertion() }
}
step("Check positions of tokens on 'Main Screen'") {
onMainScreen {
tokenWithTitleAndPosition(bitcoinTitle, 1).assertIsDisplayed()
tokenWithTitleAndPosition(ethereumTitle, 0).assertIsDisplayed()
}
}
}
}
@AllureId("2754")
@DisplayName("Organize tokens: sort by balance")
@Test
fun checkSortByBalanceTest() {
setupHooks().run {
val ethereumTitle = "Ethereum"
val bitcoinTitle = "Bitcoin"
val polygonTitle = "Polygon"
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
}
step("Click on 'Synchronize addresses' button" ) {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Check positions of tokens on 'Main Screen'") {
onMainScreen {
tokenWithTitleAndPosition(bitcoinTitle, 0).assertIsDisplayed()
tokenWithTitleAndPosition(ethereumTitle, 1).assertIsDisplayed()
tokenWithTitleAndPosition(polygonTitle, 2).assertIsDisplayed()
}
}
step("Click 'Organize tokens' button") {
onMainScreen { organizeTokensButton().clickWithAssertion() }
}
step("Check positions of tokens on 'Organize tokens' screen") {
onOrganizeTokensScreen {
tokenWithTitleAndPosition(bitcoinTitle, 1).assertIsDisplayed()
tokenWithTitleAndPosition(ethereumTitle, 2).assertIsDisplayed()
tokenWithTitleAndPosition(polygonTitle, 3).assertIsDisplayed()
}
}
step("Click 'By Balance' button") {
onOrganizeTokensScreen {
sortByBalanceButton.clickWithAssertion()
}
}
step("Check positions of tokens by balance on 'Organize tokens' screen") {
onOrganizeTokensScreen {
tokenWithTitleAndPosition(ethereumTitle, 1).assertIsDisplayed()
tokenWithTitleAndPosition(polygonTitle, 2).assertIsDisplayed()
tokenWithTitleAndPosition(bitcoinTitle, 3).assertIsDisplayed()
}
}
step("Click 'Apply' button") {
onOrganizeTokensScreen { applyButton.clickWithAssertion() }
}
step("Check positions of tokens by balance on 'Organize tokens' screen") {
onMainScreen {
tokenWithTitleAndPosition(ethereumTitle, 0).assertIsDisplayed()
tokenWithTitleAndPosition(polygonTitle, 1).assertIsDisplayed()
tokenWithTitleAndPosition(bitcoinTitle, 2).assertIsDisplayed()
}
}
}
}
}