tangem-app-android-audited/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt
2026-06-01 20:14:22 +03:00

393 lines
No EOL
14 KiB
Kotlin

package com.tangem.screens
import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.hasAnyAncestor
import androidx.compose.ui.test.swipeUp
import com.tangem.common.BaseTestCase
import com.tangem.common.extensions.getQuantityString
import com.tangem.common.extensions.hasLazyListItemPosition
import com.tangem.common.utils.LazyListItemNode
import com.tangem.core.ui.test.*
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
import androidx.compose.ui.test.hasText as withText
import com.tangem.core.res.R as CoreResR
import com.tangem.core.ui.R as CoreUiR
class MainScreenPageObject(private val semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<MainScreenPageObject>(semanticsProvider = semanticsProvider) {
private val lazyList = KLazyListNode(
semanticsProvider = semanticsProvider,
viewBuilderAction = { hasTestTag(MainScreenTestTags.SCREEN_CONTAINER) },
itemTypeBuilder = {
itemType(::LazyListItemNode)
},
positionMatcher = { position ->
SemanticsMatcher.expectValue(
LazyListItemPositionSemantics,
position
)
}
)
val screenContainer: KNode = child {
hasTestTag(MainScreenTestTags.SCREEN_CONTAINER)
}
val synchronizeAddressesButton: KNode = lazyList.child {
hasText(getResourceString(R.string.common_generate_addresses))
}
val buyButton: KNode = child {
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
hasAnyDescendant(withText(getResourceString(R.string.common_buy)))
useUnmergedTree = true
}
val addFundsButton: KNode = child {
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
hasText(getResourceString(R.string.common_add_funds))
}
val sendButton: KNode = child {
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
hasAnyDescendant(withText(getResourceString(R.string.common_send)))
useUnmergedTree = true
}
val receiveButton: KNode = child {
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
hasAnyDescendant(withText(getResourceString(R.string.common_receive)))
useUnmergedTree = true
}
val sellButton: KNode = child {
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
hasAnyDescendant(withText(getResourceString(R.string.common_sell)))
useUnmergedTree = true
}
val swapButton: KNode = child {
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
hasAnyDescendant(withText(getResourceString(R.string.common_swap)))
useUnmergedTree = true
}
val walletNameText: KNode = child {
hasTestTag(MainScreenTestTags.CARD_TITLE)
useUnmergedTree = true
}
val walletImage: KNode = child {
hasTestTag(MainScreenTestTags.CARD_IMAGE)
useUnmergedTree = true
}
/**
* Collapses the collapsing header via a touch-based swipe so that items near the bottom
* of the lazy list fall within screen bounds before programmatic childWith scroll.
* Required because TangemCollapsingTopBar places the body at y=collapsingHeight, which
* pushes lower list items off-screen when the header is expanded.
*/
private fun collapseHeader() {
screenContainer {
performTouchInput { swipeUp(startY = visibleSize.height * 0.6f, endY = visibleSize.height * 0.1f) }
}
}
val restoringProgressText: KNode = child {
hasTestTag(MainScreenTestTags.SYNC_PROGRESS_TEXT)
useUnmergedTree = true
}
val walletImportedBanner: KNode = child {
hasTestTag(WalletNotificationTestTags.ASSETS_DISCOVERY_BANNER)
useUnmergedTree = true
}
val walletImportedBannerCheckHereButton: KNode = child {
hasAnyAncestor(withTestTag(WalletNotificationTestTags.ASSETS_DISCOVERY_BANNER))
hasText(getResourceString(CoreResR.string.main_manage_tokens))
useUnmergedTree = true
}
@OptIn(ExperimentalTestApi::class)
fun marketPriceBlock(): LazyListItemNode {
collapseHeader()
return lazyList.childWith<LazyListItemNode> {
hasTestTag(MarketPriceBlockTestTags.BLOCK)
useUnmergedTree = true
}
}
val marketPriceText: KNode = child {
hasTestTag(MarketPriceBlockTestTags.TEXT)
useUnmergedTree = true
}
val transactionsExplorerIcon: KNode = child {
hasTestTag(TransactionHistoryBlockTestTags.EXPLORER_ICON)
useUnmergedTree = true
}
val transactionsTitle: KNode = child {
hasTestTag(TransactionHistoryBlockTestTags.TITLE_TEXT)
hasText(getResourceString(R.string.common_transactions))
useUnmergedTree = true
}
val transactionsExplorerText: KNode = child {
hasTestTag(TransactionHistoryBlockTestTags.EXPLORER_TEXT)
hasText(getResourceString(R.string.common_explorer))
useUnmergedTree = true
}
val emptyTransactionBlock: KNode = child {
hasTestTag(EmptyTransactionBlockTestTags.BLOCK)
}
val emptyTransactionBlockIcon: KNode = child {
hasTestTag(EmptyTransactionBlockTestTags.ICON)
}
val emptyTransactionBlockText: KNode = child {
hasTestTag(EmptyTransactionBlockTestTags.TEXT)
}
val emptyTransactionBlockExploreButton: KNode = child {
hasTestTag(EmptyTransactionBlockTestTags.EXPLORE_BUTTON)
}
val notificationContainer: KNode = child {
hasTestTag(NotificationTestTags.CONTAINER)
useUnmergedTree = true
}
val devCardNotificationIcon: KNode = child {
hasAnySibling(withText(getResourceString(R.string.warning_developer_card_title)))
hasTestTag(NotificationTestTags.ICON)
useUnmergedTree = true
}
val devCardNotificationTitle: KNode = child {
hasTestTag(NotificationTestTags.TITLE)
hasText(getResourceString(R.string.warning_developer_card_title))
useUnmergedTree = true
}
val devCardNotificationMessage: KNode = child {
hasTestTag(NotificationTestTags.MESSAGE)
hasText(getResourceString(R.string.warning_developer_card_message))
useUnmergedTree = true
}
val missingAddressNotificationIcon: KNode = child {
hasAnySibling(withText(getResourceString(R.string.warning_missing_derivation_title)))
hasTestTag(NotificationTestTags.ICON)
useUnmergedTree = true
}
val missingAddressNotificationTitle: KNode = child {
hasTestTag(NotificationTestTags.TITLE)
hasText(getResourceString(R.string.warning_missing_derivation_title))
useUnmergedTree = true
}
fun missingAddressNotificationMessage(networkCount: Int): KNode = child {
hasTestTag(NotificationTestTags.MESSAGE)
hasText(
getQuantityString(
R.plurals.warning_missing_derivation_message,
networkCount,
networkCount
)
)
useUnmergedTree = true
}
val totalBalanceContainer: KNode = child {
hasTestTag(MainScreenTestTags.WALLET_LIST_ITEM)
}
val totalBalanceMenuRenameWallet: KNode = child {
hasTestTag(MainScreenTestTags.TOTAL_BALANCE_MENU_ITEM)
hasText(getResourceString(R.string.common_rename))
}
val totalBalanceMenuDeleteWallet: KNode = child {
hasTestTag(MainScreenTestTags.TOTAL_BALANCE_MENU_ITEM)
hasText(getResourceString(R.string.common_delete))
}
val totalBalanceText: KNode = child {
hasAnyAncestor(withTestTag(MainScreenTestTags.WALLET_BALANCE))
addSemanticsMatcher(SemanticsMatcher.keyIsDefined(SemanticsProperties.Text))
}
val notificationYesButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(R.string.common_yes))
useUnmergedTree = true
}
val notificationNoButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(R.string.common_no))
useUnmergedTree = true
}
val snackbarCopiedAddressMessage: KNode = child {
hasText(getResourceString(CoreUiR.string.wallet_notification_address_copied))
}
val addAndManageButtonNode: KNode = child {
hasTestTag(MainScreenTestTags.ADD_AND_MANAGE_BUTTON)
useUnmergedTree = true
}
/**
* Main account header on the main screen. Click to expand/collapse its tokens list.
*/
fun mainAccount(): LazyListItemNode = accountWithName(getResourceString(CoreUiR.string.account_main_account_title))
/**
* Account header on the main screen, located by its visible name. Click to expand/collapse its tokens list.
* The account's title text lives on a descendant of the test-tagged node, so we match by descendant.
*/
@OptIn(ExperimentalTestApi::class)
fun accountWithName(name: String): LazyListItemNode {
collapseHeader()
return lazyList.childWith<LazyListItemNode> {
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
hasAnyDescendant(withText(name))
useUnmergedTree = true
}
}
@OptIn(ExperimentalTestApi::class)
fun tokenRowWithTitle(tokenTitle: String): LazyListItemNode {
return lazyList.childWith<LazyListItemNode> {
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
hasText(tokenTitle)
useUnmergedTree = true
}
}
/**
* Find token list item with title and address
*/
@OptIn(ExperimentalTestApi::class)
fun tokenWithTitleAndAddress(tokenTitle: String): KNode {
collapseHeader()
return lazyList.childWith<LazyListItemNode> {
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
hasText(tokenTitle)
useUnmergedTree = true
}.child<KNode> {
hasTestTag(TokenElementsTestTags.TOKEN_FIAT_AMOUNT)
useUnmergedTree = true
}
}
@OptIn(ExperimentalTestApi::class)
fun tokenWithCustomDerivationIcon(tokenTitle: String): KNode {
collapseHeader()
return lazyList.childWith<LazyListItemNode> {
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
hasText(tokenTitle)
useUnmergedTree = true
}.child<KNode> {
hasTestTag(TokenElementsTestTags.TOKEN_CUSTOM_DERIVATION_ICON)
useUnmergedTree = true
}
}
@OptIn(ExperimentalTestApi::class)
fun addAndManageButton(): KNode {
collapseHeader()
return lazyList.childWith<LazyListItemNode> {
hasTestTag(MainScreenTestTags.ADD_AND_MANAGE_BUTTON)
}.child<KNode> {
hasText(getResourceString(CoreResR.string.main_add_and_manage_tokens))
useUnmergedTree = true
}
}
val addAndManageButtonWithoutLazySearch: KNode = child {
hasTestTag(MainScreenTestTags.ADD_AND_MANAGE_BUTTON)
hasText(getResourceString(CoreResR.string.main_add_and_manage_tokens))
useUnmergedTree = true
}
val searchThroughMarketPlaceholder: KNode = child {
hasText(getResourceString(R.string.markets_search_title_placeholder))
useUnmergedTree = true
}
fun tokenNetworkGroupTitle(tokenNetwork: String): KNode {
collapseHeader()
return lazyList.child {
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
hasAnyChild(withText(tokenNetwork))
useUnmergedTree = true
}
}
@OptIn(ExperimentalTestApi::class)
fun tokenWithTitleAndPosition(tokenTitle: String, index: Int): KNode {
collapseHeader()
return lazyList.childWith<LazyListItemNode> {
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
hasText(tokenTitle)
hasLazyListItemPosition(index)
useUnmergedTree = true
}.child<KNode> {
hasTestTag(TokenElementsTestTags.TOKEN_TITLE)
useUnmergedTree = true
}
}
fun KNode.assertIsUnreachable() {
this {
hasAnyAncestor(withText(getResourceString(R.string.common_unreachable)))
assertIsDisplayed()
}
}
/**
* 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
}
}
}
fun assertTokensCount(expectedCount: Int) {
semanticsProvider
.onAllNodes(withTestTag(TokenElementsTestTags.TOKEN_PRICE))
.assertCountEquals(expectedCount)
}
}
internal fun BaseTestCase.onMainScreen(function: MainScreenPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)