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 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 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. 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 - The WireMock scenarios are usually shared across platforms, but the branch may differ
(see `reference/running-and-debugging.md`). (see `reference/running-and-debugging.md`).

View file

@ -175,7 +175,10 @@ fun BaseTestCase.openDeviceSettingsScreen() {
onDetailsScreen { walletNameButton.performClick() } onDetailsScreen { walletNameButton.performClick() }
} }
step("Click on 'Device settings' button") { 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)) hasText(getResourceString(R.string.wallet_connect_title))
} }
private val walletBlock: KNode = child { // Match the wallet row by its own tag (not position-0 clickable, which races with the async-loading
hasTestTag(DetailsScreenTestTags.SCREEN_ITEM) // "Add Wallet" button and otherwise triggers a re-scan → "already saved" dialog).
} val walletNameButton: KNode = child {
hasTestTag(DetailsScreenTestTags.USER_WALLET_ITEM)
val walletNameButton: KNode = walletBlock.child { useUnmergedTree = true
hasClickAction()
hasPosition(0)
}
val scanCardButton: KNode = walletBlock.child {
hasText(getResourceString(R.string.scan_card_settings_button))
} }
val buyTangemButton: KNode = child { 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.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString import io.github.kakaocup.kakao.common.utilities.getResourceString
import androidx.compose.ui.test.hasTestTag as withTestTag
class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<DialogPageObject>(semanticsProvider = semanticsProvider) { ComposeScreen<DialogPageObject>(semanticsProvider = semanticsProvider) {
@ -25,8 +26,10 @@ class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
hasTestTag(BaseDialogTestTags.TEXT) hasTestTag(BaseDialogTestTags.TEXT)
} }
// Tag is on the OutlineTextField wrapper; the editable node is its descendant with a SetText action.
val inputField: KNode = child { val inputField: KNode = child {
hasTestTag(BaseDialogTestTags.TEXT_INPUT_FIELD) hasSetTextAction()
hasAnyAncestor(withTestTag(BaseDialogTestTags.TEXT_INPUT_FIELD))
useUnmergedTree = true useUnmergedTree = true
} }

View file

@ -1,5 +1,6 @@
package com.tangem.screens package com.tangem.screens
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase import com.tangem.common.BaseTestCase
import com.tangem.core.ui.test.TopAppBarTestTags 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.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString 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 androidx.compose.ui.test.hasText as withText
class WalletSettingsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : class WalletSettingsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
@ -22,6 +24,31 @@ class WalletSettingsPageObject(semanticsProvider: SemanticsNodeInteractionsProvi
hasTestTag(WalletSettingsScreenTestTags.SCREEN_ITEM) 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 { val linkMoreCardsButton: KNode = walletSettingsItem.child {
hasText(getResourceString(R.string.details_row_title_create_backup)) hasText(getResourceString(R.string.details_row_title_create_backup))
} }

View file

@ -1,11 +1,13 @@
package com.tangem.tests package com.tangem.tests
import com.tangem.common.BaseTestCase 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.clickWithAssertion
import com.tangem.common.extensions.isDisplayedSafely
import com.tangem.common.utils.resetWireMockScenarioState import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.domain.models.scan.ProductType
import com.tangem.scenarios.openMainScreen import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.* import com.tangem.screens.*
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId import io.qameta.allure.kotlin.AllureId
@ -23,7 +25,7 @@ class AppCurrencyTest : BaseTestCase() {
val appSettingsState = "AppSettings" val appSettingsState = "AppSettings"
val targetCurrency = "EUR" val targetCurrency = "EUR"
val targetSymbol = "" val targetSymbol = ""
val token = "Polygon" val token = "Bitcoin"
setupHooks( setupHooks(
additionalAfterSection = { resetWireMockScenarioState(currenciesScenario) }, additionalAfterSection = { resetWireMockScenarioState(currenciesScenario) },
@ -32,8 +34,9 @@ class AppCurrencyTest : BaseTestCase() {
setWireMockScenarioState(scenarioName = currenciesScenario, state = appSettingsState) setWireMockScenarioState(scenarioName = currenciesScenario, state = appSettingsState)
} }
step("Open 'Main Screen'") { step("Open 'Main Screen'") {
openMainScreen(productType = ProductType.Wallet2) openMainScreen()
} }
synchronizeAddresses()
step("Open wallet details") { step("Open wallet details") {
onMainScreenTopBar { moreButton.clickWithAssertion() } onMainScreenTopBar { moreButton.clickWithAssertion() }
} }
@ -53,18 +56,30 @@ class AppCurrencyTest : BaseTestCase() {
onAppCurrencySelectorScreen { currencyItem(targetCurrency).performClick() } onAppCurrencySelectorScreen { currencyItem(targetCurrency).performClick() }
} }
step("Return to 'Main' screen") { step("Return to 'Main' screen") {
device.uiDevice.pressBack() var displayed = false
var attempts = 0
while (!displayed && attempts < 4) {
onMainScreen { displayed = screenContainer.isDisplayedSafely() }
if (!displayed) {
device.uiDevice.pressBack() device.uiDevice.pressBack()
waitForIdle() waitForIdle()
attempts++
}
}
} }
step("Assert total balance contains '$targetSymbol' on 'Main' screen") { 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'") { step("Click on token '$token'") {
onMainScreen { tokenWithTitleAndAddress(token).clickWithAssertion() } onMainScreen { tokenWithTitleAndAddress(token).clickWithAssertion() }
} }
step("Assert token fiat balance contains '$targetSymbol'") { 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 { onWalletSettingsScreen {
step("Assert 'Link more cards' button is visible") { step("Assert 'Link more cards' button is visible") {
scrollToLinkMoreCards()
linkMoreCardsButton.assertIsDisplayed() linkMoreCardsButton.assertIsDisplayed()
} }
step("Assert 'Card Settings' button is visible") { step("Assert 'Card Settings' button is visible") {
scrollToDeviceSettings()
deviceSettingsButton.assertIsDisplayed() deviceSettingsButton.assertIsDisplayed()
} }
step("Assert 'Referral program' button is visible") { step("Assert 'Referral program' button is visible") {
scrollToReferralProgram()
referralProgramButton.assertIsDisplayed() referralProgramButton.assertIsDisplayed()
} }
step("Assert 'Forget wallet' button is visible") { step("Assert 'Forget wallet' button is visible") {
forgetWalletButton.assertIsDisplayed() scrollToForgetWallet()
}
}
}
@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") {
forgetWalletButton.assertIsDisplayed() forgetWalletButton.assertIsDisplayed()
} }
} }

