Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-25 19:00:27 +02:00
parent c45a6af247
commit 8094af242d
23 changed files with 1001 additions and 16 deletions

1
.gitignore vendored
View file

@ -48,3 +48,4 @@ find-latest-release-branch.output
# Claude
/.claude/worktrees/
CLAUDE.local.md

View file

@ -183,6 +183,9 @@ abstract class BaseTestCase : TestCase(
"GASLESS_APPROVAL_ENABLED" to true,
"MAIN_SCREEN_QR_SCANNING_ENABLED" to true,
"ADD_AND_MANAGE_TOKENS_ENABLED" to true,
"VISA_ONBOARDING_ENABLED" to true,
"AND_15101_TANGEM_PAY_HOT_WALLET_ONBOARDING" to true,
"AND_15310_ADD_FUNDS_STAGE1" to true,
)
)
}

View file

@ -59,4 +59,7 @@ object TestConstants {
const val SEED_PHRASE_24 = "force visit fresh brown razor target ill scissors figure cave feel genre cargo category " +
"bread much nature basic fun iron benefit egg error prosper"
const val SVS_SEED_PHRASE_12 = "diagram thunder merit soup muscle amused refuse usual ring couch popular wash"
const val TANGEM_PAY_ELIGIBILITY_SCENARIO = "tangem_pay_eligibility"
const val TANGEM_PAY_ACCESS_CODE = "517384"
}

View file

