Updated on 2026-08-14

This commit is contained in:
Tangem 2025-07-15 21:08:36 +03:00
parent 96a3074bdb
commit d616571df4
29 changed files with 421 additions and 19 deletions

View file

@ -2,6 +2,8 @@ package com.tangem.common
import android.Manifest
import androidx.compose.ui.test.junit4.createEmptyComposeRule
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.printToLog
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.intent.Intents
import androidx.test.rule.GrantPermissionRule
@ -79,4 +81,22 @@ abstract class BaseTestCase : TestCase(
additionalAfterSection()
Intents.release()
}
/**
* Prints the Compose semantics tree to logcat for debugging UI tests.
*
* @param useUnmergedTree When true, shows unmerged tree with all individual nodes.
* Use for accessing inner elements of compound components.
* Default: false (merged tree - accessibility view).
* @param tag Log tag for filtering in logcat. Default: "SEMANTIC_TREE".
* @param maxDepth Maximum nesting level to print. Use to avoid log overflow.
* Default: Int.MAX_VALUE (unlimited depth).
*/
fun printSemanticTree(
useUnmergedTree: Boolean = false,
tag: String = "SEMANTIC_TREE",
maxDepth: Int = Int.MAX_VALUE)
{
composeTestRule.onRoot(useUnmergedTree = useUnmergedTree).printToLog(tag, maxDepth)
}
}

View file

@ -0,0 +1,14 @@
package com.tangem.common.utils
import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import io.github.kakaocup.compose.node.element.lazylist.KLazyListItemNode
/**
* Represents an item node in a LazyList for Compose UI testing.
* Allows test actions (clicks, scrolls) and assertions on individual list items.
*/
class LazyListItemNode(
semanticsNode: SemanticsNode,
semanticsProvider: SemanticsNodeInteractionsProvider,
) : KLazyListItemNode<LazyListItemNode>(semanticsNode, semanticsProvider)

View file

@ -32,9 +32,9 @@ class OpenMainScreenScenario(
assertIsDisplayed()
}
}
ComposeScreen.onComposeScreen<TopBarPageObject>(testRule) {
ComposeScreen.onComposeScreen<MarketsTooltipPageObject>(testRule) {
step("Close Markets tooltip"){
performClick()
contentContainer.performClick()
}
}
}

View file

