Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-06 13:39:30 +03:00
commit 92849d43c2
1407 changed files with 64092 additions and 11990 deletions

View file

@ -184,9 +184,7 @@ abstract class BaseTestCase : TestCase(
toggleStates = mapOf(
"SWAP_REDESIGN_ENABLED" to false,
"ACCOUNTS_FEATURE_ENABLED" to true,
"GASLESS_APPROVAL_ENABLED" to true,
"MAIN_SCREEN_QR_SCANNING_ENABLED" to true,
"ADD_AND_MANAGE_TOKENS_ENABLED" to true,
"ASSETS_DISCOVERY_ENABLED" to true,
"VISA_ONBOARDING_ENABLED" to true,
// Version-gated toggles released in versions <= 6.0 — forced on so tests run against the actual
@ -195,20 +193,13 @@ abstract class BaseTestCase : TestCase(
// 5.37
"HEDERA_ERC20_ENABLED" to true,
// 5.39
"STAKING_ETH_ENABLED" to true,
"DYNAMIC_ADDRESSES_ENABLED" to true,
"SOLANA_TX_HISTORY_ENABLED" to true,
"SOLANA_SCALED_UI_AMOUNT_ENABLED" to true,
"SWAP_AB_ENABLED" to true,
"AND_15310_ADD_FUNDS_STAGE1" to true,
"AND_15009_SWAP_PROVIDER_FILTER_ENABLED" to true,
"AND_15101_TANGEM_PAY_HOT_WALLET_ONBOARDING" to true,
"AND_15402_ADI_MAIN_SCREEN_DEFAULT_ENABLED" to true,
"AND_15103_SWAP_RATE_EXPERIENCE_ENABLED" to true,
"AND_15122_SWAP_PREDEFINED_BUTTONS_ENABLED" to true,
"TWI_1512_HIDE_STORIES_FOR_REFERRAL_ENABLED" to true,
// 5.39.2
"AND_15154_YIELD_PROMO_ENABLED" to true,
// 5.40
"TWI_1377_MANAGE_FUNDS" to true,
// 6.0

View file

@ -46,6 +46,7 @@ object TestConstants {
const val ALLURE_LABEL_VALUE = "Kaspresso"
const val USER_TOKENS_API_SCENARIO = "user_tokens_api"
const val COINS_API_SCENARIO = "coins_api"
const val REFERRAL_API_SCENARIO = "referral_api"
const val QUOTES_API_SCENARIO = "quotes_api"
const val CREATE_USER_WALLET_API_SCENARIO = "create_user_wallet_api"

View file

@ -0,0 +1,20 @@
package com.tangem.common.extensions
import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.performClick
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.test.AppSettingsScreenTestTags
import com.tangem.core.ui.test.TokenDetailsTopBarTestTags
import com.tangem.core.ui.test.TopAppBarTestTags
import com.tangem.core.ui.test.TopNavigationTestTags
fun BaseTestCase.tapBackButton() {
waitForIdle()
composeTestRule.onNode(
hasTestTag(TopNavigationTestTags.BACK_BUTTON)
or hasTestTag(TopAppBarTestTags.CLOSE_BUTTON)
or hasTestTag(TokenDetailsTopBarTestTags.BACK_BUTTON)
or hasTestTag(AppSettingsScreenTestTags.BACK_BUTTON),
useUnmergedTree = true,
).performClick()
}

View file

@ -42,6 +42,17 @@ object AddressComparisonHelper {
assertTrue(report, false)
}
fun derivationPathForBlockchain(json: String, blockchain: String): String {
val array = JSONArray(json)
for (i in 0 until array.length()) {
val entry = array.getJSONObject(i)
if (entry.getString("blockchain").equals(blockchain, ignoreCase = true)) {
return entry.getString("derivationPath").trim()
}
}
error("No entry with blockchain '$blockchain' found in addresses JSON")
}
private fun parseAndNormalize(json: String): List<AddressEntry> {
val array = JSONArray(json)
val entries = mutableListOf<AddressEntry>()

View file

@ -4,6 +4,8 @@ import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
fun getClipboardText(context: Context): String? {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
@ -27,12 +29,15 @@ fun clearClipboard(
clipboard.clearPrimaryClip()
}
// Kotlin's assert() is a no-op on device (JVM assertions disabled) — use JUnit asserts here.
fun assertClipboardTextEquals(
expected: String,
context: Context = ApplicationProvider.getApplicationContext()
) {
assertEquals("Clipboard text mismatch", expected, getClipboardText(context))
}
fun assertClipboardIsEmpty(context: Context = ApplicationProvider.getApplicationContext()) {
val actual = getClipboardText(context)
assert(actual == expected) {
"Clipboard text mismatch.\nExpected: '$expected'\nActual: '$actual'"
}
assertTrue("Expected empty clipboard but was: '$actual'", actual.isNullOrEmpty())
}

View file

@ -1,6 +1,5 @@
package com.tangem.scenarios
import android.view.KeyEvent
import androidx.test.core.app.ApplicationProvider
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_VERY_LONG
@ -57,31 +56,6 @@ fun BaseTestCase.verifyAddresses(seedPhrase: String, apiAddressesJson: String) {
}
}
private const val TESTER_MENU_MAX_ATTEMPTS = 3
/**
* Presses 'Volume Down' twice to open tester menu.
* Retries up to [TESTER_MENU_MAX_ATTEMPTS] times if the menu doesn't appear.
*/
private fun BaseTestCase.openTesterMenu() {
repeat(TESTER_MENU_MAX_ATTEMPTS) { attempt ->
waitForIdle()
device.uiDevice.pressKeyCode(KeyEvent.KEYCODE_VOLUME_DOWN)
device.uiDevice.pressKeyCode(KeyEvent.KEYCODE_VOLUME_DOWN)
val opened = runCatching {
onTesterMenuScreen { addressesInfoButton.assertIsDisplayed() }
}.isSuccess
if (opened) {
TangemLogger.i("Tester menu opened on attempt ${attempt + 1}")
return
}
TangemLogger.w("Tester menu not opened on attempt ${attempt + 1}, retrying...")
}
error("Failed to open tester menu after $TESTER_MENU_MAX_ATTEMPTS attempts")
}
/**
* Polls [walletManagersStore] until the wallet manager count stops growing for [WALLET_MANAGERS_SETTLE_MS].

View file

@ -57,6 +57,17 @@ fun BaseTestCase.selectStablecoinAsFeeToken(coinName: String, tokenName: String)
step("Select '$tokenName' as the fee-paying token") {
onSendFeeSelectorBottomSheet { feeTokenItem(tokenName).performClick() }
}
step("Wait until the '$tokenName' fee is loaded and 'Apply' is enabled") {
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) {
runCatching {
onSendFeeSelectorBottomSheet {
networkFeeTitle.assertIsDisplayed()
feeTokenItem(tokenName).assertIsDisplayed()
applyButton.assertIsEnabled()
}
}.isSuccess
}
}
}
/**

View file

@ -0,0 +1,154 @@
package com.tangem.scenarios
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.extensions.performTextInputInChunks
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.screens.accounts.onAccountDetailsScreen
import com.tangem.screens.onAddCustomTokenScreen
import com.tangem.screens.onDetailsScreen
import com.tangem.screens.onDialog
import com.tangem.screens.onManageTokensScreen
import com.tangem.screens.onWalletSettingsScreen
import io.github.kakaocup.kakao.common.utilities.getResourceString
import io.qameta.allure.kotlin.Allure.step
import kotlinx.coroutines.runBlocking
import com.tangem.core.res.R as CoreResR
private fun mainAccountName(): String = getResourceString(CoreResR.string.account_main_account_title)
// flakySafely is unavailable in BaseTestCase extensions — wait until the assertion/action stops throwing.
private fun BaseTestCase.awaitSuccess(block: () -> Unit) {
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT) { runCatching(block).isSuccess }
}
fun BaseTestCase.openManageTokens(accountName: String = mainAccountName()) {
openWalletSettingsScreen()
openAccountDetails(accountName)
step("Click on 'Manage tokens' button") {
onAccountDetailsScreen { manageTokensButton.clickWithAssertion() }
}
}
fun BaseTestCase.openAddCustomToken(accountName: String = mainAccountName()) {
openManageTokens(accountName)
step("Click on 'Add custom token' button") {
onManageTokensScreen { addCustomTokenButton.clickWithAssertion() }
}
}
fun BaseTestCase.addCustomTokenWithCustomDerivation(network: String, contract: String, derivationPath: String) {
step("Click on network: '$network'") {
awaitSuccess { onAddCustomTokenScreen { scrollToNetwork(network) } }
onAddCustomTokenScreen { networkRow(network).performClick() }
}
step("Enter contract address: '$contract'") {
awaitSuccess { onAddCustomTokenScreen { contractAddressField.assertExists() } }
onAddCustomTokenScreen { contractAddressField.performTextInputInChunks(contract) }
}
step("Click on 'Derivation path' field") {
awaitSuccess { onAddCustomTokenScreen { derivationSelectorField.assertExists() } }
onAddCustomTokenScreen { derivationSelectorField.performClick() }
}
step("Click on 'Custom derivation' button") {
awaitSuccess { onAddCustomTokenScreen { customDerivationButton.assertIsDisplayed() } }
onAddCustomTokenScreen { customDerivationButton.performClick() }
}
step("Enter custom derivation path: '$derivationPath'") {
awaitSuccess { onDialog { inputField.assertIsDisplayed() } }
onDialog { inputField.performTextInputInChunks(derivationPath) }
awaitSuccess { onDialog { okButton.assertIsEnabled() } }
onDialog { okButton.performClick() }
}
step("Click on 'Add token' button") {
awaitSuccess { onAddCustomTokenScreen { addTokenButton.assertIsEnabled() } }
onAddCustomTokenScreen { addTokenButton.performClick() }
}
navigateBackToMainFromManageTokens()
}
private const val MAX_BACKS_TO_MANAGE_TOKENS = 5
// The device-backs to close the sheet vary — press back until Manage Tokens is shown, then fail loudly.
private fun BaseTestCase.closeAddCustomTokenSheet() {
repeat(MAX_BACKS_TO_MANAGE_TOKENS) {
if (runCatching { onManageTokensScreen { searchField.assertIsDisplayed() } }.isSuccess) return
device.uiDevice.pressBack()
waitForIdle()
}
onManageTokensScreen { searchField.assertIsDisplayed() }
}
private fun BaseTestCase.navigateBackToWalletSettings() {
step("Click on 'Manage tokens' screen 'Back' button") {
waitForIdle()
onManageTokensScreen { topAppBarBackButton.performClick() }
}
step("Click on 'Account details' screen 'Back' button") {
waitForIdle()
onAccountDetailsScreen { topAppBarBackButton.performClick() }
}
}
fun BaseTestCase.forgetCurrentWalletAndReArmForNextScan() {
step("Close 'Add custom token' bottom sheet") { closeAddCustomTokenSheet() }
navigateBackToWalletSettings()
step("Click on 'Forget wallet' button") {
waitForIdle()
onWalletSettingsScreen {
scrollToForgetWallet()
forgetWalletButton.clickWithAssertion()
}
}
step("Confirm forgetting the wallet") {
awaitSuccess { onDialog { forgetButton.assertIsDisplayed() } }
onDialog { forgetButton.clickWithAssertion() }
}
// Markets tooltip is a one-time app-level flag; re-arm it so the next scan's openMainScreen can dismiss it.
step("Re-arm the markets tooltip for the next scan") {
runBlocking {
appPreferencesStore.editData { it.set(PreferencesKeys.SHOULD_SHOW_MARKETS_TOOLTIP_KEY, value = true) }
}
}
}
fun BaseTestCase.assertDerivationPathsInSelector(networkToOpen: String, vararg expectedPaths: Pair<String, String>) {
step("Click on network: '$networkToOpen'") {
awaitSuccess { onAddCustomTokenScreen { scrollToNetwork(networkToOpen) } }
onAddCustomTokenScreen { networkRow(networkToOpen).performClick() }
}
step("Click on 'Derivation path' field") {
awaitSuccess { onAddCustomTokenScreen { derivationSelectorField.assertExists() } }
onAddCustomTokenScreen { derivationSelectorField.performClick() }
}
expectedPaths.forEach { (networkId, path) ->
step("Assert '$networkId' derivation path is '$path'") {
awaitSuccess { onAddCustomTokenScreen { scrollToDerivationRow(networkId) } }
onAddCustomTokenScreen { derivationPath(networkId, path).assertIsDisplayed() }
}
}
}
fun BaseTestCase.toggleTokenNetworkInManageTokens(tokenTitle: String, networkTitle: String) {
step("Click on token: '$tokenTitle'") {
awaitSuccess { onManageTokensScreen { tokenItem(tokenTitle).assertIsDisplayed() } }
onManageTokensScreen { tokenItem(tokenTitle).performClick() }
}
step("Click on '$networkTitle' switch") {
awaitSuccess { onManageTokensScreen { networkSwitch(networkTitle).assertIsDisplayed() } }
onManageTokensScreen { networkSwitch(networkTitle).performClick() }
}
}
fun BaseTestCase.navigateBackToMainFromManageTokens() {
navigateBackToWalletSettings()
step("Click on 'Wallet settings' screen 'Back' button") {
waitForIdle()
onWalletSettingsScreen { topAppBarBackButton.performClick() }
}
step("Click on 'Details' screen 'Back' button") {
waitForIdle()
onDetailsScreen { topAppBarBackButton.performClick() }
}
}

View file

@ -0,0 +1,27 @@
package com.tangem.scenarios
import com.tangem.common.BaseTestCase
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.screens.onDetailsScreen
import com.tangem.screens.onMainScreenTopBar
import com.tangem.screens.onReferralProgramScreen
import com.tangem.screens.onWalletSettingsScreen
import io.qameta.allure.kotlin.Allure.step
fun BaseTestCase.referralTakeParticipate() {
step("Open 'Details' screen") {
onMainScreenTopBar { moreButton.clickWithAssertion() }
}
step("Open 'Wallet settings' screen") {
onDetailsScreen { walletNameButton.clickWithAssertion() }
}
step("Open 'Referral program' screen") {
onWalletSettingsScreen { referralProgramButton.clickWithAssertion() }
}
step("Tap 'Participate'") {
onReferralProgramScreen { participateButton.clickWithAssertion() }
}
step("Verify referral code is displayed") {
onReferralProgramScreen { personalCodeCard.assertIsDisplayed() }
}
}

View file

@ -0,0 +1,33 @@
package com.tangem.scenarios
import android.view.KeyEvent
import com.tangem.common.BaseTestCase
import com.tangem.screens.onTesterMenuScreen
import com.tangem.utils.logging.TangemLogger
private const val TESTER_MENU_MAX_ATTEMPTS = 3
/**
* Opens the tester menu by sending two 'Volume Down' key events in a single shell command.
* VolumeButtonDoublePressObserver needs two ACTION_DOWN within 300ms; two separate pressKeyCode
* calls are too slow, but both events in one `input keyevent` invocation arrive back-to-back within
* the window. Retries up to [TESTER_MENU_MAX_ATTEMPTS] times if it doesn't appear.
*/
fun BaseTestCase.openTesterMenu() {
repeat(TESTER_MENU_MAX_ATTEMPTS) { attempt ->
waitForIdle()
val volumeDown = KeyEvent.KEYCODE_VOLUME_DOWN
device.uiDevice.executeShellCommand("input keyevent $volumeDown $volumeDown")
val opened = runCatching {
onTesterMenuScreen { addressesInfoButton.assertIsDisplayed() }
}.isSuccess
if (opened) {
TangemLogger.i("Tester menu opened on attempt ${attempt + 1}")
return
}
TangemLogger.w("Tester menu not opened on attempt ${attempt + 1}, retrying...")
}
error("Failed to open tester menu after $TESTER_MENU_MAX_ATTEMPTS attempts")
}

View file

@ -0,0 +1,67 @@
package com.tangem.screens
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import androidx.compose.ui.test.performScrollToNode
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.test.AddCustomTokenScreenTestTags
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
import androidx.compose.ui.test.hasTestTag as withTestTag
import androidx.compose.ui.test.hasText as withText
import com.tangem.core.res.R as CoreResR
class AddCustomTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<AddCustomTokenPageObject>(semanticsProvider = semanticsProvider) {
val selectorList: KNode = child {
hasTestTag(AddCustomTokenScreenTestTags.SELECTOR_LIST)
}
fun networkRow(networkName: String): KNode = child {
hasTestTag(AddCustomTokenScreenTestTags.networkRow(networkName))
}
@OptIn(ExperimentalTestApi::class)
fun scrollToNetwork(networkName: String) = selectorList {
performScrollToNode(withTestTag(AddCustomTokenScreenTestTags.networkRow(networkName)))
}
@OptIn(ExperimentalTestApi::class)
fun scrollToDerivationRow(networkId: String) = selectorList {
performScrollToNode(withTestTag(AddCustomTokenScreenTestTags.derivationRow(networkId)))
}
fun derivationPath(networkId: String, path: String): KNode = child {
useUnmergedTree = true
hasTestTag(AddCustomTokenScreenTestTags.derivationRow(networkId))
hasAnyDescendant(withText(path))
}
val contractAddressField: KNode = child {
hasTestTag(AddCustomTokenScreenTestTags.CONTRACT_ADDRESS_FIELD)
}
val derivationSelectorField: KNode = child {
hasTestTag(AddCustomTokenScreenTestTags.DERIVATION_SELECTOR_FIELD)
}
val customDerivationButton: KNode = child {
hasTestTag(AddCustomTokenScreenTestTags.CUSTOM_DERIVATION_BUTTON)
}
val warningNotification: KNode = child {
hasTestTag(AddCustomTokenScreenTestTags.WARNING_NOTIFICATION)
}
val addTokenButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(CoreResR.string.common_add_token))
}
}
internal fun BaseTestCase.onAddCustomTokenScreen(function: AddCustomTokenPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -14,6 +14,10 @@ class AppSettingsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider
hasTestTag(AppSettingsScreenTestTags.CURRENCY_BUTTON)
useUnmergedTree = true
}
val backButton: KNode = child {
hasTestTag(AppSettingsScreenTestTags.BACK_BUTTON)
}
}
internal fun BaseTestCase.onAppSettingsScreen(function: AppSettingsPageObject.() -> Unit) =

View file

@ -14,6 +14,10 @@ import androidx.compose.ui.test.hasText as withText
class DetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<DetailsPageObject>(semanticsProvider = semanticsProvider) {
val screenContainer: KNode = child {
hasTestTag(DetailsScreenTestTags.SCREEN_CONTAINER)
}
val topAppBarBackButton: KNode = child {
hasTestTag(TopAppBarTestTags.CLOSE_BUTTON)
}

View file

@ -2,6 +2,7 @@ package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.res.R as CoreResR
import com.tangem.core.ui.R
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.test.BaseDialogTestTags
@ -10,6 +11,7 @@ import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onCompose
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
import androidx.compose.ui.test.hasTestTag as withTestTag
import androidx.compose.ui.test.hasText as withText
class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<DialogPageObject>(semanticsProvider = semanticsProvider) {
@ -18,6 +20,12 @@ class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
hasTestTag(BaseDialogTestTags.CONTAINER)
}
fun containerWithText(text: String): KNode = child {
hasTestTag(BaseDialogTestTags.CONTAINER)
hasAnyDescendant(withText(text = text, substring = true))
useUnmergedTree = true
}
val title: KNode = child {
hasTestTag(BaseDialogTestTags.TITLE)
}
@ -87,6 +95,11 @@ class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
hasText(getResourceString(R.string.common_ok))
}
val forgetButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(CoreResR.string.common_forget))
}
val changeButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.common_change))