@ -1,13 +1,20 @@
package com.tangem.scenarios
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.test.waitUntilAtLeastOneExists
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.extensions.isDisplayedSafely
import com.tangem.core.ui.R as CoreUiR
import com.tangem.domain.models.scan.ProductType
import com.tangem.screens.*
import com.tangem.tap.domain.sdk.mocks.MockContent
import com.tangem.tap.domain.sdk.mocks.MockProvider
import com.tangem.utils.StringsSigns.DASH_SIGN
import io.github.kakaocup.kakao.common.utilities.getResourceString
import io.qameta.allure.kotlin.Allure.step
fun BaseTestCase.scanCard(
@ -56,7 +63,8 @@ fun BaseTestCase.openMainScreen(
}
}
fun BaseTestCase.openMainScreenWithExistingHotWallet(seedPhrase: String) {
@OptIn(ExperimentalTestApi::class)
fun BaseTestCase.openMainScreenWithExistingHotWallet(seedPhrase: String, accessCode: String = "") {
step("Click on 'Get started' button") {
onStoriesScreen { getStartedButton.clickWithAssertion() }
}
@ -84,11 +92,39 @@ fun BaseTestCase.openMainScreenWithExistingHotWallet(seedPhrase: String) {
continueButton.performClick()
}
}
step("Click on 'Skip' button") {
onImportWalletScreen { skipButton.performClick() }
}
step("Click on 'Skip anyway' dialog button") {
onDialog { skipAnywayButton.performClick() }
if (accessCode.isNotEmpty()) {
step("Enter access code '$accessCode' (create)") {
onHotWalletAccessCodeScreen {
accessCodeInput.performClick()
accessCodeInput.performTextInput(accessCode)
}
}
step("Re-enter access code '$accessCode' (confirm)") {
// Create+confirm screens share ACCESS_CODE_INPUT — gate on confirm-screen title.
composeTestRule.waitUntilAtLeastOneExists(
hasText(getResourceString(CoreUiR.string.access_code_confirm_title)),
timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG,
)
onHotWalletAccessCodeScreen {
accessCodeInput.performClick()
accessCodeInput.performTextInput(accessCode)
}
}
step("Dismiss biometry prompt if shown") {
waitForIdle()
onBiometryDialog {
if (dontAllowButton.isDisplayedSafely()) {
dontAllowButton.performClick()
}
}
}
} else {
step("Click on 'Skip' button") {
onImportWalletScreen { skipButton.performClick() }
}
step("Click on 'Skip anyway' dialog button") {
onDialog { skipAnywayButton.performClick() }
}
}
step("Click on 'Finish' button") {
onImportWalletScreen {

View file

@ -1,10 +1,20 @@
package com.tangem.scenarios
import androidx.compose.ui.test.click
import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.longClick
import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.test.performTouchInput
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.HOLD_DURATION_MS
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
import com.tangem.common.extensions.assertVisibility
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.extensions.isDisplayedSafely
import com.tangem.core.ui.R as CoreUiR
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.test.HotWalletAccessCodeTestTags
import com.tangem.screens.*
import io.github.kakaocup.kakao.common.utilities.getResourceString
import io.qameta.allure.kotlin.Allure.step
@ -283,6 +293,29 @@ fun BaseTestCase.chooseReceiveToken(tokenName: String) {
}
}
/** Holds the last BASE_BUTTON; enters [accessCode] if a hot wallet prompts for it. */
fun BaseTestCase.confirmSwapByHolding(accessCode: String? = null) {
val buttonMatcher = hasTestTag(BaseButtonTestTags.BUTTON)
val buttons = composeTestRule.onAllNodes(buttonMatcher)
// HoldToConfirm is always last — withdraw renders an extra BASE_BUTTON for notifications.
val swapButton = buttons[buttons.fetchSemanticsNodes().lastIndex]
swapButton.performTouchInput { longClick(durationMillis = HOLD_DURATION_MS) }
waitForIdle()
val accessCodeInput = hasTestTag(HotWalletAccessCodeTestTags.ACCESS_CODE_INPUT)
val swapInProgressText = hasText(getResourceString(CoreUiR.string.swap_in_progress))
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) {
composeTestRule.onAllNodes(accessCodeInput).fetchSemanticsNodes().isNotEmpty() ||
composeTestRule.onAllNodes(swapInProgressText, useUnmergedTree = true)
.fetchSemanticsNodes().isNotEmpty()
}
val needsAccessCode =
composeTestRule.onAllNodes(accessCodeInput).fetchSemanticsNodes().isNotEmpty()
if (needsAccessCode && accessCode != null) {
composeTestRule.onNode(accessCodeInput).performTextInput(accessCode)
waitForIdle()
}
}
sealed class SwapEntryPoint {
object MainScreen : SwapEntryPoint()
object TokenDetails : SwapEntryPoint()

View file

@ -0,0 +1,34 @@
package com.tangem.scenarios
import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.swipeDown
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.SVS_SEED_PHRASE_12
import com.tangem.common.constants.TestConstants.TANGEM_PAY_ACCESS_CODE
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.core.ui.test.TangemPayTestTags
import com.tangem.screens.tangempay.*
import io.qameta.allure.kotlin.Allure.step
fun BaseTestCase.openTangemPay() {
step("Import hot wallet from Tangem Pay seed phrase (with access code)") {
openMainScreenWithExistingHotWallet(SVS_SEED_PHRASE_12, accessCode = TANGEM_PAY_ACCESS_CODE)
}
step("Click on Tangem Pay tile") {
onTangemPayMainScreen { mainScreenTile.clickWithAssertion() }
}
step("Assert payment account balance is displayed") {
onTangemPayMainScreen { balance.assertIsDisplayed() }
}
}
// Compose Test gesture — UiAutomator swipe doesn't reach Material3 PullToRefreshBox's NestedScrollConnection.
fun BaseTestCase.pullToRefreshTangemPay() {
val balance = composeTestRule.onNode(hasTestTag(TangemPayTestTags.PAYMENT_ACCOUNT_BALANCE))
balance.performTouchInput {
swipeDown(startY = 0f, endY = visibleSize.height.toFloat() * 6f, durationMillis = 800)
}
composeTestRule.mainClock.advanceTimeBy(2_000L)
waitForIdle()
}

View file

@ -0,0 +1,24 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import androidx.compose.ui.test.hasText as withText
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.R as CoreUiR
import com.tangem.core.ui.test.BaseButtonTestTags
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
class BiometryDialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<BiometryDialogPageObject>(semanticsProvider = semanticsProvider) {
val dontAllowButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasAnyDescendant(withText(getResourceString(CoreUiR.string.save_user_wallet_agreement_dont_allow)))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onBiometryDialog(function: BiometryDialogPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,20 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.test.HotWalletAccessCodeTestTags
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
class HotWalletAccessCodePageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<HotWalletAccessCodePageObject>(semanticsProvider = semanticsProvider) {
val accessCodeInput: KNode = child {
hasTestTag(HotWalletAccessCodeTestTags.ACCESS_CODE_INPUT)
useUnmergedTree = true
}
}
internal fun BaseTestCase.onHotWalletAccessCodeScreen(function: HotWalletAccessCodePageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,31 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import androidx.compose.ui.test.hasText as withText
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.R
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.test.TransactionSuccessScreenTestTags
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
// Legacy swap feature's success screen — lacks CONTAINER testTag that SendSuccessPageObject relies on.
class SwapSuccessPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<SwapSuccessPageObject>(semanticsProvider = semanticsProvider) {
val title: KNode = child {
hasTestTag(TransactionSuccessScreenTestTags.TITLE)
useUnmergedTree = true
}
val closeButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasAnyDescendant(withText(getResourceString(R.string.common_close)))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onSwapSuccessScreen(function: SwapSuccessPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -38,6 +38,12 @@ class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
useUnmergedTree = true
}
// Tangem Pay withdraw hides FEE_SELECTOR_BLOCK; gate on the "Network fee" label instead.
val networkFeeTitle: KNode = child {
hasText(getResourceString(R.string.common_network_fee_title))
useUnmergedTree = true
}
val selectFeeIcon: KNode = child {
hasTestTag(FeeSelectorBlockTestTags.SELECT_FEE_ICON)
useUnmergedTree = true

View file

@ -0,0 +1,31 @@
package com.tangem.screens.tangempay
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.res.R as CoreResR
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
class TangemPayAddFundsSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<TangemPayAddFundsSheetPageObject>(semanticsProvider = semanticsProvider) {
val swapOption: KNode = child {
hasText(getResourceString(CoreResR.string.common_exchange))
useUnmergedTree = true
}
val receiveOption: KNode = child {
hasText(getResourceString(CoreResR.string.common_receive))
useUnmergedTree = true
}
val title: KNode = child {
hasText(getResourceString(CoreResR.string.tangempay_card_details_add_funds))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onTangemPayAddFundsSheet(function: TangemPayAddFundsSheetPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,70 @@
package com.tangem.screens.tangempay
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.test.TangemPayTestTags
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
class TangemPayCardPagePageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<TangemPayCardPagePageObject>(semanticsProvider = semanticsProvider) {
val changePinRow: KNode = child {
hasTestTag(TangemPayTestTags.CHANGE_PIN_ROW)
useUnmergedTree = true
}
val freezeCardRow: KNode = child {
hasTestTag(TangemPayTestTags.FREEZE_CARD_ROW)
useUnmergedTree = true
}
val cardFrozenBadge: KNode = child {
hasTestTag(TangemPayTestTags.CARD_FROZEN_BADGE)
useUnmergedTree = true
}
val showDetailsButton: KNode = child {
hasTestTag(TangemPayTestTags.CARD_DETAILS_SHOW_BUTTON)
useUnmergedTree = true
}
val hideDetailsButton: KNode = child {
hasTestTag(TangemPayTestTags.CARD_DETAILS_HIDE_BUTTON)
useUnmergedTree = true
}
val numberValue: KNode = child {
hasTestTag(TangemPayTestTags.CARD_DETAILS_NUMBER_VALUE)
useUnmergedTree = true
}
val expirationValue: KNode = child {
hasTestTag(TangemPayTestTags.CARD_DETAILS_EXPIRATION_VALUE)
useUnmergedTree = true
}
val cvcValue: KNode = child {
hasTestTag(TangemPayTestTags.CARD_DETAILS_CVC_VALUE)
useUnmergedTree = true
}
val copyNumberButton: KNode = child {
hasTestTag(TangemPayTestTags.CARD_DETAILS_COPY_NUMBER)
useUnmergedTree = true
}
val copyExpirationButton: KNode = child {
hasTestTag(TangemPayTestTags.CARD_DETAILS_COPY_EXPIRATION)
useUnmergedTree = true
}
val copyCvcButton: KNode = child {
hasTestTag(TangemPayTestTags.CARD_DETAILS_COPY_CVC)
useUnmergedTree = true
}
}
internal fun BaseTestCase.onTangemPayCardPageScreen(function: TangemPayCardPagePageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,55 @@
package com.tangem.screens.tangempay
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.test.TangemPayTestTags
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
class TangemPayChangePinPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<TangemPayChangePinPageObject>(semanticsProvider = semanticsProvider) {
val title: KNode = child {
hasTestTag(TangemPayTestTags.PIN_SCREEN_TITLE)
useUnmergedTree = true
}
val description: KNode = child {
hasTestTag(TangemPayTestTags.PIN_SCREEN_DESCRIPTION)
useUnmergedTree = true
}
val inputField: KNode = child {
hasTestTag(TangemPayTestTags.PIN_INPUT_FIELD)
useUnmergedTree = true
}
val submitButton: KNode = child {
hasTestTag(TangemPayTestTags.PIN_SUBMIT_BUTTON)
useUnmergedTree = true
}
val errorMessage: KNode = child {
hasTestTag(TangemPayTestTags.PIN_ERROR_MESSAGE)
useUnmergedTree = true
}
val successTitle: KNode = child {
hasTestTag(TangemPayTestTags.PIN_SUCCESS_TITLE)
useUnmergedTree = true
}
val successDescription: KNode = child {
hasTestTag(TangemPayTestTags.PIN_SUCCESS_DESCRIPTION)
useUnmergedTree = true
}
val doneButton: KNode = child {
hasTestTag(TangemPayTestTags.PIN_DONE_BUTTON)
useUnmergedTree = true
}
}
internal fun BaseTestCase.onTangemPayChangePinScreen(function: TangemPayChangePinPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,34 @@
package com.tangem.screens.tangempay
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.test.WarningBottomSheetTestTags
import com.tangem.core.res.R as CoreResR
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
class TangemPayFreezeConfirmationPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<TangemPayFreezeConfirmationPageObject>(semanticsProvider = semanticsProvider) {
val freezeTitle: KNode = child {
hasTestTag(WarningBottomSheetTestTags.TITLE)
hasText(getResourceString(CoreResR.string.tangem_pay_freeze_card_alert_title))
useUnmergedTree = true
}
val unfreezeTitle: KNode = child {
hasTestTag(WarningBottomSheetTestTags.TITLE)
hasText(getResourceString(CoreResR.string.tangem_pay_unfreeze_card_alert_title))
useUnmergedTree = true
}
val submitButton: KNode = child {
hasTestTag(WarningBottomSheetTestTags.BUTTON_PRIMARY)
useUnmergedTree = true
}
}
internal fun BaseTestCase.onTangemPayFreezeConfirmation(function: TangemPayFreezeConfirmationPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,51 @@
package com.tangem.screens.tangempay
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.test.BaseActionButtonsBlockTestTags
import com.tangem.core.ui.test.TangemPayTestTags
import com.tangem.core.res.R as CoreResR
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
class TangemPayMainPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<TangemPayMainPageObject>(semanticsProvider = semanticsProvider) {
val mainScreenTile: KNode = child {
hasTestTag(TangemPayTestTags.MAIN_SCREEN_TILE)
useUnmergedTree = true
}
val balance: KNode = child {
hasTestTag(TangemPayTestTags.PAYMENT_ACCOUNT_BALANCE)
useUnmergedTree = true
}
val cardButton: KNode = child {
hasTestTag(TangemPayTestTags.PAYMENT_ACCOUNT_CARD_BUTTON)
useUnmergedTree = true
}
val topUpButton: KNode = child {
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
hasAnyDescendant(withText(getResourceString(CoreResR.string.tangempay_card_details_add_funds)))
useUnmergedTree = true
}
val withdrawButton: KNode = child {
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
hasAnyDescendant(withText(getResourceString(CoreResR.string.tangempay_card_details_withdraw)))
useUnmergedTree = true
}
fun transactionRowWithText(text: String): KNode = child {
hasText(text)
useUnmergedTree = true
}
}
internal fun BaseTestCase.onTangemPayMainScreen(function: TangemPayMainPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,28 @@
package com.tangem.screens.tangempay
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.test.WarningBottomSheetTestTags
import com.tangem.core.res.R as CoreResR
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
class TangemPayWithdrawNoteSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<TangemPayWithdrawNoteSheetPageObject>(semanticsProvider = semanticsProvider) {
val title: KNode = child {
hasTestTag(WarningBottomSheetTestTags.TITLE)
hasText(getResourceString(CoreResR.string.tangempay_withdrawal_note_title))
useUnmergedTree = true
}
val gotItButton: KNode = child {
hasTestTag(WarningBottomSheetTestTags.BUTTON_PRIMARY)
useUnmergedTree = true
}
}
internal fun BaseTestCase.onTangemPayWithdrawNoteSheet(function: TangemPayWithdrawNoteSheetPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,236 @@
package com.tangem.tests.tangempay
import androidx.test.platform.app.InstrumentationRegistry
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.TANGEM_PAY_ELIGIBILITY_SCENARIO
import com.tangem.common.extensions.assertTextContainsSafe
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.extensions.extractText
import com.tangem.common.extensions.pullToRefresh
import com.tangem.common.utils.assertClipboardTextEquals
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.scenarios.*
import com.tangem.screens.tangempay.*
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 TangemPayTest : BaseTestCase() {
@AllureId("4549")
@DisplayName("Tangem Pay: change PIN code from card details")
@Test
fun changePin_SetsNewPinCode_FromCardDetails() {
val newPin = "5217"
val pinSetupScenario = "tangem_pay_pin_setup"
val pinNotSetState = "PinNotSet"
val eligibilityState = "PaeraCustomer"
setupHooks(
additionalBeforeSection = {
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO, eligibilityState)
setWireMockScenarioState(pinSetupScenario, pinNotSetState)
},
additionalAfterSection = {
resetWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO)
resetWireMockScenarioState(pinSetupScenario)
},
).run {
openTangemPay()
step("Click on card button") {
onTangemPayMainScreen { cardButton.clickWithAssertion() }
}
step("Click on 'Change PIN' row") {
onTangemPayCardPageScreen { changePinRow.clickWithAssertion() }
}
step("Assert PIN screen is displayed") {
onTangemPayChangePinScreen { title.assertIsDisplayed() }
}
step("Enter PIN '$newPin'") {
onTangemPayChangePinScreen { inputField.performTextInput(newPin) }
}
step("Click on 'Submit' button") {
onTangemPayChangePinScreen { submitButton.performClick() }
}
step("Assert success screen is displayed") {
onTangemPayChangePinScreen { successTitle.assertIsDisplayed() }
}
step("Click on 'Done' button") {
onTangemPayChangePinScreen { doneButton.clickWithAssertion() }
}
}
}
@AllureId("4969")
@DisplayName("Tangem Pay: balance updates after transaction on payment account screen")
@Test
fun balanceUpdatesAfterTransaction_OnPaymentAccountScreen() {
val balanceScenario = "tangem_pay_balance_update"
val initialState = "InitialBalance"
val afterTransactionState = "AfterTransaction"
val eligibilityState = "PaeraCustomer"
setupHooks(
additionalBeforeSection = {
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO, eligibilityState)
setWireMockScenarioState(balanceScenario, initialState)
},
additionalAfterSection = {
resetWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO)
resetWireMockScenarioState(balanceScenario)
},
).run {
openTangemPay()
step("Assert initial balance contains '10'") {
onTangemPayMainScreen { balance.assertTextContainsSafe("10", substring = true) }
}
step("Switch WireMock scenario '$balanceScenario' to '$afterTransactionState'") {
setWireMockScenarioState(balanceScenario, afterTransactionState)
}
step("Pull to refresh") { pullToRefresh() }
step("Assert updated balance contains '9'") {
onTangemPayMainScreen { balance.assertTextContainsSafe("9", substring = true) }
}
}
}
@AllureId("4970")
@DisplayName("Tangem Pay: new transaction appears after mocked charge")
@Test
fun transactionList_NewTransactionAppears_AfterMockedCharge() {
val historyScenario = "tangem_pay_transaction_history"
val initialState = "InitialEmpty"
val afterTransactionState = "AfterTransaction"
val eligibilityState = "PaeraCustomer"
val merchantName = "Mock Merchant"
setupHooks(
additionalBeforeSection = {
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO, eligibilityState)
setWireMockScenarioState(historyScenario, initialState)
},
additionalAfterSection = {
resetWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO)
resetWireMockScenarioState(historyScenario)
},
).run {
openTangemPay()
step("Assert transaction from '$merchantName' is not displayed") {
onTangemPayMainScreen {
transactionRowWithText(merchantName).assertDoesNotExist()
}
}
step("Switch WireMock scenario '$historyScenario' to '$afterTransactionState'") {
setWireMockScenarioState(historyScenario, afterTransactionState)
}
step("Pull to refresh") { pullToRefresh() }
step("Assert transaction from '$merchantName' is displayed") {
onTangemPayMainScreen {
transactionRowWithText(merchantName).assertIsDisplayed()
}
}
}
}
@AllureId("4974")
@DisplayName("Tangem Pay: reveal and copy card number, expiration and CVC")
@Test
fun revealAndCopyCardDetails_NumberExpirationCVC() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val eligibilityState = "PaeraCustomer"
setupHooks(
additionalBeforeSection = {
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO, eligibilityState)
},
additionalAfterSection = {
resetWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO)
},
).run {
openTangemPay()
step("Click on card button") {
onTangemPayMainScreen { cardButton.clickWithAssertion() }
}
step("Click on 'Show details' button") {
onTangemPayCardPageScreen { showDetailsButton.clickWithAssertion() }
}
step("Assert number, expiration and CVC values are visible") {
onTangemPayCardPageScreen {
numberValue.assertIsDisplayed()
expirationValue.assertIsDisplayed()
cvcValue.assertIsDisplayed()
}
}
var displayedNumber = ""
var displayedExpiration = ""
var displayedCvc = ""
onTangemPayCardPageScreen {
displayedNumber = numberValue.extractText()
displayedExpiration = expirationValue.extractText()
displayedCvc = cvcValue.extractText()
}
step("Click on 'Copy card number' button") {
onTangemPayCardPageScreen { copyNumberButton.clickWithAssertion() }
waitForIdle()
}
step("Assert clipboard contains card number") {
// Displayed number has spaces for readability; clipboard copies digits only.
assertClipboardTextEquals(displayedNumber.replace(" ", ""), context)
}
step("Click on 'Copy expiration' button") {
onTangemPayCardPageScreen { copyExpirationButton.clickWithAssertion() }
waitForIdle()
}
step("Assert clipboard contains expiration date") {
assertClipboardTextEquals(displayedExpiration, context)
}
step("Click on 'Copy CVC' button") {
onTangemPayCardPageScreen { copyCvcButton.clickWithAssertion() }
waitForIdle()
}
step("Assert clipboard contains CVC") {
assertClipboardTextEquals(displayedCvc, context)
}
}
}
@AllureId("4971")
@DisplayName("Tangem Pay: freeze card via confirmation sheet")
@Test
fun freezeUnfreezeCard_TogglesCardState() {
val freezeScenario = "tangem_pay_card_freeze"
val startedState = "Started"
val eligibilityState = "PaeraCustomer"
setupHooks(
additionalBeforeSection = {
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO, eligibilityState)
setWireMockScenarioState(freezeScenario, startedState)
},
additionalAfterSection = {
resetWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO)
resetWireMockScenarioState(freezeScenario)
},
).run {
openTangemPay()
step("Click on card button") {
onTangemPayMainScreen { cardButton.clickWithAssertion() }
}
step("Click on freeze card row (card is active)") {
onTangemPayCardPageScreen { freezeCardRow.clickWithAssertion() }
}
step("Assert freeze confirmation sheet is displayed") {
onTangemPayFreezeConfirmation { freezeTitle.assertIsDisplayed() }
}
step("Click on 'Submit' button (confirm freeze)") {
onTangemPayFreezeConfirmation { submitButton.clickWithAssertion() }
}
step("Assert frozen badge is displayed") {
onTangemPayCardPageScreen { cardFrozenBadge.assertIsDisplayed() }
}
}
}
}

View file

@ -0,0 +1,139 @@
package com.tangem.tests.tangempay
import androidx.test.espresso.Espresso
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.TANGEM_PAY_ACCESS_CODE
import com.tangem.common.constants.TestConstants.TANGEM_PAY_ELIGIBILITY_SCENARIO
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_VERY_LONG
import com.tangem.common.extensions.assertTextContainsSafe
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.core.res.R as CoreResR
import com.tangem.scenarios.*
import com.tangem.screens.*
import com.tangem.screens.tangempay.*
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 TangemPayTopUpTest : BaseTestCase() {
@AllureId("4973")
@DisplayName("Tangem Pay: top up swaps Bitcoin to USDC and appends deposit to history")
@Test
fun topUpFromTangemPay_SwapsBitcoinToUSDC_AppendsDepositToHistory() {
val bitcoinScenario = "bitcoin_utxo"
val expressAssetsScenario = "express_api_assets"
val balanceScenario = "tangem_pay_balance_update"
val historyScenario = "tangem_pay_transaction_history"
val eligibilityState = "PaeraCustomer"
val bitcoinBalanceState = "BalanceHotWalletSvS"
val expressAssetsState = "BitcoinExchangeEnabled"
val balanceInitialState = "InitialBalance"
val balanceAfterState = "AfterDeposit"
val historyInitialState = "InitialEmpty"
val historyAfterState = "AfterDeposit"
val swapFromAmount = "0.001"
val depositLabel = getResourceString(CoreResR.string.tangem_pay_deposit)
setupHooks(
additionalBeforeSection = {
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO, eligibilityState)
setWireMockScenarioState(bitcoinScenario, bitcoinBalanceState)
setWireMockScenarioState(expressAssetsScenario, expressAssetsState)
setWireMockScenarioState(balanceScenario, balanceInitialState)
setWireMockScenarioState(historyScenario, historyInitialState)
},
additionalAfterSection = {
resetWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO)
resetWireMockScenarioState(bitcoinScenario)
resetWireMockScenarioState(expressAssetsScenario)
resetWireMockScenarioState(balanceScenario)
resetWireMockScenarioState(historyScenario)
},
).run {
openTangemPay()
step("Assert initial balance contains '10'") {
onTangemPayMainScreen { balance.assertTextContainsSafe("10", substring = true) }
}
step("Click on 'Top Up' action chip") {
onTangemPayMainScreen { topUpButton.clickWithAssertion() }
}
step("Assert 'Add Funds' sheet is displayed") {
onTangemPayAddFundsSheet { title.assertIsDisplayed() }
}
step("Click on 'Swap' option") {
onTangemPayAddFundsSheet { swapOption.clickWithAssertion() }
}
step("Click on 'Close' button on Swap stories") {
onSwapStoriesScreen { closeButton.performClick() }
}
step("Assert 'Swap' screen is displayed (USDC pre-filled as destination)") {
onSwapTokenScreen { title.assertIsDisplayed() }
}
step("Click on 'Choose token' button (from)") {
onSwapTokenScreen { chooseTokenButton.clickWithAssertion() }
}
step("Click on 'Main account'") {
onSwapSelectTokenScreen { tokenWithName("Main account").clickWithAssertion() }
}
step("Click on token 'Bitcoin'") {
waitForIdle()
onSwapSelectTokenScreen { tokenWithName("Bitcoin").clickWithAssertion() }
}
step("Enter swap amount '$swapFromAmount'") {
onSwapTokenScreen {
textInput.performClick()
textInput.performTextReplacement(swapFromAmount)
}
}
step("Dismiss keyboard") {
Espresso.closeSoftKeyboard()
waitForIdle()
}
step("Wait until provider quote + fee are loaded") {
onSwapTokenScreen {
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
networkFeeBlock.assertIsDisplayed()
feeAmount.assertIsDisplayed()
}
}
}
step("Confirm swap by holding the button") {
confirmSwapByHolding(accessCode = TANGEM_PAY_ACCESS_CODE)
}
step("Wait for 'Swap in progress' screen") {
onSwapSuccessScreen {
flakySafely(WAIT_UNTIL_TIMEOUT_VERY_LONG) { title.assertIsDisplayed() }
}
}
step("Click on 'Close' button") {
onSwapSuccessScreen { closeButton.performClick() }
}
step("Switch WireMock scenario '$balanceScenario' to '$balanceAfterState'") {
setWireMockScenarioState(balanceScenario, balanceAfterState)
}
step("Switch WireMock scenario '$historyScenario' to '$historyAfterState'") {
setWireMockScenarioState(historyScenario, historyAfterState)
}
step("Pull to refresh Tangem Pay") { pullToRefreshTangemPay() }
step("Assert balance updated to '\$110.00'") {
onTangemPayMainScreen {
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
balance.assertTextContainsSafe("110", substring = true)
}
}
}
step("Assert '$depositLabel' transaction visible in history") {
onTangemPayMainScreen {
transactionRowWithText(depositLabel).assertIsDisplayed()
}
}
}
}
}

View file

@ -0,0 +1,142 @@
package com.tangem.tests.tangempay
import androidx.test.espresso.Espresso
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.TANGEM_PAY_ACCESS_CODE
import com.tangem.common.constants.TestConstants.TANGEM_PAY_ELIGIBILITY_SCENARIO
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_VERY_LONG
import com.tangem.common.extensions.assertTextContainsSafe
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.core.res.R as CoreResR
import com.tangem.scenarios.*
import com.tangem.screens.*
import com.tangem.screens.tangempay.*
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.Ignore
import org.junit.Test
@HiltAndroidTest
class TangemPayWithdrawTest : BaseTestCase() {
@AllureId("4972")
@DisplayName("Tangem Pay: withdraw swaps USDC to Bitcoin and appends withdrawal to history")
@Ignore("[REDACTED_JIRA]")
@Test
fun withdrawFromTangemPay_SwapsUSDCToBitcoin_AppendsWithdrawalToHistory() {
val bitcoinScenario = "bitcoin_utxo"
val expressAssetsScenario = "express_api_assets"
val exchangeStatusScenario = "exchange_status_provider"
val balanceScenario = "tangem_pay_balance_update"
val historyScenario = "tangem_pay_transaction_history"
val eligibilityState = "PaeraCustomer"
val bitcoinStartedState = "Started"
val expressAssetsState = "BitcoinExchangeEnabled"
val exchangeStatusState = "Changelly"
val balanceInitialState = "InitialBalance"
val balanceAfterState = "AfterWithdraw"
val historyInitialState = "InitialEmpty"
val historyAfterState = "AfterWithdraw"
val withdrawAmount = "5"
val withdrawalLabel = getResourceString(CoreResR.string.tangem_pay_withdrawal)
setupHooks(
additionalBeforeSection = {
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO, eligibilityState)
setWireMockScenarioState(bitcoinScenario, bitcoinStartedState)
setWireMockScenarioState(expressAssetsScenario, expressAssetsState)
setWireMockScenarioState(exchangeStatusScenario, exchangeStatusState)
setWireMockScenarioState(balanceScenario, balanceInitialState)
setWireMockScenarioState(historyScenario, historyInitialState)
},
additionalAfterSection = {
resetWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO)
resetWireMockScenarioState(bitcoinScenario)
resetWireMockScenarioState(expressAssetsScenario)
resetWireMockScenarioState(exchangeStatusScenario)
resetWireMockScenarioState(balanceScenario)
resetWireMockScenarioState(historyScenario)
},
).run {
openTangemPay()
step("Assert initial balance contains '10'") {
onTangemPayMainScreen { balance.assertTextContainsSafe("10", substring = true) }
}
step("Click on 'Withdraw' action chip") {
onTangemPayMainScreen { withdrawButton.clickWithAssertion() }
}
step("Acknowledge withdrawal note sheet") {
onTangemPayWithdrawNoteSheet {
title.assertIsDisplayed()
gotItButton.clickWithAssertion()
}
}
step("Click on 'Close' button on Swap stories") {
onSwapStoriesScreen { closeButton.performClick() }
}
step("Assert 'Swap' screen is displayed (USDC pre-filled as source)") {
onSwapTokenScreen { title.assertIsDisplayed() }
}
step("Click on 'Choose token' button (to)") {
onSwapTokenScreen { chooseTokenButton.clickWithAssertion() }
}
step("Click on 'Main account'") {
onSwapSelectTokenScreen { tokenWithName("Main account").clickWithAssertion() }
}
step("Click on token 'Bitcoin'") {
waitForIdle()
onSwapSelectTokenScreen { tokenWithName("Bitcoin").clickWithAssertion() }
}
step("Enter withdraw amount '$withdrawAmount'") {
onSwapTokenScreen {
textInput.performClick()
textInput.performTextReplacement(withdrawAmount)
}
}
step("Dismiss keyboard") {
Espresso.closeSoftKeyboard()
waitForIdle()
}
step("Wait until network fee row is rendered (HoldToConfirm enabled)") {
onSwapTokenScreen {
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { networkFeeTitle.assertIsDisplayed() }
}
}
step("Confirm swap by holding the button") {
confirmSwapByHolding(accessCode = TANGEM_PAY_ACCESS_CODE)
}
step("Wait for 'Swap in progress' screen") {
onSwapSuccessScreen {
flakySafely(WAIT_UNTIL_TIMEOUT_VERY_LONG) { title.assertIsDisplayed() }
}
}
step("Click on 'Close' button") {
onSwapSuccessScreen { closeButton.performClick() }
}
step("Switch WireMock scenario '$balanceScenario' to '$balanceAfterState'") {
setWireMockScenarioState(balanceScenario, balanceAfterState)
}
step("Switch WireMock scenario '$historyScenario' to '$historyAfterState'") {
setWireMockScenarioState(historyScenario, historyAfterState)
}
step("Pull to refresh Tangem Pay") { pullToRefreshTangemPay() }
step("Assert balance updated to '\$5.00'") {
onTangemPayMainScreen {
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
balance.assertTextContainsSafe("5", substring = true)
}
}
}
step("Assert '$withdrawalLabel' transaction visible in history") {
onTangemPayMainScreen {
transactionRowWithText(withdrawalLabel).assertIsDisplayed()
}
}
}
}
}

