Updated on 2026-08-14
This commit is contained in:
parent
8683516c59
commit
c81ba95db0
17 changed files with 827 additions and 10 deletions
|
|
@ -0,0 +1,90 @@
|
|||
package com.tangem.scenarios
|
||||
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
|
||||
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
|
||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.common.utils.setWireMockScenarioState
|
||||
import com.tangem.screens.*
|
||||
import io.qameta.allure.kotlin.Allure.step
|
||||
|
||||
/**
|
||||
* From the recipient step: fill the address and advance to the 'Send confirm' screen.
|
||||
* Uses `composeTestRule.waitUntil` because `flakySafely` is unavailable in extensions on [BaseTestCase].
|
||||
*/
|
||||
fun BaseTestCase.enterRecipientAndOpenSendConfirm(recipientAddress: String) {
|
||||
step("Type recipient address") {
|
||||
onSendAddressScreen { addressTextField.performTextReplacement(recipientAddress) }
|
||||
}
|
||||
step("Click on 'Next' button until 'Send confirm' screen opens") {
|
||||
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
runCatching { openSendConfirmScreenViaNextButton() }.isSuccess
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Enter the send amount, then fill the recipient and open the 'Send confirm' screen. */
|
||||
fun BaseTestCase.enterAmountAndOpenSendConfirm(amount: String, recipientAddress: String) {
|
||||
step("Type '$amount' in input text field") {
|
||||
onSendScreen {
|
||||
amountInputTextField.performClick()
|
||||
amountInputTextField.performTextReplacement(amount)
|
||||
}
|
||||
}
|
||||
step("Click on 'Next' button") {
|
||||
onSendScreen { nextButton.clickWithAssertion() }
|
||||
}
|
||||
enterRecipientAndOpenSendConfirm(recipientAddress)
|
||||
}
|
||||
|
||||
/**
|
||||
* On the 'Send confirm' screen, open the network-fee selector and switch the fee token from the
|
||||
* native coin to the given (stablecoin) token — the core gasless action repeated across the suite.
|
||||
*/
|
||||
fun BaseTestCase.selectStablecoinAsFeeToken(coinName: String, tokenName: String) {
|
||||
step("Click on 'Network fee' block") {
|
||||
onSendConfirmScreen {
|
||||
feeSelectorBlock.assertIsDisplayed()
|
||||
feeSelectorBlock.performClick()
|
||||
}
|
||||
}
|
||||
step("Click on '$coinName' fee token to open 'Choose token'") {
|
||||
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
runCatching { onSendFeeSelectorBottomSheet { feeTokenItem(coinName).performClick() } }.isSuccess
|
||||
}
|
||||
}
|
||||
step("Select '$tokenName' as the fee-paying token") {
|
||||
onSendFeeSelectorBottomSheet { feeTokenItem(tokenName).performClick() }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open an existing hot wallet (gasless signing needs a hot wallet, not the mock card), set the
|
||||
* portfolio and quotes mocks, and reach the send amount input for the given token.
|
||||
*/
|
||||
fun BaseTestCase.openGaslessSendScreenWithHotWallet(
|
||||
seedPhrase: String,
|
||||
tokenName: String,
|
||||
userTokensState: String,
|
||||
quotesState: String,
|
||||
) {
|
||||
step("Set WireMock scenario '$USER_TOKENS_API_SCENARIO' to '$userTokensState'") {
|
||||
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = userTokensState)
|
||||
}
|
||||
step("Set WireMock scenario '$QUOTES_API_SCENARIO' to '$quotesState'") {
|
||||
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = quotesState)
|
||||
}
|
||||
step("Open 'Main' screen with existing hot wallet") {
|
||||
openMainScreenWithExistingHotWallet(seedPhrase)
|
||||
}
|
||||
step("Click on token with name: '$tokenName'") {
|
||||
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
|
||||
}
|
||||
step("Click on 'Transfer' button") {
|
||||
onTokenDetailsScreen { transferButton.clickWithAssertion() }
|
||||
}
|
||||
step("Click on 'Send' button in bottom sheet") {
|
||||
onTransferBottomSheet { sendButton.clickWithAssertion() }
|
||||
}
|
||||
}
|
||||
|
|
@ -84,6 +84,18 @@ class SendConfirmPageObject(semanticsProvider: SemanticsNodeInteractionsProvider
|
|||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
fun warningMessageContaining(textPart: String): KNode = child {
|
||||
hasTestTag(NotificationTestTags.MESSAGE)
|
||||
hasText(textPart, substring = true)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
fun warningTitleContaining(textPart: String): KNode = child {
|
||||
hasTestTag(NotificationTestTags.TITLE)
|
||||
hasText(textPart, substring = true)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
fun warningIcon(message: String): KNode = child {
|
||||
hasTestTag(NotificationTestTags.ICON)
|
||||
hasAnySibling(withText(message))
|
||||
|
|
@ -145,6 +157,12 @@ class SendConfirmPageObject(semanticsProvider: SemanticsNodeInteractionsProvider
|
|||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
fun feeBlockCurrency(symbol: String): KNode = child {
|
||||
hasTestTag(FeeSelectorBlockTestTags.SELECTOR_BLOCK)
|
||||
hasAnyDescendant(withText(symbol))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val refreshButton: KNode = child {
|
||||
hasTestTag(BaseButtonTestTags.BUTTON)
|
||||
hasText(getResourceString(CoreUiR.string.warning_button_refresh))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
package com.tangem.screens
|
||||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.test.BaseBottomSheetTestTags
|
||||
import com.tangem.core.ui.test.BaseButtonTestTags
|
||||
import com.tangem.core.ui.test.SelectNetworkFeeBottomSheetTestTags
|
||||
import com.tangem.core.ui.test.TokenElementsTestTags
|
||||
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
|
||||
|
||||
/**
|
||||
* Gasless fee selector modal: the `NetworkFee` route (fee-paying token row + selected speed) and the
|
||||
* `ChooseToken` route. The `ChooseSpeed` route is covered by [SendSelectNetworkFeeBottomSheetPageObject].
|
||||
*/
|
||||
class SendFeeSelectorBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<SendFeeSelectorBottomSheetPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
val networkFeeTitle: KNode = child {
|
||||
hasTestTag(BaseBottomSheetTestTags.TITLE)
|
||||
hasText(getResourceString(R.string.common_network_fee_title))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val chooseTokenTitle: KNode = child {
|
||||
hasTestTag(BaseBottomSheetTestTags.TITLE)
|
||||
hasText(getResourceString(R.string.fee_selector_choose_token_title))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val feeTokenRow: KNode = child {
|
||||
hasTestTag(TokenElementsTestTags.TOKEN_TITLE)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
fun feeTokenItem(tokenName: String): KNode = child {
|
||||
hasTestTag(TokenElementsTestTags.TOKEN_TITLE)
|
||||
hasAnyChild(withText(tokenName))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
fun feeSpeedItemTitle(speed: String): KNode = child {
|
||||
hasTestTag(SelectNetworkFeeBottomSheetTestTags.REGULAR_ITEM_TITLE)
|
||||
hasText(speed)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val applyButton: KNode = child {
|
||||
hasTestTag(BaseButtonTestTags.BUTTON)
|
||||
hasAnyDescendant(withText(getResourceString(R.string.common_apply)))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val notEnoughFundsError: KNode = child {
|
||||
hasText(getResourceString(R.string.gasless_not_enough_funds_to_cover_token_fee))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onSendFeeSelectorBottomSheet(function: SendFeeSelectorBottomSheetPageObject.() -> Unit) =
|
||||
onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.tangem.screens
|
||||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.test.TransactionHistoryItemTestTags
|
||||
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 androidx.compose.ui.test.hasText as withText
|
||||
|
||||
class TxHistoryPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<TxHistoryPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
fun transactionItem(title: String): KNode = child {
|
||||
hasTestTag(TransactionHistoryItemTestTags.ITEM)
|
||||
hasAnyDescendant(withText(title))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
fun transactionAmount(title: String): KNode = transactionItem(title).child {
|
||||
hasTestTag(TransactionHistoryItemTestTags.AMOUNT)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
fun transactionCurrency(title: String): KNode = transactionItem(title).child {
|
||||
hasTestTag(TransactionHistoryItemTestTags.CURRENCY)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
fun transactionConfirmedStatus(title: String): KNode = transactionItem(title).child {
|
||||
hasTestTag(TransactionHistoryItemTestTags.STATUS_CONFIRMED)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onTxHistoryScreen(function: TxHistoryPageObject.() -> Unit) =
|
||||
onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -0,0 +1,317 @@
|
|||
package com.tangem.tests.send.gasless
|
||||
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.constants.TestConstants.ETHEREUM_RECIPIENT_ADDRESS
|
||||
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
|
||||
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
|
||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.common.utils.resetWireMockScenarioState
|
||||
import com.tangem.common.utils.setWireMockScenarioState
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.scenarios.enterAmountAndOpenSendConfirm
|
||||
import com.tangem.scenarios.enterRecipientAndOpenSendConfirm
|
||||
import com.tangem.scenarios.openSendScreen
|
||||
import com.tangem.scenarios.selectStablecoinAsFeeToken
|
||||
import com.tangem.screens.onSendConfirmScreen
|
||||
import com.tangem.screens.onSendFeeSelectorBottomSheet
|
||||
import com.tangem.screens.onSendScreen
|
||||
import com.tangem.screens.onSendSelectNetworkFeeBottomSheet
|
||||
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
|
||||
|
||||
/**
|
||||
* Gasless network-fee behaviour on the send summary (fee selector): availability, calculation,
|
||||
* speed options, switching the fee token, and balance-driven notifications. All run on the default
|
||||
* (cold) wallet without signing a transaction.
|
||||
*/
|
||||
@HiltAndroidTest
|
||||
class GaslessFeeTest : BaseTestCase() {
|
||||
|
||||
private val scenarioState = "PolygonUSDC"
|
||||
private val tokenName = "USDC"
|
||||
private val nativeTokenName = "Polygon"
|
||||
private val tokenAmount = "1"
|
||||
|
||||
@AllureId("5061")
|
||||
@DisplayName("Gasless: Network fee on summary is selectable and the stablecoin is available for the fee")
|
||||
@Test
|
||||
fun checkNetworkFeeTokenSelectionAvailableTest() {
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||
}
|
||||
).run {
|
||||
step("Open 'Send' screen for '$tokenName'") {
|
||||
openSendScreen(tokenName = tokenName, mockState = scenarioState)
|
||||
}
|
||||
step("Enter amount '$tokenAmount' and open the 'Send confirm' screen") {
|
||||
enterAmountAndOpenSendConfirm(amount = tokenAmount, recipientAddress = ETHEREUM_RECIPIENT_ADDRESS)
|
||||
}
|
||||
step("Assert 'Network fee' block with token selection is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onSendConfirmScreen {
|
||||
feeSelectorTitle.assertIsDisplayed()
|
||||
selectFeeIcon.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
step("Click on 'Network fee' block") {
|
||||
onSendConfirmScreen { feeSelectorBlock.performClick() }
|
||||
}
|
||||
step("Assert 'Network fee' bottom sheet is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onSendFeeSelectorBottomSheet { networkFeeTitle.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Click on '$nativeTokenName' fee token to open 'Choose token'") {
|
||||
onSendFeeSelectorBottomSheet { feeTokenItem(nativeTokenName).performClick() }
|
||||
}
|
||||
step("Assert 'Choose token' bottom sheet is displayed") {
|
||||
onSendFeeSelectorBottomSheet { chooseTokenTitle.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert '$tokenName' is available for the fee payment") {
|
||||
onSendFeeSelectorBottomSheet { feeTokenItem(tokenName).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("5062")
|
||||
@DisplayName("Gasless: network fee for a stablecoin is calculated and shown in the stablecoin")
|
||||
@Test
|
||||
fun checkFeeCalculatedInStablecoinTest() {
|
||||
val marketSpeed = getResourceString(R.string.common_fee_selector_option_market)
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||
}
|
||||
).run {
|
||||
step("Open 'Send' screen for '$tokenName'") {
|
||||
openSendScreen(tokenName = tokenName, mockState = scenarioState)
|
||||
}
|
||||
step("Enter amount '$tokenAmount' and open the 'Send confirm' screen") {
|
||||
enterAmountAndOpenSendConfirm(amount = tokenAmount, recipientAddress = ETHEREUM_RECIPIENT_ADDRESS)
|
||||
}
|
||||
step("Pay the network fee with '$tokenName' via the fee selector") {
|
||||
selectStablecoinAsFeeToken(coinName = nativeTokenName, tokenName = tokenName)
|
||||
}
|
||||
step("Assert the fee is shown under the '$marketSpeed' speed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onSendFeeSelectorBottomSheet { feeSpeedItemTitle(marketSpeed).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Click on 'Apply' button") {
|
||||
onSendFeeSelectorBottomSheet { applyButton.performClick() }
|
||||
}
|
||||
step("Assert the network fee is calculated in '$tokenName' (not in the coin) on the summary") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onSendConfirmScreen {
|
||||
feeBlockCurrency(tokenName).assertIsDisplayed()
|
||||
feeAmount.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("5064")
|
||||
@DisplayName("Gasless: only Market speed is available when paying the fee with a stablecoin")
|
||||
@Test
|
||||
fun checkOnlyMarketSpeedAvailableForStablecoinFeeTest() {
|
||||
val marketSpeed = getResourceString(R.string.common_fee_selector_option_market)
|
||||
val fastSpeed = getResourceString(R.string.common_fee_selector_option_fast)
|
||||
val slowSpeed = getResourceString(R.string.common_fee_selector_option_slow)
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||
}
|
||||
).run {
|
||||
step("Open 'Send' screen for '$tokenName'") {
|
||||
openSendScreen(tokenName = tokenName, mockState = scenarioState)
|
||||
}
|
||||
step("Enter amount '$tokenAmount' and open the 'Send confirm' screen") {
|
||||
enterAmountAndOpenSendConfirm(amount = tokenAmount, recipientAddress = ETHEREUM_RECIPIENT_ADDRESS)
|
||||
}
|
||||
step("Click on 'Network fee' block") {
|
||||
onSendConfirmScreen {
|
||||
feeSelectorBlock.assertIsDisplayed()
|
||||
feeSelectorBlock.performClick()
|
||||
}
|
||||
}
|
||||
step("Assert 'Network fee' bottom sheet is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onSendFeeSelectorBottomSheet { networkFeeTitle.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Click on '$nativeTokenName' fee token to open 'Choose token'") {
|
||||
onSendFeeSelectorBottomSheet { feeTokenItem(nativeTokenName).performClick() }
|
||||
}
|
||||
step("Assert 'Choose token' bottom sheet is displayed") {
|
||||
onSendFeeSelectorBottomSheet { chooseTokenTitle.assertIsDisplayed() }
|
||||
}
|
||||
step("Select '$tokenName' as the fee-paying token") {
|
||||
onSendFeeSelectorBottomSheet { feeTokenItem(tokenName).performClick() }
|
||||
}
|
||||
step("Assert 'Network fee' bottom sheet is displayed after token selection") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onSendFeeSelectorBottomSheet { networkFeeTitle.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Assert '$marketSpeed' speed is displayed") {
|
||||
onSendFeeSelectorBottomSheet { feeSpeedItemTitle(marketSpeed).assertIsDisplayed() }
|
||||
}
|
||||
step("Assert '$fastSpeed' speed is not displayed") {
|
||||
onSendFeeSelectorBottomSheet { feeSpeedItemTitle(fastSpeed).assertIsNotDisplayed() }
|
||||
}
|
||||
step("Assert '$slowSpeed' speed is not displayed") {
|
||||
onSendFeeSelectorBottomSheet { feeSpeedItemTitle(slowSpeed).assertIsNotDisplayed() }
|
||||
}
|
||||
step("Click on '$marketSpeed' fee row") {
|
||||
onSendFeeSelectorBottomSheet { feeSpeedItemTitle(marketSpeed).performClick() }
|
||||
}
|
||||
step("Assert 'Choose speed' bottom sheet did not open for stablecoin fee") {
|
||||
onSendSelectNetworkFeeBottomSheet { chooseSpeedTitle.assertIsNotDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("5068")
|
||||
@DisplayName("Gasless: switching the fee token back to the coin restores the standard fee flow")
|
||||
@Test
|
||||
fun checkSwitchFeeTokenBackToCoinTest() {
|
||||
val nativeSymbol = "POL"
|
||||
val feeCoverageTitle = getResourceString(R.string.send_network_fee_warning_title)
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||
}
|
||||
).run {
|
||||
step("Open 'Send' screen for '$tokenName'") {
|
||||
openSendScreen(tokenName = tokenName, mockState = scenarioState)
|
||||
}
|
||||
step("Enter amount '$tokenAmount' and open the 'Send confirm' screen") {
|
||||
enterAmountAndOpenSendConfirm(amount = tokenAmount, recipientAddress = ETHEREUM_RECIPIENT_ADDRESS)
|
||||
}
|
||||
step("Pay the network fee with '$tokenName' via the fee selector") {
|
||||
selectStablecoinAsFeeToken(coinName = nativeTokenName, tokenName = tokenName)
|
||||
}
|
||||
step("Open the fee token selector again via the '$tokenName' fee token") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onSendFeeSelectorBottomSheet { feeTokenItem(tokenName).performClick() }
|
||||
}
|
||||
}
|
||||
step("Switch the fee token back to '$nativeTokenName'") {
|
||||
onSendFeeSelectorBottomSheet { feeTokenItem(nativeTokenName).performClick() }
|
||||
}
|
||||
step("Click on 'Apply' button") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onSendFeeSelectorBottomSheet { applyButton.performClick() }
|
||||
}
|
||||
}
|
||||
step("Assert the network fee is now paid in '$nativeSymbol' on the summary") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onSendConfirmScreen { feeBlockCurrency(nativeSymbol).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Assert 'Network fee coverage' notification is not displayed (standard fee flow)") {
|
||||
onSendConfirmScreen { warningTitle(feeCoverageTitle).assertIsNotDisplayed() }
|
||||
}
|
||||
step("Assert 'Send' button is enabled") {
|
||||
onSendConfirmScreen { sendButton.assertIsEnabled() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("5063")
|
||||
@DisplayName("Gasless: insufficient stablecoin balance to cover the fee shows error and blocks send")
|
||||
@Test
|
||||
fun checkInsufficientBalanceForFeeTest() {
|
||||
val usdcBalanceScenario = "polygon_usdc_balance"
|
||||
val lowBalanceState = "LowBalance"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||
resetWireMockScenarioState(usdcBalanceScenario)
|
||||
}
|
||||
).run {
|
||||
step("Set WireMock scenario '$usdcBalanceScenario' to '$lowBalanceState'") {
|
||||
setWireMockScenarioState(scenarioName = usdcBalanceScenario, state = lowBalanceState)
|
||||
}
|
||||
step("Open 'Send' screen for '$tokenName'") {
|
||||
openSendScreen(tokenName = tokenName, mockState = scenarioState)
|
||||
}
|
||||
step("Click on 'Max' button") {
|
||||
onSendScreen { maxButton.performClick() }
|
||||
}
|
||||
step("Click on 'Next' button") {
|
||||
onSendScreen { nextButton.clickWithAssertion() }
|
||||
}
|
||||
step("Enter the recipient and open the 'Send confirm' screen") {
|
||||
enterRecipientAndOpenSendConfirm(ETHEREUM_RECIPIENT_ADDRESS)
|
||||
}
|
||||
step("Pay the network fee with '$tokenName' via the fee selector") {
|
||||
selectStablecoinAsFeeToken(coinName = nativeTokenName, tokenName = tokenName)
|
||||
}
|
||||
step("Assert 'Not enough funds' error is displayed in the fee selector") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onSendFeeSelectorBottomSheet { notEnoughFundsError.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Assert 'Apply' button is disabled (cannot pay the fee with insufficient balance)") {
|
||||
onSendFeeSelectorBottomSheet { applyButton.assertIsNotEnabled() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("5097")
|
||||
@DisplayName("Gasless: no insufficient-coin-for-fee notification is shown when gasless covers the fee")
|
||||
@Test
|
||||
fun checkNoInsufficientCoinNotificationWhenGaslessTest() {
|
||||
val coinBalanceScenario = "polygon_coin_balance"
|
||||
val zeroBalanceState = "ZeroBalance"
|
||||
val feeBlockedTitlePart = getResourceString(R.string.warning_send_blocked_funds_for_fee_title, "X")
|
||||
.substringAfter("X ")
|
||||
.trim()
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||
resetWireMockScenarioState(coinBalanceScenario)
|
||||
}
|
||||
).run {
|
||||
step("Set WireMock scenario '$coinBalanceScenario' to '$zeroBalanceState'") {
|
||||
setWireMockScenarioState(scenarioName = coinBalanceScenario, state = zeroBalanceState)
|
||||
}
|
||||
step("Open 'Send' screen for '$tokenName'") {
|
||||
openSendScreen(tokenName = tokenName, mockState = scenarioState)
|
||||
}
|
||||
step("Enter amount '$tokenAmount' and open the 'Send confirm' screen") {
|
||||
enterAmountAndOpenSendConfirm(amount = tokenAmount, recipientAddress = ETHEREUM_RECIPIENT_ADDRESS)
|
||||
}
|
||||
step("Assert the fee defaults to '$tokenName' (gasless covers the missing coin)") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onSendConfirmScreen { feeBlockCurrency(tokenName).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Assert the insufficient-coin-for-fee notification is not shown") {
|
||||
onSendConfirmScreen { warningTitleContaining(feeBlockedTitlePart).assertIsNotDisplayed() }
|
||||
}
|
||||
step("Assert 'Send' button is enabled") {
|
||||
onSendConfirmScreen { sendButton.assertIsEnabled() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
package com.tangem.tests.send.gasless
|
||||
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.constants.TestConstants.ETHEREUM_RECIPIENT_ADDRESS
|
||||
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
|
||||
import com.tangem.common.constants.TestConstants.SVS_SEED_PHRASE_12
|
||||
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
|
||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.common.utils.resetWireMockScenarioState
|
||||
import com.tangem.common.utils.setWireMockScenarioState
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.scenarios.*
|
||||
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
|
||||
|
||||
/**
|
||||
* Gasless send lifecycle: signing and broadcasting a stablecoin-fee transaction (hot wallet),
|
||||
* the max-amount fee reservation, and the completed gasless transaction in the token history.
|
||||
*/
|
||||
@HiltAndroidTest
|
||||
class GaslessSendTest : BaseTestCase() {
|
||||
|
||||
private val scenarioState = "PolygonUSDC"
|
||||
private val tokenName = "USDC"
|
||||
private val currencySymbol = "USDC"
|
||||
private val nativeTokenName = "Polygon"
|
||||
private val hotWalletTokensState = "PolygonUSDCHotWallet"
|
||||
private val tokenAmount = "1"
|
||||
|
||||
@AllureId("5069")
|
||||
@DisplayName("Gasless: max amount reserves the stablecoin fee and stays sendable")
|
||||
@Test
|
||||
fun checkMaxAmountSendTest() {
|
||||
val feeCoverageTitle = getResourceString(R.string.send_network_fee_warning_title)
|
||||
val feeCoverageMessagePart = getResourceString(R.string.common_network_fee_warning_content, "", "")
|
||||
.substringBefore("(")
|
||||
.trim()
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||
}
|
||||
).run {
|
||||
step("Open the send flow for '$tokenName' on an existing hot wallet") {
|
||||
openGaslessSendScreenWithHotWallet(
|
||||
seedPhrase = SVS_SEED_PHRASE_12,
|
||||
tokenName = tokenName,
|
||||
userTokensState = hotWalletTokensState,
|
||||
quotesState = scenarioState,
|
||||
)
|
||||
}
|
||||
step("Click on 'Max' button") {
|
||||
onSendScreen { maxButton.performClick() }
|
||||
}
|
||||
step("Click on 'Next' button") {
|
||||
onSendScreen { nextButton.clickWithAssertion() }
|
||||
}
|
||||
step("Enter the recipient and open the 'Send confirm' screen") {
|
||||
enterRecipientAndOpenSendConfirm(ETHEREUM_RECIPIENT_ADDRESS)
|
||||
}
|
||||
step("Pay the network fee with '$tokenName' via the fee selector") {
|
||||
selectStablecoinAsFeeToken(coinName = nativeTokenName, tokenName = tokenName)
|
||||
}
|
||||
step("Click on 'Apply' button") {
|
||||
onSendFeeSelectorBottomSheet { applyButton.performClick() }
|
||||
}
|
||||
step("Assert 'Network fee coverage' notification title is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onSendConfirmScreen { warningTitle(feeCoverageTitle).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Assert 'Network fee coverage' notification text is displayed (amount reduced by fee)") {
|
||||
onSendConfirmScreen { warningMessageContaining(feeCoverageMessagePart).assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Send' button is enabled (enough left for the fee)") {
|
||||
onSendConfirmScreen { sendButton.assertIsEnabled() }
|
||||
}
|
||||
step("Sign, send and open the 'Transaction sent' screen") {
|
||||
openSendSuccessScreenViaLongClickOnSendButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("5065")
|
||||
@DisplayName("Gasless: sign and send a stablecoin transaction with the stablecoin fee")
|
||||
@Test
|
||||
fun checkSignAndSendGaslessTransactionTest() {
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||
}
|
||||
).run {
|
||||
step("Open the send flow for '$tokenName' on an existing hot wallet") {
|
||||
openGaslessSendScreenWithHotWallet(
|
||||
seedPhrase = SVS_SEED_PHRASE_12,
|
||||
tokenName = tokenName,
|
||||
userTokensState = hotWalletTokensState,
|
||||
quotesState = scenarioState,
|
||||
)
|
||||
}
|
||||
step("Enter amount '$tokenAmount' and open the 'Send confirm' screen") {
|
||||
enterAmountAndOpenSendConfirm(amount = tokenAmount, recipientAddress = ETHEREUM_RECIPIENT_ADDRESS)
|
||||
}
|
||||
step("Pay the network fee with '$tokenName' via the fee selector") {
|
||||
selectStablecoinAsFeeToken(coinName = nativeTokenName, tokenName = tokenName)
|
||||
}
|
||||
step("Click on 'Apply' button") {
|
||||
onSendFeeSelectorBottomSheet { applyButton.performClick() }
|
||||
}
|
||||
step("Assert gasless fee is paid in '$currencySymbol' and 'Send' is enabled") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onSendConfirmScreen {
|
||||
feeBlockCurrency(currencySymbol).assertIsDisplayed()
|
||||
sendButton.assertIsEnabled()
|
||||
}
|
||||
}
|
||||
}
|
||||
step("Sign, send and open the 'Transaction sent' screen") {
|
||||
openSendSuccessScreenViaLongClickOnSendButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("5066")
|
||||
@DisplayName("Gasless: completed gasless transaction is shown in token transaction history")
|
||||
@Test
|
||||
fun checkGaslessTransactionInHistoryTest() {
|
||||
val sentAmount = "1.00"
|
||||
val gaslessFeeAmount = "0.10"
|
||||
val sentTitle = getResourceString(R.string.common_sent)
|
||||
val gaslessFeeTitle = getResourceString(R.string.gasless_transaction_fee)
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||
}
|
||||
).run {
|
||||
step("Set WireMock scenario '$USER_TOKENS_API_SCENARIO' to '$scenarioState'") {
|
||||
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = scenarioState)
|
||||
}
|
||||
step("Set WireMock scenario '$QUOTES_API_SCENARIO' to '$scenarioState'") {
|
||||
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = scenarioState)
|
||||
}
|
||||
step("Open 'Main' screen") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Click on token with name: '$tokenName'") {
|
||||
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Token details' screen is displayed") {
|
||||
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
|
||||
}
|
||||
step("Wait for gasless '$gaslessFeeTitle' transaction in history") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTxHistoryScreen { transactionItem(gaslessFeeTitle).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Assert '$sentTitle' transaction is displayed") {
|
||||
onTxHistoryScreen { transactionItem(sentTitle).assertIsDisplayed() }
|
||||
}
|
||||
step("Assert '$sentTitle' amount '$sentAmount' is displayed in '$currencySymbol'") {
|
||||
onTxHistoryScreen {
|
||||
transactionAmount(sentTitle).assertTextContains(sentAmount, substring = true)
|
||||
transactionCurrency(sentTitle).assertTextEquals(currencySymbol)
|
||||
}
|
||||
}
|
||||
step("Assert gasless '$gaslessFeeTitle' amount '$gaslessFeeAmount' is displayed in '$currencySymbol'") {
|
||||
onTxHistoryScreen {
|
||||
transactionAmount(gaslessFeeTitle).assertTextContains(gaslessFeeAmount, substring = true)
|
||||
transactionCurrency(gaslessFeeTitle).assertTextEquals(currencySymbol)
|
||||
}
|
||||
}
|
||||
step("Assert gasless '$gaslessFeeTitle' status is confirmed") {
|
||||
onTxHistoryScreen { transactionConfirmedStatus(gaslessFeeTitle).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -60,6 +60,7 @@ internal class DefaultAuthProvider(
|
|||
override fun getGaslessServiceApiKey(apiEnvironment: Provider<ApiEnvironment>): ProviderSuspend<String> {
|
||||
return ProviderSuspend {
|
||||
when (apiEnvironment.invoke()) {
|
||||
ApiEnvironment.MOCK,
|
||||
ApiEnvironment.DEV,
|
||||
-> environmentConfig.gaslessTxApiKeyDev
|
||||
ApiEnvironment.PROD -> environmentConfig.gaslessTxApiKey
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue