Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-01 09:12:45 +02:00
parent 383d42f6b3
commit 2110b5f541
10 changed files with 87 additions and 74 deletions

View file

@ -45,6 +45,16 @@ When the user asks to **port** an iOS test to Android:
XCUITest/accessibility identifiers → Compose `testTag`; iOS `*Screen` page objects → Kotlin page
objects in `com/tangem/screens/`; XCTest assertions → Kaspresso/Truth assertions. Re-derive the real
Android `testTag`s and string resources from production source — never reuse iOS identifier strings.
- **Card/wallet mock mapping — where iOS uses `wallet2`, Android uses the default `Wallet`**
(`openMainScreen()` with no `productType``ProductType.Wallet`). Do NOT port iOS `.wallet2` to
`ProductType.Wallet2`. Other cards map directly: iOS `.twin``ProductType.Twins`, `.xrpNote`
`ProductType.Note`, `.four12``Firmware412MockContent` (via the `mockContent` param). `ProductType.Wallet2`
exists but is a distinct Wallet-2.0-card case, not the iOS-`wallet2` analog.
- **A wallet has no balance until you sync.** The default `Wallet` mock starts with missing derivations
("Some addresses are missing"); the fiat balance shows `—` until you call `synchronizeAddresses()` after
`openMainScreen()` (mirror `TotalBalanceUpdateTest`). Any test asserting a balance/fiat-equivalent must
sync first, then `waitUntil` the value loads — balances re-load asynchronously (e.g. after an app-currency
change the equivalent repaints with a delay).
- The WireMock scenarios are usually shared across platforms, but the branch may differ
(see `reference/running-and-debugging.md`).

View file

@ -175,7 +175,10 @@ fun BaseTestCase.openDeviceSettingsScreen() {
onDetailsScreen { walletNameButton.performClick() }
}
step("Click on 'Device settings' button") {
onWalletSettingsScreen { deviceSettingsButton.clickWithAssertion() }
onWalletSettingsScreen {
scrollToDeviceSettings()
deviceSettingsButton.clickWithAssertion()
}
}
}

View file

