diff --git a/app/src/androidTest/kotlin/com/tangem/common/extensions/KNode.kt b/app/src/androidTest/kotlin/com/tangem/common/extensions/KNode.kt index 3ce482fb53..f57b43288a 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/extensions/KNode.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/extensions/KNode.kt @@ -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) + } +} diff --git a/app/src/androidTest/kotlin/com/tangem/screens/AddTokenBottomSheetPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/AddTokenBottomSheetPageObject.kt new file mode 100644 index 0000000000..fc864f40e2 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/AddTokenBottomSheetPageObject.kt @@ -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(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) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SwapChooseTokenPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SwapChooseTokenPageObject.kt index 6da3d7a7f5..77b3e18ad1 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/SwapChooseTokenPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/SwapChooseTokenPageObject.kt @@ -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) = diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SwapSelectTokenPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SwapSelectTokenPageObject.kt index 83e09a9a09..b07573e5da 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/SwapSelectTokenPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/SwapSelectTokenPageObject.kt @@ -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) = diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt index 30fe6823bd..21f6b80423 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt @@ -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 { diff --git a/app/src/androidTest/kotlin/com/tangem/tests/swap/SearchAndSwapTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/swap/SearchAndSwapTest.kt new file mode 100644 index 0000000000..ce2450ee04 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/swap/SearchAndSwapTest.kt @@ -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() } + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenTest.kt index 515ca716f9..22046cdc8a 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenTest.kt @@ -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") { diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/SwapTokenScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/SwapTokenScreenTestTags.kt index 9cdf82f3f3..726604b2b1 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/SwapTokenScreenTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/SwapTokenScreenTestTags.kt @@ -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" diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index 813e71cc11..2fdde2b04a 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -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 -> {