Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-16 16:23:13 +05:00
parent 82117cec21
commit 99400590a6
24 changed files with 443 additions and 47 deletions

View file

@ -6,4 +6,6 @@ object TestConstants {
const val RECIPIENT_ADDRESS = "0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0"
const val WAIT_UNTIL_TIMEOUT = 20_000L
const val MARKETS_MAIN_NETWORK_SUFFIX = "MAIN"
}

View file

@ -1,11 +1,15 @@
package com.tangem.common.extensions
import androidx.test.uiautomator.By
import com.tangem.common.BaseTestCase
import com.tangem.wallet.R
import io.github.kakaocup.kakao.common.utilities.getResourceString
fun BaseTestCase.swipeUp(
startHeightRatio: Float = 0.8f,
endHeightRatio: Float = 0.03f,
steps: Int = 15
fun BaseTestCase.swipeVertical(
direction: SwipeDirection,
startHeightRatio: Float = if (direction == SwipeDirection.UP) 0.8f else 0.03f,
endHeightRatio: Float = if (direction == SwipeDirection.UP) 0.03f else 0.8f,
steps: Int = 15,
) {
device.uiDevice.swipe(
device.uiDevice.displayWidth / 2,
@ -14,4 +18,41 @@ fun BaseTestCase.swipeUp(
(device.uiDevice.displayHeight * endHeightRatio).toInt(),
steps
)
}
fun BaseTestCase.pullToRefresh() {
swipeVertical(
direction = SwipeDirection.DOWN,
startHeightRatio = 0.2f,
endHeightRatio = 0.8f,
steps = 2000
)
}
fun BaseTestCase.swipeMarketsBlock(direction: SwipeDirection) {
val searchBarText = device.uiDevice
.findObject(By.textContains(getResourceString(R.string.markets_search_header_title)))
val bounds = searchBarText.visibleBounds
val centerX = bounds.centerX()
val startY = bounds.centerY()
val endY = when (direction) {
SwipeDirection.UP -> 50
SwipeDirection.DOWN -> device.uiDevice.displayHeight - 100
}
device.uiDevice.swipe(centerX, startY, centerX, endY, 100)
}
fun BaseTestCase.openTheAppFromRecents() {
device.uiDevice.pressRecentApps()
val centerX = device.uiDevice.displayWidth / 2
val centerY = device.uiDevice.displayHeight / 3
device.uiDevice.click(centerX, centerY)
device.uiDevice.click(centerX, centerY)
}
enum class SwipeDirection {
UP, DOWN
}

View file

@ -5,4 +5,5 @@ import io.github.kakaocup.compose.node.element.KNode
fun KNode.clickWithAssertion() {
assertIsDisplayed()
performClick()
}
}

View file

@ -0,0 +1,22 @@
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.BaseBottomSheetTestTags
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 BaseBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<BaseBottomSheetPageObject>(semanticsProvider = semanticsProvider) {
val hideButton: KNode = child {
hasText(getResourceString(R.string.token_details_hide_token))
hasTestTag(BaseBottomSheetTestTags.ACTION_TITLE)
}
}
internal fun BaseTestCase.onBottomSheet(function: BaseBottomSheetPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -31,6 +31,16 @@ class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.common_confirm))
}
val continueButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.common_continue))
}
val okButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.common_ok))
}
}
internal fun BaseTestCase.onDialog(function: DialogPageObject.() -> Unit) =

View file

