Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-16 15:23:26 +03:00
parent 17df23f198
commit bcee90926c
9 changed files with 339 additions and 13 deletions

View file

@ -58,3 +58,12 @@ fun KNode.clickAndWaitFor(
throw AssertionError("Condition not met after $maxRetries click attempts")
}
fun KNode.performTextInputInChunks(
text: String,
chunkSize: Int = 2
) {
text.chunked(chunkSize).forEach { chunk ->
performTextInput(chunk)
}
}

View file

@ -0,0 +1,29 @@
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 com.tangem.core.ui.test.BaseButtonTestTags
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 AddTokenBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<AddTokenBottomSheetPageObject>(semanticsProvider = semanticsProvider) {
val title: KNode = child {
hasTestTag(BaseBottomSheetTestTags.TITLE)
hasText(getResourceString(R.string.common_add_token))
}
val addButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(R.string.common_add))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onAddTokenBottomSheet(function: AddTokenBottomSheetPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -5,6 +5,7 @@ import com.tangem.common.BaseTestCase
import com.tangem.core.ui.R
import com.tangem.core.ui.test.AppBarWithSearchTestTags
import com.tangem.core.ui.test.BuyTokenScreenTestTags
import com.tangem.core.ui.test.MarketsTestTags
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
@ -46,6 +47,13 @@ class SwapChooseTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProv
}
useUnmergedTree = true
}
fun marketsTokenWithTitle(title: String): KNode {
return child {
hasTestTag(MarketsTestTags.TOKENS_LIST_ITEM)
hasText(title)
}
}
}
internal fun BaseTestCase.onSwapChooseTokenScreen(function: SwapChooseTokenPageObject.() -> Unit) =

View file

@ -3,10 +3,7 @@ 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.SearchBarTestTags
import com.tangem.core.ui.test.SwapSelectTokenScreenTestTags
import com.tangem.core.ui.test.TokenElementsTestTags
import com.tangem.core.ui.test.TopAppBarTestTags
import com.tangem.core.ui.test.*
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
@ -48,6 +45,11 @@ class SwapSelectTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProv
useUnmergedTree = true
}
val searchBarBlock: KNode = child {
hasTestTag(BaseSearchBarTestTags.SEARCH_BAR)
useUnmergedTree = true
}
val searchBarIcon: KNode = child {
hasTestTag(SearchBarTestTags.ICON)
useUnmergedTree = true
@ -58,12 +60,28 @@ class SwapSelectTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProv
useUnmergedTree = true
}
val tryAgainButton: KNode = child {
hasText(getResourceString(R.string.alert_button_try_again))
useUnmergedTree = true
}
val unableToLoadData: KNode = child {
hasText(getResourceString(R.string.markets_loading_error_title))
useUnmergedTree = true
}
fun tokenWithName(tokenName: String): KNode = child {
hasTestTag(TokenElementsTestTags.TOKEN_TITLE)
hasAnyChild(withText(tokenName))
useUnmergedTree = true
}
fun marketsTokenWithName(title: String): KNode {
return child {
hasTestTag(MarketsTestTags.TOKENS_LIST_ITEM)
hasText(title)
}
}
}
internal fun BaseTestCase.onSwapSelectTokenScreen(function: SwapSelectTokenPageObject.() -> Unit) =

View file