View file

@ -21,6 +21,7 @@ object TangemPayTestTags {
// Card management (card page settings)
const val CHANGE_PIN_ROW = "TANGEM_PAY_CHANGE_PIN_ROW"
const val FREEZE_CARD_ROW = "TANGEM_PAY_FREEZE_CARD_ROW"
const val CARD_FROZEN_BADGE = "TANGEM_PAY_CARD_FROZEN_BADGE"
// Freeze confirmation bottom sheet
const val FREEZE_CONFIRMATION_SUBMIT_BUTTON = "TANGEM_PAY_FREEZE_CONFIRMATION_SUBMIT_BUTTON"

View file

@ -9,6 +9,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.style.LineBreak
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
@ -22,6 +23,7 @@ import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.fields.PinTextColor
import com.tangem.core.ui.components.fields.PinTextField
import com.tangem.core.ui.test.HotWalletAccessCodeTestTags
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.haptic.TangemHapticEffect
import com.tangem.core.ui.res.LocalHapticManager
@ -81,13 +83,15 @@ internal fun HotAccessCodeRequestFullScreenContent(state: HotAccessCodeRequestUM
SpacerH24()
PinTextField(
modifier = Modifier.animateEnterExit(
enter = slideInVertically(
tween(),
initialOffsetY = { it + 200 },
) + fadeIn(tween()),
exit = slideOutVertically(tween(300)) { it - 200 } + fadeOut(tween()),
),
modifier = Modifier
.testTag(HotWalletAccessCodeTestTags.ACCESS_CODE_INPUT)
.animateEnterExit(
enter = slideInVertically(
tween(),
initialOffsetY = { it + 200 },
) + fadeIn(tween()),
exit = slideOutVertically(tween(300)) { it - 200 } + fadeOut(tween()),
),
length = 6,
isPasswordVisual = true,
value = state.accessCode,

View file

@ -188,7 +188,8 @@ private fun TangemPayCardDetailsHiddenBlock(state: TangemPayCardDetailsUM, modif
bottom.linkTo(cardNumberRef.bottom)
}
.padding(bottom = 8.dp)
.size(16.dp),
.size(16.dp)
.testTag(TangemPayTestTags.CARD_FROZEN_BADGE),
painter = painterResource(id = R.drawable.ic_snow_24),
contentDescription = null,
tint = TangemTheme.colors.icon.constant,
@ -201,7 +202,8 @@ private fun TangemPayCardDetailsHiddenBlock(state: TangemPayCardDetailsUM, modif
bottom.linkTo(cardNumberRef.bottom)
}
.padding(bottom = 8.dp)
.size(16.dp),
.size(16.dp)
.testTag(TangemPayTestTags.CARD_FROZEN_BADGE),
color = TangemTheme.colors.text.constantWhite,
strokeWidth = 1.dp,
)

View file

@ -300,7 +300,8 @@ private fun TangemPayCardItem(card: TangemPayDetailsBalanceBlockState.Card, modi
Box(
modifier = modifier
.clip(RoundedCornerShape(4.dp))
.clickable(onClick = card.onClick),
.clickable(onClick = card.onClick)
.testTag(TangemPayTestTags.PAYMENT_ACCOUNT_CARD_BUTTON),
) {
Image(
modifier = Modifier.fillMaxSize(),