Updated on 2026-08-14

This commit is contained in:
Tangem 2025-07-18 13:22:41 +03:00
commit 23b66545b2
29 changed files with 427 additions and 23 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) }
}
}
}
}

View file

@ -177,6 +177,13 @@ object WalletMockContent : MockContent {
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/84'/0'/0'/0/0") to ExtendedPublicKey( // btc
publicKey = byteArrayOf(3, 45, 58, -110, -52, -51, -83, -4, -45, -118, 119, 37, 123, -17, 66, -83, 61, -106, 115, 47, 121, 66, 84, -122, -57, -45, 7, -79, 70, -13, 28, -125, -52),
chainCode = byteArrayOf(93, 51, 52, -66, -39, -38, 34, -84, 50, 1, -127, -20, 80, -20, -30, -72, 2, 1, -78, -81, -17, 51, -52, -25, 12, 108, 50, 89, -66, 18, 65, 70),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),

View file

@ -16,6 +16,7 @@ import androidx.compose.runtime.Immutable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
@ -29,6 +30,7 @@ import com.tangem.core.ui.components.fields.SimpleDialogTextField
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.DialogTestTags
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@ -216,7 +218,8 @@ private fun TangemDialog(
shape = TangemTheme.shapes.roundedCornersLarge,
color = TangemTheme.colors.background.primary,
)
.padding(vertical = TangemTheme.dimens.spacing24),
.padding(vertical = TangemTheme.dimens.spacing24)
.testTag(DialogTestTags.DIALOG_CONTAINER),
) {
if (title != null) {
Text(

View file

@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
@ -24,6 +25,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.core.ui.components.ResizableText
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.DialogTestTags
import com.tangem.core.ui.utils.MultipleClickPreventer
@Suppress("LongParameterList")
@ -47,7 +49,9 @@ fun TangemButton(
val multipleClickPreventer = remember { MultipleClickPreventer.get() }
Button(
modifier = modifier.heightIn(min = size.toHeightDp()),
modifier = modifier
.heightIn(min = size.toHeightDp())
.testTag(DialogTestTags.BUTTON),
onClick = {
multipleClickPreventer.processEvent { if (!showProgress) onClick() }
},

View file

@ -7,12 +7,14 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.PopUpMenuTestTags
@Suppress("ComposableEventParameterNaming")
@Composable
@ -23,7 +25,8 @@ fun TangemDropdownItem(item: TangemDropdownMenuItem, dismissParent: () -> Unit,
dismissParent()
item.onClick()
}
.padding(vertical = TangemTheme.dimens.spacing8, horizontal = TangemTheme.dimens.spacing16),
.padding(vertical = TangemTheme.dimens.spacing8, horizontal = TangemTheme.dimens.spacing16)
.testTag(PopUpMenuTestTags.BUTTON),
text = item.title.resolveReference(),
style = TangemTheme.typography.button.copy(color = item.textColorProvider()),
)

View file

@ -17,11 +17,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.*
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupPositionProvider
import androidx.compose.ui.window.PopupProperties
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.PopUpMenuTestTags
import kotlin.math.max
import kotlin.math.min
@ -138,7 +140,8 @@ private fun DropdownMenuContent(
Column(
modifier = modifier
.width(IntrinsicSize.Max)
.verticalScroll(rememberScrollState()),
.verticalScroll(rememberScrollState())
.testTag(PopUpMenuTestTags.CONTAINER),
content = content,
)
}

View file

@ -12,6 +12,7 @@ import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
@ -30,6 +31,7 @@ import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.MainScreenTestTags
import kotlinx.collections.immutable.persistentListOf
import org.burnoutcrew.reorderable.ReorderableLazyListState
import java.util.UUID
@ -100,7 +102,8 @@ fun TokenItem(
state = state.iconState,
modifier = Modifier
.layoutId(layoutId = LayoutId.ICON)
.padding(end = TangemTheme.dimens.spacing8),
.padding(end = TangemTheme.dimens.spacing8)
.testTag(MainScreenTestTags.TOKEN_ICON),
)
TokenTitle(
@ -108,7 +111,8 @@ fun TokenItem(
modifier = Modifier
.layoutId(layoutId = LayoutId.TITLE)
.padding(end = TangemTheme.dimens.spacing8)
.padding(bottom = betweenRowsMargin),
.padding(bottom = betweenRowsMargin)
.testTag(MainScreenTestTags.TOKEN_TITLE),
)
TokenFiatAmount(
@ -116,26 +120,32 @@ fun TokenItem(
isBalanceHidden = isBalanceHidden,
modifier = Modifier
.layoutId(layoutId = LayoutId.FIAT_AMOUNT)
.padding(bottom = betweenRowsMargin),
.padding(bottom = betweenRowsMargin)
.testTag(MainScreenTestTags.TOKEN_FIAT_AMOUNT),
)
TokenPrice(
state = state.subtitleState,
modifier = Modifier
.layoutId(layoutId = LayoutId.CRYPTO_PRICE)
.padding(end = TangemTheme.dimens.spacing8),
.padding(end = TangemTheme.dimens.spacing8)
.testTag(MainScreenTestTags.TOKEN_PRICE),
)
TokenCryptoAmount(
state = state.subtitle2State,
isBalanceHidden = isBalanceHidden,
modifier = Modifier.layoutId(layoutId = LayoutId.CRYPTO_AMOUNT),
modifier = Modifier
.layoutId(layoutId = LayoutId.CRYPTO_AMOUNT)
.testTag(MainScreenTestTags.TOKEN_CRYPTO_AMOUNT),
)
NonFiatContentBlock(
state = state,
reorderableTokenListState = reorderableTokenListState,
modifier = Modifier.layoutId(layoutId = LayoutId.NON_FIAT_CONTENT),
modifier = Modifier
.layoutId(layoutId = LayoutId.NON_FIAT_CONTENT)
.testTag(MainScreenTestTags.TOKEN_NO_ADDRESS),
)
}
}

View file

@ -0,0 +1,6 @@
package com.tangem.core.ui.test
object DialogTestTags {
const val DIALOG_CONTAINER = "DIALOG_CONTAINER"
const val BUTTON = "DIALOG_BUTTON"
}

View file

@ -4,4 +4,13 @@ object MainScreenTestTags {
const val SCREEN_CONTAINER = "MAIN_SCREEN_CONTAINER"
const val MORE_BUTTON = "MAIN_SCREEN_MORE_BUTTON"
const val TOP_BAR = "MAIN_SCREEN_TOP_BAR"
const val TOKEN_LIST_ITEM = "MAIN_SCREEN_TOKEN_LIST_ITEM"
const val TOKEN_TITLE = "MAIN_SCREEN_TOKEN_TITLE"
const val TOKEN_ICON = "MAIN_SCREEN_TOKEN_ICON"
const val TOKEN_PRICE = "MAIN_SCREEN_TOKEN_PRICE"
const val TOKEN_FIAT_AMOUNT = "MAIN_SCREEN_TOKEN_FIAT_AMOUNT"
const val TOKEN_CRYPTO_AMOUNT = "MAIN_SCREEN_TOKEN_CRYPTO_AMOUNT"
const val TOKEN_NO_ADDRESS = "MAIN_SCREEN_TOKEN_NO_ADDRESS"
const val WALLET_BALANCE = "MAIN_SCREEN_WALLET_BALANCE"
const val WALLET_LIST_ITEM = "MAIN_SCREEN_WALLET_LIST_ITEM"
}

View file

@ -0,0 +1,5 @@
package com.tangem.core.ui.test
object MarketTooltipTestTags {
const val CONTAINER = "MARKETS_TOOLTIP_CONTAINER"
}

View file

@ -0,0 +1,6 @@
package com.tangem.core.ui.test
object PopUpMenuTestTags {
const val CONTAINER = "POP_UP_MENU_CONTAINER"
const val BUTTON = "POP_UP_MENU_BUTTON"
}

View file

@ -0,0 +1,5 @@
package com.tangem.core.ui.test
object TokenDetailsScreenTestTags {
const val SCREEN_CONTAINER = "TOKEN_DETAILS_SCREEN_CONTAINER"
}

View file

@ -0,0 +1,6 @@
package com.tangem.core.ui.test
object TokenDetailsTopBarTestTags {
const val MORE_BUTTON = "TOKEN_DETAILS_TOP_BAR_MORE_BUTTON"
const val BACK_BUTTON = "TOKEN_DETAILS_TOP_BAR_BACK_BUTTON"
}

View file

@ -0,0 +1,29 @@
package com.tangem.core.ui.utils
import androidx.compose.ui.semantics.SemanticsPropertyKey
import androidx.compose.ui.semantics.SemanticsPropertyReceiver
/**
* Semantics key for storing and retrieving the visual position
* of an item within a LazyList. Used to verify item ordering
* and visibility in UI tests.
*/
val LazyListItemPositionSemantics = SemanticsPropertyKey<Int>("LazyListItemPosition")
/**
* Provides access to the item position semantics property
* through semantics property receiver DSL.
*/
var SemanticsPropertyReceiver.lazyListItemPosition by LazyListItemPositionSemantics
/**
* Semantics key for storing the total length/count of items
* in a LazyList. Enables validation of list completeness in tests.
*/
val LazyListLengthSemantics = SemanticsPropertyKey<Int>("LazyListLength")
/**
* Provides access to the list length semantics property
* through semantics property receiver DSL.
*/
var SemanticsPropertyReceiver.lazyListLength by LazyListLengthSemantics

View file

@ -9,6 +9,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
@ -29,6 +30,7 @@ import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.components.transactions.txHistoryItems
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.TokenDetailsScreenTestTags
import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData
import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
@ -79,7 +81,9 @@ internal fun TokenDetailsScreen(
modifier = Modifier.padding(scaffoldPaddings),
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
modifier = Modifier
.fillMaxSize()
.testTag(TokenDetailsScreenTestTags.SCREEN_CONTAINER),
state = listState,
contentPadding = PaddingValues(
bottom = TangemTheme.dimens.spacing16 + bottomBarHeight,

View file

@ -10,6 +10,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.util.fastForEach
@ -17,6 +18,7 @@ import com.tangem.core.ui.components.dropdownmenu.TangemDropdownItem
import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenu
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.TokenDetailsTopBarTestTags
import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarConfig
import com.tangem.features.tokendetails.impl.R
@ -32,6 +34,7 @@ internal fun TokenDetailsTopAppBar(config: TokenDetailsTopAppBarConfig) {
painter = painterResource(id = R.drawable.ic_back_24),
tint = TangemTheme.colors.icon.primary1,
contentDescription = "Back",
modifier = Modifier.testTag(TokenDetailsTopBarTestTags.BACK_BUTTON),
)
}
},
@ -46,6 +49,7 @@ internal fun TokenDetailsTopAppBar(config: TokenDetailsTopAppBarConfig) {
painter = painterResource(id = R.drawable.ic_more_vertical_24),
tint = TangemTheme.colors.icon.primary1,
contentDescription = "More",
modifier = Modifier.testTag(TokenDetailsTopBarTestTags.MORE_BUTTON),
)
}
}

View file

@ -63,6 +63,7 @@ import com.tangem.core.ui.res.LocalWindowSize
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.MainScreenTestTags
import com.tangem.core.ui.test.MarketTooltipTestTags
import com.tangem.core.ui.utils.lineTo
import com.tangem.core.ui.utils.moveTo
import com.tangem.core.ui.utils.toPx
@ -490,7 +491,9 @@ private fun MarketsTooltip(
val slideOffset = 40.dp.toPx()
AnimatedVisibility(
modifier = modifier.offset { IntOffset(x = 0, y = tooltipOffset.roundToPx()) },
modifier = modifier
.offset { IntOffset(x = 0, y = tooltipOffset.roundToPx()) }
.testTag(MarketTooltipTestTags.CONTAINER),
visible = visible,
enter = slideIn(
animationSpec = spring(

View file

@ -20,11 +20,13 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.MainScreenTestTags
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState
import com.tangem.feature.wallet.presentation.wallet.ui.components.common.WalletCard
@ -63,7 +65,9 @@ internal fun WalletsList(
WalletCard(
state = state,
isBalanceHidden = isBalanceHidden,
modifier = Modifier.width(itemWidth),
modifier = Modifier
.width(itemWidth)
.testTag(MainScreenTestTags.WALLET_LIST_ITEM),
)
}
}

View file

@ -25,6 +25,7 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow
@ -48,6 +49,7 @@ import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemDimens
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.MainScreenTestTags
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDropDownItems
@ -278,7 +280,9 @@ private fun Balance(state: WalletCardState, isBalanceHidden: Boolean, modifier:
when (state) {
is WalletCardState.Content -> {
ResizableText(
modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size32),
modifier = Modifier
.defaultMinSize(minHeight = TangemTheme.dimens.size32)
.testTag(MainScreenTestTags.WALLET_BALANCE),
text = balance,
fontSizeRange = FontSizeRange(min = 16.sp, max = TangemTheme.typography.h2.fontSize),
overflow = TextOverflow.Ellipsis,

View file

@ -10,13 +10,17 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextAlign
import com.tangem.core.ui.components.tokenlist.TokenListItem
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.MainScreenTestTags
import com.tangem.core.ui.utils.lazyListItemPosition
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState
import kotlinx.collections.immutable.ImmutableList
@ -61,11 +65,14 @@ private fun LazyListScope.contentItems(
TokenListItem(
state = item,
isBalanceHidden = isBalanceHidden,
modifier = modifier.roundedShapeItemDecoration(
currentIndex = index,
lastIndex = items.lastIndex,
backgroundColor = TangemTheme.colors.background.primary,
),
modifier = modifier
.roundedShapeItemDecoration(
currentIndex = index,
lastIndex = items.lastIndex,
backgroundColor = TangemTheme.colors.background.primary,
)
.testTag(MainScreenTestTags.TOKEN_LIST_ITEM)
.semantics { lazyListItemPosition = index },
)
},
)