@ -52,8 +52,8 @@ class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
hasTestTag(SwapTokenScreenTestTags.RECEIVE_AMOUNT_SHIMMER)
}
val swapTokensOnscreenButton: KNode = child {
hasTestTag(SwapTokenScreenTestTags.SWAP_BUTTON)
val replaceTokensButton: KNode = child {
hasTestTag(SwapTokenScreenTestTags.REPLACE_TOKENS_BUTTON)
}
val receiveAmount: KNode = child {

View file

@ -0,0 +1,262 @@
package com.tangem.tests.swap
import com.tangem.common.BaseTestCase
import com.tangem.common.R
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.extensions.performTextInputInChunks
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.scenarios.SwapEntryPoint
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.openSwapScreen
import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.*
import dagger.hilt.android.testing.HiltAndroidTest
import io.github.kakaocup.kakao.common.utilities.getResourceString
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Test
@HiltAndroidTest
class SearchAndSwapTest : BaseTestCase() {
@AllureId("8520")
@DisplayName("Search and Swap: add token without derivation")
@Test
fun addTokenWithoutDerivationTest() {
val swapTokenName = "Ethereum"
val receiveTokenName = "Tether"
val swapTokenSymbol = "ETH"
val receiveTokenSymbol = "USDT"
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Open 'Swap' screen") {
openSwapScreen(from = SwapEntryPoint.MainScreen)
}
step("Click on token with name '$swapTokenName'") {
onSwapSelectTokenScreen { tokenWithName(swapTokenName).performClick() }
waitForIdle()
}
step("Click on 'Search' text field") {
onSwapSelectTokenScreen { searchBarPlaceholderText.performClick() }
}
step("Type '$receiveTokenName' in input text field") {
onSwapSelectTokenScreen { searchBarBlock.performTextInputInChunks(receiveTokenName) }
}
step("Click on token with name '$receiveTokenName'") {
flakySafely(WAIT_UNTIL_TIMEOUT) {
onSwapSelectTokenScreen { marketsTokenWithName(receiveTokenName).clickWithAssertion() }
}
}
step("Click on 'Add' button") {
onAddTokenBottomSheet { addButton.performClick() }
}
step("Assert swap token symbol: '$swapTokenSymbol' is displayed") {
onSwapTokenScreen { swapTokenSymbol(swapTokenSymbol).assertIsDisplayed() }
}
step("Assert receive token symbol: '$receiveTokenSymbol' is displayed") {
onSwapTokenScreen { receiveTokenSymbol(receiveTokenSymbol).assertIsDisplayed() }
}
}
}
@AllureId("8519")
@DisplayName("Search and Swap: add token with derivation")
@Test
fun addTokenWithDerivationTest() {
val swapTokenName = "Ethereum"
val receiveTokenName = "TRON"
val swapTokenSymbol = "ETH"
val receiveTokenSymbol = "TRX"
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Open 'Swap' screen") {
openSwapScreen(from = SwapEntryPoint.MainScreen)
}
step("Click on token with name '$swapTokenName'") {
onSwapSelectTokenScreen { tokenWithName(swapTokenName).performClick() }
waitForIdle()
}
step("Click on 'Search' text field") {
onSwapSelectTokenScreen { searchBarPlaceholderText.performClick() }
}
step("Type '$receiveTokenSymbol' in input text field") {
onSwapSelectTokenScreen { searchBarBlock.performTextInputInChunks(receiveTokenSymbol) }
}
step("Click on token with name '$receiveTokenName'") {
flakySafely(WAIT_UNTIL_TIMEOUT) {
onSwapSelectTokenScreen { marketsTokenWithName(receiveTokenName).clickWithAssertion() }
}
}
step("Click on 'Add' button") {
onAddTokenBottomSheet { addButton.performClick() }
}
step("Assert swap token symbol: '$swapTokenSymbol' is displayed") {
onSwapTokenScreen { swapTokenSymbol(swapTokenSymbol).assertIsDisplayed() }
}
step("Assert receive token symbol: '$receiveTokenSymbol' is displayed") {
onSwapTokenScreen { receiveTokenSymbol(receiveTokenSymbol).assertIsDisplayed() }
}
}
}
@AllureId("8523")
@DisplayName("Search and Swap: Markets error")
@Test
fun marketsErrorTest() {
val swapTokenName = "Ethereum"
val scenarioName = "coins_list_api"
val scenarioState = "Error"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(scenarioName)
}
).run {
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
setWireMockScenarioState(scenarioName, scenarioState)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Open 'Swap' screen") {
openSwapScreen(from = SwapEntryPoint.MainScreen)
}
step("Click on token with name '$swapTokenName'") {
onSwapSelectTokenScreen { tokenWithName(swapTokenName).performClick() }
waitForIdle()
}
step("Assert 'Unable to load data...' error is displayed") {
onSwapSelectTokenScreen { unableToLoadData.assertIsDisplayed() }
}
step("Assert 'Try again' button is displayed") {
onSwapSelectTokenScreen { tryAgainButton.assertIsDisplayed() }
}
}
}
@AllureId("8522")
@DisplayName("Search and Swap: check 'Unsupported token pair' warning")
@Test
fun unsupportedTokenPairTest() {
val swapTokenName = "Ethereum"
val receiveTokenName = "Pepe"
val warningTitle = getResourceString(R.string.warning_express_unsupported_pair_title)
val warningMessage = getResourceString(R.string.warning_express_unsupported_pair_description)
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Open 'Swap' screen") {
openSwapScreen(from = SwapEntryPoint.MainScreen)
}
step("Click on token with name '$swapTokenName'") {
onSwapSelectTokenScreen { tokenWithName(swapTokenName).performClick() }
waitForIdle()
}
step("Click on 'Search' text field") {
onSwapSelectTokenScreen { searchBarPlaceholderText.performClick() }
}
step("Type '$receiveTokenName' in input text field") {
onSwapSelectTokenScreen { searchBarBlock.performTextInputInChunks(receiveTokenName) }
}
step("Click on token with name '$receiveTokenName'") {
flakySafely(WAIT_UNTIL_TIMEOUT) {
onSwapSelectTokenScreen { marketsTokenWithName(receiveTokenName).clickWithAssertion() }
}
}
step("Click on 'Add' button") {
onAddTokenBottomSheet { addButton.performClick() }
}
step("Assert warning title '$warningTitle' is displayed") {
onSwapTokenScreen { warningTitle(warningTitle).assertIsDisplayed() }
}
step("Assert warning message '$warningMessage' is displayed") {
onSwapTokenScreen { warningMessage(warningMessage).assertIsDisplayed() }
}
step("Assert warning icon is displayed'") {
onSwapTokenScreen { warningIcon(warningMessage).assertIsDisplayed() }
}
}
}
@AllureId("8521")
@DisplayName("Swap: search token on Swap token screen")
@Test
fun networkFeeTest() {
val tokenTitle = "Ethereum"
val swapTokenSymbol = "TRX"
val receiveTokenSymbol = "ETH"
val swapTokenName = "TRON"
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Click on token with name: '$tokenTitle'") {
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
}
step("Open 'Swap' screen") {
openSwapScreen(from = SwapEntryPoint.TokenDetails)
}
step("Click on 'Replace tokens' button") {
onSwapTokenScreen { replaceTokensButton.performClick() }
}
step("Click on 'Select token' icon") {
onSwapTokenScreen { selectTokenIcon.performClick() }
}
step("Click on 'Search' icon") {
onSwapChooseTokenScreen { searchIcon.performClick() }
}
step("Click on 'Search' text field") {
onSwapChooseTokenScreen { searchTextField.performClick() }
}
step("Type '$swapTokenSymbol' in 'Search' text field") {
onSwapChooseTokenScreen { searchTextField.performTextInputInChunks(swapTokenSymbol) }
}
step("Click on token with name: '$swapTokenName'") {
flakySafely(WAIT_UNTIL_TIMEOUT) {
onSwapChooseTokenScreen { marketsTokenWithTitle(swapTokenName).performClick() }
}
}
step("Click on 'Add' button") {
onAddTokenBottomSheet { addButton.performClick() }
}
step("Assert swap token symbol: '$swapTokenSymbol' is displayed") {
onSwapTokenScreen { swapTokenSymbol(swapTokenSymbol).assertIsDisplayed() }
}
step("Assert receive token symbol: '$receiveTokenSymbol' is displayed") {
onSwapTokenScreen { receiveTokenSymbol(receiveTokenSymbol).assertIsDisplayed() }
}
}
}
}