View file

@ -6,6 +6,7 @@ import com.tangem.common.R
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.test.BaseSearchBarTestTags
import com.tangem.core.ui.test.ManageTokensScreenTestTags
import com.tangem.core.ui.test.SearchBarTestTags
import com.tangem.core.ui.test.SwitchTestTags
import com.tangem.core.ui.test.TopAppBarTestTags
import io.github.kakaocup.compose.node.element.ComposeScreen
@ -17,6 +18,7 @@ import androidx.compose.ui.test.hasText as withText
import androidx.compose.ui.test.hasAnySibling as withAnySibling
import androidx.compose.ui.test.hasAnyDescendant as withAnyDescendant
import androidx.compose.ui.test.hasAnyAncestor as withAnyAncestor
import com.tangem.core.res.R as CoreResR
class ManageTokensPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<ManageTokensPageObject>(semanticsProvider = semanticsProvider) {
@ -35,11 +37,43 @@ class ManageTokensPageObject(semanticsProvider: SemanticsNodeInteractionsProvide
hasTestTag(BaseSearchBarTestTags.SEARCH_BAR)
}
val searchClearButton: KNode = child {
hasTestTag(SearchBarTestTags.CLEAR_BUTTON)
}
val addCustomTokenButton: KNode = child {
hasTestTag(TopAppBarTestTags.MORE_BUTTON)
}
fun tokenItem(tokenName: String): KNode = child {
hasTestTag(ManageTokensScreenTestTags.TOKEN_ITEM)
hasText(tokenName)
}
fun networkStandard(networkName: String, standard: String): KNode = child {
useUnmergedTree = true
addSemanticsMatcher(
withText(standard).and(
withAnyAncestor(
withTestTag(ManageTokensScreenTestTags.NETWORK_NAME)
.and(withAnyDescendant(withText(networkName, ignoreCase = true))),
),
),
)
}
fun networkName(networkName: String): KNode = child {
useUnmergedTree = true
addSemanticsMatcher(
withTestTag(ManageTokensScreenTestTags.NETWORK_NAME)
.and(withAnyDescendant(withText(networkName))),
)
}
val contractAddressCopiedMessage: KNode = child {
hasText(getResourceString(CoreResR.string.contract_address_copied_message))
}
fun networkSwitch(networkName: String): KNode = child {
useUnmergedTree = true
addSemanticsMatcher(

View file

@ -78,6 +78,11 @@ class ReferralProgramPageObject(semanticsProvider: SemanticsNodeInteractionsProv
hasTestTag(BaseButtonTestTags.TEXT)
useUnmergedTree = true
}
val personalCodeCard: KNode = child {
hasTestTag(ReferralProgramScreenTestTags.PERSONAL_CODE_CARD)
useUnmergedTree = true
}
}
internal fun BaseTestCase.onReferralProgramScreen(function: ReferralProgramPageObject.() -> Unit) =

View file

@ -114,6 +114,12 @@ class TokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvide
useUnmergedTree = true
}
fun tokenTitle(name: String): KNode = child {
hasTestTag(TokenDetailsScreenTestTags.TOKEN_TITLE)
hasAnyDescendant(withText(text = name, substring = true))
useUnmergedTree = true
}
fun networkFeeNotificationMessage(
currencyName: String,
networkName: String,

View file

@ -59,15 +59,15 @@ class AppCurrencyTest : BaseTestCase() {
onAppSettingsScreen { currencyButton.assertIsDisplayed() }
}
}
step("Return to 'Details' screen") {
waitForIdle()
device.uiDevice.pressBack()
step("Return to 'Details' screen via 'Back' button") {
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
onAppSettingsScreen { backButton.clickWithAssertion() }
onDetailsScreen { screenContainer.assertIsDisplayed() }
}
}
step("Return to 'Main' screen via 'Back' button") {
onDetailsScreen { topAppBarBackButton.clickWithAssertion() }
}
step("Assert 'Main' screen is opened") {
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
onDetailsScreen { topAppBarBackButton.clickWithAssertion() }
onMainScreen { screenContainer.assertIsDisplayed() }
}
}

