diff --git a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt index 3b89b53e54..6acf072e3c 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt @@ -8,6 +8,8 @@ import androidx.test.core.app.ActivityScenario import androidx.test.espresso.intent.Intents import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeoutOrNull import com.kaspersky.components.alluresupport.interceptors.step.ScreenshotStepInterceptor import com.kaspersky.components.alluresupport.withForcedAllureSupport import com.kaspersky.components.composesupport.config.addComposeSupport @@ -20,13 +22,14 @@ import com.tangem.common.rules.ApiEnvironmentRule import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.walletmanager.WalletManagersStore import com.tangem.datasource.utils.WireMockRedirectInterceptor import com.tangem.domain.promo.PromoRepository import com.tangem.domain.promo.models.PromoId +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.tap.MainActivity import dagger.hilt.android.testing.HiltAndroidRule import io.qameta.allure.kotlin.Allure -import kotlinx.coroutines.runBlocking import org.junit.Rule import org.junit.rules.RuleChain import org.junit.rules.TestRule @@ -58,6 +61,12 @@ abstract class BaseTestCase : TestCase( @Inject lateinit var promoRepository: PromoRepository + @Inject + lateinit var walletManagersStore: WalletManagersStore + + @Inject + lateinit var getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase + private val hiltRule = HiltAndroidRule(this) private val apiEnvironmentRule = ApiEnvironmentRule() private val permissionRule = GrantPermissionRule.grant( @@ -72,7 +81,11 @@ abstract class BaseTestCase : TestCase( private val semanticTreePrinterRule = object : TestWatcher() { override fun failed(e: Throwable?, description: Description?) { - runCatching { printAllRoots() } + runCatching { + runBlocking { + withTimeoutOrNull(SEMANTIC_TREE_PRINT_TIMEOUT_MS) { printAllRoots() } + } + } } } @@ -174,5 +187,6 @@ abstract class BaseTestCase : TestCase( private companion object { const val WIREMOCK_BASE_URL_ARG = "wiremockBaseUrl" + const val SEMANTIC_TREE_PRINT_TIMEOUT_MS = 5_000L } } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/common/constants/TestConstants.kt b/app/src/androidTest/kotlin/com/tangem/common/constants/TestConstants.kt index fd5d74d374..8ad9db91da 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/constants/TestConstants.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/constants/TestConstants.kt @@ -35,6 +35,7 @@ object TestConstants { const val WAIT_UNTIL_TIMEOUT = 20_000L const val WAIT_UNTIL_TIMEOUT_LONG = 30_000L + const val WAIT_UNTIL_TIMEOUT_VERY_LONG = 60_000L const val MARKETS_MAIN_NETWORK_SUFFIX = "MAIN" @@ -43,4 +44,14 @@ object TestConstants { const val USER_TOKENS_API_SCENARIO = "user_tokens_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" + const val SEED_PHRASE_15 = "genuine try deer upset connect sausage diary rule price shallow fit faculty leopard " + + "hawk when" + const val SEED_PHRASE_18 = "crush idle include refuse expose kiss slot budget uphold when dinner certain holiday " + + "slow word armor butter suffer" + const val SEED_PHRASE_21 = "employ space oval venue wash clog zebra cover icon wash assist word debris inform " + + "cable meadow add game meat rigid pride" + const val SEED_PHRASE_24 = "force visit fresh brown razor target ill scissors figure cave feel genre cargo category " + + "bread much nature basic fun iron benefit egg error prosper" } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/common/utils/AddressComparisonHelper.kt b/app/src/androidTest/kotlin/com/tangem/common/utils/AddressComparisonHelper.kt new file mode 100644 index 0000000000..2faf0b96ab --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/common/utils/AddressComparisonHelper.kt @@ -0,0 +1,88 @@ +package com.tangem.common.utils + +import com.tangem.utils.logging.TangemLogger +import org.json.JSONArray +import org.json.JSONObject +import org.junit.Assert.assertTrue + +/** + * Compares addresses from the app (clipboard JSON) with reference addresses from the QA tools API. + * + * Both JSON arrays contain objects with fields: blockchain, derivationPath, token (nullable), addresses (array). + * Comparison normalizes and sorts both arrays before diffing. + */ +object AddressComparisonHelper { + + fun compareAddresses(appJson: String, apiJson: String) { + val appEntries = parseAndNormalize(appJson) + val apiEntries = parseAndNormalize(apiJson) + + val missingInApp = apiEntries - appEntries.toSet() + val extraInApp = appEntries - apiEntries.toSet() + + if (missingInApp.isEmpty() && extraInApp.isEmpty()) { + TangemLogger.i("Address comparison passed: ${appEntries.size} entries match") + return + } + + val report = buildString { + appendLine("Address comparison FAILED") + if (missingInApp.isNotEmpty()) { + appendLine("\nMissing in app (expected from API but not found):") + missingInApp.forEach { appendLine(" - $it") } + } + if (extraInApp.isNotEmpty()) { + appendLine("\nExtra in app (found in app but not in API):") + extraInApp.forEach { appendLine(" - $it") } + } + appendLine("\nApp entries: ${appEntries.size}, API entries: ${apiEntries.size}") + } + + TangemLogger.e(report) + assertTrue(report, false) + } + + private fun parseAndNormalize(json: String): List { + val array = JSONArray(json) + val entries = mutableListOf() + + for (i in 0 until array.length()) { + val obj = array.getJSONObject(i) + entries.add( + AddressEntry( + blockchain = normalizeBlockchainName(obj.getString("blockchain")), + derivationPath = obj.getString("derivationPath").trim(), + token = obj.optString("token", null)?.trim()?.lowercase(), + addresses = parseAddresses(obj).sorted(), + ), + ) + } + + return entries.sortedWith( + compareBy { it.blockchain } + .thenBy { it.derivationPath } + .thenBy { it.token }, + ) + } + + private val blockchainNameOverrides = mapOf( + "chia network" to "chia", + ) + + private fun normalizeBlockchainName(name: String): String { + val normalized = name.trim().lowercase() + return blockchainNameOverrides[normalized] ?: normalized + } + + private fun parseAddresses(obj: JSONObject): List { + val addressesArray = obj.getJSONArray("addresses") + return (0 until addressesArray.length()).map { addressesArray.getString(it) } + } + + private data class AddressEntry( + val blockchain: String, + val derivationPath: String, + val token: String?, + val addresses: List, + ) +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/common/utils/NetworkUtils.kt b/app/src/androidTest/kotlin/com/tangem/common/utils/NetworkUtils.kt index 48c2a5cc5a..27b8c945ad 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/utils/NetworkUtils.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/utils/NetworkUtils.kt @@ -16,10 +16,10 @@ fun getWcUri( TangemLogger.i("Getting WC URI for network: $network") val client = OkHttpClient.Builder() - .connectTimeout(30, TimeUnit.SECONDS) // Таймаут подключения - .readTimeout(60, TimeUnit.SECONDS) // Таймаут чтения ответа - .writeTimeout(30, TimeUnit.SECONDS) // Таймаут записи - .callTimeout(90, TimeUnit.SECONDS) // Общий таймаут запроса + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .callTimeout(90, TimeUnit.SECONDS) .build() val request = Request.Builder() @@ -58,6 +58,59 @@ fun getWcUri( } } +fun getAddressesFromApi( + seedKey: String, + baseUrl: String = "[REDACTED_ENV_URL]", +): String? { + TangemLogger.i("Getting addresses for seed key: $seedKey") + + val client = OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .callTimeout(90, TimeUnit.SECONDS) + .build() + + val request = Request.Builder() + .url("$baseUrl/addresses") + .get() + .build() + + return try { + client.newCall(request).execute().use { response -> + TangemLogger.i("Response code: ${response.code}") + + if (response.isSuccessful) { + val body = response.body?.string() ?: "" + + val contentType = response.header("Content-Type") ?: "" + if (!contentType.contains("application/json") && !body.trimStart().startsWith("{")) { + TangemLogger.e("Unexpected response (not JSON), Content-Type: $contentType, body: $body") + return null + } + + val jsonObject = JSONObject(body) + val data = jsonObject.optJSONObject("data") ?: jsonObject + val seedData = data.optJSONArray(seedKey) + + if (seedData != null) { + TangemLogger.i("Got addresses for $seedKey: ${seedData.length()} entries") + seedData.toString() + } else { + TangemLogger.e("No data found for seed key: $seedKey") + null + } + } else { + val errorBody = response.body?.string() ?: "No error body" + TangemLogger.e("Request failed: ${response.code}, body: $errorBody") + null + } + } + } catch (e: Exception) { + TangemLogger.e("Error getting addresses", e) + null + } +} + fun checkServiceHealth( baseUrl: String = "[REDACTED_ENV_URL]" ): String? { diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/AddressScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/AddressScenarios.kt new file mode 100644 index 0000000000..f259dd60c4 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/AddressScenarios.kt @@ -0,0 +1,122 @@ +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 +import com.tangem.common.utils.AddressComparisonHelper +import com.tangem.common.utils.getClipboardText +import com.tangem.screens.onMainScreen +import com.tangem.screens.onTesterMenuScreen +import com.tangem.utils.StringsSigns.DASH_SIGN +import com.tangem.utils.logging.TangemLogger +import io.qameta.allure.kotlin.Allure.step +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout +import org.junit.Assert.assertNotNull + +private const val WALLET_MANAGERS_SETTLE_MS = 5_000L +private const val WALLET_MANAGERS_POLL_INTERVAL_MS = 500L + +fun BaseTestCase.verifyAddresses(seedPhrase: String, apiAddressesJson: String) { + var appAddressesJson: String? = null + + step("Open 'Main Screen' with existing hot wallet") { + openMainScreenWithExistingHotWallet(seedPhrase) + } + step("Assert wallet balance = '$DASH_SIGN'") { + composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_VERY_LONG) { + runCatching { onMainScreen { totalBalanceText.assertTextContains(DASH_SIGN) } }.isSuccess + } + } + step("Assert 'Organize tokens' button is enabled") { + composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_VERY_LONG) { + runCatching { onMainScreen { organizeTokensButton().assertIsEnabled() } }.isSuccess + } + } + step("Wait for all wallet managers to initialize") { + awaitWalletManagersStabilized() + } + step("Open tester menu") { + openTesterMenu() + } + step("Click on 'Addresses info' button") { + onTesterMenuScreen { addressesInfoButton.performClick() } + } + step("Click on 'JSON' tab") { + onTesterMenuScreen { jsonTab.performClick() } + } + step("Click on 'Copy' button") { + onTesterMenuScreen { copyButton.performClick() } + } + step("Get addresses JSON from clipboard") { + appAddressesJson = getClipboardText(ApplicationProvider.getApplicationContext()) + assertNotNull("Clipboard is empty after copying addresses", appAddressesJson) + } + step("Compare app addresses with API reference") { + AddressComparisonHelper.compareAddresses( + appJson = requireNotNull(appAddressesJson), + apiJson = apiAddressesJson, + ) + } +} + +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]. + + * + * Uses [getAllSync] with a polling interval instead of Flow, because the Flow only emits on changes — + * if the count stabilizes, there would be no new emission to check the settle timeout against. + */ +private fun BaseTestCase.awaitWalletManagersStabilized() { + val walletId = getSelectedWalletSyncUseCase().getOrNull()?.walletId + ?: error("No selected wallet found") + var lastSize = -1 + var stableStart = System.currentTimeMillis() + + runBlocking { + withTimeout(WAIT_UNTIL_TIMEOUT_VERY_LONG) { + while (true) { + val currentSize = walletManagersStore.getAllSync(walletId).size + val now = System.currentTimeMillis() + + if (currentSize != lastSize) { + TangemLogger.i("Wallet managers count: $currentSize (was $lastSize)") + lastSize = currentSize + stableStart = now + } else if (now - stableStart >= WALLET_MANAGERS_SETTLE_MS) { + TangemLogger.i("Wallet managers stabilized at $currentSize entries") + return@withTimeout + } + + delay(WALLET_MANAGERS_POLL_INTERVAL_MS) + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt index 2d11046a5c..beb2b2d14f 100644 --- a/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt @@ -59,6 +59,57 @@ fun BaseTestCase.openMainScreen( } } +fun BaseTestCase.openMainScreenWithExistingHotWallet(seedPhrase: String) { + step("Click on 'Accept' button") { + onDisclaimerScreen { acceptButton.clickWithAssertion() } + } + step("Click on 'Get started' button") { + onStoriesScreen { getStartedButton.clickWithAssertion() } + } + step("Click on 'Start with Mobile Wallet' button") { + onCreateWalletStartScreen { startWithMobileWalletButton.performClick() } + } + step("Click on 'Import existing wallet' button") { + onCreateMobileWalletScreen { importExistingWalletButton.performClick() } + } + step("Click on 'Phrase text field'") { + onImportWalletScreen { phraseTextField.performClick() } + } + step("Type seed phrase in 'Phrase text field'") { + onImportWalletScreen { phraseTextField.performTextReplacement(seedPhrase) } + } + step("Click on 'Import' button") { + onImportWalletScreen { + importButton.assertIsEnabled() + importButton.performClick() + } + } + step("Click on 'Continue' button") { + onImportWalletScreen { + continueButton.assertIsEnabled() + continueButton.performClick() + } + } + step("Click on 'Skip' button") { + onImportWalletScreen { skipButton.performClick() } + } + step("Click on 'Skip anyway' dialog button") { + onDialog { skipAnywayButton.performClick() } + } + step("Click on 'Finish' button") { + onImportWalletScreen { + finishButton.assertIsEnabled() + finishButton.performClick() + } + } + step("Assert 'Main' screen is displayed") { + onMainScreen { screenContainer.assertIsDisplayed() } + } + step("Dismiss Market Tooltip by clicking close button") { + onMarketsTooltipScreen { closeButton.clickWithAssertion() } + } +} + fun BaseTestCase.synchronizeAddresses( balance: String? = null, isBalanceAvailable: Boolean = true diff --git a/app/src/androidTest/kotlin/com/tangem/screens/CreateMobileWalletPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/CreateMobileWalletPageObject.kt new file mode 100644 index 0000000000..e83def9363 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/CreateMobileWalletPageObject.kt @@ -0,0 +1,23 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.common.R +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 + +class CreateMobileWalletPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val importExistingWalletButton: KNode = child { + hasTestTag(BaseButtonTestTags.TEXT) + hasText(getResourceString(R.string.hw_import_existing_wallet)) + useUnmergedTree = true + } +} + +internal fun BaseTestCase.onCreateMobileWalletScreen(function: CreateMobileWalletPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/CreateWalletStartPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/CreateWalletStartPageObject.kt index b06335d752..6d875cd034 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/CreateWalletStartPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/CreateWalletStartPageObject.kt @@ -17,6 +17,11 @@ class CreateWalletStartPageObject(semanticsProvider: SemanticsNodeInteractionsPr hasText(getResourceString(OnboardingImplR.string.welcome_unlock_card)) useUnmergedTree = true } + + val startWithMobileWalletButton: KNode = child { + hasText(getResourceString(OnboardingImplR.string.welcome_create_wallet_mobile_title)) + useUnmergedTree = true + } } internal fun BaseTestCase.onCreateWalletStartScreen(function: CreateWalletStartPageObject.() -> Unit) = diff --git a/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt index 13eef6b87d..ff268b8969 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt @@ -64,6 +64,11 @@ class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : hasTestTag(BaseButtonTestTags.BUTTON) hasText(getResourceString(R.string.common_change)) } + + val skipAnywayButton: KNode = child { + hasTestTag(BaseButtonTestTags.BUTTON) + hasText(getResourceString(R.string.access_code_alert_skip_ok)) + } } internal fun BaseTestCase.onDialog(function: DialogPageObject.() -> Unit) = diff --git a/app/src/androidTest/kotlin/com/tangem/screens/ImportWalletPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/ImportWalletPageObject.kt new file mode 100644 index 0000000000..26d1af7a54 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/ImportWalletPageObject.kt @@ -0,0 +1,54 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.common.R +import com.tangem.core.ui.test.BaseButtonTestTags +import com.tangem.core.ui.test.ImportWalletScreenTestTags +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 +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 ImportWalletPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val phraseTextField: KNode = child { + hasTestTag(ImportWalletScreenTestTags.PHRASE_TEXT_FIELD) + useUnmergedTree = true + } + + val passphraseTextField: KNode = child { + hasTestTag(ImportWalletScreenTestTags.PASSPHRASE_TEXT_FIELD) + useUnmergedTree = true + } + + val importButton: KNode = child { + hasTestTag(BaseButtonTestTags.BUTTON) + hasAnyChild(withText(getResourceString(R.string.common_import))) + useUnmergedTree = true + } + + val continueButton: KNode = child { + hasTestTag(BaseButtonTestTags.BUTTON) + hasAnyChild(withText(getResourceString(R.string.common_continue))) + useUnmergedTree = true + } + + val skipButton: KNode = child { + hasTestTag(TopAppBarTestTags.MORE_BUTTON) + hasText(getResourceString(R.string.common_skip)) + useUnmergedTree = true + } + + val finishButton: KNode = child { + hasTestTag(BaseButtonTestTags.BUTTON) + hasAnyChild(withText(getResourceString(R.string.common_finish))) + useUnmergedTree = true + } +} + +internal fun BaseTestCase.onImportWalletScreen(function: ImportWalletPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/TesterMenuPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/TesterMenuPageObject.kt new file mode 100644 index 0000000000..dce99464a3 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/TesterMenuPageObject.kt @@ -0,0 +1,39 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.feature.tester.impl.R as TesterImplR +import com.tangem.core.ui.test.BaseButtonTestTags +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 +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.utilities.getResourceString + +class TesterMenuPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val backButton: KNode = child { + hasTestTag(TopAppBarTestTags.CLOSE_BUTTON) + useUnmergedTree = true + } + + val addressesInfoButton: KNode = child { + hasTestTag(BaseButtonTestTags.TEXT) + hasText(getResourceString(TesterImplR.string.addresses_info)) + useUnmergedTree = true + } + + val jsonTab: KNode = child { + hasText("JSON") + useUnmergedTree = true + } + + val copyButton: KNode = child { + hasContentDescription("Copy") + useUnmergedTree = true + } +} + +internal fun BaseTestCase.onTesterMenuScreen(function: TesterMenuPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/hotWallet/AddressesTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/hotWallet/AddressesTest.kt new file mode 100644 index 0000000000..46695fa68b --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/hotWallet/AddressesTest.kt @@ -0,0 +1,86 @@ +package com.tangem.tests.hotWallet + +import com.tangem.common.BaseTestCase +import com.tangem.common.annotations.ApiEnv +import com.tangem.common.annotations.ApiEnvConfig +import com.tangem.common.constants.TestConstants.SEED_PHRASE_12 +import com.tangem.common.constants.TestConstants.SEED_PHRASE_15 +import com.tangem.common.constants.TestConstants.SEED_PHRASE_18 +import com.tangem.common.constants.TestConstants.SEED_PHRASE_21 +import com.tangem.common.constants.TestConstants.SEED_PHRASE_24 +import com.tangem.common.utils.checkServiceHealth +import com.tangem.common.utils.getAddressesFromApi +import com.tangem.datasource.api.common.config.ApiConfig +import com.tangem.datasource.api.common.config.ApiEnvironment +import com.tangem.scenarios.verifyAddresses +import dagger.hilt.android.testing.HiltAndroidTest +import io.qameta.allure.kotlin.AllureId +import io.qameta.allure.kotlin.junit4.DisplayName +import org.junit.Assert.assertNotNull +import org.junit.Test + +@HiltAndroidTest +class AddressesTest : BaseTestCase() { + + private var apiAddressesJson: String? = null + + private fun setupAddressTestHooks(seedKey: String) = setupHooks( + additionalBeforeAppLaunchSection = { + val status = checkServiceHealth() + assertNotNull("QA tools service is unreachable ([REDACTED_ENV_URL]", status) + + apiAddressesJson = getAddressesFromApi(seedKey) + assertNotNull("Failed to fetch reference addresses for '$seedKey'", apiAddressesJson) + }, + ) + + @ApiEnv(ApiEnvConfig(ApiConfig.ID.TangemTech, ApiEnvironment.PROD)) + @AllureId("1792") + @DisplayName("Hot wallet: auto derivation addresses for seed 12") + @Test + fun seed12AddressesTest() { + setupAddressTestHooks("twelve").run { + verifyAddresses(SEED_PHRASE_12, requireNotNull(apiAddressesJson)) + } + } + + @ApiEnv(ApiEnvConfig(ApiConfig.ID.TangemTech, ApiEnvironment.PROD)) + @AllureId("5106") + @DisplayName("Hot wallet: auto derivation addresses for seed 15") + @Test + fun seed15AddressesTest() { + setupAddressTestHooks("fifteen").run { + verifyAddresses(SEED_PHRASE_15, requireNotNull(apiAddressesJson)) + } + } + + @ApiEnv(ApiEnvConfig(ApiConfig.ID.TangemTech, ApiEnvironment.PROD)) + @AllureId("5107") + @DisplayName("Hot wallet: auto derivation addresses for seed 18") + @Test + fun seed18AddressesTest() { + setupAddressTestHooks("eighteen").run { + verifyAddresses(SEED_PHRASE_18, requireNotNull(apiAddressesJson)) + } + } + + @ApiEnv(ApiEnvConfig(ApiConfig.ID.TangemTech, ApiEnvironment.PROD)) + @AllureId("5108") + @DisplayName("Hot wallet: auto derivation addresses for seed 21") + @Test + fun seed21AddressesTest() { + setupAddressTestHooks("twenty_one").run { + verifyAddresses(SEED_PHRASE_21, requireNotNull(apiAddressesJson)) + } + } + + @ApiEnv(ApiEnvConfig(ApiConfig.ID.TangemTech, ApiEnvironment.PROD)) + @AllureId("5109") + @DisplayName("Hot wallet: auto derivation addresses for seed 24") + @Test + fun seed24AddressesTest() { + setupAddressTestHooks("twenty_four").run { + verifyAddresses(SEED_PHRASE_24, requireNotNull(apiAddressesJson)) + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/ImportWalletScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/ImportWalletScreenTestTags.kt new file mode 100644 index 0000000000..3e641be3e5 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/ImportWalletScreenTestTags.kt @@ -0,0 +1,6 @@ +package com.tangem.core.ui.test + +object ImportWalletScreenTestTags { + const val PHRASE_TEXT_FIELD = "IMPORT_WALLET_PHRASE_TEXT_FIELD" + const val PASSPHRASE_TEXT_FIELD = "IMPORT_WALLET_PASSPHRASE_TEXT_FIELD" +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/AddExistingWalletImportContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/AddExistingWalletImportContent.kt index d7bb9338b0..3b85b823dc 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/AddExistingWalletImportContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/AddExistingWalletImportContent.kt @@ -19,6 +19,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.TextFieldValue @@ -39,6 +40,7 @@ import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.TangemTextFieldsDefault import com.tangem.core.ui.components.keyboardAsState import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.test.ImportWalletScreenTestTags import com.tangem.features.hotwallet.addexistingwallet.im.port.ui.utils.InvalidWordsColorTransformation import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -73,9 +75,10 @@ internal fun AddExistingWalletImportContent(state: AddExistingWalletImportUM, mo ) OutlineTextFieldWithIcon( - modifier = modifier + modifier = Modifier .padding(horizontal = 16.dp) - .fillMaxWidth(), + .fillMaxWidth() + .testTag(ImportWalletScreenTestTags.PASSPHRASE_TEXT_FIELD), value = state.passPhrase, onValueChange = state.passPhraseChange, iconResId = R.drawable.ic_information_24, @@ -130,7 +133,8 @@ private fun PhraseBlock(state: AddExistingWalletImportUM, modifier: Modifier = M OutlinedTextField( modifier = Modifier .fillMaxWidth() - .height(TangemTheme.dimens.size142), + .height(TangemTheme.dimens.size142) + .testTag(ImportWalletScreenTestTags.PHRASE_TEXT_FIELD), value = state.words, onValueChange = state.wordsChange, textStyle = TangemTheme.typography.body1,