@ -0,0 +1,31 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.R
import com.tangem.core.ui.test.DialogTestTags
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.kakao.common.utilities.getResourceString
class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<DialogPageObject>(semanticsProvider = semanticsProvider) {
val dialogContainer: KNode = child {
hasTestTag(DialogTestTags.DIALOG_CONTAINER)
}
val cancelButton: KNode = child {
hasTestTag(DialogTestTags.BUTTON)
hasText(getResourceString(R.string.common_cancel))
}
val hideButton: KNode = child {
hasTestTag(DialogTestTags.BUTTON)
hasText(getResourceString(R.string.token_details_hide_alert_hide))
}
}
internal fun BaseTestCase.onDialog(function: DialogPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -1,16 +1,85 @@
package com.tangem.screens
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.utils.LazyListItemNode
import com.tangem.core.ui.test.MainScreenTestTags
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.hasTestTag as withTestTag
class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<MainScreenPageObject>(
ComposeScreen<MainScreenPageObject>(semanticsProvider = semanticsProvider) {
private val lazyList = KLazyListNode(
semanticsProvider = semanticsProvider,
viewBuilderAction = { hasTestTag(MainScreenTestTags.SCREEN_CONTAINER) }
viewBuilderAction = { hasTestTag(MainScreenTestTags.SCREEN_CONTAINER) },
itemTypeBuilder = {
itemType(::LazyListItemNode)
},
positionMatcher = { position ->
SemanticsMatcher.expectValue(
LazyListItemPositionSemantics,
position
)
}
)
val synchronizeAddressesButton: KNode = child {
hasText(getResourceString(R.string.common_generate_addresses))
}
/**
* Find token list item with title and address
*/
@OptIn(ExperimentalTestApi::class)
fun tokenWithTitleAndAddress(tokenTitle: String): KNode {
return lazyList.childWith<LazyListItemNode> {
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
hasText(tokenTitle)
}.child<KNode> {
hasTestTag(MainScreenTestTags.TOKEN_FIAT_AMOUNT)
useUnmergedTree = true
}
}
/**
* Find node with wallet balance using lazyList. This construction doesn't affect next step with lazyList.
*/
@OptIn(ExperimentalTestApi::class)
fun walletBalance(): KNode {
return lazyList.childWith<LazyListItemNode> {
hasAnyDescendant(withTestTag(MainScreenTestTags.WALLET_LIST_ITEM))
}.child<KNode> {
hasTestTag(MainScreenTestTags.WALLET_BALANCE)
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.
*/
fun assertTokenDoesNotExist(tokenTitle: String) {
try {
tokenWithTitleAndAddress(tokenTitle).assertExists()
throw AssertionError("Token with title '$tokenTitle' should not exist but was found")
} catch (e: AssertionError) {
if (e.message?.contains("No node found") == true) {
return
} else {
throw e
}
}
}
}
internal fun BaseTestCase.onMainScreen(function: MainScreenPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,19 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.test.MarketTooltipTestTags
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
class MarketsTooltipPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<MarketsTooltipPageObject>(semanticsProvider = semanticsProvider) {
val contentContainer: KNode = child {
hasTestTag(MarketTooltipTestTags.CONTAINER)
}
}
internal fun BaseTestCase.onMarketsTooltipScreen(function: MarketsTooltipPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,26 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.R
import com.tangem.core.ui.test.PopUpMenuTestTags
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.kakao.common.utilities.getResourceString
class PopUpMenuPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<PopUpMenuPageObject>(semanticsProvider = semanticsProvider) {
val popUpContainer: KNode = child {
hasTestTag(PopUpMenuTestTags.CONTAINER)
}
val hideTokenButton: KNode = child {
hasTestTag(PopUpMenuTestTags.BUTTON)
hasText(getResourceString(R.string.token_details_hide_token))
}
}
internal fun BaseTestCase.onPopUpMenu(function: PopUpMenuPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,19 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.test.TokenDetailsScreenTestTags
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
class TokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<TokenDetailsPageObject>(semanticsProvider = semanticsProvider) {
val screenContainer: KNode = child {
hasTestTag(TokenDetailsScreenTestTags.SCREEN_CONTAINER)
}
}
internal fun BaseTestCase.onTokenDetailsScreen(function: TokenDetailsPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,26 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.test.TokenDetailsTopBarTestTags
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
class TokenDetailsTopBarPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<TokenDetailsTopBarPageObject>(semanticsProvider = semanticsProvider) {
val moreButton: KNode = child {
hasTestTag(TokenDetailsTopBarTestTags.MORE_BUTTON)
useUnmergedTree = true
}
val backButton: KNode = child {
hasTestTag(TokenDetailsTopBarTestTags.BACK_BUTTON)
useUnmergedTree = true
}
}
internal fun BaseTestCase.onTokenDetailsTopBar(function: TokenDetailsTopBarPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,58 @@
package com.tangem.tests
import com.tangem.common.BaseTestCase
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.scenarios.OpenMainScreenScenario
import com.tangem.screens.*
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 HideTokenTest : BaseTestCase() {
@AllureId("880")
@DisplayName("Hide token in 'Token details' screen by 'Hide' button in topBar menu")
@Test
fun hideWalletTokenByHideButtonTest() {
val tokenTitle = "Polygon"
val balance = "<$0.01"
setupHooks().run {
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
}
step("Click on 'Synchronize addresses' button" ) {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Assert wallet balance = $balance") {
onMainScreen { walletBalance().assertTextContains(balance) }
}
step("Click on token with name: '$tokenTitle'") {
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
}
step("Assert 'Token details screen' open") {
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
}
step("Click 'More button'") {
onTokenDetailsTopBar { moreButton.clickWithAssertion() }
}
step("Click 'Hide token' button") {
onPopUpMenu {
popUpContainer.assertIsDisplayed()
hideTokenButton.clickWithAssertion()
}
}
step("Click 'Hide' button in dialog") {
onDialog {
dialogContainer.assertIsDisplayed()
hideButton.clickWithAssertion()
}
}
step("Assert token: '$tokenTitle' is not displayed") {
onMainScreen { assertTokenDoesNotExist(tokenTitle) }
}
}
}
}