View file

@ -3,6 +3,7 @@ package com.tangem.tests.addFunds
import com.tangem.common.BaseTestCase
import com.tangem.common.extensions.assertTextContainsSafe
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.domain.models.scan.ProductType
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.onAddTokenBottomSheet
@ -11,6 +12,7 @@ import com.tangem.screens.onBuyTokenDetailsScreen
import com.tangem.screens.onMainScreen
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.Issue
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Test
@ -83,4 +85,24 @@ class BuyTest : BaseTestCase() {
}
}
}
@AllureId("3613")
@DisplayName("On-ramp Buy: S2C card doesn't have Buy and Sell options")
@Test
@Issue("[REDACTED_TASK_KEY]")
fun buyAndSellIsNotAvailableForS2CCardTest() {
setupHooks().run {
step("Open 'Main' screen") {
openMainScreen(productType = ProductType.Start2Coin)
}
step("Verify 'Add funds' button is displayed") {
onMainScreen { addFundsButton.assertIsDisplayed() }
}
step("Verify Buy/Sell action buttons are hidden") {
onMainScreen {
buyButton.assertDoesNotExist()
}
}
}
}
}

View file

@ -0,0 +1,251 @@
package com.tangem.tests.main
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.COINS_API_SCENARIO
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
import com.tangem.common.extensions.performTextInputInChunks
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.domain.models.scan.ProductType
import com.tangem.scenarios.addCustomTokenWithCustomDerivation
import com.tangem.scenarios.assertDerivationPathsInSelector
import com.tangem.scenarios.forgetCurrentWalletAndReArmForNextScan
import com.tangem.scenarios.navigateBackToMainFromManageTokens
import com.tangem.scenarios.openAddCustomToken
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.onAddCustomTokenScreen
import com.tangem.screens.onMainScreen
import com.tangem.tap.domain.sdk.mocks.content.Wallet1LegacyDerivationMockContent
import com.tangem.tap.domain.sdk.mocks.content.Wallet2NoEd25519Slip0010MockContent
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Assert.assertFalse
import org.junit.Test
@HiltAndroidTest
class AddCustomTokenTest : BaseTestCase() {
private val richState = "ManageTokensRich"
private val tokenTitle = "Tether"
private val ethereumNetwork = "Ethereum"
private val solanaNetwork = "Solana"
private val bitcoinNetworkId = "bitcoin"
private val ethereumClassicNetworkId = "ethereum-classic"
private val ethContract = "0xdac17f958d2ee523a2206206994597c13d831ec7"
private val solanaContract = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
private val customDerivationPath = "m/44'/60'/0'/0/1"
@AllureId("772")
@DisplayName("Add custom token: added token appears on Main")
@Test
fun addCustomTokenAppearsOnMainTest() {
setupHooks(
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
).run {
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
setWireMockScenarioState(COINS_API_SCENARIO, richState)
}
step("Open 'Main Screen'") { openMainScreen() }
step("Synchronize addresses") {
flakySafely { onMainScreen { synchronizeAddressesButton.assertIsDisplayed() } }
synchronizeAddresses(assertBalance = false)
}
step("Open 'Add custom token' screen") { openAddCustomToken() }
step("Click on network: '$ethereumNetwork'") {
flakySafely { onAddCustomTokenScreen { scrollToNetwork(ethereumNetwork) } }
onAddCustomTokenScreen { networkRow(ethereumNetwork).performClick() }
}
step("Enter contract address: '$ethContract'") {
flakySafely { onAddCustomTokenScreen { contractAddressField.assertExists() } }
// Chunked input spans several validation ticks — the model drops the first sampled form change.
onAddCustomTokenScreen { contractAddressField.performTextInputInChunks(ethContract) }
}
step("Click on 'Add token' button") {
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { onAddCustomTokenScreen { addTokenButton.assertIsEnabled() } }
onAddCustomTokenScreen { addTokenButton.performClick() }
}
step("Navigate back to 'Main Screen'") { navigateBackToMainFromManageTokens() }
step("Assert token: '$tokenTitle' is displayed on Main") {
flakySafely { onMainScreen { tokenWithTitleAndAddress(tokenTitle).assertIsDisplayed() } }
}
}
}
@AllureId("775")
@DisplayName("Add custom token: custom derivation path is accepted")
@Test
fun addCustomTokenWithCustomDerivationTest() {
setupHooks(
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
).run {
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
setWireMockScenarioState(COINS_API_SCENARIO, richState)
}
step("Open 'Main Screen'") { openMainScreen() }
step("Synchronize addresses") {
flakySafely { onMainScreen { synchronizeAddressesButton.assertIsDisplayed() } }
synchronizeAddresses(assertBalance = false)
}
step("Open 'Add custom token' screen") { openAddCustomToken() }
step("Add custom token with custom derivation on '$ethereumNetwork'") {
addCustomTokenWithCustomDerivation(
network = ethereumNetwork,
contract = ethContract,
derivationPath = customDerivationPath,
)
}
step("Assert token: '$tokenTitle' is displayed on Main") {
flakySafely { onMainScreen { tokenWithTitleAndAddress(tokenTitle).assertIsDisplayed() } }
}
}
}
@AllureId("771")
@DisplayName("Add custom token: custom derivation indicator is shown on Main")
@Test
fun customDerivationIndicatorOnMainTest() {
setupHooks(
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
).run {
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
setWireMockScenarioState(COINS_API_SCENARIO, richState)
}
step("Open 'Main Screen'") { openMainScreen() }
step("Synchronize addresses") {
flakySafely { onMainScreen { synchronizeAddressesButton.assertIsDisplayed() } }
synchronizeAddresses(assertBalance = false)
}
step("Open 'Add custom token' screen") { openAddCustomToken() }
step("Add custom token with custom derivation on '$ethereumNetwork'") {
addCustomTokenWithCustomDerivation(
network = ethereumNetwork,
contract = ethContract,
derivationPath = customDerivationPath,
)
}
step("Assert custom derivation indicator for '$tokenTitle' is displayed on Main") {
flakySafely { onMainScreen { tokenWithCustomDerivationIcon(tokenTitle).assertIsDisplayed() } }
}
}
}
@AllureId("770")
@DisplayName("Add custom token: derivation field is available for a non-EVM network")
@Test
fun derivationAvailableForNonEvmNetworkTest() {
setupHooks(
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
).run {
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
setWireMockScenarioState(COINS_API_SCENARIO, richState)
}
step("Open 'Main Screen'") { openMainScreen() }
step("Open 'Add custom token' screen") { openAddCustomToken() }
step("Click on network: '$solanaNetwork'") {
flakySafely { onAddCustomTokenScreen { scrollToNetwork(solanaNetwork) } }
onAddCustomTokenScreen { networkRow(solanaNetwork).performClick() }
}
step("Assert 'Derivation path' field is available") {
flakySafely { onAddCustomTokenScreen { derivationSelectorField.assertExists() } }
}
}
}
@AllureId("769")
@DisplayName("Add custom token: Solana token on a modern card shows no unsupported warning")
@Test
fun solanaTokenModernCardNoWarningTest() {
setupHooks(
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
).run {
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
setWireMockScenarioState(COINS_API_SCENARIO, richState)
}
step("Open 'Main Screen'") { openMainScreen() }
step("Open 'Add custom token' screen") { openAddCustomToken() }
step("Click on network: '$solanaNetwork'") {
flakySafely { onAddCustomTokenScreen { scrollToNetwork(solanaNetwork) } }
onAddCustomTokenScreen { networkRow(solanaNetwork).performClick() }
}
step("Enter contract address: '$solanaContract'") {
flakySafely { onAddCustomTokenScreen { contractAddressField.assertExists() } }
onAddCustomTokenScreen { contractAddressField.performTextInputInChunks(solanaContract) }
}
step("Assert 'Add token' button is enabled") {
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { onAddCustomTokenScreen { addTokenButton.assertIsEnabled() } }
}
step("Assert unsupported-token warning is not displayed") {
onAddCustomTokenScreen { warningNotification.assertDoesNotExist() }
}
}
}
@AllureId("777")
@DisplayName("Add custom token: network with a missing curve is not offered")
@Test
fun missingCurveNetworkNotOfferedTest() {
val bitcoinNetwork = "Bitcoin"
setupHooks(
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
).run {
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
setWireMockScenarioState(COINS_API_SCENARIO, richState)
}
step("Open 'Main Screen'") { openMainScreen(mockContent = Wallet2NoEd25519Slip0010MockContent) }
step("Open 'Add custom token' screen") { openAddCustomToken() }
step("Assert supported network '$bitcoinNetwork' is displayed") {
flakySafely { onAddCustomTokenScreen { scrollToNetwork(bitcoinNetwork) } }
}
step("Assert '$solanaNetwork' network is not displayed for the missing-curve card") {
onAddCustomTokenScreen { selectorList.assertIsDisplayed() }
val solanaOffered = runCatching { onAddCustomTokenScreen { scrollToNetwork(solanaNetwork) } }.isSuccess
assertFalse("'$solanaNetwork' should not be offered for a missing-curve card", solanaOffered)
}
}
}
@AllureId("776")
@DisplayName("Add custom token: derivation paths match the card version (V1/V2/V3)")
@Test
fun derivationPathsMatchCardVersionTest() {
setupHooks(
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
).run {
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
setWireMockScenarioState(COINS_API_SCENARIO, richState)
}
step("Assert derivation paths for a legacy-batch V1 Wallet card") {
openMainScreen(mockContent = Wallet1LegacyDerivationMockContent)
openAddCustomToken()
assertDerivationPathsInSelector(
ethereumNetwork,
bitcoinNetworkId to "m/44'/0'/0'/0/0",
ethereumClassicNetworkId to "m/44'/61'/0'/0/0",
)
forgetCurrentWalletAndReArmForNextScan()
}
step("Assert derivation paths for a V2 Wallet card") {
openMainScreen()
openAddCustomToken()
assertDerivationPathsInSelector(
ethereumNetwork,
bitcoinNetworkId to "m/44'/0'/0'/0/0",
ethereumClassicNetworkId to "m/44'/60'/0'/0/0",
)
forgetCurrentWalletAndReArmForNextScan()
}
step("Assert derivation paths for a V3 Wallet 2 card") {
openMainScreen(productType = ProductType.Wallet2)
openAddCustomToken()
assertDerivationPathsInSelector(
ethereumNetwork,
bitcoinNetworkId to "m/84'/0'/0'/0/0",
ethereumClassicNetworkId to "m/44'/61'/0'/0/0",
)
}
}
}
}