View file

@ -63,7 +63,7 @@ class SwapTokenScreenTest : BaseTestCase() {
step("Assert 'Swap tokens on screen' button is displayed") {
onSwapTokenScreen {
flakySafely(WAIT_UNTIL_TIMEOUT) {
swapTokensOnscreenButton.assertIsDisplayed()
replaceTokensButton.assertIsDisplayed()
}
}
}
@ -203,7 +203,7 @@ class SwapTokenScreenTest : BaseTestCase() {
step("Assert 'Swap tokens on screen' button is displayed") {
onSwapTokenScreen {
flakySafely(WAIT_UNTIL_TIMEOUT) {
swapTokensOnscreenButton.assertIsDisplayed()
replaceTokensButton.assertIsDisplayed()
}
}
}
@ -307,7 +307,7 @@ class SwapTokenScreenTest : BaseTestCase() {
step("Assert 'Swap tokens on screen' button is displayed") {
onSwapTokenScreen {
flakySafely(WAIT_UNTIL_TIMEOUT) {
swapTokensOnscreenButton.assertIsDisplayed()
replaceTokensButton.assertIsDisplayed()
}
}
}
@ -397,7 +397,7 @@ class SwapTokenScreenTest : BaseTestCase() {
step("Assert 'Swap tokens on screen' button is displayed") {
onSwapTokenScreen {
flakySafely(WAIT_UNTIL_TIMEOUT) {
swapTokensOnscreenButton.assertIsDisplayed()
replaceTokensButton.assertIsDisplayed()
}
}
}
@ -442,7 +442,7 @@ class SwapTokenScreenTest : BaseTestCase() {
onSwapTokenScreen { receiveTokenSymbol(receiveTokenSymbol).assertIsDisplayed() }
}
step("Click on 'Swap tokens on screen' button") {
onSwapTokenScreen { swapTokensOnscreenButton.performClick() }
onSwapTokenScreen { replaceTokensButton.performClick() }
waitForIdle()
}
step("Assert new swap token symbol: '$receiveTokenSymbol' is displayed") {

View file

@ -10,7 +10,7 @@ object SwapTokenScreenTestTags {
const val RECEIVE_CARD = "SWAP_TOKEN_SCREEN_RECEIVE_CARD"
const val RECEIVE_AMOUNT_SHIMMER = "SWAP_TOKEN_SCREEN_RECEIVE_AMOUNT_SHIMMER"
const val PROVIDERS_BLOCK = "SWAP_TOKEN_SCREEN_PROVIDERS_BLOCK"
const val SWAP_BUTTON = "SWAP_TOKEN_SCREEN_SWAP_BUTTON"
const val REPLACE_TOKENS_BUTTON = "SWAP_TOKEN_SCREEN_REPLACE_TOKENS_BUTTON"
const val TOKEN = "SWAP_TOKEN_SCREEN_TOKEN"
const val TOKEN_SYMBOL = "SWAP_TOKEN_SCREEN_TOKEN_SYMBOL"
const val TOKEN_ICON = "SWAP_TOKEN_SCREEN_TOKEN_ICON"

View file

@ -284,7 +284,7 @@ private fun SwapButton(state: SwapStateHolder, modifier: Modifier = Modifier) {
indication = ripple(),
interactionSource = remember { MutableInteractionSource() },
)
.testTag(SwapTokenScreenTestTags.SWAP_BUTTON),
.testTag(SwapTokenScreenTestTags.REPLACE_TOKENS_BUTTON),
) {
when (state.changeCardsButtonState) {
ChangeCardsButtonState.UPDATE_IN_PROGRESS -> {