Updated on 2026-08-14

This commit is contained in:
Tangem 2025-08-08 17:22:39 +03:00
parent 1c5fa97fdf
commit e5408a2ac7
23 changed files with 517 additions and 20 deletions

View file

@ -2,4 +2,6 @@ package com.tangem.common.constants
object TestConstants {
const val TOTAL_BALANCE = "$3,299.18"
const val WAIT_UNTIL_TIMEOUT = 20_000L
}

View file

@ -0,0 +1,42 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.test.SelectNetworkFeeBottomSheetTestTags
import com.tangem.core.ui.test.TopAppBarTestTags
import com.tangem.wallet.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.kakao.common.utilities.getResourceString
import androidx.compose.ui.test.hasText as withText
class SelectNetworkFeePageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<SelectNetworkFeePageObject>(semanticsProvider = semanticsProvider) {
val title: KNode = child {
hasTestTag(TopAppBarTestTags.TITLE)
hasText(getResourceString(R.string.common_fee_selector_title))
useUnmergedTree = true
}
val marketSelectorItem: KNode = child {
hasTestTag(SelectNetworkFeeBottomSheetTestTags.SELECTOR_ITEM)
hasAnyChild(withText(getResourceString(R.string.common_fee_selector_option_market)))
useUnmergedTree = true
}
val fastSelectorItem: KNode = child {
hasTestTag(SelectNetworkFeeBottomSheetTestTags.SELECTOR_ITEM)
hasAnyChild(withText(getResourceString(R.string.common_fee_selector_option_fast)))
useUnmergedTree = true
}
val readMoreTextBlock: KNode = child {
hasTestTag(SelectNetworkFeeBottomSheetTestTags.READ_MORE_TEXT)
useUnmergedTree = true
}
}
internal fun BaseTestCase.onSelectNetworkFeeBottomSheet(function: SelectNetworkFeePageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,20 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.test.SwapStoriesScreenTestTags
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 SwapStoriesPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<SwapStoriesPageObject>(semanticsProvider = semanticsProvider) {
val closeButton: KNode = child {
hasTestTag(SwapStoriesScreenTestTags.CLOSE_BUTTON)
useUnmergedTree = true
}
}
internal fun BaseTestCase.onSwapStoriesScreen(function: SwapStoriesPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,80 @@
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.BaseButtonTestTags
import com.tangem.core.ui.test.NotificationTestTags
import com.tangem.core.ui.test.SwapTokenScreenTestTags
import com.tangem.core.ui.test.TopAppBarTestTags
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
import androidx.compose.ui.test.hasTestTag as withTestTag
class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<SwapTokenPageObject>(semanticsProvider = semanticsProvider) {
val title: KNode = child {
hasTestTag(TopAppBarTestTags.TITLE)
hasText(getResourceString(R.string.common_swap))
useUnmergedTree = true
}
val closeButton: KNode = child {
hasTestTag(TopAppBarTestTags.CLOSE_BUTTON)
}
val textInput: KNode = child {
hasParent(withTestTag(SwapTokenScreenTestTags.SWAP_TEXT_FIELD))
useUnmergedTree = true
}
val networkFeeBlock: KNode = child {
hasTestTag(SwapTokenScreenTestTags.NETWORK_FEE_BLOCK)
useUnmergedTree = true
}
val receiveAmountShimmer: KNode = child {
hasTestTag(SwapTokenScreenTestTags.RECEIVE_AMOUNT_SHIMMER)
}
val swapTokensOnscreenButton: KNode = child {
hasTestTag(SwapTokenScreenTestTags.SWAP_BUTTON)
}
val receiveAmount: KNode = child {
hasTestTag(SwapTokenScreenTestTags.RECEIVE_TEXT_FIELD)
useUnmergedTree = true
}
val providersBlock: KNode = child {
hasTestTag(SwapTokenScreenTestTags.PROVIDERS_BLOCK)
useUnmergedTree = true
}
val errorNotificationTitle: KNode = child {
hasTestTag(NotificationTestTags.TITLE)
useUnmergedTree = true
}
val errorNotificationText: KNode = child {
hasTestTag(NotificationTestTags.TEXT)
useUnmergedTree = true
}
val refreshButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.warning_button_refresh))
}
val swapButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.common_swap))
}
}
internal fun BaseTestCase.onSwapTokenScreen(function: SwapTokenPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -1,11 +1,18 @@
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.TokenDetailsScreenTestTags
import com.tangem.core.ui.utils.LazyListItemPositionSemantics
import com.tangem.features.tokendetails.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
class TokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<TokenDetailsPageObject>(semanticsProvider = semanticsProvider) {
@ -13,6 +20,41 @@ class TokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvide
val screenContainer: KNode = child {
hasTestTag(TokenDetailsScreenTestTags.SCREEN_CONTAINER)
}
val title: KNode = child {
hasTestTag(TokenDetailsScreenTestTags.TOKEN_TITLE)
}
private val horizontalActionChips = KLazyListNode(
semanticsProvider = semanticsProvider,
viewBuilderAction = { hasTestTag(TokenDetailsScreenTestTags.HORIZONTAL_ACTION_CHIPS) },
itemTypeBuilder = { itemType(::LazyListItemNode) },
positionMatcher = { position ->
SemanticsMatcher.expectValue(
LazyListItemPositionSemantics,
position
)
}
)
@OptIn(ExperimentalTestApi::class)
val swapButton: LazyListItemNode = horizontalActionChips.childWith<LazyListItemNode> {
hasTestTag(TokenDetailsScreenTestTags.ACTION_BUTTON)
hasText(getResourceString(R.string.common_swap))
}
@OptIn(ExperimentalTestApi::class)
val sellButton: LazyListItemNode = horizontalActionChips.childWith<LazyListItemNode> {
hasTestTag(TokenDetailsScreenTestTags.ACTION_BUTTON)
hasText(getResourceString(R.string.common_sell))
}
@OptIn(ExperimentalTestApi::class)
val buyButton: LazyListItemNode = horizontalActionChips.childWith<LazyListItemNode> {
hasTestTag(TokenDetailsScreenTestTags.ACTION_BUTTON)
hasText(getResourceString(R.string.common_buy))
}
}
internal fun BaseTestCase.onTokenDetailsScreen(function: TokenDetailsPageObject.() -> Unit) =

View file

@ -2,6 +2,7 @@ package com.tangem.tests
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.TOTAL_BALANCE
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState
@ -372,14 +373,14 @@ class BuyTokenTest : BaseTestCase() {
}
step("Assert unavailable provider name is displayed") {
onSelectProviderBottomSheet {
flakySafely(timeoutMs = 20_000) {
flakySafely(WAIT_UNTIL_TIMEOUT) {
unavailableProviderItem.assertIsDisplayed()
}
}
}
step("Assert available provider name is displayed") {
onSelectProviderBottomSheet {
flakySafely(timeoutMs = 20_000) {
flakySafely(WAIT_UNTIL_TIMEOUT) {
availableProviderItem.assertIsDisplayed()
}
}

View file

@ -0,0 +1,240 @@
package com.tangem.tests
import androidx.compose.ui.test.hasText
import com.tangem.common.BaseTestCase
import com.tangem.common.annotations.ApiEnv
import com.tangem.common.annotations.ApiEnvConfig
import com.tangem.common.constants.TestConstants.TOTAL_BALANCE
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiEnvironment
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 SwapTokenTest : BaseTestCase() {
@ApiEnv(
ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD)
)
@AllureId("3546")
@DisplayName("Swap: network fee")
@Test
fun networkFeeTest() {
val inputAmount = "100"
setupHooks().run {
val tokenTitle = "Polygon"
val balance = TOTAL_BALANCE
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("Click on token with name: '$tokenTitle'") {
onTokenDetailsScreen { title.assertIsDisplayed() }
}
step("Click on 'Swap' button") {
onTokenDetailsScreen { swapButton.performClick() }
}
step("Close 'Stories' screen") {
onSwapStoriesScreen { closeButton.clickWithAssertion() }
}
step("Assert 'Swap' screen title is displayed") {
onSwapTokenScreen { title.assertIsDisplayed() }
}
step("Assert 'Close' button is displayed") {
onSwapTokenScreen { closeButton.assertIsDisplayed() }
}
step("Assert 'Swap tokens on screen' button is displayed") {
onSwapTokenScreen {
flakySafely(WAIT_UNTIL_TIMEOUT) {
swapTokensOnscreenButton.assertIsDisplayed()
}
}
}
step("Assert receive amount is displayed") {
onSwapTokenScreen {
flakySafely(WAIT_UNTIL_TIMEOUT) {
receiveAmount.assertIsDisplayed()
}
}
}
step("Input swap amount = '$inputAmount'") {
composeTestRule.waitForIdle()
onSwapTokenScreen {
textInput.clickWithAssertion()
textInput.performTextReplacement(inputAmount)
}
}
step("Assert input amount = '$inputAmount'") {
onSwapTokenScreen { textInput.assertTextEquals(inputAmount) }
}
step("Assert 'Providers' block is displayed") {
onSwapTokenScreen {
flakySafely(WAIT_UNTIL_TIMEOUT) {
providersBlock.assertIsDisplayed()
}
}
}
step("Assert 'Network fee' block is displayed") {
onSwapTokenScreen {
flakySafely(WAIT_UNTIL_TIMEOUT) {
networkFeeBlock.assertIsDisplayed()
}
}
}
step("Assert receive amount is not equal to '0'") {
onSwapTokenScreen { receiveAmount.assert(!hasText("0")) }
}
}
}
@AllureId("3549")
@DisplayName("Swap: network error test")
@Test
fun networkErrorSwapTest() {
setupHooks().run {
val tokenTitle = "Polygon"
val balance = TOTAL_BALANCE
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("Click on token with name: '$tokenTitle'") {
onTokenDetailsScreen { title.assertIsDisplayed() }
}
step("Click on 'Swap' button") {
onTokenDetailsScreen { swapButton.performClick() }
}
step("Close 'Stories' screen") {
onSwapStoriesScreen { closeButton.clickWithAssertion() }
}
step("Assert 'Swap' screen title is displayed") {
onSwapTokenScreen { title.assertIsDisplayed() }
}
step("Assert error notification title is displayed") {
onSwapTokenScreen { errorNotificationTitle.assertIsDisplayed() }
}
step("Assert error notification text is displayed") {
onSwapTokenScreen { errorNotificationText.assertIsDisplayed() }
}
step("Assert 'Refresh' button is displayed") {
onSwapTokenScreen { refreshButton.assertIsDisplayed() }
}
}
}
@ApiEnv(
ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD)
)
@AllureId("3546")
@DisplayName("Swap: change network fee")
@Test
fun changeNetworkFeeTest() {
val inputAmount = "100"
setupHooks().run {
val tokenTitle = "Polygon"
val balance = TOTAL_BALANCE
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("Click on token with name: '$tokenTitle'") {
onTokenDetailsScreen { title.assertIsDisplayed() }
}
step("Click on 'Swap' button") {
onTokenDetailsScreen { swapButton.performClick() }
}
step("Close 'Stories' screen") {
onSwapStoriesScreen { closeButton.clickWithAssertion() }
}
step("Assert 'Swap' screen title is displayed") {
onSwapTokenScreen { title.assertIsDisplayed() }
}
step("Assert 'Swap tokens on screen' button is displayed") {
onSwapTokenScreen {
flakySafely(WAIT_UNTIL_TIMEOUT) {
swapTokensOnscreenButton.assertIsDisplayed()
}
}
}
step("Assert receive amount is displayed") {
onSwapTokenScreen {
flakySafely(WAIT_UNTIL_TIMEOUT) {
receiveAmount.assertIsDisplayed()
}
}
}
step("Input swap amount = '$inputAmount'") {
composeTestRule.waitForIdle()
onSwapTokenScreen {
textInput.clickWithAssertion()
textInput.performTextReplacement(inputAmount)
}
}
step("Assert input amount = '$inputAmount'") {
onSwapTokenScreen { textInput.assertTextEquals(inputAmount) }
}
step("Click on 'Network fee' block") {
onSwapTokenScreen {
flakySafely(WAIT_UNTIL_TIMEOUT) {
networkFeeBlock.clickWithAssertion()
}
}
}
step("Assert 'Select fee' bottom sheet title is displayed") {
onSelectNetworkFeeBottomSheet { title.assertIsDisplayed() }
}
step("Assert 'Market' item is displayed") {
onSelectNetworkFeeBottomSheet { marketSelectorItem.assertIsDisplayed() }
}
step("Assert 'Fast' item is displayed") {
onSelectNetworkFeeBottomSheet { fastSelectorItem.assertIsDisplayed() }
}
step("Assert 'Read more' text block is displayed") {
onSelectNetworkFeeBottomSheet { readMoreTextBlock.assertIsDisplayed() }
}
step("Click on 'Fast' item") {
onSelectNetworkFeeBottomSheet { fastSelectorItem.assertIsDisplayed() }
}
step("Assert 'Network fee' block is displayed") {
onSwapTokenScreen {
flakySafely(WAIT_UNTIL_TIMEOUT) {
networkFeeBlock.assertIsDisplayed()
}
}
}
}
}
}