View file

@ -0,0 +1,250 @@
package com.tangem.tests.main
import androidx.compose.ui.test.longClick
import androidx.compose.ui.test.performTextInput
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.COINS_API_SCENARIO
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.assertClipboardIsEmpty
import com.tangem.common.utils.assertClipboardTextEquals
import com.tangem.common.utils.clearClipboard
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.openManageTokens
import com.tangem.scenarios.toggleTokenNetworkInManageTokens
import com.tangem.screens.onDialog
import com.tangem.screens.onManageTokensScreen
import com.tangem.tap.domain.sdk.mocks.content.Firmware451MockContent
import com.tangem.tap.domain.sdk.mocks.content.Wallet2NoEd25519Slip0010MockContent
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
import com.tangem.core.res.R as CoreResR
@HiltAndroidTest
class ManageTokensTest : BaseTestCase() {
private val richState = "ManageTokensRich"
private val solanaTokenTitle = "USD Coin"
private val solanaNetworkTitle = "SOLANA"
private val solanaName = "Solana"
@AllureId("765")
@DisplayName("Manage tokens: network standard labels are shown for token networks")
@Test
fun networkStandardLabelsTest() {
val tokenTitle = "Tether"
val ethereum = "Ethereum"
val bnbSmartChain = "BNB Smart Chain"
val tron = "Tron"
val erc20 = "ERC20"
val bep20 = "BEP20"
val trc20 = "TRC20"
setupHooks(
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
).run {
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
setWireMockScenarioState(COINS_API_SCENARIO, richState)
}
step("Open 'Main Screen'") { openMainScreen() }
step("Open 'Manage tokens' screen") { openManageTokens() }
step("Click on token: '$tokenTitle'") {
flakySafely { onManageTokensScreen { tokenItem(tokenTitle).assertIsDisplayed() } }
onManageTokensScreen { tokenItem(tokenTitle).performClick() }
}
step("Assert '$ethereum' network standard '$erc20' is displayed") {
flakySafely {
onManageTokensScreen { networkStandard(networkName = ethereum, standard = erc20).assertIsDisplayed() }
}
}
step("Assert '$bnbSmartChain' network standard '$bep20' is displayed") {
flakySafely {
onManageTokensScreen {
networkStandard(networkName = bnbSmartChain, standard = bep20).assertIsDisplayed()
}
}
}
step("Assert '$tron' network standard '$trc20' is displayed") {
flakySafely {
onManageTokensScreen { networkStandard(networkName = tron, standard = trc20).assertIsDisplayed() }
}
}
}
}
@AllureId("667")
@DisplayName("Manage tokens: search by name and ticker filters the list")
@Test
fun searchByNameAndTickerTest() {
val tokenTitle = "Tether"
val nameQuery = "Tether"
val tickerQuery = "USDT"
val emptyQuery = "Zzqnotoken"
setupHooks(
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
).run {
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
setWireMockScenarioState(COINS_API_SCENARIO, richState)
}
step("Open 'Main Screen'") { openMainScreen() }
step("Open 'Manage tokens' screen") { openManageTokens() }
step("Search by name: '$nameQuery'") {
onManageTokensScreen {
searchField.performClick()
searchField.performTextInput(nameQuery)
}
}
step("Assert token: '$tokenTitle' is displayed") {
flakySafely { onManageTokensScreen { tokenItem(tokenTitle).assertIsDisplayed() } }
}
step("Clear search field") {
onManageTokensScreen { searchClearButton.clickWithAssertion() }
}
step("Search by ticker: '$tickerQuery'") {
onManageTokensScreen { searchField.performTextInput(tickerQuery) }
}
step("Assert token: '$tokenTitle' is displayed") {
flakySafely { onManageTokensScreen { tokenItem(tokenTitle).assertIsDisplayed() } }
}
step("Clear search field") {
onManageTokensScreen { searchClearButton.clickWithAssertion() }
}
step("Search by unknown query: '$emptyQuery'") {
onManageTokensScreen { searchField.performTextInput(emptyQuery) }
}
step("Assert token: '$tokenTitle' is not displayed") {
flakySafely { onManageTokensScreen { tokenItem(tokenTitle).assertDoesNotExist() } }
}
}
}
@AllureId("763")
@DisplayName("Manage tokens: enabling Solana network on a modern card shows no warning")
@Test
fun solanaNetworkNoWarningTest() {
setupHooks(
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
).run {
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
setWireMockScenarioState(COINS_API_SCENARIO, richState)
}
step("Open 'Main Screen'") { openMainScreen() }
step("Open 'Manage tokens' screen") { openManageTokens() }
step("Enable '$solanaNetworkTitle' network for token: '$solanaTokenTitle'") {
toggleTokenNetworkInManageTokens(tokenTitle = solanaTokenTitle, networkTitle = solanaNetworkTitle)
}
step("Assert hide-token alert is not displayed") {
waitForIdle()
onDialog { dialogContainer.assertDoesNotExist() }
}
}
}
@AllureId("719")
@DisplayName("Manage tokens: long tap on a token network copies the contract address, main network does not")
@Test
fun copyContractAddressOnNetworkLongTapTest() {
val tokenTitle = "Tether"
val tokenNetworkTitle = "ETHEREUM"
val coinTitle = "Bitcoin"
val coinNetworkTitle = "BITCOIN"
val contractAddress = "0xdac17f958d2ee523a2206206994597c13d831ec7"
setupHooks(
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
).run {
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
setWireMockScenarioState(COINS_API_SCENARIO, richState)
}
step("Open 'Main Screen'") { openMainScreen() }
step("Open 'Manage tokens' screen") { openManageTokens() }
step("Click on token: '$tokenTitle'") {
flakySafely { onManageTokensScreen { tokenItem(tokenTitle).assertIsDisplayed() } }
onManageTokensScreen { tokenItem(tokenTitle).performClick() }
}
step("Long click on '$tokenNetworkTitle' network row") {
flakySafely { onManageTokensScreen { networkName(tokenNetworkTitle).assertIsDisplayed() } }
onManageTokensScreen {
networkName(tokenNetworkTitle).performTouchInput {
longClick(position = center, durationMillis = 1000L)
}
}
}
step("Assert contract-address-copied message is displayed") {
flakySafely { onManageTokensScreen { contractAddressCopiedMessage.assertIsDisplayed() } }
}
step("Assert clipboard contains '$tokenTitle' contract address") {
assertClipboardTextEquals(contractAddress)
}
step("Clear clipboard") { clearClipboard() }
step("Click on token: '$coinTitle'") {
flakySafely { onManageTokensScreen { tokenItem(coinTitle).assertIsDisplayed() } }
onManageTokensScreen { tokenItem(coinTitle).performClick() }
}
step("Long click on '$coinNetworkTitle' main network row") {
flakySafely { onManageTokensScreen { networkName(coinNetworkTitle).assertIsDisplayed() } }
onManageTokensScreen {
networkName(coinNetworkTitle).performTouchInput {
longClick(position = center, durationMillis = 1000L)
}
}
}
step("Assert clipboard is empty after long click on main network") {
waitForIdle()
assertClipboardIsEmpty()
}
}
}
@AllureId("737")
@DisplayName("Manage tokens: enabling Solana on an old-firmware card shows firmware-limitation warning")
@Test
fun solanaFirmwareLimitationWarningTest() {
val warningMessage = getResourceString(CoreResR.string.alert_manage_tokens_unsupported_message, solanaName)
setupHooks(
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
).run {
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
setWireMockScenarioState(COINS_API_SCENARIO, richState)
}
step("Open 'Main Screen'") { openMainScreen(mockContent = Firmware451MockContent) }
step("Open 'Manage tokens' screen") { openManageTokens() }
step("Enable '$solanaNetworkTitle' network for token: '$solanaTokenTitle'") {
toggleTokenNetworkInManageTokens(tokenTitle = solanaTokenTitle, networkTitle = solanaNetworkTitle)
}
step("Assert firmware-limitation warning is displayed") {
flakySafely { onDialog { containerWithText(warningMessage).assertIsDisplayed() } }
}
}
}
@AllureId("767")
@DisplayName("Manage tokens: enabling Solana on a card missing its curve shows unsupported-curve warning")
@Test
fun solanaUnsupportedCurveWarningTest() {
val warningMessage =
getResourceString(CoreResR.string.alert_manage_tokens_unsupported_curve_message, solanaName)
setupHooks(
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
).run {
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
setWireMockScenarioState(COINS_API_SCENARIO, richState)
}
step("Open 'Main Screen'") { openMainScreen(mockContent = Wallet2NoEd25519Slip0010MockContent) }
step("Open 'Manage tokens' screen") { openManageTokens() }
step("Enable '$solanaNetworkTitle' network for token: '$solanaTokenTitle'") {
toggleTokenNetworkInManageTokens(tokenTitle = solanaTokenTitle, networkTitle = solanaNetworkTitle)
}
step("Assert unsupported-curve warning is displayed") {
flakySafely { onDialog { containerWithText(warningMessage).assertIsDisplayed() } }
}
}
}
}

