From 8094af242dcac949b16800569019b75630722370 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 25 May 2026 19:00:27 +0200 Subject: [PATCH] Updated on 2026-08-14 --- .gitignore | 1 + .../kotlin/com/tangem/common/BaseTestCase.kt | 3 + .../tangem/common/constants/TestConstants.kt | 3 + .../com/tangem/scenarios/BaseScenarios.kt | 48 +++- .../com/tangem/scenarios/SwapScenarios.kt | 33 +++ .../tangem/scenarios/TangemPayScenarios.kt | 34 +++ .../screens/BiometryDialogPageObject.kt | 24 ++ .../screens/HotWalletAccessCodePageObject.kt | 20 ++ .../tangem/screens/SwapSuccessPageObject.kt | 31 +++ .../com/tangem/screens/SwapTokenPageObject.kt | 6 + .../TangemPayAddFundsSheetPageObject.kt | 31 +++ .../tangempay/TangemPayCardPagePageObject.kt | 70 ++++++ .../tangempay/TangemPayChangePinPageObject.kt | 55 ++++ .../TangemPayFreezeConfirmationPageObject.kt | 34 +++ .../tangempay/TangemPayMainPageObject.kt | 51 ++++ .../TangemPayWithdrawNoteSheetPageObject.kt | 28 +++ .../tangem/tests/tangempay/TangemPayTest.kt | 236 ++++++++++++++++++ .../tests/tangempay/TangemPayTopUpTest.kt | 139 +++++++++++ .../tests/tangempay/TangemPayWithdrawTest.kt | 142 +++++++++++ .../tangem/core/ui/test/TangemPayTestTags.kt | 1 + .../HotAccessCodeRequestFullScreenContent.kt | 18 +- .../tangempay/ui/TangemPayCardDetailsBlock.kt | 6 +- .../tangempay/ui/TangemPayDetailsScreen.kt | 3 +- 23 files changed, 1001 insertions(+), 16 deletions(-) create mode 100644 app/src/androidTest/kotlin/com/tangem/scenarios/TangemPayScenarios.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/BiometryDialogPageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/HotWalletAccessCodePageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/SwapSuccessPageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/tangempay/TangemPayAddFundsSheetPageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/tangempay/TangemPayCardPagePageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/tangempay/TangemPayChangePinPageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/tangempay/TangemPayFreezeConfirmationPageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/tangempay/TangemPayMainPageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/tangempay/TangemPayWithdrawNoteSheetPageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/tests/tangempay/TangemPayTest.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/tests/tangempay/TangemPayTopUpTest.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/tests/tangempay/TangemPayWithdrawTest.kt diff --git a/.gitignore b/.gitignore index aed7684b11..a0514e3436 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,4 @@ find-latest-release-branch.output # Claude /.claude/worktrees/ +CLAUDE.local.md diff --git a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt index f8e317b50a..8b307f4571 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt @@ -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, ) ) } diff --git a/app/src/androidTest/kotlin/com/tangem/common/constants/TestConstants.kt b/app/src/androidTest/kotlin/com/tangem/common/constants/TestConstants.kt index 4e6cfb4444..fc4d42796b 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/constants/TestConstants.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/constants/TestConstants.kt @@ -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" } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt index 49e40ea9f1..911b7526d1 100644 --- a/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt @@ -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 { diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/SwapScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/SwapScenarios.kt index 748c45459c..d3933b0416 100644 --- a/app/src/androidTest/kotlin/com/tangem/scenarios/SwapScenarios.kt +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/SwapScenarios.kt @@ -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() diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/TangemPayScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/TangemPayScenarios.kt new file mode 100644 index 0000000000..3e9542762c --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/TangemPayScenarios.kt @@ -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() +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/BiometryDialogPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/BiometryDialogPageObject.kt new file mode 100644 index 0000000000..f45550a113 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/BiometryDialogPageObject.kt @@ -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(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) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/HotWalletAccessCodePageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/HotWalletAccessCodePageObject.kt new file mode 100644 index 0000000000..e351207566 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/HotWalletAccessCodePageObject.kt @@ -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(semanticsProvider = semanticsProvider) { + + val accessCodeInput: KNode = child { + hasTestTag(HotWalletAccessCodeTestTags.ACCESS_CODE_INPUT) + useUnmergedTree = true + } +} + +internal fun BaseTestCase.onHotWalletAccessCodeScreen(function: HotWalletAccessCodePageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SwapSuccessPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SwapSuccessPageObject.kt new file mode 100644 index 0000000000..3ba2a38384 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/SwapSuccessPageObject.kt @@ -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(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) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt index ce2c554f0f..d58c82d8df 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt @@ -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 diff --git a/app/src/androidTest/kotlin/com/tangem/screens/tangempay/TangemPayAddFundsSheetPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/tangempay/TangemPayAddFundsSheetPageObject.kt new file mode 100644 index 0000000000..599c234915 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/tangempay/TangemPayAddFundsSheetPageObject.kt @@ -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(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) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/tangempay/TangemPayCardPagePageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/tangempay/TangemPayCardPagePageObject.kt new file mode 100644 index 0000000000..b9e4b888dd --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/tangempay/TangemPayCardPagePageObject.kt @@ -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(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) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/tangempay/TangemPayChangePinPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/tangempay/TangemPayChangePinPageObject.kt new file mode 100644 index 0000000000..075b616193 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/tangempay/TangemPayChangePinPageObject.kt @@ -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(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) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/tangempay/TangemPayFreezeConfirmationPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/tangempay/TangemPayFreezeConfirmationPageObject.kt new file mode 100644 index 0000000000..78a2d77b9c --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/tangempay/TangemPayFreezeConfirmationPageObject.kt @@ -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(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) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/tangempay/TangemPayMainPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/tangempay/TangemPayMainPageObject.kt new file mode 100644 index 0000000000..731172c3e2 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/tangempay/TangemPayMainPageObject.kt @@ -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(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) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/tangempay/TangemPayWithdrawNoteSheetPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/tangempay/TangemPayWithdrawNoteSheetPageObject.kt new file mode 100644 index 0000000000..869c867cfc --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/tangempay/TangemPayWithdrawNoteSheetPageObject.kt @@ -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(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) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/tangempay/TangemPayTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/tangempay/TangemPayTest.kt new file mode 100644 index 0000000000..f9925bcdcc --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/tangempay/TangemPayTest.kt @@ -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() } + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/tangempay/TangemPayTopUpTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/tangempay/TangemPayTopUpTest.kt new file mode 100644 index 0000000000..4143126b3a --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/tangempay/TangemPayTopUpTest.kt @@ -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() + } + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/tangempay/TangemPayWithdrawTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/tangempay/TangemPayWithdrawTest.kt new file mode 100644 index 0000000000..33f0823bfc --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/tangempay/TangemPayWithdrawTest.kt @@ -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() + } + } + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/TangemPayTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/TangemPayTestTags.kt index ffd6ed9254..3a6f095c0b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/TangemPayTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/TangemPayTestTags.kt @@ -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" diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt index 26e9825ea2..cb7adc8348 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt @@ -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, diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardDetailsBlock.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardDetailsBlock.kt index 5a21bce025..d7bdadc4d4 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardDetailsBlock.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardDetailsBlock.kt @@ -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, ) diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt index c76dac772f..5808fa864f 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt @@ -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(),