Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-04 12:09:11 +03:00
commit 8c84593e15
11 changed files with 286 additions and 27 deletions

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) 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> { private fun parseAndNormalize(json: String): List<AddressEntry> {
val array = JSONArray(json) val array = JSONArray(json)
val entries = mutableListOf<AddressEntry>() val entries = mutableListOf<AddressEntry>()

View file

@ -1,6 +1,5 @@
package com.tangem.scenarios package com.tangem.scenarios
import android.view.KeyEvent
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import com.tangem.common.BaseTestCase import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_VERY_LONG 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]. * Polls [walletManagersStore] until the wallet manager count stops growing for [WALLET_MANAGERS_SETTLE_MS].

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

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

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

@ -3,16 +3,18 @@ package com.tangem.core.ui.ds2.button
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.NonRestartableComposable import androidx.compose.runtime.NonRestartableComposable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import com.tangem.core.ui.ds.image.TangemIconUM import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.res.generated.icons.Icons import com.tangem.core.ui.res.generated.icons.Icons
import com.tangem.core.ui.res.generated.icons.ic_arrow_left_20 import com.tangem.core.ui.res.generated.icons.ic_arrow_left_20
import com.tangem.core.ui.res.generated.icons.ic_cross_20 import com.tangem.core.ui.res.generated.icons.ic_cross_20
import com.tangem.core.ui.test.TopNavigationTestTags
@Composable @Composable
@NonRestartableComposable @NonRestartableComposable
fun TangemButton.Back(modifier: Modifier = Modifier, onClick: () -> Unit) { fun TangemButton.Back(modifier: Modifier = Modifier, onClick: () -> Unit) {
TangemButton( TangemButton(
modifier = modifier, modifier = modifier.testTag(TopNavigationTestTags.BACK_BUTTON),
variant = TangemButton.Variant.Material, variant = TangemButton.Variant.Material,
iconStart = TangemIconUM.Icon(Icons.ic_arrow_left_20), iconStart = TangemIconUM.Icon(Icons.ic_arrow_left_20),
onClick = onClick, onClick = onClick,

View file

@ -5,4 +5,5 @@ object ReferralProgramScreenTestTags {
const val CONDITION_BLOCK = "REFERRAL_PROGRAM_SCREEN_CONDITION_BLOCK" const val CONDITION_BLOCK = "REFERRAL_PROGRAM_SCREEN_CONDITION_BLOCK"
const val INFO_FOR_YOU_TEXT = "REFERRAL_PROGRAM_SCREEN_INFO_FOR_YOU_TEXT" const val INFO_FOR_YOU_TEXT = "REFERRAL_PROGRAM_SCREEN_INFO_FOR_YOU_TEXT"
const val INFO_FOR_YOUR_FRIEND_TEXT = "REFERRAL_PROGRAM_INFO_FOR_YOUR_FRIEND_TEXT" const val INFO_FOR_YOUR_FRIEND_TEXT = "REFERRAL_PROGRAM_INFO_FOR_YOUR_FRIEND_TEXT"
const val PERSONAL_CODE_CARD = "REFERRAL_PROGRAM_SCREEN_PERSONAL_CODE_CARD"
} }

View file

@ -0,0 +1,5 @@
package com.tangem.core.ui.test
object TopNavigationTestTags {
const val BACK_BUTTON = "TOP_NAVIGATION_BACK_BUTTON"
}

View file

@ -16,6 +16,7 @@ import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
@ -41,6 +42,7 @@ import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.LocalTopSnackbarHostState import com.tangem.core.ui.res.LocalTopSnackbarHostState
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.ReferralProgramScreenTestTags
import com.tangem.feature.referral.domain.models.ExpectedAward import com.tangem.feature.referral.domain.models.ExpectedAward
import com.tangem.feature.referral.domain.models.ExpectedAwards import com.tangem.feature.referral.domain.models.ExpectedAwards
import com.tangem.feature.referral.models.ReferralStateHolder import com.tangem.feature.referral.models.ReferralStateHolder
@ -261,6 +263,7 @@ private fun ExtraItems(extraItems: List<ExpectedAward>, overallItemsLastIndex: I
private fun PersonalCodeCard(code: String, accountAward: ReferralStateHolder.AccountAward?) { private fun PersonalCodeCard(code: String, accountAward: ReferralStateHolder.AccountAward?) {
Column( Column(
modifier = Modifier modifier = Modifier
.testTag(ReferralProgramScreenTestTags.PERSONAL_CODE_CARD)
.clip(RoundedCornerShape(TangemTheme.dimens.radius12)) .clip(RoundedCornerShape(TangemTheme.dimens.radius12))
.background(color = TangemTheme.colors.background.primary) .background(color = TangemTheme.colors.background.primary)
.fillMaxWidth() .fillMaxWidth()