Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-21 13:10:18 +04:00
parent c846d8c276
commit d30f231520
19 changed files with 613 additions and 32 deletions

View file

@ -43,6 +43,7 @@ object TestConstants {
const val ALLURE_LABEL_VALUE = "Kaspresso"
const val USER_TOKENS_API_SCENARIO = "user_tokens_api"
const val REFERRAL_API_SCENARIO = "referral_api"
const val QUOTES_API_SCENARIO = "quotes_api"
const val SEED_PHRASE_12 = "they cram join fantasy unfair observe true theory buffalo bus exchange walk"

View file

@ -0,0 +1,102 @@
package com.tangem.scenarios
import com.tangem.common.BaseTestCase
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.screens.accounts.onAccountDetailsScreen
import com.tangem.screens.accounts.onArchivedAccountsScreen
import com.tangem.screens.onDetailsScreen
import com.tangem.screens.onDialog
import com.tangem.screens.onMainScreenTopBar
import com.tangem.screens.onWalletSettingsScreen
import io.qameta.allure.kotlin.Allure.step
fun BaseTestCase.openWalletSettingsScreen() {
step("Open 'Wallet details' screen") {
onMainScreenTopBar { moreButton.clickWithAssertion() }
}
step("Open 'Wallet settings' screen") {
onDetailsScreen { walletNameButton.clickWithAssertion() }
}
}
fun BaseTestCase.openAccountDetails(accountName: String) {
step("Click on account: '$accountName'") {
onWalletSettingsScreen { accountItem(accountName).clickWithAssertion() }
}
step("Assert 'Account details' screen is displayed") {
onAccountDetailsScreen { screenContainer.assertIsDisplayed() }
}
}
fun BaseTestCase.archiveAccount() {
step("Assert 'Archive' button is displayed") {
onAccountDetailsScreen { archiveAccountButton.assertIsDisplayed() }
}
step("Click on 'Archive' button") {
onAccountDetailsScreen { archiveAccountButton.clickWithAssertion() }
}
step("Confirm archivation in dialog") {
onDialog { archiveButton.clickWithAssertion() }
}
}
fun BaseTestCase.assertArchiveConfirmationDialog() {
step("Assert confirmation dialog is displayed") {
onDialog { dialogContainer.assertIsDisplayed() }
}
step("Assert confirmation dialog has 'Cancel' button") {
onDialog { cancelButton.assertIsDisplayed() }
}
step("Assert confirmation dialog has 'Archive' button") {
onDialog { archiveButton.assertIsDisplayed() }
}
}
fun BaseTestCase.assertErrorDialog(expectedTitle: String, expectedMessage: String) {
step("Assert error dialog is displayed") {
onDialog { dialogContainer.assertIsDisplayed() }
}
step("Assert error dialog has proper title") {
onDialog {
title.assertTextContains(expectedTitle)
}
}
step("Assert error dialog has explanatory text") {
onDialog {
text.assertTextContains(expectedMessage)
}
}
step("Assert error dialog has 'OK' button") {
onDialog { okButton.assertIsDisplayed() }
}
}
fun BaseTestCase.dismissErrorDialog() {
step("Dismiss error dialog by clicking 'Ok' button") {
onDialog { okButton.clickWithAssertion() }
}
}
fun BaseTestCase.openArchivedAccountsScreen() {
step("Click on 'Archived accounts' button") {
onWalletSettingsScreen { openArchivedAccountsButton.clickWithAssertion() }
}
}
fun BaseTestCase.assertArchivedAccountIsDisplayed(accountName: String) {
step("Assert archived account with name '$accountName' is displayed") {
onArchivedAccountsScreen {
findArchivedAccountItemByName(accountName)
.container.assertIsDisplayed()
}
}
}
fun BaseTestCase.restoreArchivedAccount(accountName: String) {
step("Restore account with name '$accountName'") {
onArchivedAccountsScreen {
findArchivedAccountItemByName(accountName)
.restoreButton.clickWithAssertion()
}
}
}

View file

@ -40,6 +40,11 @@ class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
hasText(getResourceString(R.string.common_confirm))
}
val archiveButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.account_details_archive_action))
}
val continueButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.common_continue))

View file