View file

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

View file

@ -4,4 +4,5 @@ object DetailsScreenTestTags {
const val SCREEN_CONTAINER = "DETAILS_SCREEN_CONTAINER" const val SCREEN_CONTAINER = "DETAILS_SCREEN_CONTAINER"
const val SCREEN_ITEM = "DETAILS_SCREEN_ITEM" const val SCREEN_ITEM = "DETAILS_SCREEN_ITEM"
const val VERSION_NAME = "DETAILS_SCREEN_VERSION_NAME" 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.clip
import androidx.compose.ui.draw.scale import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.vectorResource import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter 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.extensions.resolveReference
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.DetailsScreenTestTags
import com.tangem.features.details.component.UserWalletListComponent import com.tangem.features.details.component.UserWalletListComponent
import com.tangem.features.details.component.preview.PreviewUserWalletListComponent import com.tangem.features.details.component.preview.PreviewUserWalletListComponent
import com.tangem.features.details.entity.UserWalletListUM import com.tangem.features.details.entity.UserWalletListUM
@ -69,7 +71,9 @@ internal fun UserWalletListBlock(state: UserWalletListUM, modifier: Modifier = M
model = walletState, model = walletState,
reorderableListState = reorderableListState, reorderableListState = reorderableListState,
walletReorderUM = state.walletReorderUM, walletReorderUM = state.walletReorderUM,
modifier = Modifier.fillMaxWidth(), modifier = Modifier
.fillMaxWidth()
.testTag(DetailsScreenTestTags.USER_WALLET_ITEM),
) )
} }
item(key = "add_wallet_button") { item(key = "add_wallet_button") {