Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-08 14:31:40 +03:00
parent 8683516c59
commit c81ba95db0
17 changed files with 827 additions and 10 deletions

View file

@ -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() }
}
}

View file

@ -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))

View file

@ -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)

View file

@ -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)

View file

@ -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() }
}
}
}
}

View file

@ -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() }
}
}
}
}

View file

@ -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