diff --git a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt index 46e41677b6..d42aeddab4 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt @@ -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) + } } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/common/utils/LazyListItemNode.kt b/app/src/androidTest/kotlin/com/tangem/common/utils/LazyListItemNode.kt new file mode 100644 index 0000000000..1dfc811901 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/common/utils/LazyListItemNode.kt @@ -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(semanticsNode, semanticsProvider) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/OpenMainScreenScenario.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/OpenMainScreenScenario.kt index 7a2117accb..dd160f1b25 100644 --- a/app/src/androidTest/kotlin/com/tangem/scenarios/OpenMainScreenScenario.kt +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/OpenMainScreenScenario.kt @@ -32,9 +32,9 @@ class OpenMainScreenScenario( assertIsDisplayed() } } - ComposeScreen.onComposeScreen(testRule) { + ComposeScreen.onComposeScreen(testRule) { step("Close Markets tooltip"){ - performClick() + contentContainer.performClick() } } } diff --git a/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt new file mode 100644 index 0000000000..960b371248 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt @@ -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(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) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt index 73cf124ab4..9a7dd37c66 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt @@ -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( + ComposeScreen(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 { + hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM) + hasText(tokenTitle) + }.child { + 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 { + hasAnyDescendant(withTestTag(MainScreenTestTags.WALLET_LIST_ITEM)) + }.child { + 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) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/MarketsTooltipPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/MarketsTooltipPageObject.kt new file mode 100644 index 0000000000..08ae1af7d8 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/MarketsTooltipPageObject.kt @@ -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(semanticsProvider = semanticsProvider) { + + val contentContainer: KNode = child { + hasTestTag(MarketTooltipTestTags.CONTAINER) + } +} + +internal fun BaseTestCase.onMarketsTooltipScreen(function: MarketsTooltipPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/PopUpMenuPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/PopUpMenuPageObject.kt new file mode 100644 index 0000000000..546da863e9 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/PopUpMenuPageObject.kt @@ -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(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) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/TokenDetailsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/TokenDetailsPageObject.kt new file mode 100644 index 0000000000..2f8eae1c51 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/TokenDetailsPageObject.kt @@ -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(semanticsProvider = semanticsProvider) { + + val screenContainer: KNode = child { + hasTestTag(TokenDetailsScreenTestTags.SCREEN_CONTAINER) + } +} + +internal fun BaseTestCase.onTokenDetailsScreen(function: TokenDetailsPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/TokenDetailsTopBarPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/TokenDetailsTopBarPageObject.kt new file mode 100644 index 0000000000..9e6f42ef5f --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/TokenDetailsTopBarPageObject.kt @@ -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(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) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/HideTokenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/HideTokenTest.kt new file mode 100644 index 0000000000..dd0dee7e9e --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/HideTokenTest.kt @@ -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) } + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/WalletMockContent.kt b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/WalletMockContent.kt index 8b3f45673e..f562753a55 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/WalletMockContent.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/WalletMockContent.kt @@ -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), diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt index 0eab14b8e9..bf1fc3a926 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt @@ -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( diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt index efacaad36c..3a2dfe9d3d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt @@ -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() } }, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/dropdownmenu/TangemDropdownItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/dropdownmenu/TangemDropdownItem.kt index b29403e17d..dff51e77e2 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/dropdownmenu/TangemDropdownItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/dropdownmenu/TangemDropdownItem.kt @@ -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()), ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/dropdownmenu/TangemDropdownMenu.kt b/core/ui/src/main/java/com/tangem/core/ui/components/dropdownmenu/TangemDropdownMenu.kt index 6974bc2d7a..d5b7e7ea6d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/dropdownmenu/TangemDropdownMenu.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/dropdownmenu/TangemDropdownMenu.kt @@ -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, ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt index 9c7b913a98..135ace246d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt @@ -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), ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/DialogTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/DialogTestTags.kt new file mode 100644 index 0000000000..8f911d8632 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/DialogTestTags.kt @@ -0,0 +1,6 @@ +package com.tangem.core.ui.test + +object DialogTestTags { + const val DIALOG_CONTAINER = "DIALOG_CONTAINER" + const val BUTTON = "DIALOG_BUTTON" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/MainScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/MainScreenTestTags.kt index a999018849..9c1a0b31f8 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/MainScreenTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/MainScreenTestTags.kt @@ -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" } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/MarketTooltipTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/MarketTooltipTestTags.kt new file mode 100644 index 0000000000..383bc099fd --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/MarketTooltipTestTags.kt @@ -0,0 +1,5 @@ +package com.tangem.core.ui.test + +object MarketTooltipTestTags { + const val CONTAINER = "MARKETS_TOOLTIP_CONTAINER" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/PopUpMenuTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/PopUpMenuTestTags.kt new file mode 100644 index 0000000000..debcf2c9f1 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/PopUpMenuTestTags.kt @@ -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" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/TokenDetailsScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/TokenDetailsScreenTestTags.kt new file mode 100644 index 0000000000..54bc9893ca --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/TokenDetailsScreenTestTags.kt @@ -0,0 +1,5 @@ +package com.tangem.core.ui.test + +object TokenDetailsScreenTestTags { + const val SCREEN_CONTAINER = "TOKEN_DETAILS_SCREEN_CONTAINER" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/TokenDetailsTopBarTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/TokenDetailsTopBarTestTags.kt new file mode 100644 index 0000000000..9dc3f1d044 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/TokenDetailsTopBarTestTags.kt @@ -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" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/LazyList.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/LazyList.kt new file mode 100644 index 0000000000..fd3442302e --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/LazyList.kt @@ -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("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("LazyListLength") + +/** + * Provides access to the list length semantics property + * through semantics property receiver DSL. + */ +var SemanticsPropertyReceiver.lazyListLength by LazyListLengthSemantics \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index beafb5b029..a64108bc51 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -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, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsTopAppBar.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsTopAppBar.kt index 648e7aee48..e6bc3e7af0 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsTopAppBar.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsTopAppBar.kt @@ -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) ) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 6d5b80febe..f499ecb99e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -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( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt index 7294996ccd..73f9b11041 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt @@ -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), ) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt index a11dba3be1..9b0b6f2e1e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt @@ -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, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt index dd27fcfa85..b660e95e6d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt @@ -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 @@ -65,7 +69,8 @@ private fun LazyListScope.contentItems( currentIndex = index, lastIndex = items.lastIndex, backgroundColor = TangemTheme.colors.background.primary, - ), + ).testTag(MainScreenTestTags.TOKEN_LIST_ITEM) + .semantics { lazyListItemPosition = index }, ) }, )