@ -23,17 +23,11 @@ class DetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
hasText(getResourceString(R.string.wallet_connect_title))
}
private val walletBlock: KNode = child {
hasTestTag(DetailsScreenTestTags.SCREEN_ITEM)
}
val walletNameButton: KNode = walletBlock.child {
hasClickAction()
hasPosition(0)
}
val scanCardButton: KNode = walletBlock.child {
hasText(getResourceString(R.string.scan_card_settings_button))
// Match the wallet row by its own tag (not position-0 clickable, which races with the async-loading
// "Add Wallet" button and otherwise triggers a re-scan → "already saved" dialog).
val walletNameButton: KNode = child {
hasTestTag(DetailsScreenTestTags.USER_WALLET_ITEM)
useUnmergedTree = true
}
val buyTangemButton: KNode = child {

View file

@ -9,6 +9,7 @@ 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
class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<DialogPageObject>(semanticsProvider = semanticsProvider) {
@ -25,8 +26,10 @@ class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
hasTestTag(BaseDialogTestTags.TEXT)
}
// Tag is on the OutlineTextField wrapper; the editable node is its descendant with a SetText action.
val inputField: KNode = child {
hasTestTag(BaseDialogTestTags.TEXT_INPUT_FIELD)
hasSetTextAction()
hasAnyAncestor(withTestTag(BaseDialogTestTags.TEXT_INPUT_FIELD))
useUnmergedTree = true
}

View file

@ -1,5 +1,6 @@
package com.tangem.screens
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.test.TopAppBarTestTags
@ -9,6 +10,7 @@ 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
class WalletSettingsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
@ -22,6 +24,31 @@ class WalletSettingsPageObject(semanticsProvider: SemanticsNodeInteractionsProvi
hasTestTag(WalletSettingsScreenTestTags.SCREEN_ITEM)
}
// The Accounts section loads async and can push rows below the fold — scroll before asserting/clicking.
private val scrollableContainer: KNode = child {
hasTestTag(WalletSettingsScreenTestTags.SCREEN_CONTAINER)
}
@OptIn(ExperimentalTestApi::class)
fun scrollToText(text: String) = scrollableContainer { performScrollToNode(withText(text)) }
@OptIn(ExperimentalTestApi::class)
fun scrollToDeviceSettings() = scrollToText(getResourceString(R.string.card_settings_title))
@OptIn(ExperimentalTestApi::class)
fun scrollToLinkMoreCards() = scrollToText(getResourceString(R.string.details_row_title_create_backup))
@OptIn(ExperimentalTestApi::class)
fun scrollToReferralProgram() = scrollToText(getResourceString(R.string.details_referral_title))
@OptIn(ExperimentalTestApi::class)
fun scrollToForgetWallet() = scrollToText(getResourceString(R.string.settings_forget_wallet))
@OptIn(ExperimentalTestApi::class)
fun scrollToRenameButton() = scrollableContainer {
performScrollToNode(withTestTag(WalletSettingsScreenTestTags.RENAME_BUTTON))
}
val linkMoreCardsButton: KNode = walletSettingsItem.child {
hasText(getResourceString(R.string.details_row_title_create_backup))
}

View file

@ -1,11 +1,13 @@
package com.tangem.tests
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.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.domain.models.scan.ProductType
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.*
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
@ -23,7 +25,7 @@ class AppCurrencyTest : BaseTestCase() {
val appSettingsState = "AppSettings"
val targetCurrency = "EUR"
val targetSymbol = ""
val token = "Polygon"
val token = "Bitcoin"
setupHooks(
additionalAfterSection = { resetWireMockScenarioState(currenciesScenario) },
@ -32,8 +34,9 @@ class AppCurrencyTest : BaseTestCase() {
setWireMockScenarioState(scenarioName = currenciesScenario, state = appSettingsState)
}
step("Open 'Main Screen'") {
openMainScreen(productType = ProductType.Wallet2)
openMainScreen()
}
synchronizeAddresses()
step("Open wallet details") {
onMainScreenTopBar { moreButton.clickWithAssertion() }
}
@ -53,18 +56,30 @@ class AppCurrencyTest : BaseTestCase() {
onAppCurrencySelectorScreen { currencyItem(targetCurrency).performClick() }
}
step("Return to 'Main' screen") {
device.uiDevice.pressBack()
device.uiDevice.pressBack()
waitForIdle()
var displayed = false
var attempts = 0
while (!displayed && attempts < 4) {
onMainScreen { displayed = screenContainer.isDisplayedSafely() }
if (!displayed) {
device.uiDevice.pressBack()
waitForIdle()
attempts++
}
}
}
step("Assert total balance contains '$targetSymbol' on 'Main' screen") {
onMainScreen { totalBalanceText.assertTextContains(targetSymbol) }
// Balance re-loads in the new currency async after the switch — wait for the € equivalent.
composeTestRule.waitUntil(WAIT_UNTIL_TIMEOUT_LONG) {
runCatching { onMainScreen { totalBalanceText.assertTextContains(targetSymbol) } }.isSuccess
}
}
step("Click on token '$token'") {
onMainScreen { tokenWithTitleAndAddress(token).clickWithAssertion() }
}
step("Assert token fiat balance contains '$targetSymbol'") {
onTokenDetailsScreen { fiatBalance.assertTextContains(targetSymbol) }
composeTestRule.waitUntil(WAIT_UNTIL_TIMEOUT_LONG) {
runCatching { onTokenDetailsScreen { fiatBalance.assertTextContains(targetSymbol) } }.isSuccess
}
}
}
}

View file

@ -49,66 +49,19 @@ class DetailsTest : BaseTestCase() {
}
onWalletSettingsScreen {
step("Assert 'Link more cards' button is visible") {
scrollToLinkMoreCards()
linkMoreCardsButton.assertIsDisplayed()
}
step("Assert 'Card Settings' button is visible") {
scrollToDeviceSettings()
deviceSettingsButton.assertIsDisplayed()
}
step("Assert 'Referral program' button is visible") {
scrollToReferralProgram()
referralProgramButton.assertIsDisplayed()
}
step("Assert 'Forget wallet' button is visible") {
forgetWalletButton.assertIsDisplayed()
}
}
}
@DisplayName("Details: (Wallet 2.0) fields")
@Test
fun wallet2DetailsTest() =
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen(productType = ProductType.Wallet2)
}
onMainScreenTopBar {
step("Open wallet details") {
moreButton.clickWithAssertion()
}
}
onDetailsScreen {
step("Assert 'Wallet connect' button is visible") {
walletConnectButton.assertIsDisplayed()
}
step("Assert 'Scan card' button is visible") {
scanCardButton.assertIsDisplayed()
}
step("Assert 'Buy Tangem card' button is visible") {
buyTangemButton.assertIsDisplayed()
}
step("Assert 'App settings' button is visible") {
appSettingsButton.assertIsDisplayed()
}
step("Assert 'Contact support' button is visible") {
contactSupportButton.assertIsDisplayed()
}
step("Assert 'Terms or service' button is visible") {
toSButton.assertIsDisplayed()
}
step("Open 'Wallet settings' screen") {
walletNameButton.clickWithAssertion()
}
}
onWalletSettingsScreen {
step("Assert 'Link more cards' button does not exist") {
linkMoreCardsButton.assertIsNotDisplayed()
}
step("Assert 'Card Settings' button is visible") {
deviceSettingsButton.assertIsDisplayed()
}
step("Assert 'Referral program' button is visible") {
referralProgramButton.assertIsDisplayed()
}
step("Assert 'Forget wallet' button is visible") {
scrollToForgetWallet()
forgetWalletButton.assertIsDisplayed()
}
}

View file

@ -29,7 +29,10 @@ class WalletRenameTest : BaseTestCase() {
onDetailsScreen { walletNameButton.clickWithAssertion() }
}
step("Click on 'Rename' button") {
onWalletSettingsScreen { renameWalletButton.clickWithAssertion() }
onWalletSettingsScreen {
scrollToRenameButton()
renameWalletButton.clickWithAssertion()
}
}
step("Enter new wallet name '$newWalletName'") {
onDialog { inputField.performTextReplacement(newWalletName) }

View file

@ -4,4 +4,5 @@ object DetailsScreenTestTags {
const val SCREEN_CONTAINER = "DETAILS_SCREEN_CONTAINER"
const val SCREEN_ITEM = "DETAILS_SCREEN_ITEM"
const val VERSION_NAME = "DETAILS_SCREEN_VERSION_NAME"
const val USER_WALLET_ITEM = "DETAILS_SCREEN_USER_WALLET_ITEM"
}

View file

@ -22,6 +22,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
@ -34,6 +35,7 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.DetailsScreenTestTags
import com.tangem.features.details.component.UserWalletListComponent
import com.tangem.features.details.component.preview.PreviewUserWalletListComponent
import com.tangem.features.details.entity.UserWalletListUM
@ -69,7 +71,9 @@ internal fun UserWalletListBlock(state: UserWalletListUM, modifier: Modifier = M
model = walletState,
reorderableListState = reorderableListState,
walletReorderUM = state.walletReorderUM,
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.testTag(DetailsScreenTestTags.USER_WALLET_ITEM),
)
}
item(key = "add_wallet_button") {