@ -38,6 +38,18 @@ class WalletSettingsPageObject(semanticsProvider: SemanticsNodeInteractionsProvi
hasText(getResourceString(R.string.settings_forget_wallet))
}
val accountsListContainer: KNode = walletSettingsItem.child {
hasTestTag(WalletSettingsScreenTestTags.ACCOUNTS_CONTAINER)
}
val addAccountButton: KNode = walletSettingsItem.child {
hasText(getResourceString(R.string.account_form_create_button))
}
val openArchivedAccountsButton: KNode = walletSettingsItem.child {
hasText(getResourceString(R.string.account_archived_accounts))
}
fun accountItem(accountName: String): KNode = walletSettingsItem.child {
hasTestTag(WalletSettingsScreenTestTags.USER_ACCOUNT_ITEM)
hasAnyDescendant(withText(accountName))

View file

@ -1,8 +1,8 @@
package com.tangem.screens
package com.tangem.screens.accounts
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.test.AccountDetailsScreenTestTags
import com.tangem.core.ui.test.accounts.AccountDetailsScreenTestTags
import com.tangem.core.ui.test.TopAppBarTestTags
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
@ -11,6 +11,10 @@ import io.github.kakaocup.compose.node.element.KNode
class AccountDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<AccountDetailsPageObject>(semanticsProvider = semanticsProvider) {
val screenContainer: KNode = child {
hasTestTag(AccountDetailsScreenTestTags.ACCOUNT_DETAILS_CONTAINER)
}
val topAppBarBackButton: KNode = child {
hasTestTag(TopAppBarTestTags.CLOSE_BUTTON)
}
@ -18,7 +22,16 @@ class AccountDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvi
val manageTokensButton: KNode = child {
hasTestTag(AccountDetailsScreenTestTags.MANAGE_TOKENS_BUTTON)
}
val archiveAccountButton: KNode = child {
hasTestTag(AccountDetailsScreenTestTags.ARCHIVE_ACCOUNT_BUTTON)
}
val editAccountButton: KNode = child {
hasTestTag(AccountDetailsScreenTestTags.EDIT_ACCOUNT_BUTTON)
}
}
internal fun BaseTestCase.onAccountDetails(function: AccountDetailsPageObject.() -> Unit) =
internal fun BaseTestCase.onAccountDetailsScreen(function: AccountDetailsPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,70 @@
package com.tangem.screens.accounts
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.R
import com.tangem.core.ui.test.TopAppBarTestTags
import com.tangem.core.ui.test.accounts.AccountRowTestTags
import com.tangem.core.ui.test.accounts.ArchivedAccountsScreenTestTags
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 ArchivedAccountsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<ArchivedAccountsPageObject>(semanticsProvider = semanticsProvider) {
val screenContainer: KNode = child {
hasTestTag(ArchivedAccountsScreenTestTags.ARCHIVED_ACCOUNTS_SCREEN_CONTAINER)
}
val topAppBarBackButton: KNode = child {
hasTestTag(TopAppBarTestTags.CLOSE_BUTTON)
}
val topAppBarTitle: KNode = child {
hasTestTag(TopAppBarTestTags.TITLE)
hasText(getResourceString(R.string.account_archived_accounts))
useUnmergedTree = true
}
/**
* Returns a composite handle for a single archived account row, scoped by account name.
* All sub-elements (icon, title, tokens, networks, restore button) are children of this row.
*/
fun findArchivedAccountItemByName(accountName: String): ArchivedAccountRow {
val container: KNode = child {
hasTestTag(ArchivedAccountsScreenTestTags.ARCHIVED_ACCOUNT_ITEM)
hasAnyDescendant(withText(accountName))
useUnmergedTree = true
}
return ArchivedAccountRow(container)
}
class ArchivedAccountRow(val container: KNode) {
val icon: KNode = container.child {
hasTestTag(AccountRowTestTags.ICON)
useUnmergedTree = true
}
val title: KNode = container.child {
hasTestTag(AccountRowTestTags.TITLE)
useUnmergedTree = true
}
val subtitle: KNode = container.child {
hasTestTag(AccountRowTestTags.SUBTITLE)
useUnmergedTree = true
}
val restoreButton: KNode = container.child {
hasTestTag(ArchivedAccountsScreenTestTags.RESTORE_BUTTON)
useUnmergedTree = true
}
}
}
internal fun BaseTestCase.onArchivedAccountsScreen(function: ArchivedAccountsPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,253 @@
package com.tangem.tests.accounts
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.REFERRAL_API_SCENARIO
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.core.ui.R
import com.tangem.scenarios.*
import com.tangem.screens.accounts.onAccountDetailsScreen
import com.tangem.screens.accounts.onArchivedAccountsScreen
import com.tangem.screens.onDialog
import com.tangem.screens.onWalletSettingsScreen
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 AccountArchivationsTest : BaseTestCase() {
private val userTokensScenario = USER_TOKENS_API_SCENARIO
private val referralScenario = REFERRAL_API_SCENARIO
@Test
@AllureId("5979")
@DisplayName("Accounts: Verify main account archivation button not available")
fun mainAccountArchivationAttemptTest() {
val mainAccountName = "Main account"
setupHooks().run {
step("Open 'Main Screen'") { openMainScreen() }
step("Synchronize addresses") { synchronizeAddresses() }
step("Open wallet settings") { openWalletSettingsScreen() }
step("Open wallet account with name $mainAccountName") { openAccountDetails(mainAccountName) }
step("Assert archive button is NOT displayed") {
onAccountDetailsScreen { archiveAccountButton.assertDoesNotExist() }
}
}
}
@Test
@AllureId("5974")
@DisplayName("Accounts: archive a non-main account")
fun archiveSuccessfullyAccountTest() {
val accountToArchiveName = "Account 2"
val userAccountsState = "TwoAccountsArchivable"
setupHooks(
additionalBeforeSection = {
setWireMockScenarioState(userTokensScenario, userAccountsState)
},
additionalAfterSection = {
resetWireMockScenarioState(userTokensScenario)
},
).run {
step("Open 'Main Screen'") { openMainScreen() }
step("Synchronize addresses") { synchronizeAddresses() }
step("Open wallet settings") { openWalletSettingsScreen() }
step("Open wallet account with name $accountToArchiveName") {
openAccountDetails(accountToArchiveName)
}
step("Assert 'Archive' button is displayed") {
onAccountDetailsScreen { archiveAccountButton.assertIsDisplayed() }
}
step("Click on 'Archive' button") {
onAccountDetailsScreen { archiveAccountButton.clickWithAssertion() }
}
step("Verify 'Archive confirmation' dialog appeared with all elements") {
assertArchiveConfirmationDialog()
}
step("Click 'Archive' in confirmation menu") {
onDialog { archiveButton.clickWithAssertion() }
}
step("Verify app returned to 'Wallet settings' screen") {
onWalletSettingsScreen { addAccountButton.assertIsDisplayed() }
}
step("Verify archived account '$accountToArchiveName' is no longer listed") {
onWalletSettingsScreen {
accountItem(accountToArchiveName).assertDoesNotExist()
}
}
}
}
@Test
@AllureId("6844")
@DisplayName("Accounts: account archivation error on UI")
fun archiveAccountErrorTest() {
val accountToArchiveName = "Account 2"
val userAccountsState = "TwoAccountsArchivable"
val userAccountsErrorState = "AccountsPutError"
setupHooks(
additionalBeforeSection = {
setWireMockScenarioState(userTokensScenario, userAccountsState)
},
additionalAfterSection = {
resetWireMockScenarioState(userTokensScenario)
},
).run {
step("Open 'Main Screen'") { openMainScreen() }
step("Synchronize addresses") { synchronizeAddresses() }
step("Open wallet settings") { openWalletSettingsScreen() }
step("Open wallet account with name $accountToArchiveName") {
openAccountDetails(accountToArchiveName)
}
step("Forcing network error scenario") {
setWireMockScenarioState(userTokensScenario, userAccountsErrorState)
}
step("Attempt to archive the account") { archiveAccount() }
step("Assert error dialog details") {
assertErrorDialog(
expectedTitle = getResourceString(R.string.common_something_went_wrong),
expectedMessage = getResourceString(R.string.account_generic_error_dialog_message),
)
}
}
}
@Test
@AllureId("5981")
@DisplayName("Accounts: archive account with referral program error")
fun archiveAccountReferralErrorTest() {
val accountToArchiveName = "Account 2"
val userAccountsState = "TwoAccountsArchivableAndReferral"
val referralActiveState = "Participating"
setupHooks(
additionalBeforeSection = {
setWireMockScenarioState(userTokensScenario, userAccountsState)
setWireMockScenarioState(referralScenario, referralActiveState)
},
additionalAfterSection = {
resetWireMockScenarioState(userTokensScenario)
resetWireMockScenarioState(referralScenario)
},
).run {
step("Open 'Main Screen'") { openMainScreen() }
step("Synchronize addresses") { synchronizeAddresses() }
step("Open wallet settings") { openWalletSettingsScreen() }
step("Open wallet account with name $accountToArchiveName") {
openAccountDetails(accountToArchiveName)
}
step("Attempt to archive the account") { archiveAccount() }
step("Assert error dialog details") {
assertErrorDialog(
expectedTitle = getResourceString(R.string.account_could_not_archive_referral_program_title),
expectedMessage = getResourceString(R.string.account_could_not_archive_referral_program_message),
)
}
step("Dismiss error dialog") { dismissErrorDialog() }
step("Assert 'Archive' button is still visible after error") {
onAccountDetailsScreen { archiveAccountButton.assertIsDisplayed() }
}
}
}
@Test
@AllureId("5976")
@DisplayName("Accounts: restore an archived account")
fun restoreArchivedAccountTest() {
val archivedAccountName = "Account 3"
val userAccountsInitialState = "TwoAccountsWithArchivedAccounts"
val userAccountsAfterArchivationState = "ReadyToRestore"
setupHooks(
additionalBeforeSection = {
setWireMockScenarioState(userTokensScenario, userAccountsInitialState)
},
additionalAfterSection = {
resetWireMockScenarioState(userTokensScenario)
},
).run {
step("Open 'Main Screen'") { openMainScreen() }
step("Synchronize addresses") { synchronizeAddresses() }
step("Open wallet settings") { openWalletSettingsScreen() }
step("Open 'Archived accounts' screen") { openArchivedAccountsScreen() }
step("Verify archived wallet account with name '$archivedAccountName' is present") {
assertArchivedAccountIsDisplayed(archivedAccountName)
}
step("Switch WireMock to '$userAccountsAfterArchivationState' users scenario state") {
setWireMockScenarioState(userTokensScenario, userAccountsAfterArchivationState)
}
step("Restore account with name '$archivedAccountName'") {
restoreArchivedAccount(archivedAccountName)
}
step("Assert 'Wallet settings' screen is displayed") {
onWalletSettingsScreen { addAccountButton.assertIsDisplayed() }
}
step("Assert restored account '$archivedAccountName' appears in 'Active accounts' list") {
onWalletSettingsScreen { accountItem(archivedAccountName).assertIsDisplayed() }
}
}
}
@Test
@AllureId("7962")
@DisplayName("Accounts: restore archived account error")
fun restoreArchivedAccountErrorTest() {
val archivedAccountName = "Account 3"
val userAccountsInitialState = "TwoAccountsWithArchivedAccounts"
val userAccountsRestorationErrorState = "AccountsPutError"
setupHooks(
additionalBeforeSection = {
setWireMockScenarioState(userTokensScenario, userAccountsInitialState)
},
additionalAfterSection = {
resetWireMockScenarioState(userTokensScenario)
},
).run {
step("Open 'Main Screen'") { openMainScreen() }
step("Synchronize addresses") { synchronizeAddresses() }
step("Open wallet settings") { openWalletSettingsScreen() }
step("Open 'Archived accounts' screen") { openArchivedAccountsScreen() }
step("Verify archived wallet account with name '$archivedAccountName' is present") {
assertArchivedAccountIsDisplayed(archivedAccountName)
}
step("Switch WireMock to '$userAccountsRestorationErrorState' user" +
"accounts scenario state to simulate restoration failure") {
setWireMockScenarioState(userTokensScenario, userAccountsRestorationErrorState)
}
step("Attempt restore account with name '$archivedAccountName'") {
restoreArchivedAccount(archivedAccountName)
}
step("Assert error dialog details") {
assertErrorDialog(
expectedTitle = getResourceString(R.string.common_something_went_wrong),
expectedMessage = getResourceString(R.string.account_generic_error_dialog_message),
)
}
step("Dismiss error dialog") { dismissErrorDialog() }
step("Assert archived account '$archivedAccountName' is still in archived list") {
onArchivedAccountsScreen {
findArchivedAccountItemByName(archivedAccountName)
.container.assertIsDisplayed()
}
}
}
}
}

View file

@ -9,6 +9,7 @@ import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.*
import com.tangem.screens.accounts.onAccountDetailsScreen
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
@ -93,7 +94,7 @@ class HideTokenTest : BaseTestCase() {
onWalletSettingsScreen { accountItem(accountName).performClick() }
}
step("Click on 'Manage tokens' button") {
onAccountDetails { manageTokensButton.performClick() }
onAccountDetailsScreen { manageTokensButton.performClick() }
}
step("Click on token: '$tokenTitle'") {
onManageTokensScreen { tokenItem(tokenTitle).performClick() }
@ -118,7 +119,7 @@ class HideTokenTest : BaseTestCase() {
}
step("Click on 'Account details' screen 'Back' button") {
waitForIdle()
onAccountDetails { topAppBarBackButton.performClick() }
onAccountDetailsScreen { topAppBarBackButton.performClick() }
}
step("Click on 'Wallet settings' screen 'Back' button") {
waitForIdle()
@ -162,7 +163,7 @@ class HideTokenTest : BaseTestCase() {
onWalletSettingsScreen { accountItem(accountName).performClick() }
}
step("Click on 'Manage tokens' button") {
onAccountDetails { manageTokensButton.performClick() }
onAccountDetailsScreen { manageTokensButton.performClick() }
}
step("Click on token: '$tokenTitle'") {
onManageTokensScreen { tokenItem(tokenTitle).performClick() }
@ -187,7 +188,7 @@ class HideTokenTest : BaseTestCase() {
}
step("Click on 'Account details' screen 'Back' button") {
waitForIdle()
onAccountDetails { topAppBarBackButton.performClick() }
onAccountDetailsScreen { topAppBarBackButton.performClick() }
}
step("Click on 'Wallet settings' screen 'Back' button") {
waitForIdle()
@ -308,5 +309,4 @@ class HideTokenTest : BaseTestCase() {
}
}
}
}