View file

@ -0,0 +1,178 @@
package com.tangem.tests.referral
import androidx.test.core.app.ApplicationProvider
import com.tangem.common.BaseTestCase
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.extensions.tapBackButton
import com.tangem.common.utils.AddressComparisonHelper
import com.tangem.common.utils.getClipboardText
import com.tangem.common.utils.resetWireMockScenarios
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.domain.models.scan.ProductType
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.openTesterMenu
import com.tangem.scenarios.referralTakeParticipate
import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.onDetailsScreen
import com.tangem.screens.onMainScreen
import com.tangem.screens.onMainScreenTopBar
import com.tangem.screens.onTesterMenuScreen
import com.tangem.screens.onWalletSettingsScreen
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Assert.assertEquals
import org.junit.Test
@HiltAndroidTest
class ReferralTest : BaseTestCase() {
@AllureId("3630")
@DisplayName("Referral program: Token and Blockchain added on Main screen after participation")
@Test
fun referralTokenAndBlockchainAddedAfterParticipationTest() {
val tokenNetwork = "Tron"
val token = "Tether"
setupHooks(
additionalAfterSection = {
resetWireMockScenarios()
}
).run {
step("Open 'Main' screen") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Verify network $tokenNetwork and token $token is not displaying") {
onMainScreen {
assertTokenDoesNotExist(tokenNetwork)
assertTokenDoesNotExist(token)
}
}
step("Take participate in Referral program") {
referralTakeParticipate()
}
step("Return to the 'Main' screen") {
tapBackButton()
onWalletSettingsScreen { screenContainer.assertIsDisplayed() }
tapBackButton()
onDetailsScreen { screenContainer.assertIsDisplayed() }
tapBackButton()
onMainScreen { screenContainer.assertIsDisplayed() }
}
step("Verify network $tokenNetwork and token $token is displayed on 'Main' screen") {
onMainScreen {
tokenWithTitleAndAddress(tokenNetwork)
tokenWithTitleAndAddress(token)
}
}
}
}
@AllureId("10098")
@DisplayName("Referral program: Token added on Main screen after participation")
@Test
fun referralTokenAddedAfterParticipationTest() {
val userWalletScenarioName = "user_tokens_api"
val userWalletState = "Tron"
val tokenNetwork = "Tron"
val token = "Tether"
setupHooks(
additionalAfterSection = {
resetWireMockScenarios()
}
).run {
step("Set wiremock scenario: $userWalletScenarioName to state $userWalletState") {
setWireMockScenarioState(scenarioName = userWalletScenarioName, state = userWalletState)
}
step("Open 'Main' screen") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Verify token $token is not displaying") {
onMainScreen { assertTokenDoesNotExist(token) }
}
step("Verify network $tokenNetwork is displaying") {
onMainScreen { tokenWithTitleAndAddress(tokenNetwork) }
}
step("Take participate in Referral program") {
referralTakeParticipate()
}
step("Return to the 'Main' screen") {
tapBackButton()
onWalletSettingsScreen { screenContainer.assertIsDisplayed() }
tapBackButton()
onDetailsScreen { screenContainer.assertIsDisplayed() }
tapBackButton()
onMainScreen { screenContainer.assertIsDisplayed() }
}
step("Verify $token is displayed on 'Main' screen") {
onMainScreen { tokenWithTitleAndAddress(token) }
}
}
}
@AllureId("3629")
@DisplayName("Referral program: Participating unavailable for No-Wallet cards")
@Test
fun referralUnavailableForNoWalletCardsTest() {
setupHooks().run {
step("Open 'Main' screen") {
openMainScreen(productType = ProductType.Note)
}
step("Open 'Details' screen") {
onMainScreenTopBar { moreButton.clickWithAssertion() }
}
step("Open 'Wallet settings' screen") {
onDetailsScreen { walletNameButton.clickWithAssertion() }
}
step("Verify 'Referral program' button does not displaying") {
onWalletSettingsScreen { referralProgramButton.assertDoesNotExist() }
}
}
}
@AllureId("3636")
@DisplayName("Referral program: Verify wallet derivation")
@Test
fun referralVerifyWalletDerivationTest() {
val tokenNetwork = "Tron"
val expectedDerivationPath = "m/44'/195'/0'/0/0"
setupHooks(
additionalAfterSection = {
resetWireMockScenarios()
}
).run {
step("Open 'Main' screen") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Take participate in Referral program") {
referralTakeParticipate()
}
step("Open Debug menu") {
openTesterMenu()
}
step("Open 'Addresses info' screen") {
onTesterMenuScreen { addressesInfoButton.clickWithAssertion() }
}
step("Verify $tokenNetwork derivation path is '$expectedDerivationPath'") {
onTesterMenuScreen { jsonTab.clickWithAssertion() }
onTesterMenuScreen { copyButton.clickWithAssertion() }
val addressesJson = getClipboardText(ApplicationProvider.getApplicationContext())
?: error("Clipboard is empty after copying addresses")
val actualDerivationPath =
AddressComparisonHelper.derivationPathForBlockchain(addressesJson, tokenNetwork)
assertEquals(expectedDerivationPath, actualDerivationPath)
}
}
}
}

