diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/SwapScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/SwapScenarios.kt index a86ca56854..cc2c466cb3 100644 --- a/app/src/androidTest/kotlin/com/tangem/scenarios/SwapScenarios.kt +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/SwapScenarios.kt @@ -170,6 +170,45 @@ fun BaseTestCase.checkStoriesChanges() { } } +fun BaseTestCase.selectFeeType(feeType: FeeType, selectedFeeAmount: String) { + step("Click on 'Select fee' icon") { + onSwapTokenScreen { selectFeeIcon.performClick() } + } + + when (feeType) { + FeeType.Market -> { + step("Click on 'Market' item") { + onSwapSelectNetworkFeeBottomSheet { marketSelectorItem.performClick() } + } + step("Assert fee amount is equal to 'Market' fee:'$selectedFeeAmount'") { + onSwapTokenScreen { feeAmount.assertTextContains(selectedFeeAmount, substring = true) } + } + } + FeeType.Fast -> { + step("Click on 'Fast' item") { + onSwapSelectNetworkFeeBottomSheet { fastSelectorItem.performClick() } + } + step("Assert fee amount is equal to 'Fast' fee:'$selectedFeeAmount'") { + onSwapTokenScreen { feeAmount.assertTextContains(selectedFeeAmount, substring = true) } + } + } + } +} + +fun BaseTestCase.chackUnableToCoverFeeNotification(networkName: String, currencySymbol: String) { + step("Assert 'Unable to cover '$networkName' fee notification title is displayed'") { + onSwapTokenScreen { unableToCoverFeeNotificationTitle(networkName).assertIsDisplayed() } + } + step("Assert 'Unable to cover '$networkName' fee notification text is displayed'") { + onSwapTokenScreen { + unableToCoverFeeNotificationText( + currencyName = networkName, + currencySymbol = currencySymbol + ).assertIsDisplayed() + } + } +} + sealed class SwapEntryPoint { object MainScreen : SwapEntryPoint() object TokenDetails : SwapEntryPoint() @@ -177,4 +216,9 @@ sealed class SwapEntryPoint { object TokenActionsBottomSheet : SwapEntryPoint() } +enum class FeeType { + Market, + Fast +} + diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SwapChooseTokenPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SwapChooseTokenPageObject.kt index e8e256017c..6da3d7a7f5 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/SwapChooseTokenPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/SwapChooseTokenPageObject.kt @@ -3,7 +3,8 @@ 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.TokenElementsTestTags +import com.tangem.core.ui.test.AppBarWithSearchTestTags +import com.tangem.core.ui.test.BuyTokenScreenTestTags 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 @@ -21,11 +22,30 @@ class SwapChooseTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProv hasText(getResourceString(R.string.exchange_tokens_available_tokens_header)) } - fun tokenWithTitle(tokenTitle: String): KNode = child { - hasTestTag(TokenElementsTestTags.TOKEN_TITLE) - hasAnyDescendant(withText(tokenTitle)) - useUnmergedTree = true + val searchIcon: KNode = child { + hasTestTag(AppBarWithSearchTestTags.SEARCH_ICON) + } + + val searchTextField: KNode = child { + hasTestTag(AppBarWithSearchTestTags.TEXT_FIELD) + } + + val noTokensFoundText: KNode = child { + hasText(getResourceString(R.string.express_token_list_empty_search)) + } + + fun tokenWithTitle(tokenTitle: String, availableForSwap: Boolean = true): KNode = child { + hasTestTag(BuyTokenScreenTestTags.LAZY_LIST_ITEM) + hasAnyDescendant(withText(tokenTitle)) + if (!availableForSwap) { + hasAnyDescendant( + withText( + getResourceString(R.string.tokens_list_unavailable_to_swap_source_header) + ) + ) } + useUnmergedTree = true + } } internal fun BaseTestCase.onSwapChooseTokenScreen(function: SwapChooseTokenPageObject.() -> Unit) = diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SwapSelectNetworkFeeBottomSheetPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SwapSelectNetworkFeeBottomSheetPageObject.kt index 6705194521..007cfe289c 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/SwapSelectNetworkFeeBottomSheetPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/SwapSelectNetworkFeeBottomSheetPageObject.kt @@ -2,6 +2,7 @@ package com.tangem.screens import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import com.tangem.common.BaseTestCase +import com.tangem.core.ui.test.BaseButtonTestTags import com.tangem.core.ui.test.SelectNetworkFeeBottomSheetTestTags import com.tangem.wallet.R import io.github.kakaocup.compose.node.element.ComposeScreen @@ -33,6 +34,12 @@ class SwapSelectNetworkFeeBottomSheetPageObject(semanticsProvider: SemanticsNode hasTestTag(SelectNetworkFeeBottomSheetTestTags.LEARN_MORE_TEXT) useUnmergedTree = true } + + val applyButton: KNode = child { + hasTestTag(BaseButtonTestTags.TEXT) + hasText(getResourceString(R.string.common_apply)) + useUnmergedTree = true + } } internal fun BaseTestCase.onSwapSelectNetworkFeeBottomSheet(function: SwapSelectNetworkFeeBottomSheetPageObject.() -> Unit) = diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt index 15be0dee77..2d705445e0 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt @@ -43,6 +43,11 @@ class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) useUnmergedTree = true } + val feeAmount: KNode = child { + hasParent(withTestTag(FeeSelectorBlockTestTags.FEE_AMOUNT)) + useUnmergedTree = true + } + val receiveAmountShimmer: KNode = child { hasTestTag(SwapTokenScreenTestTags.RECEIVE_AMOUNT_SHIMMER) } @@ -71,6 +76,43 @@ class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) useUnmergedTree = true } + fun unableToCoverFeeNotificationTitle(networkName: String): KNode = child { + hasTestTag(NotificationTestTags.TITLE) + hasText( + getResourceString( + R.string.warning_express_not_enough_fee_for_token_tx_title, + networkName + ) + ) + useUnmergedTree = true + } + + fun unableToCoverFeeNotificationText(currencyName: String, currencySymbol: String): KNode = child { + hasTestTag(NotificationTestTags.MESSAGE) + hasText( + getResourceString( + R.string.warning_express_not_enough_fee_for_token_tx_description, + currencyName, + currencySymbol + ) + ) + useUnmergedTree = true + } + + fun unableToCoverFeeNotificationIcon(networkName: String): KNode = child { + hasTestTag(NotificationTestTags.ICON) + hasAnySibling(withTestTag(NotificationTestTags.TITLE)) + hasAnySibling( + withText( + getResourceString( + R.string.warning_express_not_enough_fee_for_token_tx_title, + networkName, + ) + ) + ) + useUnmergedTree = true + } + val refreshButton: KNode = child { hasTestTag(BaseButtonTestTags.BUTTON) hasText(getResourceString(R.string.warning_button_refresh)) @@ -95,15 +137,30 @@ class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) useUnmergedTree = true } + val insufficientFundsErrorTitle: KNode = child { + hasTestTag(SendScreenTestTags.AMOUNT_CONTAINER_TITLE) + hasText(getResourceString(R.string.swapping_insufficient_funds)) + useUnmergedTree = true + } + val receiveFiatAmount: KNode = child { hasTestTag(SwapTokenScreenTestTags.RECEIVE_FIAT_AMOUNT) } + val receiveFiatAmountWithPriceImpactWarning: KNode = child { + hasTestTag(SwapTokenScreenTestTags.RECEIVE_FIAT_AMOUNT_WITH_PRICE_IMPACT_WARNING) + } + + val receiveFiatAmountInformationIcon: KNode = child { + hasTestTag(SwapTokenScreenTestTags.RECEIVE_FIAT_AMOUNT_INFORMATION_ICON) + useUnmergedTree = true + } + val swapFiatAmount: KNode = child { hasTestTag(SwapTokenScreenTestTags.SWAP_FIAT_AMOUNT) } - val changeTokenIcon: KNode = child { + val selectTokenIcon: KNode = child { hasTestTag(SwapTokenScreenTestTags.SELECT_TOKEN_ICON) } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapChooseTokenScreenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapChooseTokenScreenTest.kt new file mode 100644 index 0000000000..9bf3c4f8a1 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapChooseTokenScreenTest.kt @@ -0,0 +1,182 @@ +package com.tangem.tests.swap + +import com.tangem.common.BaseTestCase +import com.tangem.common.annotations.ApiEnv +import com.tangem.common.annotations.ApiEnvConfig +import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO +import com.tangem.common.extensions.* +import com.tangem.common.utils.resetWireMockScenarioState +import com.tangem.common.utils.setWireMockScenarioState +import com.tangem.datasource.api.common.config.ApiConfig +import com.tangem.datasource.api.common.config.ApiEnvironment +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.qameta.allure.kotlin.AllureId +import io.qameta.allure.kotlin.junit4.DisplayName +import org.junit.Test + +@HiltAndroidTest +class SwapChooseTokenScreenTest : BaseTestCase() { + + @ApiEnv( + ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD) + ) + @AllureId("8505") + @DisplayName("Swap: check available to swap tokens list") + @Test + fun checkAvailableToSwapTokensListTest() { + val tokenTitle = "Polygon" + val inputAmount = "100" + val ethereum = "Ethereum" + val polExMatic = "POL (ex-MATIC)" + val bitcoin = "Bitcoin" + val scenarioState = "CustomTokenAndJesusAdded" + val jesusCoin = "Jesus Coin" + val salam = "Salam" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(USER_TOKENS_API_SCENARIO) + } + ).run { + + step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$scenarioState'") { + setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = scenarioState) + } + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Click on token with name: '$tokenTitle'") { + onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() } + } + step("Assert title: '$tokenTitle' is displayed") { + onTokenDetailsScreen { title.assertIsDisplayed() } + } + step("Open 'Swap' screen") { + openSwapScreen(from = SwapEntryPoint.TokenDetails) + } + step("Assert 'You swap' block is displayed") { + onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } + } + step("Input swap amount = '$inputAmount'") { + waitForIdle() + onSwapTokenScreen { + textInput.clickWithAssertion() + textInput.performTextReplacement(inputAmount) + } + } + step("Click on 'Select token' icon") { + onSwapTokenScreen { selectTokenIcon.performClick() } + } + step("Assert '$ethereum' is displayed") { + onSwapChooseTokenScreen { tokenWithTitle(ethereum).assertIsDisplayed() } + } + step("Assert '$polExMatic' is displayed") { + onSwapChooseTokenScreen { tokenWithTitle(polExMatic).assertIsDisplayed() } + } + step("Assert '$bitcoin' is not displayed") { + onSwapChooseTokenScreen { tokenWithTitle(bitcoin).assertIsNotDisplayed() } + } + step("Assert '$jesusCoin' is displayed and unavailable for swap") { + onSwapChooseTokenScreen { + tokenWithTitle( + tokenTitle = jesusCoin, + availableForSwap = false + ).assertIsDisplayed() + } + } + step("Assert custom token without backend id '$salam' is not displayed") { + onSwapChooseTokenScreen { tokenWithTitle(salam).assertIsNotDisplayed() } + } + } + } + + @ApiEnv( + ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD) + ) + @AllureId("8506") + @DisplayName("Swap: check search on choose swap token screen") + @Test + fun checkSearchOnSwapChooseTokenScreenTest() { + val tokenTitle = "Polygon" + val inputAmount = "100" + val ethereum = "Ethereum" + val polExMatic = "POL (ex-MATIC)" + val polExMaticSymbol = "POL" + val invalidSearchText = "f" + val validSearchText = "pol" + + setupHooks().run { + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Click on token with name: '$tokenTitle'") { + onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() } + } + step("Assert title: '$tokenTitle' is displayed") { + onTokenDetailsScreen { title.assertIsDisplayed() } + } + step("Open 'Swap' screen") { + openSwapScreen(from = SwapEntryPoint.TokenDetails) + } + step("Assert 'You swap' block is displayed") { + onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } + } + step("Input swap amount = '$inputAmount'") { + waitForIdle() + onSwapTokenScreen { + textInput.clickWithAssertion() + textInput.performTextReplacement(inputAmount) + } + } + 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 invalid search text: '$invalidSearchText' in 'Search' text field") { + onSwapChooseTokenScreen { searchTextField.performTextReplacement(invalidSearchText) } + } + step("Assert '$ethereum' is not displayed") { + onSwapChooseTokenScreen { tokenWithTitle(ethereum).assertIsNotDisplayed() } + } + step("Assert '$polExMatic' is not displayed") { + onSwapChooseTokenScreen { tokenWithTitle(polExMatic).assertIsNotDisplayed() } + } + step("Press 'Delete' button") { + device.uiDevice.pressDelete() + } + step("Type valid search text: '$validSearchText' in 'Search' text field") { + onSwapChooseTokenScreen { searchTextField.performTextReplacement(validSearchText) } + } + step("Assert '$ethereum' is not displayed") { + onSwapChooseTokenScreen { tokenWithTitle(ethereum).assertIsNotDisplayed() } + } + step("Assert '$polExMatic' is displayed") { + onSwapChooseTokenScreen { tokenWithTitle(polExMatic).assertIsDisplayed() } + } + step("Select new receive token: $polExMatic") { + onSwapChooseTokenScreen { tokenWithTitle(polExMatic).performClick() } + } + step("Assert new receive token symbol: '$polExMaticSymbol' is displayed") { + onSwapTokenScreen { receiveTokenSymbol(polExMaticSymbol).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 2048e0fa7e..e4112e9c00 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenTest.kt @@ -4,16 +4,16 @@ 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.USER_TOKENS_API_SCENARIO import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG import com.tangem.common.extensions.* +import com.tangem.common.utils.resetWireMockScenarioState import com.tangem.common.utils.resetWireMockScenarios +import com.tangem.common.utils.setWireMockScenarioState import com.tangem.datasource.api.common.config.ApiConfig import com.tangem.datasource.api.common.config.ApiEnvironment -import com.tangem.scenarios.SwapEntryPoint -import com.tangem.scenarios.openMainScreen -import com.tangem.scenarios.openSwapScreen -import com.tangem.scenarios.synchronizeAddresses +import com.tangem.scenarios.* import com.tangem.screens.* import dagger.hilt.android.testing.HiltAndroidTest import io.qameta.allure.kotlin.AllureId @@ -266,6 +266,9 @@ class SwapTokenScreenTest : BaseTestCase() { } } + @ApiEnv( + ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD) + ) @AllureId("2828") @DisplayName("Swap: network fee") @Test @@ -317,6 +320,9 @@ class SwapTokenScreenTest : BaseTestCase() { } } + @ApiEnv( + ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD) + ) @AllureId("575") @DisplayName("Swap: check UI") @Test @@ -354,7 +360,7 @@ class SwapTokenScreenTest : BaseTestCase() { onSwapTokenScreen { receiveTokenSymbol(receiveTokenSymbol).assertIsDisplayed() } } step("Click on 'Select token' icon") { - onSwapTokenScreen { changeTokenIcon.performClick() } + onSwapTokenScreen { selectTokenIcon.performClick() } } step("Select new receive token: $newReceiveToken") { onSwapChooseTokenScreen { tokenWithTitle(newReceiveToken).performClick() } @@ -401,6 +407,9 @@ class SwapTokenScreenTest : BaseTestCase() { } } + @ApiEnv( + ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD) + ) @AllureId("5162") @DisplayName("Swap: check swap tokens switch") @Test @@ -444,4 +453,293 @@ class SwapTokenScreenTest : BaseTestCase() { } } } + + @AllureId("573") + @DisplayName("Swap: check 'Swap' button availability") + @Test + fun checkSwapButtonAvailabilityTest() { + val polygon = "Polygon" + val bitcoin = "Bitcoin" + val salam = "Salam" + val jesusCoin = "Jesus Coin" + val myria = "Myria" + val scenarioState = "CustomTokenAndJesusAdded" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(USER_TOKENS_API_SCENARIO) + } + ).run { + + step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$scenarioState'") { + setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = scenarioState) + } + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Click on token with name: '$polygon'") { + onMainScreen { tokenWithTitleAndAddress(polygon).clickWithAssertion() } + } + step("Assert 'Swap' button is not dimmed. Swap available") { + onTokenDetailsScreen { swapButton().assertIsDimmed(false) } + } + step("Press 'Back' button") { + device.uiDevice.pressBack() + } + step("Click on token with name: '$bitcoin'. Swap unavailable") { + onMainScreen { tokenWithTitleAndAddress(bitcoin).clickWithAssertion() } + } + step("Assert 'Swap' button is dimmed") { + onTokenDetailsScreen { swapButton().assertIsDimmed(true) } + } + step("Press 'Back' button") { + device.uiDevice.pressBack() + } + step("Click on unknown custom token with name: '$salam'. Swap unavailable") { + onMainScreen { tokenWithTitleAndAddress(salam).clickWithAssertion() } + } + step("Assert 'Swap' button is dimmed") { + onTokenDetailsScreen { swapButton().assertIsDimmed(true) } + } + step("Press 'Back' button") { + device.uiDevice.pressBack() + } + step("Swipe up") { + onMainScreen { tokenWithTitleAndAddress(jesusCoin).assertIsDisplayed() } + waitForIdle() + swipeVertical(SwipeDirection.UP) + } + step("Click on token with name: '$jesusCoin' in 'Ethereum' network. Swap unavailable") { + waitForIdle() + onMainScreen { tokenWithTitleAndAddress(jesusCoin).clickWithAssertion() } + } + step("Assert 'Swap' button is dimmed") { + onTokenDetailsScreen { swapButton().assertIsDimmed(true) } + } + step("Press 'Back' button") { + device.uiDevice.pressBack() + } + step("Click on custom token with 'exchangeAvailable=true': '$myria'. Swap unavailable") { + onMainScreen { tokenWithTitleAndAddress(myria).clickWithAssertion() } + } + step("Assert 'Swap' button is dimmed") { + onTokenDetailsScreen { swapButton().assertIsDimmed(true) } + } + } + } + + @ApiEnv( + ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD) + ) + @AllureId("583") + @DisplayName("Swap: check switch fee type (enable to cover 'Market' and 'Fast' fee)") + @Test + fun enableToCoverMarketAndFastFeeTest() { + val tokenName = "Ethereum" + val inputAmount = "0.99" + val market = "Market" + val fast = "Fast" + val marketFeeAmount = "$1.12" + val fastFeeAmount = "$1.43" + + setupHooks().run { + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Click on token with name: '$tokenName'") { + onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() } + } + step("Open 'Swap' screen") { + openSwapScreen(from = SwapEntryPoint.TokenDetails) + } + step("Assert 'You swap' block is displayed") { + onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } + } + step("Input swap amount = '$inputAmount'") { + waitForIdle() + onSwapTokenScreen { + textInput.clickWithAssertion() + textInput.performTextReplacement(inputAmount) + } + } + step("Select '$market' fee type") { + flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { + selectFeeType(FeeType.Market, selectedFeeAmount = marketFeeAmount) + } + } + step("Select '$fast' fee type") { + flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { + selectFeeType(FeeType.Fast, selectedFeeAmount = fastFeeAmount) + } + } + } + } + + @ApiEnv( + ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD) + ) + @AllureId("8536") + @DisplayName("Swap: check switch fee type (unable to cover 'Market' and 'Fast' fee)") + @Test + fun unableToCoverMarketAndFastFeeTest() { + val tokenName = "POL (ex-MATIC)" + val inputAmount = "0.0001" + val market = "Market" + val fast = "Fast" + val marketFeeAmount = "$18,932" + val fastFeeAmount = "$24,139" + val scenarioName = "eth_network_balance" + val scenarioState = "LessThanDollar" + val networkName = "Ethereum" + val currencySymbol = "ETH" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(scenarioName) + } + ).run { + + step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") { + setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState) + } + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Click on token with name: '$tokenName'") { + onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() } + } + step("Open 'Swap' screen") { + openSwapScreen(from = SwapEntryPoint.TokenDetails) + } + step("Assert 'You swap' block is displayed") { + onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } + } + step("Input swap amount = '$inputAmount'") { + waitForIdle() + onSwapTokenScreen { + textInput.clickWithAssertion() + textInput.performTextReplacement(inputAmount) + } + } + step("Select '$market' fee type") { + flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { + selectFeeType(FeeType.Market, selectedFeeAmount = marketFeeAmount) + } + } + step("Check 'Unable to cover '$networkName' fee notification") { + chackUnableToCoverFeeNotification(networkName = networkName, currencySymbol = currencySymbol) + } + step("Assert 'Swap' button is disabled") { + onSwapTokenScreen { swapButton.assertIsNotEnabled() } + } + step("Select '$fast' fee type") { + flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { + selectFeeType(FeeType.Fast, selectedFeeAmount = fastFeeAmount) + } + } + step("Check 'Unable to cover '$networkName' fee notification") { + chackUnableToCoverFeeNotification(networkName = networkName, currencySymbol = currencySymbol) + } + step("Assert 'Swap' button is disabled") { + onSwapTokenScreen { swapButton.assertIsNotEnabled() } + } + } + } + + @ApiEnv( + ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD) + ) + @AllureId("8537") + @DisplayName("Swap: check switch fee type (unable to cover 'Fast' fee)") + @Test + fun unableToCoverFastFeeTest() { + val tokenName = "POL (ex-MATIC)" + val inputAmount = "3000" + val fastFeeType = "Fast" + val fastFeeAmount = "$2," + val marketFeeType = "Market" + val marketFeeAmount = "$1." + val scenarioName = "eth_fee_history" + val scenarioState = "UnableToCoverFastFee" + val networkName = "Ethereum" + val currencySymbol = "ETH" + + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(scenarioName) + } + ).run { + + step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") { + setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState) + } + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Click on token with name: '$tokenName'") { + onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() } + } + step("Open 'Swap' screen") { + openSwapScreen(from = SwapEntryPoint.TokenDetails) + } + step("Assert 'You swap' block is displayed") { + onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } + } + step("Input swap amount = '$inputAmount'") { + waitForIdle() + onSwapTokenScreen { + textInput.clickWithAssertion() + textInput.performTextReplacement(inputAmount) + } + } + step("Assert 'Swap' button is enabled") { + flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { + onSwapTokenScreen { swapButton.assertIsEnabled() } + } + } + step("Select '$fastFeeType' fee type") { + flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { + selectFeeType(FeeType.Fast, selectedFeeAmount = fastFeeAmount) + } + } + step("Assert fee amount is equal to '$fastFeeType' fee:'$fastFeeAmount'") { + onSwapTokenScreen { feeAmount.assertTextContains(fastFeeAmount, substring = true) } + } + step("Check 'Unable to cover '$networkName' fee notification") { + chackUnableToCoverFeeNotification(networkName = networkName, currencySymbol = currencySymbol) + } + step("Assert 'Swap' button is disabled") { + onSwapTokenScreen { swapButton.assertIsNotEnabled() } + } + step("Select '$marketFeeType' fee type") { + flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { + selectFeeType(FeeType.Market, selectedFeeAmount = marketFeeAmount) + } + } + step("Assert fee amount is equal to '$marketFeeType' fee:'$marketFeeAmount'") { + onSwapTokenScreen { feeAmount.assertTextContains(marketFeeAmount, substring = true) } + } + step("Assert 'Swap' button is enabled") { + waitForIdle() + onSwapTokenScreen { swapButton.assertIsEnabled() } + } + } + } } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenWarningsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenWarningsTest.kt new file mode 100644 index 0000000000..4ba809b933 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenWarningsTest.kt @@ -0,0 +1,314 @@ +package com.tangem.tests.swap + +import com.tangem.common.BaseTestCase +import com.tangem.common.R +import com.tangem.common.annotations.ApiEnv +import com.tangem.common.annotations.ApiEnvConfig +import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO +import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO +import com.tangem.common.extensions.* +import com.tangem.common.utils.resetWireMockScenarioState +import com.tangem.common.utils.setWireMockScenarioState +import com.tangem.datasource.api.common.config.ApiConfig +import com.tangem.datasource.api.common.config.ApiEnvironment +import com.tangem.scenarios.SwapEntryPoint +import com.tangem.scenarios.chackUnableToCoverFeeNotification +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 SwapTokenScreenWarningsTest : BaseTestCase() { + + @ApiEnv( + ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD) + ) + @AllureId("580") + @DisplayName("Swap: check 'Insufficient funds' warning") + @Test + fun checkSwapInsufficientFundsWarningTest() { + val tokenTitle = "Polygon" + val inputAmount = "1000" + + setupHooks().run { + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Click on token with name: '$tokenTitle'") { + onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() } + } + step("Assert title: '$tokenTitle' is displayed") { + onTokenDetailsScreen { title.assertIsDisplayed() } + } + step("Open 'Swap' screen") { + openSwapScreen(from = SwapEntryPoint.TokenDetails) + } + step("Assert 'You swap' block is displayed") { + onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } + } + step("Input swap amount = '$inputAmount'") { + waitForIdle() + onSwapTokenScreen { + textInput.clickWithAssertion() + textInput.performTextReplacement(inputAmount) + } + } + step("Assert 'Insufficient funds' error is displayed") { + waitForIdle() + onSwapTokenScreen { insufficientFundsErrorTitle.assertIsDisplayed() } + } + } + } + + @AllureId("8502") + @DisplayName("Swap: check 'Unable to cover network fee' warning") + @Test + fun checkUnableToCoverBlockchainFeeWarningTest() { + val tokenTitle = "USDC" + val inputAmount = "1000" + val tokensScenarioState = "SolanaUSDC" + val balanceScenarioName = "solana_balance" + val balanceScenarioState = "Empty" + val networkName = "Solana" + val currencySymbol = "SOL" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(USER_TOKENS_API_SCENARIO) + resetWireMockScenarioState(QUOTES_API_SCENARIO) + resetWireMockScenarioState(balanceScenarioState) + } + ).run { + + step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$tokensScenarioState'") { + setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = tokensScenarioState) + } + step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: $tokensScenarioState") { + setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = tokensScenarioState) + } + step("Set WireMock scenario: '$balanceScenarioName' to state: $balanceScenarioState") { + setWireMockScenarioState(scenarioName = balanceScenarioName, state = balanceScenarioState) + } + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Click on token with name: '$tokenTitle'") { + onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() } + } + step("Assert title: '$tokenTitle' is displayed") { + onTokenDetailsScreen { title.assertIsDisplayed() } + } + step("Open 'Swap' screen") { + openSwapScreen(from = SwapEntryPoint.TokenDetails) + } + step("Assert 'You swap' block is displayed") { + onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } + } + step("Input swap amount = '$inputAmount'") { + waitForIdle() + onSwapTokenScreen { + textInput.clickWithAssertion() + textInput.performTextReplacement(inputAmount) + } + } + step("Check 'Unable to cover '$networkName' fee notification") { + chackUnableToCoverFeeNotification(networkName = networkName, currencySymbol = currencySymbol) + } + step("Assert 'Unable to cover '$networkName' fee notification icon is displayed'") { + onSwapTokenScreen { unableToCoverFeeNotificationIcon(networkName).assertIsDisplayed() } + } + step("Assert 'Swap' button is disabled") { + onSwapTokenScreen { swapButton.assertIsNotEnabled() } + } + } + } + + @AllureId("8503") + @DisplayName("Swap: check 'High price impact' warning on CEX") + @Test + fun checkHighPriceImpactWarningCEXTest() { + val tokenTitle = "USDC" + val inputAmount = "100" + val currencySymbol = "SOL" + val slippagePercent = "5%" + val tokensScenarioState = "SolanaUSDC" + val exchangeQuoteScenarioName = "exchange_quote_solana" + val exchangeQuoteScenarioState = "HighPriceImpact" + val dialogTitle = getResourceString(R.string.swapping_alert_title) + val dialogText = getResourceString( + R.string.swapping_alert_cex_description_with_slippage, + currencySymbol, + slippagePercent + ) + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(USER_TOKENS_API_SCENARIO) + resetWireMockScenarioState(QUOTES_API_SCENARIO) + resetWireMockScenarioState(exchangeQuoteScenarioName) + } + ).run { + + step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$tokensScenarioState'") { + setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = tokensScenarioState) + } + step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: $tokensScenarioState") { + setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = tokensScenarioState) + } + step("Set WireMock scenario: '$exchangeQuoteScenarioName' to state: $exchangeQuoteScenarioState") { + setWireMockScenarioState( + scenarioName = exchangeQuoteScenarioName, + state = exchangeQuoteScenarioState + ) + } + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Click on token with name: '$tokenTitle'") { + onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() } + } + step("Assert title: '$tokenTitle' is displayed") { + onTokenDetailsScreen { title.assertIsDisplayed() } + } + step("Open 'Swap' screen") { + openSwapScreen(from = SwapEntryPoint.TokenDetails) + } + step("Assert 'You swap' block is displayed") { + onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } + } + step("Input swap amount = '$inputAmount'") { + waitForIdle() + onSwapTokenScreen { + textInput.clickWithAssertion() + textInput.performTextReplacement(inputAmount) + } + } + step("Assert fiat amount with warning is displayed") { + onSwapTokenScreen { receiveFiatAmountWithPriceImpactWarning.assertTextContains("%", substring = true) } + } + step("Assert receive amount information icon is displayed") { + onSwapTokenScreen { receiveFiatAmountInformationIcon.assertIsDisplayed() } + } + step("Click on receive amount information icon") { + onSwapTokenScreen { receiveFiatAmountInformationIcon.performClick() } + } + step("Assert information dialog is displayed") { + onDialog { dialogContainer.assertIsDisplayed() } + } + step("Assert information dialog title is displayed") { + onDialog { title.assertTextEquals(dialogTitle) } + } + step("Assert information dialog text for CEX is displayed") { + onDialog { text.assertTextEquals(dialogText) } + } + step("Assert dialog 'OK' button is displayed") { + onDialog { okButton.assertIsDisplayed() } + } + } + } + + @AllureId("8504") + @DisplayName("Swap: check 'High price impact' warning on DEX") + @Test + fun checkHighPriceImpactWarningDEXTest() { + val tokenTitle = "Polygon" + val inputAmount = "1000" + val slippagePercent = "3.5%" + val dialogTitle = getResourceString(R.string.swapping_alert_title) + val highPriceImpactDescription = getResourceString(R.string.swapping_high_price_impact_description) + val swappingAlertDEXDescription = getResourceString(R.string.swapping_alert_dex_description) + val swappingAlertDEXDescriptionWithSlippage = getResourceString( + R.string.swapping_alert_dex_description_with_slippage, + slippagePercent + ) + val pairsToScenarioName = "polygon_pos_to_pairs" + val pairsFromScenarioName = "polygon_pos_from_pairs" + val scenarioState = "DexProvider" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(pairsToScenarioName) + resetWireMockScenarioState(pairsFromScenarioName) + } + ).run { + step("Set WireMock scenario: '$pairsToScenarioName' to state: $scenarioState") { + setWireMockScenarioState(scenarioName = pairsToScenarioName, state = scenarioState) + } + step("Set WireMock scenario: '$pairsFromScenarioName' to state: $scenarioState") { + setWireMockScenarioState(scenarioName = pairsFromScenarioName, state = scenarioState) + } + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Click on token with name: '$tokenTitle'") { + onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() } + } + step("Assert title: '$tokenTitle' is displayed") { + onTokenDetailsScreen { title.assertIsDisplayed() } + } + step("Open 'Swap' screen") { + openSwapScreen(from = SwapEntryPoint.TokenDetails) + } + step("Assert 'You swap' block is displayed") { + onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } + } + step("Input swap amount = '$inputAmount'") { + waitForIdle() + onSwapTokenScreen { + textInput.clickWithAssertion() + textInput.performTextReplacement(inputAmount) + } + } + step("Assert fiat amount with warning is displayed") { + onSwapTokenScreen { receiveFiatAmountWithPriceImpactWarning.assertTextContains("%", substring = true) } + } + step("Assert receive amount information icon is displayed") { + onSwapTokenScreen { receiveFiatAmountInformationIcon.assertIsDisplayed() } + } + step("Click on receive amount information icon") { + onSwapTokenScreen { receiveFiatAmountInformationIcon.performClick() } + } + step("Assert information dialog is displayed") { + onDialog { dialogContainer.assertIsDisplayed() } + } + step("Assert information dialog title is displayed") { + onDialog { title.assertTextEquals(dialogTitle) } + } + step("Assert information dialog text for DEX is displayed") { + onDialog { + text.assertTextContains(highPriceImpactDescription, substring = true) + text.assertTextContains(swappingAlertDEXDescription, substring = true) + text.assertTextContains(swappingAlertDEXDescriptionWithSlippage, substring = true) + } + } + step("Assert dialog 'OK' button is displayed") { + onDialog { okButton.assertIsDisplayed() } + } + step("Click on 'OK' button") { + onDialog { okButton.performClick() } + } + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithSearch.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithSearch.kt index f6f4dd18c0..32bf5c0667 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithSearch.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithSearch.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.ImeAction @@ -30,6 +31,7 @@ import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.TangemTextFieldsDefault import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.AppBarWithSearchTestTags /** * App bar with close icon and search functionality @@ -135,7 +137,8 @@ private fun CollapsedSearchView( contentDescription = null, modifier = Modifier .clickable { onExpandedChange(true) } - .padding(end = TangemTheme.dimens.spacing16), + .padding(end = TangemTheme.dimens.spacing16) + .testTag(AppBarWithSearchTestTags.SEARCH_ICON), ) } } @@ -210,7 +213,8 @@ private fun ExpandedSearchView( modifier = Modifier .fillMaxWidth() .focusRequester(textFieldFocusRequester) - .onFocusChanged { onFocusChange(it.hasFocus) }, + .onFocusChanged { onFocusChange(it.hasFocus) } + .testTag(AppBarWithSearchTestTags.TEXT_FIELD), placeholder = { Text(text = placeholderSearchText) }, diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/AppBarWithSearchTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/AppBarWithSearchTestTags.kt new file mode 100644 index 0000000000..d9b8583dcb --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/AppBarWithSearchTestTags.kt @@ -0,0 +1,6 @@ +package com.tangem.core.ui.test + +object AppBarWithSearchTestTags { + const val SEARCH_ICON = "APP_BAR_WITH_SEARCH_SEARCH_ICON" + const val TEXT_FIELD = "APP_BAR_WITH_SEARCH_TEXT_FIELD" +} \ No newline at end of file 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 3ddbefea69..9cdf82f3f3 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 @@ -16,5 +16,7 @@ object SwapTokenScreenTestTags { const val TOKEN_ICON = "SWAP_TOKEN_SCREEN_TOKEN_ICON" const val SELECT_TOKEN_ICON = "SWAP_TOKEN_SCREEN_SELECT_TOKEN_ICON" const val RECEIVE_FIAT_AMOUNT = "SWAP_TOKEN_SCREEN_RECEIVE_FIAT_AMOUNT" + const val RECEIVE_FIAT_AMOUNT_WITH_PRICE_IMPACT_WARNING = "SWAP_TOKEN_SCREEN_RECEIVE_FIAT_AMOUNT_WITH_PRICE_IMPACT" + const val RECEIVE_FIAT_AMOUNT_INFORMATION_ICON = "SWAP_TOKEN_SCREEN_PRICE_IMPACT_INFORMATION_ICON" const val SWAP_FIAT_AMOUNT = "SWAP_TOKEN_SCREEN_SWAP_FIAT_AMOUNT" } \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt index 25aac0f046..b29e4b8257 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt @@ -328,6 +328,9 @@ private fun Content( ), color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, + modifier = Modifier.testTag( + SwapTokenScreenTestTags.RECEIVE_FIAT_AMOUNT_WITH_PRICE_IMPACT_WARNING, + ), ) } else { AnimatedContent(targetState = amountEquivalent, label = "") { amount -> @@ -355,7 +358,9 @@ private fun Content( } else { TangemTheme.colors.text.tertiary }, - modifier = Modifier.align(Alignment.CenterVertically), + modifier = Modifier + .align(Alignment.CenterVertically) + .testTag(SwapTokenScreenTestTags.RECEIVE_FIAT_AMOUNT_INFORMATION_ICON), ) } }