@ -0,0 +1,60 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.MARKETS_MAIN_NETWORK_SUFFIX
import com.tangem.common.utils.LazyListItemNode
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.test.MarketsTestTags
import com.tangem.core.ui.test.TopAppBarTestTags
import com.tangem.core.ui.utils.LazyListItemPositionSemantics
import com.tangem.features.onramp.impl.R
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.compose.node.element.lazylist.KLazyListNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
import androidx.compose.ui.test.hasText as withText
class MarketsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<MarketsPageObject>(semanticsProvider = semanticsProvider) {
private val lazyList = KLazyListNode(
semanticsProvider = semanticsProvider,
viewBuilderAction = { hasTestTag(MarketsTestTags.TOKENS_LIST) },
itemTypeBuilder = { itemType(::LazyListItemNode) },
positionMatcher = { position ->
SemanticsMatcher.expectValue(
LazyListItemPositionSemantics,
position
)
}
)
val addToPortfolioButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(R.string.common_add_to_portfolio))
useUnmergedTree = true
}
val mainNetworkSwitch: KNode = child<KNode> {
hasAnyDescendant(withText(MARKETS_MAIN_NETWORK_SUFFIX))
useUnmergedTree = true
}.child { hasTestTag(MarketsTestTags.ADD_TO_PORTFOLIO_SWITCH) }
val topBarBackButton: KNode = child {
hasTestTag(TopAppBarTestTags.CLOSE_BUTTON)
useUnmergedTree = true
}
fun tokenWithTitle(title: String): KNode {
return lazyList.child<KNode> {
hasText(title)
useUnmergedTree = true
}
}
}
internal fun BaseTestCase.onMarketsScreen(function: MarketsPageObject.() -> 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.BaseSearchBarTestTags
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 SearchBarPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<SearchBarPageObject>(semanticsProvider = semanticsProvider) {
val searchField: KNode = child {
hasTestTag(BaseSearchBarTestTags.SEARCH_BAR)
}
}
internal fun BaseTestCase.onSearchBar(function: SearchBarPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -4,6 +4,7 @@ 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.BaseSearchBarTestTags
import com.tangem.core.ui.test.SelectCountryBottomSheetTestTags
import com.tangem.core.ui.utils.LazyListItemPositionSemantics
import com.tangem.features.onramp.impl.R
@ -12,9 +13,8 @@ import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onCompose
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.compose.node.element.lazylist.KLazyListNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
import androidx.compose.ui.test.hasText as withText
import androidx.compose.ui.test.hasTestTag as withTestTag
import androidx.compose.ui.test.hasText as withText
class SelectCountryPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<SelectCountryPageObject>(semanticsProvider = semanticsProvider) {
@ -33,7 +33,7 @@ class SelectCountryPageObject(semanticsProvider: SemanticsNodeInteractionsProvid
)
val searchBar: KNode = child {
hasTestTag(SelectCountryBottomSheetTestTags.SEARCH_BAR)
hasTestTag(BaseSearchBarTestTags.SEARCH_BAR)
useUnmergedTree = true
}

View file

@ -3,9 +3,9 @@ package com.tangem.tests
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.TOTAL_BALANCE
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.screens.*
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.*
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
@ -33,7 +33,7 @@ class HideTokenTest : BaseTestCase() {
step("Assert 'Token details screen' open") {
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
}
step("Click 'More button'") {
step("Click 'More' button") {
onTokenDetailsTopBar { moreButton.clickWithAssertion() }
}
step("Click 'Hide token' button") {

View file

@ -3,12 +3,13 @@ package com.tangem.tests
import androidx.compose.ui.test.onAllNodesWithText
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.TOTAL_BALANCE
import com.tangem.common.extensions.SwipeDirection
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.extensions.swipeUp
import com.tangem.screens.onMainScreen
import com.tangem.screens.onOrganizeTokensScreen
import com.tangem.common.extensions.swipeVertical
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.onMainScreen
import com.tangem.screens.onOrganizeTokensScreen
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
@ -28,12 +29,12 @@ class OrganizeTokensTest : BaseTestCase() {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Click on 'Synchronize addresses' button" ) {
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Swipe to 'Organize tokens' button") {
swipeUp()
swipeUp()
swipeVertical(SwipeDirection.UP)
swipeVertical(SwipeDirection.UP)
}
step("Click 'Organize tokens' button") {
onMainScreen { organizeTokensButton().clickWithAssertion() }
@ -57,8 +58,8 @@ class OrganizeTokensTest : BaseTestCase() {
onMainScreen { tokenNetworkGroupTitle(tokenNetwork).assertIsDisplayed() }
}
step("Swipe to 'Organize tokens' button") {
swipeUp()
swipeUp()
swipeVertical(SwipeDirection.UP)
swipeVertical(SwipeDirection.UP)
}
step("Click 'Organize tokens' button") {
onMainScreen { organizeTokensButton().clickWithAssertion() }
@ -106,8 +107,8 @@ class OrganizeTokensTest : BaseTestCase() {
}
}
step("Swipe to 'Organize tokens' button") {
swipeUp()
swipeUp()
swipeVertical(SwipeDirection.UP)
swipeVertical(SwipeDirection.UP)
}
step("Click 'Organize tokens' button") {
onMainScreen { organizeTokensButton().clickWithAssertion() }
@ -133,7 +134,7 @@ class OrganizeTokensTest : BaseTestCase() {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Click on 'Synchronize addresses' button" ) {
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Check positions of tokens on 'Main Screen'") {
@ -143,8 +144,8 @@ class OrganizeTokensTest : BaseTestCase() {
}
}
step("Swipe to 'Organize tokens' button") {
swipeUp()
swipeUp()
swipeVertical(SwipeDirection.UP)
swipeVertical(SwipeDirection.UP)
}
step("Click 'Organize tokens' button") {
onMainScreen { organizeTokensButton().clickWithAssertion() }
@ -194,8 +195,8 @@ class OrganizeTokensTest : BaseTestCase() {
}
}
step("Swipe to 'Organize tokens' button") {
swipeUp()
swipeUp()
swipeVertical(SwipeDirection.UP)
swipeVertical(SwipeDirection.UP)
}
step("Click 'Organize tokens' button") {
onMainScreen { organizeTokensButton().clickWithAssertion() }
@ -234,6 +235,4 @@ class OrganizeTokensTest : BaseTestCase() {
}
}
}
}

View file

@ -2,8 +2,9 @@ package com.tangem.tests
import androidx.test.InstrumentationRegistry.getTargetContext
import com.tangem.common.BaseTestCase
import com.tangem.common.extensions.SwipeDirection
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.extensions.swipeUp
import com.tangem.common.extensions.swipeVertical
import com.tangem.screens.onDisclaimerScreen
import com.tangem.screens.onStoriesScreen
import dagger.hilt.android.testing.HiltAndroidTest
@ -37,7 +38,8 @@ class TermsOfServiceTest : BaseTestCase() {
step("Assert 'Stories' screen is opened") {
onStoriesScreen {
scanButton.assertIsDisplayed()
orderButton.assertIsDisplayed()}
orderButton.assertIsDisplayed()
}
}
}
}
@ -67,7 +69,7 @@ class TermsOfServiceTest : BaseTestCase() {
device.uiDevice.pressRecentApps()
}
step("Stop app by swipe") {
swipeUp(startHeightRatio = 0.5f)
swipeVertical(SwipeDirection.UP, startHeightRatio = 0.5f)
}
step("Launch app") {
device.apps.launch(packageName)
@ -90,7 +92,7 @@ class TermsOfServiceTest : BaseTestCase() {
device.uiDevice.pressRecentApps()
}
step("Stop app by swipe") {
swipeUp(startHeightRatio = 0.5f)
swipeVertical(SwipeDirection.UP, startHeightRatio = 0.5f)
}
step("Launch app") {
device.apps.launch(packageName)
@ -98,9 +100,9 @@ class TermsOfServiceTest : BaseTestCase() {
step("Assert 'Stories' screen is opened") {
onStoriesScreen {
scanButton.assertIsDisplayed()
orderButton.assertIsDisplayed()}
orderButton.assertIsDisplayed()
}
}
}
}
}

View file

@ -2,11 +2,12 @@ package com.tangem.tests
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.TOTAL_BALANCE
import com.tangem.common.extensions.SwipeDirection
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.extensions.swipeUp
import com.tangem.common.extensions.swipeVertical
import com.tangem.common.utils.getWcUri
import com.tangem.screens.*
import com.tangem.scenarios.*
import com.tangem.screens.*
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
@ -140,7 +141,7 @@ class WalletConnectTest : BaseTestCase() {
device.uiDevice.pressRecentApps()
}
step("Stop app by swipe") {
swipeUp(startHeightRatio = 0.8f)
swipeVertical(SwipeDirection.UP, startHeightRatio = 0.8f)
}
step("Create WC session buy deeplink") {
openAppByDeepLink(deepLinkUri)

View file

@ -0,0 +1,209 @@
package com.tangem.tests.balance
import androidx.compose.ui.test.longClick
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.TOTAL_BALANCE
import com.tangem.common.extensions.*
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses
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 TotalBalanceUpdateTest : BaseTestCase() {
@Test
@AllureId("150")
@DisplayName("Total balance: check balance update after pull to refresh")
fun afterPullToRefreshTest() {
val scenarioName = "eth_network_balance"
val scenarioState = "Empty"
val updatedBalance = "$763.55"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(scenarioName)
}
).run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(TOTAL_BALANCE)
}
step("Assert $TOTAL_BALANCE is displayed in total balance") {
onMainScreen { totalBalanceText.assertTextContains(TOTAL_BALANCE) }
}
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
}
step("Perform pull to refresh") {
pullToRefresh()
}
step("Assert Total balance is updated from $TOTAL_BALANCE to $updatedBalance") {
onMainScreen { totalBalanceText.assertTextContains(updatedBalance) }
}
}
}
@Test
@AllureId("3997")
@DisplayName("Total balance: check balance update after token added")
fun afterAddTokenTest() {
val tokenTitle = "XRP"
val scenarioName = "quotes_api"
val scenarioState = "Ripple"
val updatedBalance = "$3,307.18"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(scenarioName)
}
).run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(TOTAL_BALANCE)
}
step("Assert $TOTAL_BALANCE is displayed in total balance") {
onMainScreen { totalBalanceText.assertTextContains(TOTAL_BALANCE) }
}
step("Open 'Markets screen'") {
swipeMarketsBlock(SwipeDirection.UP)
composeTestRule.waitForIdle()
}
step("Click on $tokenTitle token") {
onMarketsScreen { tokenWithTitle(tokenTitle).clickWithAssertion() }
}
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
}
step("Click on 'Add to portfolio' button") {
onMarketsScreen { addToPortfolioButton.clickWithAssertion() }
}
step("Toggle the main network switch") {
onMarketsScreen { mainNetworkSwitch.performClick() }
}
step("Click on 'Continue' button") {
onDialog { continueButton.clickWithAssertion() }
}
step("Assert 'Continue' is not displayed") {
onDialog { continueButton.assertIsNotDisplayed() }
}
step("Go back to 'Markets: tokens list'") {
composeTestRule.waitForIdle()
onMarketsScreen { topBarBackButton.clickWithAssertion() }
}
step("Close 'Markets screen'") {
onSearchBar { searchField.assertIsDisplayed() }
swipeMarketsBlock(SwipeDirection.DOWN)
}
step("Assert $updatedBalance is displayed in total balance") {
onMainScreen { totalBalanceText.assertTextContains(updatedBalance) }
}
}
}
@Test
@AllureId("4000")
@DisplayName("Total balance: check balance update after collapse/expand")
fun afterCollapseAndExpandTest() {
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(TOTAL_BALANCE)
}
step("Assert $TOTAL_BALANCE is displayed in total balance") {
onMainScreen { totalBalanceText.assertTextContains(TOTAL_BALANCE) }
}
step("Press 'Home' to collapse the app") {
device.uiDevice.pressHome()
}
step("Open the app from recent apps") {
openTheAppFromRecents()
}
step("Assert $TOTAL_BALANCE is displayed in total balance") {
onMainScreen { totalBalanceText.assertTextContains(TOTAL_BALANCE) }
}
}
}
@Test
@AllureId("3998")
@DisplayName("Total balance: check balance update after hide token")
fun afterHideTokenTest() {
val tokenTitle = "Polygon"
val updatedBalance = "$3,114.34"
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(TOTAL_BALANCE)
}
step("Assert $TOTAL_BALANCE is displayed in total balance") {
onMainScreen { totalBalanceText.assertTextContains(TOTAL_BALANCE) }
}
step("Long click on token with name: '$tokenTitle'") {
composeTestRule.waitForIdle()
onMainScreen {
tokenWithTitleAndAddress(tokenTitle).performTouchInput {
longClick(
position = center,
durationMillis = 1000L
)
}
}
}
step("Click 'Hide token' button") {
onBottomSheet { hideButton.clickWithAssertion() }
}
step("Click 'Hide' button in dialog") {
onDialog {
dialogContainer.assertIsDisplayed()
okButton.clickWithAssertion()
}
}
step("Assert $updatedBalance is displayed in total balance") {
swipeVertical(SwipeDirection.DOWN)
onMainScreen { totalBalanceText.assertTextContains(updatedBalance) }
}
}
}
@Test
@AllureId("4001")
@DisplayName("Total balance: check balance update after navigation")
fun afterNavigationTest() {
val tokenTitle = "Polygon"
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(TOTAL_BALANCE)
}
step("Assert $TOTAL_BALANCE is displayed in total balance") {
onMainScreen { totalBalanceText.assertTextContains(TOTAL_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 { backButton.clickWithAssertion() }
}
step("Assert $TOTAL_BALANCE is displayed in total balance") {
onMainScreen { totalBalanceText.assertTextContains(TOTAL_BALANCE) }
}
}
}
}