View file

@ -132,9 +132,9 @@ class GaslessSendTest : BaseTestCase() {
@DisplayName("Gasless: completed gasless transaction is shown in token transaction history")
@Test
fun checkGaslessTransactionInHistoryTest() {
val sentAmount = "1.00"
val operationAmount = "1.00"
val gaslessFeeAmount = "0.10"
val sentTitle = getResourceString(R.string.common_sent)
val operationTitle = getResourceString(R.string.transaction_history_operation)
val gaslessFeeTitle = getResourceString(R.string.gasless_transaction_fee)
setupHooks(
@ -166,13 +166,13 @@ class GaslessSendTest : BaseTestCase() {
onTxHistoryScreen { transactionItem(gaslessFeeTitle).assertIsDisplayed() }
}
}
step("Assert '$sentTitle' transaction is displayed") {
onTxHistoryScreen { transactionItem(sentTitle).assertIsDisplayed() }
step("Assert '$operationTitle' transaction is displayed") {
onTxHistoryScreen { transactionItem(operationTitle).assertIsDisplayed() }
}
step("Assert '$sentTitle' amount '$sentAmount' is displayed in '$currencySymbol'") {
step("Assert '$operationTitle' amount '$operationAmount' is displayed in '$currencySymbol'") {
onTxHistoryScreen {
transactionAmount(sentTitle).assertTextContains(sentAmount, substring = true)
transactionCurrency(sentTitle).assertTextEquals(currencySymbol)
transactionAmount(operationTitle).assertTextContains(operationAmount, substring = true)
transactionCurrency(operationTitle).assertTextEquals(currencySymbol)
}
}
step("Assert gasless '$gaslessFeeTitle' amount '$gaslessFeeAmount' is displayed in '$currencySymbol'") {

View file

@ -0,0 +1,123 @@
package com.tangem.tests.send.reasonBlock
import com.tangem.common.BaseTestCase
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.core.ui.R
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.onDialog
import com.tangem.screens.onMainScreen
import com.tangem.screens.onTokenDetailsScreen
import com.tangem.screens.onTransferBottomSheet
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 ReasonBlockTest : BaseTestCase() {
@AllureId("3616")
@DisplayName("Reason block: Send is unavailable if user has pending transaction")
@Test
fun reasonBlockSendUnavailableWithPendingTransactionTest() {
val txHistoryScenarioName = "dogecoin_tx_history"
val txHistoryState = "EmptyWithPendingTransaction"
val walletsScenarioName = "user_tokens_api"
val walletsState = "Dogecoin"
val token = "Dogecoin"
val reasonText = getResourceString(R.string.token_button_unavailability_reason_pending_transaction_send)
.substringBefore("%s")
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(txHistoryScenarioName)
resetWireMockScenarioState(walletsScenarioName)
}
).run {
step("Set Wiremock scenario: $txHistoryScenarioName to state $txHistoryState") {
setWireMockScenarioState(scenarioName = txHistoryScenarioName, state = txHistoryState)
}
step("Set Wiremock scenario: $walletsScenarioName to state $walletsState") {
setWireMockScenarioState(scenarioName = walletsScenarioName, state = walletsState)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Click on token with name $token") {
onMainScreen { tokenWithTitleAndAddress(token).clickWithAssertion() }
}
step("Click on 'Transfer' button") {
onTokenDetailsScreen { transferButton.clickWithAssertion() }
}
step("Verify 'Send' button is disabled") {
onTransferBottomSheet { sendButton.assertIsNotEnabled() }
}
step("Click on 'Send' button") {
onTransferBottomSheet { sendButton.clickWithAssertion() }
}
step("Assert pending-transaction reason dialog is displayed") {
onDialog { containerWithText(reasonText).assertIsDisplayed() }
}
}
}
@AllureId("3615")
@DisplayName("Reason block: Token withdrawal is unavailable if there are no fee coverage")
@Test
fun reasonBlockTokenWithdrawalUnavailableWithoutFeeCoverage() {
val userWalletsScenarioName = "user_tokens_api"
val userWalletsState = "SolanaUSDC"
val solBalanceScenarioName = "solana_get_account_info_recipient"
val solBalanceState = "ZeroBalance"
val token = "USDC"
val feeCurrencyName = "Solana"
val feeCurrencySymbol = "SOL"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(userWalletsScenarioName)
resetWireMockScenarioState(solBalanceScenarioName)
}
).run {
step("Set Wiremock scenario: $userWalletsScenarioName to state $userWalletsState") {
setWireMockScenarioState(scenarioName = userWalletsScenarioName, state = userWalletsState)
}
step("Set Wiremock scenario: $solBalanceScenarioName to state $solBalanceState") {
setWireMockScenarioState(scenarioName = solBalanceScenarioName, state = solBalanceState)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Click on token with name $token") {
onMainScreen { tokenWithTitleAndAddress(token).clickWithAssertion() }
}
step("Assert 'Insufficient $feeCurrencySymbol for fee' notification is displayed") {
onTokenDetailsScreen {
networkFeeNotificationTitle(feeCurrencyName).assertIsDisplayed()
networkFeeNotificationMessage(
currencyName = token,
networkName = feeCurrencyName,
feeCurrencyName = feeCurrencyName,
feeCurrencySymbol = feeCurrencySymbol,
).assertIsDisplayed()
}
}
step("Click on 'Go to $feeCurrencySymbol' button") {
onTokenDetailsScreen { goToBuyCurrencyButton(feeCurrencySymbol).clickWithAssertion() }
}
step("Assert $feeCurrencyName token screen is opened") {
onTokenDetailsScreen { tokenTitle(feeCurrencyName).assertIsDisplayed() }
}
}
}
}