Updated on 2026-08-14
This commit is contained in:
commit
06a915a0f4
2186 changed files with 101417 additions and 36593 deletions
|
|
@ -125,6 +125,7 @@ dependencies {
|
|||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.settings)
|
||||
implementation(projects.domain.appUpdate)
|
||||
implementation(projects.domain.tokens)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.txhistory)
|
||||
|
|
@ -166,6 +167,7 @@ dependencies {
|
|||
implementation(projects.domain.walletManager)
|
||||
implementation(projects.domain.walletManager.models)
|
||||
implementation(projects.domain.yieldSupply)
|
||||
implementation(projects.domain.polymarket)
|
||||
implementation(projects.domain.promo)
|
||||
implementation(projects.domain.blockaid)
|
||||
implementation(projects.domain.hotWallet)
|
||||
|
|
@ -195,6 +197,7 @@ dependencies {
|
|||
implementation(projects.libs.tangemSdkApi)
|
||||
|
||||
implementation(projects.data.account)
|
||||
implementation(projects.data.addressBook)
|
||||
implementation(projects.data.appCurrency)
|
||||
implementation(projects.data.appTheme)
|
||||
implementation(projects.data.balanceHiding)
|
||||
|
|
@ -202,6 +205,7 @@ dependencies {
|
|||
implementation(projects.data.card)
|
||||
implementation(projects.data.common)
|
||||
implementation(projects.data.settings)
|
||||
implementation(projects.data.appUpdate)
|
||||
implementation(projects.data.tokens)
|
||||
implementation(projects.data.assetsdiscovery)
|
||||
implementation(projects.data.txhistory)
|
||||
|
|
@ -229,6 +233,7 @@ dependencies {
|
|||
implementation(projects.data.swap)
|
||||
implementation(projects.data.walletManager)
|
||||
implementation(projects.data.yieldSupply)
|
||||
implementation(projects.data.polymarket)
|
||||
implementation(projects.data.promo)
|
||||
implementation(projects.data.hotWallet)
|
||||
implementation(projects.data.news)
|
||||
|
|
@ -271,6 +276,8 @@ dependencies {
|
|||
implementation(projects.features.details.impl)
|
||||
implementation(projects.features.disclaimer.api)
|
||||
implementation(projects.features.disclaimer.impl)
|
||||
implementation(projects.features.forceUpdate.api)
|
||||
implementation(projects.features.forceUpdate.impl)
|
||||
implementation(projects.features.pushNotifications.api)
|
||||
implementation(projects.features.pushNotifications.impl)
|
||||
implementation(projects.features.pushNotificationSettings.api)
|
||||
|
|
@ -337,8 +344,12 @@ dependencies {
|
|||
implementation(projects.features.tokenRecieve.impl)
|
||||
implementation(projects.features.yieldSupply.api)
|
||||
implementation(projects.features.yieldSupply.impl)
|
||||
implementation(projects.features.polymarket.api)
|
||||
implementation(projects.features.polymarket.impl)
|
||||
implementation(projects.features.approval.api)
|
||||
implementation(projects.features.approval.impl)
|
||||
implementation(projects.features.forYou.api)
|
||||
implementation(projects.features.forYou.impl)
|
||||
|
||||
/** AndroidX libraries */
|
||||
implementation(deps.androidx.core.ktx)
|
||||
|
|
@ -398,6 +409,7 @@ dependencies {
|
|||
kapt(deps.hilt.compilerx)
|
||||
|
||||
/** Other libraries */
|
||||
implementation(deps.arrow.fx)
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
implementation(deps.material)
|
||||
implementation(deps.googlePlay.review)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
|
|||
import com.tangem.common.allure.FailedStepScreenshotInterceptor
|
||||
import com.tangem.common.constants.TestConstants.ALLURE_LABEL_NAME
|
||||
import com.tangem.common.constants.TestConstants.ALLURE_LABEL_VALUE
|
||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
|
||||
import com.tangem.common.rules.ApiEnvironmentRule
|
||||
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
|
|
@ -179,50 +180,29 @@ abstract class BaseTestCase : TestCase(
|
|||
|
||||
fun waitForIdle() = composeTestRule.waitForIdle()
|
||||
|
||||
/**
|
||||
* Waits until [block] stops throwing (or [timeoutMillis] elapses). Use in scenario (BaseTestCase extension)
|
||||
* code where flakySafely is unavailable; in test bodies prefer flakySafely.
|
||||
*/
|
||||
fun awaitSuccess(timeoutMillis: Long = WAIT_UNTIL_TIMEOUT, block: () -> Unit) {
|
||||
composeTestRule.waitUntil(timeoutMillis = timeoutMillis) { runCatching(block).isSuccess }
|
||||
}
|
||||
|
||||
private fun applicationInjectionRule(): ApplicationInjectionExecutionRule {
|
||||
return ApplicationInjectionExecutionRule(
|
||||
toggleStates = mapOf(
|
||||
"SWAP_REDESIGN_ENABLED" to false,
|
||||
"ACCOUNTS_FEATURE_ENABLED" to true,
|
||||
"GASLESS_APPROVAL_ENABLED" to true,
|
||||
"MAIN_SCREEN_QR_SCANNING_ENABLED" to true,
|
||||
"ADD_AND_MANAGE_TOKENS_ENABLED" to true,
|
||||
"ASSETS_DISCOVERY_ENABLED" to true,
|
||||
"VISA_ONBOARDING_ENABLED" to true,
|
||||
// Version-gated toggles released in versions <= 6.0 — forced on so tests run against the actual
|
||||
// build even when the app version resolves to 1.0.0-SNAPSHOT on CI (then 1.0.0 < x.xx would
|
||||
// disable them). On the releases/6.0 branch every toggle with version <= 6.0 ships enabled.
|
||||
// 5.37
|
||||
"HEDERA_ERC20_ENABLED" to true,
|
||||
// 5.39
|
||||
"STAKING_ETH_ENABLED" to true,
|
||||
"DYNAMIC_ADDRESSES_ENABLED" to true,
|
||||
"SOLANA_TX_HISTORY_ENABLED" to true,
|
||||
"SOLANA_SCALED_UI_AMOUNT_ENABLED" to true,
|
||||
"SWAP_AB_ENABLED" to true,
|
||||
"AND_15310_ADD_FUNDS_STAGE1" to true,
|
||||
"AND_15009_SWAP_PROVIDER_FILTER_ENABLED" to true,
|
||||
"AND_15101_TANGEM_PAY_HOT_WALLET_ONBOARDING" to true,
|
||||
"AND_15402_ADI_MAIN_SCREEN_DEFAULT_ENABLED" to true,
|
||||
"AND_15103_SWAP_RATE_EXPERIENCE_ENABLED" to true,
|
||||
"AND_15122_SWAP_PREDEFINED_BUTTONS_ENABLED" to true,
|
||||
"TWI_1512_HIDE_STORIES_FOR_REFERRAL_ENABLED" to true,
|
||||
// 5.39.2
|
||||
"AND_15154_YIELD_PROMO_ENABLED" to true,
|
||||
// 5.40
|
||||
"TWI_1377_MANAGE_FUNDS" to true,
|
||||
// 6.0
|
||||
"APP_REDESIGN_ENABLED" to true,
|
||||
"TWI_1326_YIELD_MODE_SWAP_ENABLED" to true,
|
||||
"AND_15207_SWAP_SWITCH_TO_TRANSFER_ENABLED" to true,
|
||||
"AND_15120_SWAP_INTEGRATED_APPROVE" to true,
|
||||
"AND_15596_ONBOARDING_PUSH_NOTIFICATION_DOUBLE_ASK_AB_ENABLED" to true,
|
||||
"AND_15258_QUICK_TOP_UP_ENABLED" to true,
|
||||
"AND_15368_VISA_PAY_REDESIGN" to true,
|
||||
"AND_15364_VISA_PAY_CARD_CLOSE" to true,
|
||||
"AND_15489_EXPRESS_SHARE_BUTTON_ENABLED" to true,
|
||||
"AND_15235_VISA_MULTIPLE_CARDS" to true,
|
||||
"AND_15715_SWAP_BEST_DEX_RATE_ENABLED" to true,
|
||||
// 6.1
|
||||
"TWI_1638_VA_MVP0_ENABLED" to true,
|
||||
"TWI_1403_ONBOARDING_PUSH_NOTIFICATION_DOUBLE_ASK_AB_ENABLED" to true,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ object TestConstants {
|
|||
const val ALLURE_LABEL_VALUE = "Kaspresso"
|
||||
|
||||
const val USER_TOKENS_API_SCENARIO = "user_tokens_api"
|
||||
const val COINS_API_SCENARIO = "coins_api"
|
||||
const val REFERRAL_API_SCENARIO = "referral_api"
|
||||
const val QUOTES_API_SCENARIO = "quotes_api"
|
||||
const val CREATE_USER_WALLET_API_SCENARIO = "create_user_wallet_api"
|
||||
|
|
@ -68,5 +69,7 @@ object TestConstants {
|
|||
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||
|
||||
const val TANGEM_PAY_ELIGIBILITY_SCENARIO = "tangem_pay_eligibility"
|
||||
const val TANGEM_PAY_ELIGIBILITY_CHANNELS_SCENARIO = "tangem_pay_eligibility_channels"
|
||||
const val TANGEM_PAY_KYC_STATUS_SCENARIO = "tangem_pay_kyc_status"
|
||||
const val TANGEM_PAY_ACCESS_CODE = "517384"
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.tangem.common.extensions
|
||||
|
||||
import androidx.compose.ui.semantics.SemanticsNode
|
||||
import androidx.compose.ui.semantics.SemanticsProperties
|
||||
import androidx.compose.ui.semantics.getOrNull
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionCollection
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
|
||||
/** Returns the first non-blank text found in this node or its subtree (unmerged tree). */
|
||||
fun SemanticsNode.firstTextOrNull(): String? {
|
||||
config.getOrNull(SemanticsProperties.Text)
|
||||
?.firstOrNull()?.text?.takeIf { it.isNotBlank() }
|
||||
?.let { return it }
|
||||
children.forEach { child -> child.firstTextOrNull()?.let { return it } }
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the first text of every currently displayed node in this collection, in visual order
|
||||
* (top-to-bottom, then left-to-right). Non-displayed nodes and nodes without text are skipped.
|
||||
*/
|
||||
fun SemanticsNodeInteractionCollection.displayedTextsInVisualOrder(): List<String> {
|
||||
val count = fetchSemanticsNodes().size
|
||||
return (0 until count)
|
||||
.mapNotNull { index ->
|
||||
val interaction = get(index)
|
||||
if (runCatching { interaction.assertIsDisplayed() }.isFailure) return@mapNotNull null
|
||||
val node = interaction.fetchSemanticsNode()
|
||||
val text = node.firstTextOrNull() ?: return@mapNotNull null
|
||||
node.boundsInRoot to text
|
||||
}
|
||||
.sortedWith(compareBy({ it.first.top }, { it.first.left }))
|
||||
.map { it.second }
|
||||
}
|
||||
|
|
@ -42,6 +42,17 @@ object AddressComparisonHelper {
|
|||
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> {
|
||||
val array = JSONArray(json)
|
||||
val entries = mutableListOf<AddressEntry>()
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import android.content.ClipData
|
|||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
|
||||
fun getClipboardText(context: Context): String? {
|
||||
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
|
|
@ -27,12 +29,15 @@ fun clearClipboard(
|
|||
clipboard.clearPrimaryClip()
|
||||
}
|
||||
|
||||
// Kotlin's assert() is a no-op on device (JVM assertions disabled) — use JUnit asserts here.
|
||||
fun assertClipboardTextEquals(
|
||||
expected: String,
|
||||
context: Context = ApplicationProvider.getApplicationContext()
|
||||
) {
|
||||
assertEquals("Clipboard text mismatch", expected, getClipboardText(context))
|
||||
}
|
||||
|
||||
fun assertClipboardIsEmpty(context: Context = ApplicationProvider.getApplicationContext()) {
|
||||
val actual = getClipboardText(context)
|
||||
assert(actual == expected) {
|
||||
"Clipboard text mismatch.\nExpected: '$expected'\nActual: '$actual'"
|
||||
}
|
||||
assertTrue("Expected empty clipboard but was: '$actual'", actual.isNullOrEmpty())
|
||||
}
|
||||
|
|
@ -1,10 +1,16 @@
|
|||
package com.tangem.common.utils
|
||||
|
||||
import okhttp3.Call
|
||||
import okhttp3.EventListener
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.json.JSONObject
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import java.io.IOException
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
// WC URIs embed a session symKey that lets anyone join/hijack the session — strip it before logging.
|
||||
|
|
@ -13,6 +19,79 @@ private val WC_SECRET_REGEX = Regex("(symKey(?:=|%3D))[^&\\s\"']+", RegexOption.
|
|||
private fun redactWcSecrets(text: String): String =
|
||||
WC_SECRET_REGEX.replace(text) { "${it.groupValues[1]}<redacted>" }
|
||||
|
||||
private const val DEFAULT_MAX_ATTEMPTS = 4
|
||||
private const val INITIAL_BACKOFF_MS = 500L
|
||||
|
||||
/**
|
||||
* Runs [block] with exponential backoff. Retries ONLY when [block] throws (transient failure:
|
||||
* network error, non-2xx, malformed body). A `null` return is treated as terminal (e.g. "200 but no
|
||||
* data for this key") and is NOT retried. Returns the block result, or null if all attempts failed.
|
||||
*/
|
||||
private fun <T> retryWithBackoff(
|
||||
maxAttempts: Int = DEFAULT_MAX_ATTEMPTS,
|
||||
initialDelayMs: Long = INITIAL_BACKOFF_MS,
|
||||
block: (attempt: Int) -> T,
|
||||
): T? {
|
||||
var delayMs = initialDelayMs
|
||||
var lastError: Throwable? = null
|
||||
repeat(maxAttempts) { i ->
|
||||
val attempt = i + 1
|
||||
try {
|
||||
return block(attempt)
|
||||
} catch (e: Exception) {
|
||||
lastError = e
|
||||
TangemLogger.w("Attempt $attempt/$maxAttempts failed: ${e.message}")
|
||||
if (attempt < maxAttempts) {
|
||||
try {
|
||||
Thread.sleep(delayMs)
|
||||
} catch (ie: InterruptedException) {
|
||||
Thread.currentThread().interrupt()
|
||||
TangemLogger.w("Retry backoff sleep interrupted; aborting retries")
|
||||
return null
|
||||
}
|
||||
delayMs *= 2
|
||||
}
|
||||
}
|
||||
}
|
||||
TangemLogger.e("All $maxAttempts attempts failed", lastError)
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs the actual connected endpoint (IPv4/IPv6) and whether a proxy is in the path. Lets CI logs
|
||||
* distinguish "went out via the wrong egress / IPv6 / through a local proxy" from other failures.
|
||||
*/
|
||||
private val diagnosticEventListener = object : EventListener() {
|
||||
override fun connectStart(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy) {
|
||||
TangemLogger.i("Connecting to ${inetSocketAddress.address?.hostAddress} (proxy=$proxy)")
|
||||
}
|
||||
}
|
||||
|
||||
private fun diagnosticClient(connectSec: Long, readSec: Long, callSec: Long): OkHttpClient =
|
||||
OkHttpClient.Builder()
|
||||
.connectTimeout(connectSec, TimeUnit.SECONDS)
|
||||
.readTimeout(readSec, TimeUnit.SECONDS)
|
||||
.callTimeout(callSec, TimeUnit.SECONDS)
|
||||
// Don't follow redirects: a Cloudflare Access 302 must stay visible (its `location` points at
|
||||
// cloudflareaccess.com) so logHttpFailure can flag "egress IP not allow-listed". /health and
|
||||
// /addresses have no legitimate redirects.
|
||||
.followRedirects(false)
|
||||
.followSslRedirects(false)
|
||||
.eventListener(diagnosticEventListener)
|
||||
.build()
|
||||
|
||||
/**
|
||||
* Logs enough to classify a failed response at a glance: `cf-ray`/`location`/`server` reveal a
|
||||
* Cloudflare Access 302 (egress IP not allow-listed) vs an origin 5xx vs anything else.
|
||||
*/
|
||||
private fun logHttpFailure(tag: String, response: Response, body: String) {
|
||||
TangemLogger.e(
|
||||
"$tag failed: code=${response.code} cf-ray=${response.header("cf-ray") ?: "-"} " +
|
||||
"server=${response.header("server") ?: "-"} location=${response.header("location") ?: "-"} " +
|
||||
"body=${body.take(200)}",
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests a WalletConnect URI from the qa-tools service.
|
||||
*
|
||||
|
|
@ -142,50 +221,40 @@ fun getAddressesFromApi(
|
|||
): 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 client = diagnosticClient(connectSec = 30, readSec = 60, callSec = 90)
|
||||
val request = Request.Builder()
|
||||
.url("$baseUrl/addresses")
|
||||
.get()
|
||||
.build()
|
||||
|
||||
return try {
|
||||
return retryWithBackoff { attempt ->
|
||||
TangemLogger.i("Getting addresses for '$seedKey', attempt $attempt")
|
||||
client.newCall(request).execute().use { response ->
|
||||
TangemLogger.i("Response code: ${response.code}")
|
||||
if (!response.isSuccessful) {
|
||||
// Transient (network/Access 302/5xx) — throw so retryWithBackoff retries.
|
||||
logHttpFailure("getAddressesFromApi", response, response.body?.string() ?: "")
|
||||
throw IOException("getAddressesFromApi: HTTP ${response.code}")
|
||||
}
|
||||
|
||||
if (response.isSuccessful) {
|
||||
val body = response.body?.string() ?: ""
|
||||
val body = response.body?.string() ?: ""
|
||||
val contentType = response.header("Content-Type") ?: ""
|
||||
if (!contentType.contains("application/json") && !body.trimStart().startsWith("{")) {
|
||||
logHttpFailure("getAddressesFromApi (not JSON)", response, body)
|
||||
throw IOException("getAddressesFromApi: unexpected non-JSON response")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
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 {
|
||||
val errorBody = response.body?.string() ?: "No error body"
|
||||
TangemLogger.e("Request failed: ${response.code}, body: $errorBody")
|
||||
// Terminal: server responded fine but has no data for this key — retrying won't help.
|
||||
TangemLogger.e("No data found for seed key: $seedKey")
|
||||
null
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
TangemLogger.e("Error getting addresses", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -194,43 +263,31 @@ fun checkServiceHealth(
|
|||
): String? {
|
||||
TangemLogger.i("Checking service health")
|
||||
|
||||
val client = OkHttpClient()
|
||||
val client = diagnosticClient(connectSec = 15, readSec = 30, callSec = 45)
|
||||
val request = Request.Builder()
|
||||
.url("$baseUrl/health")
|
||||
.get()
|
||||
.build()
|
||||
|
||||
return try {
|
||||
return retryWithBackoff { attempt ->
|
||||
TangemLogger.i("Checking service health, attempt $attempt")
|
||||
client.newCall(request).execute().use { response ->
|
||||
TangemLogger.i("Response code: ${response.code}")
|
||||
if (!response.isSuccessful) {
|
||||
// Transient (network/Access 302/5xx) — throw so retryWithBackoff retries.
|
||||
logHttpFailure("checkServiceHealth", response, response.body?.string() ?: "")
|
||||
throw IOException("checkServiceHealth: HTTP ${response.code}")
|
||||
}
|
||||
|
||||
if (response.isSuccessful) {
|
||||
val body = response.body?.string() ?: ""
|
||||
TangemLogger.i("Response body: $body")
|
||||
|
||||
if (body.isEmpty()) {
|
||||
TangemLogger.e("Response body is empty")
|
||||
return null
|
||||
}
|
||||
|
||||
val jsonObject = JSONObject(body)
|
||||
val status = jsonObject.optString("status", "")
|
||||
|
||||
if (status.isNotEmpty()) {
|
||||
TangemLogger.i("Got status successfully: $status")
|
||||
status
|
||||
} else {
|
||||
TangemLogger.e("Status field is missing or empty")
|
||||
null
|
||||
}
|
||||
val body = response.body?.string() ?: ""
|
||||
val status = if (body.isEmpty()) "" else JSONObject(body).optString("status", "")
|
||||
if (status.isNotEmpty()) {
|
||||
TangemLogger.i("Got status successfully: $status")
|
||||
status
|
||||
} else {
|
||||
val errorBody = response.body?.string() ?: "No error body"
|
||||
TangemLogger.e("Request failed: ${response.code}, body: $errorBody")
|
||||
null
|
||||
// Empty/malformed body from a 2xx — treat as transient and retry.
|
||||
logHttpFailure("checkServiceHealth (empty status)", response, body)
|
||||
throw IOException("checkServiceHealth: missing 'status' field")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
TangemLogger.e("Error checking health", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.common.utils
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import com.google.zxing.BinaryBitmap
|
||||
import com.google.zxing.DecodeHintType
|
||||
import com.google.zxing.RGBLuminanceSource
|
||||
import com.google.zxing.common.HybridBinarizer
|
||||
import com.google.zxing.qrcode.QRCodeReader
|
||||
|
||||
/** Decodes the text encoded in a QR-code [bitmap] (e.g. captured from a Compose node via captureToImage). */
|
||||
fun decodeQrCode(bitmap: Bitmap): String {
|
||||
val width = bitmap.width
|
||||
val height = bitmap.height
|
||||
val pixels = IntArray(width * height)
|
||||
bitmap.getPixels(pixels, 0, width, 0, 0, width, height)
|
||||
|
||||
val source = RGBLuminanceSource(width, height, pixels)
|
||||
val binaryBitmap = BinaryBitmap(HybridBinarizer(source))
|
||||
val hints = mapOf(DecodeHintType.TRY_HARDER to true)
|
||||
|
||||
return QRCodeReader().decode(binaryBitmap, hints).text
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
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
|
||||
|
|
@ -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].
|
||||
|
||||
|
|
|
|||
|
|
@ -61,6 +61,19 @@ fun BaseTestCase.openMainScreen(
|
|||
}
|
||||
}
|
||||
|
||||
/** Opens the main screen, synchronizes addresses, and opens the details of the token with [tokenName]. */
|
||||
fun BaseTestCase.openTokenDetails(tokenName: String) {
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Click on token with name: '$tokenName'") {
|
||||
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
fun BaseTestCase.openMainScreenWithExistingHotWallet(seedPhrase: String, accessCode: String = "") {
|
||||
step("Click on 'Get started' button") {
|
||||
|
|
|
|||
|
|
@ -42,7 +42,14 @@ fun BaseTestCase.enterAmountAndOpenSendConfirm(amount: String, recipientAddress:
|
|||
* On the 'Send confirm' screen, open the network-fee selector and switch the fee token from the
|
||||
* native coin to the given (stablecoin) token — the core gasless action repeated across the suite.
|
||||
*/
|
||||
fun BaseTestCase.selectStablecoinAsFeeToken(coinName: String, tokenName: String) {
|
||||
fun BaseTestCase.selectStablecoinAsFeeToken(
|
||||
coinName: String,
|
||||
tokenName: String,
|
||||
// Positive flows wait until 'Apply' is enabled (fee is computed & payable). Negative flows
|
||||
// (insufficient balance) must NOT wait for that — 'Apply' stays disabled — so pass false and let
|
||||
// the caller assert the disabled/error state itself.
|
||||
expectApplyEnabled: Boolean = true,
|
||||
) {
|
||||
step("Click on 'Network fee' block") {
|
||||
onSendConfirmScreen {
|
||||
feeSelectorBlock.assertIsDisplayed()
|
||||
|
|
@ -57,6 +64,17 @@ fun BaseTestCase.selectStablecoinAsFeeToken(coinName: String, tokenName: String)
|
|||
step("Select '$tokenName' as the fee-paying token") {
|
||||
onSendFeeSelectorBottomSheet { feeTokenItem(tokenName).performClick() }
|
||||
}
|
||||
step("Wait until the '$tokenName' fee is loaded") {
|
||||
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
runCatching {
|
||||
onSendFeeSelectorBottomSheet {
|
||||
networkFeeTitle.assertIsDisplayed()
|
||||
feeTokenItem(tokenName).assertIsDisplayed()
|
||||
if (expectApplyEnabled) applyButton.assertIsEnabled()
|
||||
}
|
||||
}.isSuccess
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
package com.tangem.scenarios
|
||||
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||
import com.tangem.common.extensions.SwipeDirection
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.common.extensions.swipeVertical
|
||||
import com.tangem.screens.onAddAndManageBottomSheet
|
||||
import com.tangem.screens.onMainScreen
|
||||
import com.tangem.screens.onOrganizeTokensScreen
|
||||
import io.qameta.allure.kotlin.Allure.step
|
||||
import org.junit.Assert.assertEquals
|
||||
|
||||
fun BaseTestCase.openOrganizeTokensScreen() {
|
||||
step("Swipe to 'Add & Manage' button") {
|
||||
|
|
@ -19,4 +22,30 @@ fun BaseTestCase.openOrganizeTokensScreen() {
|
|||
step("Click on 'Organize tokens' button in bottom sheet") {
|
||||
onAddAndManageBottomSheet { organizeTokensButton.clickWithAssertion() }
|
||||
}
|
||||
}
|
||||
|
||||
fun BaseTestCase.getMainScreenTokensOrder(): List<String> {
|
||||
var tokens: List<String> = emptyList()
|
||||
step("Read displayed token titles from 'Main' screen") {
|
||||
awaitSuccess(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onMainScreen { tokens = getDisplayedTokenTitles() }
|
||||
require(tokens.isNotEmpty()) { "No token titles found on the main screen" }
|
||||
}
|
||||
}
|
||||
return tokens
|
||||
}
|
||||
|
||||
fun BaseTestCase.assertOrganizeTokensMatch(expectedTokens: List<String>) {
|
||||
step("Open 'Organize tokens' bottom-sheet") {
|
||||
onMainScreen { clickDisplayedAddAndManageButton() }
|
||||
onAddAndManageBottomSheet { organizeTokensButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Organize tokens' list matches the main screen order") {
|
||||
onOrganizeTokensScreen {
|
||||
assertEquals(expectedTokens, getDisplayedTokenTitles())
|
||||
}
|
||||
}
|
||||
step("Return to 'Main' screen") {
|
||||
onOrganizeTokensScreen { cancelButton.clickWithAssertion() }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
package com.tangem.scenarios
|
||||
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.common.extensions.performTextInputInChunks
|
||||
import com.tangem.screens.accounts.onAccountDetailsScreen
|
||||
import com.tangem.screens.onAddCustomTokenScreen
|
||||
import com.tangem.screens.onDetailsScreen
|
||||
import com.tangem.screens.onDialog
|
||||
import com.tangem.screens.onManageTokensScreen
|
||||
import com.tangem.screens.onWalletSettingsScreen
|
||||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||
import io.qameta.allure.kotlin.Allure.step
|
||||
import com.tangem.core.res.R as CoreResR
|
||||
|
||||
private fun mainAccountName(): String = getResourceString(CoreResR.string.account_main_account_title)
|
||||
|
||||
fun BaseTestCase.openManageTokens(accountName: String = mainAccountName()) {
|
||||
openWalletSettingsScreen()
|
||||
openAccountDetails(accountName)
|
||||
step("Click on 'Manage tokens' button") {
|
||||
onAccountDetailsScreen { manageTokensButton.clickWithAssertion() }
|
||||
}
|
||||
}
|
||||
|
||||
fun BaseTestCase.openAddCustomToken(accountName: String = mainAccountName()) {
|
||||
openManageTokens(accountName)
|
||||
step("Click on 'Add custom token' button") {
|
||||
onManageTokensScreen { addCustomTokenButton.clickWithAssertion() }
|
||||
}
|
||||
}
|
||||
|
||||
fun BaseTestCase.addCustomTokenWithCustomDerivation(network: String, contract: String, derivationPath: String) {
|
||||
step("Click on network: '$network'") {
|
||||
awaitSuccess { onAddCustomTokenScreen { scrollToNetwork(network) } }
|
||||
onAddCustomTokenScreen { networkRow(network).performClick() }
|
||||
}
|
||||
step("Enter contract address: '$contract'") {
|
||||
awaitSuccess { onAddCustomTokenScreen { contractAddressField.assertExists() } }
|
||||
onAddCustomTokenScreen { contractAddressField.performTextInputInChunks(contract) }
|
||||
}
|
||||
step("Click on 'Derivation path' field") {
|
||||
awaitSuccess { onAddCustomTokenScreen { derivationSelectorField.assertExists() } }
|
||||
onAddCustomTokenScreen { derivationSelectorField.performClick() }
|
||||
}
|
||||
step("Click on 'Custom derivation' button") {
|
||||
awaitSuccess { onAddCustomTokenScreen { customDerivationButton.assertIsDisplayed() } }
|
||||
onAddCustomTokenScreen { customDerivationButton.performClick() }
|
||||
}
|
||||
step("Enter custom derivation path: '$derivationPath'") {
|
||||
awaitSuccess { onDialog { inputField.assertIsDisplayed() } }
|
||||
onDialog { inputField.performTextInputInChunks(derivationPath) }
|
||||
awaitSuccess { onDialog { okButton.assertIsEnabled() } }
|
||||
onDialog { okButton.performClick() }
|
||||
}
|
||||
step("Click on 'Add token' button") {
|
||||
awaitSuccess { onAddCustomTokenScreen { addTokenButton.assertIsEnabled() } }
|
||||
onAddCustomTokenScreen { addTokenButton.performClick() }
|
||||
}
|
||||
navigateBackToMainFromManageTokens()
|
||||
}
|
||||
|
||||
private fun BaseTestCase.navigateBackToWalletSettings() {
|
||||
step("Click on 'Manage tokens' screen 'Back' button") {
|
||||
waitForIdle()
|
||||
onManageTokensScreen { topAppBarBackButton.performClick() }
|
||||
}
|
||||
step("Click on 'Account details' screen 'Back' button") {
|
||||
waitForIdle()
|
||||
onAccountDetailsScreen { topAppBarBackButton.performClick() }
|
||||
}
|
||||
}
|
||||
|
||||
fun BaseTestCase.assertDerivationPathsInSelector(networkToOpen: String, vararg expectedPaths: Pair<String, String>) {
|
||||
step("Click on network: '$networkToOpen'") {
|
||||
awaitSuccess { onAddCustomTokenScreen { scrollToNetwork(networkToOpen) } }
|
||||
onAddCustomTokenScreen { networkRow(networkToOpen).performClick() }
|
||||
}
|
||||
step("Click on 'Derivation path' field") {
|
||||
awaitSuccess { onAddCustomTokenScreen { derivationSelectorField.assertExists() } }
|
||||
onAddCustomTokenScreen { derivationSelectorField.performClick() }
|
||||
}
|
||||
expectedPaths.forEach { (networkId, path) ->
|
||||
step("Assert '$networkId' derivation path is '$path'") {
|
||||
awaitSuccess { onAddCustomTokenScreen { scrollToDerivationRow(networkId) } }
|
||||
onAddCustomTokenScreen { derivationPath(networkId, path).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun BaseTestCase.toggleTokenNetworkInManageTokens(tokenTitle: String, networkTitle: String) {
|
||||
step("Click on token: '$tokenTitle'") {
|
||||
awaitSuccess { onManageTokensScreen { tokenItem(tokenTitle).assertIsDisplayed() } }
|
||||
onManageTokensScreen { tokenItem(tokenTitle).performClick() }
|
||||
}
|
||||
step("Click on '$networkTitle' switch") {
|
||||
awaitSuccess { onManageTokensScreen { networkSwitch(networkTitle).assertIsDisplayed() } }
|
||||
onManageTokensScreen { networkSwitch(networkTitle).performClick() }
|
||||
}
|
||||
}
|
||||
|
||||
fun BaseTestCase.navigateBackToMainFromManageTokens() {
|
||||
navigateBackToWalletSettings()
|
||||
step("Click on 'Wallet settings' screen 'Back' button") {
|
||||
waitForIdle()
|
||||
onWalletSettingsScreen { topAppBarBackButton.performClick() }
|
||||
}
|
||||
step("Click on 'Details' screen 'Back' button") {
|
||||
waitForIdle()
|
||||
onDetailsScreen { topAppBarBackButton.performClick() }
|
||||
}
|
||||
}
|
||||
|
|
@ -45,6 +45,21 @@ fun BaseTestCase.addNewCardWallet(mockContent: MockContent) {
|
|||
}
|
||||
}
|
||||
|
||||
fun BaseTestCase.addNewCardWalletWithoutSync(mockContent: MockContent) {
|
||||
step("Click 'More' button on TopBar") {
|
||||
onMainScreenTopBar { moreButton.clickWithAssertion() }
|
||||
}
|
||||
MockProvider.setMocks(mockContent)
|
||||
step("Click on 'Add Wallet' button (scans a new hardware wallet)") {
|
||||
onDetailsScreen { addWalletButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Main' screen is displayed with the new wallet") {
|
||||
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_VERY_LONG) {
|
||||
runCatching { onMainScreenTopBar { moreButton.assertIsDisplayed() } }.isSuccess
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun BaseTestCase.clickDisplayedTokenOnMain(tokenName: String) {
|
||||
step("Click on token '$tokenName' on the visible wallet") {
|
||||
onMainScreen { clickDisplayedToken(tokenName) }
|
||||
|
|
@ -86,13 +101,23 @@ fun BaseTestCase.addMissingReceiveTokenToWallet(token: String, recipientWalletNa
|
|||
runCatching { onSwapSelectTokenScreen { marketsTokenWithName(token).performClick() } }.isSuccess
|
||||
}
|
||||
}
|
||||
// The 'Add token' sheet pre-selects the recipient (the only wallet missing the token, since the source already holds it).
|
||||
step("Assert recipient wallet '$recipientWalletName' is selected") {
|
||||
step("Select recipient wallet '$recipientWalletName'") {
|
||||
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
runCatching { onAddToPortfolioScreen { walletName(recipientWalletName).assertIsDisplayed() } }.isSuccess
|
||||
runCatching { onAddToPortfolioScreen { selectedWalletRow.performClick() } }.isSuccess
|
||||
}
|
||||
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
runCatching { onAddToPortfolioScreen { walletOption(recipientWalletName).performClick() } }.isSuccess
|
||||
}
|
||||
}
|
||||
step("Click on 'Add' button") {
|
||||
onAddToPortfolioScreen { addButton.performClick() }
|
||||
step("Select network '$token'") {
|
||||
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
runCatching { onAddToPortfolioScreen { networkRow.performClick() } }.isSuccess
|
||||
}
|
||||
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
runCatching { onAddToPortfolioScreen { networkOption(token).performClick() } }.isSuccess
|
||||
}
|
||||
}
|
||||
step("Click on 'Confirm' button") {
|
||||
onAddToPortfolioScreen { confirmButton.performClick() }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,58 @@
|
|||
package com.tangem.scenarios
|
||||
|
||||
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.extractText
|
||||
import com.tangem.common.utils.decodeQrCode
|
||||
import com.tangem.screens.onAddFundsBottomSheet
|
||||
import com.tangem.screens.onReceiveAssetsBottomSheet
|
||||
import com.tangem.screens.onTokenDetailsScreen
|
||||
import com.tangem.screens.onTokenReceiveQrCodeBottomSheet
|
||||
import com.tangem.screens.onTokenReceiveWarningBottomSheet
|
||||
import io.qameta.allure.kotlin.Allure.step
|
||||
import org.junit.Assert
|
||||
|
||||
/** Opens the receive flow from a funded token's details via 'Add funds' → 'Receive'. */
|
||||
fun BaseTestCase.openReceiveViaAddFunds() {
|
||||
step("Click on 'Add funds' button") {
|
||||
onTokenDetailsScreen { addFundsButton.clickWithAssertion() }
|
||||
}
|
||||
step("Click on 'Receive' button in bottom sheet") {
|
||||
onAddFundsBottomSheet { receiveButton.clickWithAssertion() }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts the QR code encodes the displayed address for both address types of a two-address-type coin,
|
||||
* and that the two addresses differ.
|
||||
*/
|
||||
fun BaseTestCase.assertQrCodesMatchForBothAddressTypes() {
|
||||
step("Go to QR code bottom sheet for the first address type") {
|
||||
goToQrCodeBottomSheet()
|
||||
}
|
||||
var firstAddress = ""
|
||||
step("Assert QR code encodes the first displayed address") {
|
||||
firstAddress = assertQrCodeEncodesDisplayedAddress()
|
||||
}
|
||||
step("Go back to the receive addresses") {
|
||||
device.uiDevice.pressBack()
|
||||
}
|
||||
step("Switch to the second address type") {
|
||||
awaitSuccess(WAIT_UNTIL_TIMEOUT_LONG) { onReceiveAssetsBottomSheet { addressesPager.assertIsDisplayed() } }
|
||||
onReceiveAssetsBottomSheet { scrollToAddress(1) }
|
||||
}
|
||||
step("Click on 'Show QR code' button for the second address type") {
|
||||
onReceiveAssetsBottomSheet { showQrCodeButton(1).clickWithAssertion() }
|
||||
}
|
||||
var secondAddress = ""
|
||||
step("Assert QR code encodes the second displayed address") {
|
||||
secondAddress = assertQrCodeEncodesDisplayedAddress()
|
||||
}
|
||||
step("Assert the two address types are different") {
|
||||
Assert.assertNotEquals(firstAddress, secondAddress)
|
||||
}
|
||||
}
|
||||
|
||||
fun BaseTestCase.goToQrCodeBottomSheet() {
|
||||
step("Assert 'Token receive warning' bottom sheet is displayed") {
|
||||
|
|
@ -15,7 +62,7 @@ fun BaseTestCase.goToQrCodeBottomSheet() {
|
|||
onTokenReceiveWarningBottomSheet { gotItButton.performClick() }
|
||||
}
|
||||
step("Click on 'Show QR code' button") {
|
||||
onReceiveAssetsBottomSheet { showQrCodeButton.clickWithAssertion() }
|
||||
onReceiveAssetsBottomSheet { showQrCodeButton().clickWithAssertion() }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -39,3 +86,16 @@ fun BaseTestCase.checkQrCodeBottomSheetScenario() {
|
|||
onTokenReceiveQrCodeBottomSheet { shareButton.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
|
||||
/** Decodes the QR code on the receive bottom sheet and asserts it encodes the displayed address; returns that address. */
|
||||
fun BaseTestCase.assertQrCodeEncodesDisplayedAddress(): String {
|
||||
var displayedAddress = ""
|
||||
step("Assert QR code encodes the displayed address") {
|
||||
onTokenReceiveQrCodeBottomSheet {
|
||||
displayedAddress = address.extractText()
|
||||
val decoded = decodeQrCode(captureQrCodeBitmap())
|
||||
Assert.assertEquals(displayedAddress, decoded)
|
||||
}
|
||||
}
|
||||
return displayedAddress
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
}
|
||||
}
|
||||
|
|
@ -13,24 +13,37 @@ fun BaseTestCase.checkSendWarning(
|
|||
) {
|
||||
val assertDisplay = if (isDisplayed) "displayed" else "not displayed"
|
||||
|
||||
// When we expect the warning to be absent, it may still be finishing its disappear animation right after the
|
||||
// Confirm screen opens (e.g. after re-entering a valid address), so poll the Compose tree until it is actually
|
||||
// gone instead of asserting once. When we expect it to be present, assert straight away.
|
||||
fun assertWarning(block: () -> Unit) {
|
||||
if (isDisplayed) block() else awaitSuccess(block = block)
|
||||
}
|
||||
|
||||
step("Assert 'Send confirm screen' is displayed") {
|
||||
onSendConfirmScreen {
|
||||
appBarTitle.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
step("Assert warning title is $assertDisplay") {
|
||||
onSendConfirmScreen {
|
||||
warningTitle(title).assertVisibility(isDisplayed)
|
||||
assertWarning {
|
||||
onSendConfirmScreen {
|
||||
warningTitle(title).assertVisibility(isDisplayed)
|
||||
}
|
||||
}
|
||||
}
|
||||
step("Assert warning icon is $assertDisplay") {
|
||||
onSendConfirmScreen {
|
||||
sendWarningIcon(message).assertVisibility(isDisplayed)
|
||||
assertWarning {
|
||||
onSendConfirmScreen {
|
||||
sendWarningIcon(message).assertVisibility(isDisplayed)
|
||||
}
|
||||
}
|
||||
}
|
||||
step("Assert warning message is $assertDisplay") {
|
||||
onSendConfirmScreen {
|
||||
sendWarningMessage(message).assertVisibility(isDisplayed)
|
||||
assertWarning {
|
||||
onSendConfirmScreen {
|
||||
sendWarningMessage(message).assertVisibility(isDisplayed)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (sendButtonIsDisabled)
|
||||
|
|
@ -41,8 +54,10 @@ fun BaseTestCase.checkSendWarning(
|
|||
}
|
||||
else
|
||||
step("Assert 'Send' button is enabled") {
|
||||
onSendConfirmScreen {
|
||||
sendButton.assertIsEnabled()
|
||||
awaitSuccess {
|
||||
onSendConfirmScreen {
|
||||
sendButton.assertIsEnabled()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -164,6 +164,16 @@ fun BaseTestCase.checkStoriesChanges() {
|
|||
}
|
||||
}
|
||||
|
||||
fun BaseTestCase.skipSwapStories() {
|
||||
step("Skip 'Swap stories' screen if displayed") {
|
||||
onSwapStoriesScreen {
|
||||
if (closeButton.isDisplayedSafely()) {
|
||||
closeButton.performClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun BaseTestCase.selectFeeType(feeType: FeeType, selectedFeeAmount: String) {
|
||||
step("Click on 'Select fee' icon") {
|
||||
onSwapTokenScreen { selectFeeIcon.performClick() }
|
||||
|
|
@ -317,6 +327,9 @@ fun BaseTestCase.openSwapInTransferModeWithHotWallet(
|
|||
}
|
||||
|
||||
private fun BaseTestCase.navigateToSwapForToken(tokenName: String, fromAccountName: String) {
|
||||
step("Collapse header") {
|
||||
onMainScreen { collapseHeader() }
|
||||
}
|
||||
step("Scroll '$fromAccountName' into view (semantics, not touch — avoids the Markets sheet)") {
|
||||
onMainScreen { scrollToAccount(fromAccountName) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import com.tangem.screens.tangempay.*
|
|||
import io.qameta.allure.kotlin.Allure.step
|
||||
|
||||
fun BaseTestCase.openTangemPay() {
|
||||
// Existing customer: callers set the `tangem_pay_eligibility` scenario to PaeraCustomer (in
|
||||
// additionalBeforeSection, before this runs), which drives the checkCustomerWalletId mock -> Payment account.
|
||||
step("Import hot wallet from Tangem Pay seed phrase (with access code)") {
|
||||
openMainScreenWithExistingHotWallet(SVS_SEED_PHRASE_12, accessCode = TANGEM_PAY_ACCESS_CODE)
|
||||
}
|
||||
|
|
@ -23,6 +25,44 @@ fun BaseTestCase.openTangemPay() {
|
|||
}
|
||||
}
|
||||
|
||||
/** Opens Tangem Pay and taps the card to reach the card management page. */
|
||||
fun BaseTestCase.openTangemPayCardPage() {
|
||||
openTangemPay()
|
||||
step("Click on 'Card' button") {
|
||||
onTangemPayMainScreen { cardButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert card page 'More' button is displayed") {
|
||||
awaitSuccess { onTangemPayCardPageScreen { moreButton.assertIsDisplayed() } }
|
||||
}
|
||||
}
|
||||
|
||||
/** Opens the card page and taps 'Change' on the daily limit block to reach the limit setup screen. */
|
||||
fun BaseTestCase.openTangemPayDailyLimitSetup() {
|
||||
openTangemPayCardPage()
|
||||
step("Click on 'Change' daily limit button") {
|
||||
awaitSuccess { onTangemPayCardPageScreen { dailyLimitChangeButton.assertIsDisplayed() } }
|
||||
onTangemPayCardPageScreen { dailyLimitChangeButton.performClick() }
|
||||
}
|
||||
step("Assert daily limit setup screen is displayed") {
|
||||
awaitSuccess { onTangemPayDailyLimitScreen { amountField.assertIsDisplayed() } }
|
||||
onTangemPayDailyLimitScreen { setLimitsButton.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
|
||||
/** From the card page, opens the 'Replace card' reissue bottom sheet via the 'More' menu. */
|
||||
fun BaseTestCase.openReissueSheet() {
|
||||
step("Click on 'More' button") {
|
||||
onTangemPayCardPageScreen { moreButton.clickWithAssertion() }
|
||||
}
|
||||
step("Click on 'Replace card' menu item") {
|
||||
awaitSuccess { onTangemPayCardPageScreen { replaceCardMenuItem.assertIsDisplayed() } }
|
||||
onTangemPayCardPageScreen { replaceCardMenuItem.performClick() }
|
||||
}
|
||||
step("Assert reissue bottom sheet is displayed") {
|
||||
awaitSuccess { onTangemPayReissueSheet { confirmButton.assertIsDisplayed() } }
|
||||
}
|
||||
}
|
||||
|
||||
// Compose Test gesture — UiAutomator swipe doesn't reach Material3 PullToRefreshBox's NestedScrollConnection.
|
||||
fun BaseTestCase.pullToRefreshTangemPay() {
|
||||
val balance = composeTestRule.onNode(hasTestTag(TangemPayTestTags.PAYMENT_ACCOUNT_BALANCE))
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
package com.tangem.screens
|
||||
|
||||
import androidx.compose.ui.test.ExperimentalTestApi
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import androidx.compose.ui.test.performScrollToNode
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.test.AddCustomTokenScreenTestTags
|
||||
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
|
||||
import androidx.compose.ui.test.hasTestTag as withTestTag
|
||||
import androidx.compose.ui.test.hasText as withText
|
||||
import com.tangem.core.res.R as CoreResR
|
||||
|
||||
class AddCustomTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<AddCustomTokenPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
val selectorList: KNode = child {
|
||||
hasTestTag(AddCustomTokenScreenTestTags.SELECTOR_LIST)
|
||||
}
|
||||
|
||||
fun networkRow(networkName: String): KNode = child {
|
||||
hasTestTag(AddCustomTokenScreenTestTags.networkRow(networkName))
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
fun scrollToNetwork(networkName: String) = selectorList {
|
||||
performScrollToNode(withTestTag(AddCustomTokenScreenTestTags.networkRow(networkName)))
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
fun scrollToDerivationRow(networkId: String) = selectorList {
|
||||
performScrollToNode(withTestTag(AddCustomTokenScreenTestTags.derivationRow(networkId)))
|
||||
}
|
||||
|
||||
fun derivationPath(networkId: String, path: String): KNode = child {
|
||||
useUnmergedTree = true
|
||||
hasTestTag(AddCustomTokenScreenTestTags.derivationRow(networkId))
|
||||
hasAnyDescendant(withText(path))
|
||||
}
|
||||
|
||||
val contractAddressField: KNode = child {
|
||||
hasTestTag(AddCustomTokenScreenTestTags.CONTRACT_ADDRESS_FIELD)
|
||||
}
|
||||
|
||||
val derivationSelectorField: KNode = child {
|
||||
hasTestTag(AddCustomTokenScreenTestTags.DERIVATION_SELECTOR_FIELD)
|
||||
}
|
||||
|
||||
val customDerivationButton: KNode = child {
|
||||
hasTestTag(AddCustomTokenScreenTestTags.CUSTOM_DERIVATION_BUTTON)
|
||||
}
|
||||
|
||||
val warningNotification: KNode = child {
|
||||
hasTestTag(AddCustomTokenScreenTestTags.WARNING_NOTIFICATION)
|
||||
}
|
||||
|
||||
val addTokenButton: KNode = child {
|
||||
hasTestTag(BaseButtonTestTags.BUTTON)
|
||||
hasText(getResourceString(CoreResR.string.common_add_token))
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onAddCustomTokenScreen(function: AddCustomTokenPageObject.() -> Unit) =
|
||||
onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -51,6 +51,11 @@ class AddFundsBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractions
|
|||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
fun titleWithTokenName(tokenTitle: String): KNode = child {
|
||||
hasText(getResourceString(R.string.get_token_title, tokenTitle))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
fun userTokenWithTitle(tokenTitle: String): KNode = child {
|
||||
hasTestTag(BuyTokenScreenTestTags.LAZY_LIST_ITEM)
|
||||
hasAnyDescendant(withTestTag(TokenElementsTestTags.TOKEN_TITLE))
|
||||
|
|
|
|||
|
|
@ -1,24 +1,53 @@
|
|||
package com.tangem.screens
|
||||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import androidx.compose.ui.test.hasClickAction
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.test.BaseBottomSheetTestTags
|
||||
import com.tangem.wallet.R
|
||||
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
|
||||
|
||||
// Scoped to the bottom-sheet container: the swap token-search screen stays behind the sheet and
|
||||
// would otherwise make text matches (wallet tabs, market 'Ethereum' item) ambiguous.
|
||||
class AddToPortfolioPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<AddToPortfolioPageObject>(semanticsProvider = semanticsProvider) {
|
||||
ComposeScreen<AddToPortfolioPageObject>(
|
||||
semanticsProvider = semanticsProvider,
|
||||
viewBuilderAction = { hasTestTag(BaseBottomSheetTestTags.CONTAINER) },
|
||||
) {
|
||||
|
||||
fun walletName(walletName: String): KNode = child {
|
||||
hasText(walletName)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val addButton: KNode = child {
|
||||
hasText(getResourceString(R.string.common_add))
|
||||
/** The wallet row on the 'Add token' screen (label + selected value); tap to open the wallet selector. */
|
||||
val selectedWalletRow: KNode = child {
|
||||
hasText(getResourceString(R.string.wc_common_wallet))
|
||||
hasClickAction()
|
||||
}
|
||||
|
||||
/** A wallet item inside the portfolio selector screen. */
|
||||
fun walletOption(walletName: String): KNode = child {
|
||||
hasText(walletName)
|
||||
hasClickAction()
|
||||
}
|
||||
|
||||
/** The network row on the 'Add token' screen; tap to open the 'Choose network' selector. */
|
||||
val networkRow: KNode = child {
|
||||
hasText(getResourceString(R.string.wc_common_network))
|
||||
hasClickAction()
|
||||
}
|
||||
|
||||
/** A network item inside the 'Choose network' selector. */
|
||||
fun networkOption(networkName: String): KNode = child {
|
||||
hasText(networkName)
|
||||
hasClickAction()
|
||||
}
|
||||
|
||||
val confirmButton: KNode = child {
|
||||
hasText(getResourceString(R.string.common_confirm))
|
||||
hasClickAction()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,9 +26,9 @@ class AddTokenBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractions
|
|||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val addButton: KNode = child {
|
||||
val confirmButton: KNode = child {
|
||||
hasTestTag(BaseButtonTestTags.TEXT)
|
||||
hasText(getResourceString(R.string.common_add))
|
||||
hasText(getResourceString(R.string.common_confirm))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,10 @@ class AppSettingsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider
|
|||
hasTestTag(AppSettingsScreenTestTags.CURRENCY_BUTTON)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val backButton: KNode = child {
|
||||
hasTestTag(AppSettingsScreenTestTags.BACK_BUTTON)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onAppSettingsScreen(function: AppSettingsPageObject.() -> Unit) =
|
||||
|
|
|
|||
|
|
@ -10,10 +10,15 @@ import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onCompose
|
|||
import io.github.kakaocup.compose.node.element.KNode
|
||||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||
import androidx.compose.ui.test.hasText as withText
|
||||
import com.tangem.core.res.R as CoreResR
|
||||
|
||||
class DetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<DetailsPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
val screenContainer: KNode = child {
|
||||
hasTestTag(DetailsScreenTestTags.SCREEN_CONTAINER)
|
||||
}
|
||||
|
||||
val topAppBarBackButton: KNode = child {
|
||||
hasTestTag(TopAppBarTestTags.CLOSE_BUTTON)
|
||||
}
|
||||
|
|
@ -61,6 +66,12 @@ class DetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
|||
hasAnyDescendant(withText(name))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val getTangemPayRow: KNode = child {
|
||||
hasTestTag(DetailsScreenTestTags.SCREEN_ITEM)
|
||||
hasAnyDescendant(withText(getResourceString(CoreResR.string.tangempay_get_tangem_pay)))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onDetailsScreen(function: DetailsPageObject.() -> Unit) =
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.screens
|
|||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.res.R as CoreResR
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.test.BaseButtonTestTags
|
||||
import com.tangem.core.ui.test.BaseDialogTestTags
|
||||
|
|
@ -10,6 +11,7 @@ import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onCompose
|
|||
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 DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<DialogPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
|
@ -18,6 +20,12 @@ class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
|||
hasTestTag(BaseDialogTestTags.CONTAINER)
|
||||
}
|
||||
|
||||
fun containerWithText(text: String): KNode = child {
|
||||
hasTestTag(BaseDialogTestTags.CONTAINER)
|
||||
hasAnyDescendant(withText(text = text, substring = true))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val title: KNode = child {
|
||||
hasTestTag(BaseDialogTestTags.TITLE)
|
||||
}
|
||||
|
|
@ -87,6 +95,11 @@ class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
|||
hasText(getResourceString(R.string.common_ok))
|
||||
}
|
||||
|
||||
val forgetButton: KNode = child {
|
||||
hasTestTag(BaseButtonTestTags.BUTTON)
|
||||
hasText(getResourceString(CoreResR.string.common_forget))
|
||||
}
|
||||
|
||||
val changeButton: KNode = child {
|
||||
hasTestTag(BaseButtonTestTags.BUTTON)
|
||||
hasText(getResourceString(R.string.common_change))
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
package com.tangem.screens
|
||||
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.semantics.SemanticsProperties
|
||||
import androidx.compose.ui.test.*
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.extensions.displayedTextsInVisualOrder
|
||||
import com.tangem.common.extensions.firstTextOrNull
|
||||
import com.tangem.common.extensions.getQuantityString
|
||||
import com.tangem.common.extensions.hasLazyListItemPosition
|
||||
import com.tangem.common.utils.LazyListItemNode
|
||||
|
|
@ -14,6 +17,7 @@ import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onCompose
|
|||
import io.github.kakaocup.compose.node.element.KNode
|
||||
import io.github.kakaocup.compose.node.element.lazylist.KLazyListNode
|
||||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||
import kotlin.math.abs
|
||||
import androidx.compose.ui.test.hasTestTag as withTestTag
|
||||
import androidx.compose.ui.test.hasText as withText
|
||||
import com.tangem.core.res.R as CoreResR
|
||||
|
|
@ -96,7 +100,7 @@ class MainScreenPageObject(private val semanticsProvider: SemanticsNodeInteracti
|
|||
* Required because TangemCollapsingTopBar places the body at y=collapsingHeight, which
|
||||
* pushes lower list items off-screen when the header is expanded.
|
||||
*/
|
||||
private fun collapseHeader() {
|
||||
fun collapseHeader() {
|
||||
screenContainer {
|
||||
performTouchInput { swipeUp(startY = visibleSize.height * 0.6f, endY = visibleSize.height * 0.1f) }
|
||||
}
|
||||
|
|
@ -121,16 +125,75 @@ class MainScreenPageObject(private val semanticsProvider: SemanticsNodeInteracti
|
|||
error("Token '$tokenName' is not displayed on the current wallet page")
|
||||
}
|
||||
|
||||
// Adjacent pager pages stay mounted; swipe the wallet card that's actually on-screen.
|
||||
/**
|
||||
* Switches to the previous/next wallet in the pager. A horizontal swipe is a silent no-op unless
|
||||
* the collapsing balance header is fully expanded and pinned to the top, so we retry: swipe, and
|
||||
* whenever the wallet identity doesn't change, expand the header and try again.
|
||||
*/
|
||||
fun swipeToAdjacentWallet(toPrevious: Boolean) {
|
||||
val nodes = semanticsProvider.onAllNodes(withTestTag(MainScreenTestTags.WALLET_LIST_ITEM))
|
||||
for (i in 0 until nodes.fetchSemanticsNodes().size) {
|
||||
val swiped = runCatching {
|
||||
nodes[i].assertIsDisplayed()
|
||||
nodes[i].performTouchInput { if (toPrevious) swipeRight() else swipeLeft() }
|
||||
}.isSuccess
|
||||
if (swiped) return
|
||||
val before = displayedWalletIdentity()
|
||||
repeat(times = WALLET_SWITCH_ATTEMPTS) {
|
||||
swipeCurrentPage(toPrevious)
|
||||
val now = displayedWalletIdentity()
|
||||
if (now != null && now != before) return
|
||||
expandCollapsingHeader()
|
||||
}
|
||||
error("Wallet did not switch from '$before' after $WALLET_SWITCH_ATTEMPTS attempts")
|
||||
}
|
||||
|
||||
// Expand only *after* a swipe that didn't page: if the header is already pinned, a swipe-down here
|
||||
// would trigger pull-to-refresh and un-pin it, breaking the horizontal swipe.
|
||||
private fun expandCollapsingHeader() {
|
||||
onScreenPage()?.performTouchInput {
|
||||
swipeDown(startY = visibleSize.height * 0.3f, endY = visibleSize.height * 0.8f)
|
||||
}
|
||||
}
|
||||
|
||||
private fun swipeCurrentPage(toPrevious: Boolean) {
|
||||
onScreenPage()?.performTouchInput { if (toPrevious) swipeRight() else swipeLeft() }
|
||||
}
|
||||
|
||||
// Identity = title + balance: a still-restoring wallet has no CARD_TITLE but always a WALLET_BALANCE.
|
||||
private fun displayedWalletIdentity(): String? {
|
||||
val title = onScreenPageChild(withTestTag(MainScreenTestTags.CARD_TITLE))?.firstText()
|
||||
val balance = onScreenPageChild(withTestTag(MainScreenTestTags.WALLET_BALANCE))?.firstText()
|
||||
return listOfNotNull(title, balance).joinToString(separator = "|").ifBlank { null }
|
||||
}
|
||||
|
||||
/**
|
||||
* The full-width pager-page container currently on-screen. The pager keeps adjacent pages composed
|
||||
* off-screen at ±pageWidth (so [assertIsDisplayed] can't tell them apart), hence selection by
|
||||
* geometry: the on-screen page is the only one whose left edge is within half a page of x=0.
|
||||
*/
|
||||
private fun onScreenPage(): SemanticsNodeInteraction? =
|
||||
firstNodeMatching(withTestTag(MainScreenTestTags.SCREEN_CONTAINER), useUnmergedTree = false) {
|
||||
abs(it.left) < it.width / 2f
|
||||
}
|
||||
|
||||
/** A node matching [matcher] whose centre lies within the on-screen page (skips zero-size off-screen copies). */
|
||||
private fun onScreenPageChild(matcher: SemanticsMatcher): SemanticsNodeInteraction? {
|
||||
val page = onScreenPage()?.fetchSemanticsNode()?.boundsInRoot ?: return null
|
||||
return firstNodeMatching(matcher) {
|
||||
it.width > 0f && it.height > 0f && it.center.x >= page.left && it.center.x < page.right
|
||||
}
|
||||
}
|
||||
|
||||
private fun SemanticsNodeInteraction.firstText(): String? = fetchSemanticsNode().firstTextOrNull()
|
||||
|
||||
// Single geometry primitive behind the pager helpers: the first node matching [matcher] whose
|
||||
// bounds satisfy [predicate]. Replaces the per-caller onAllNodes(...)[i] loops.
|
||||
private fun firstNodeMatching(
|
||||
matcher: SemanticsMatcher,
|
||||
useUnmergedTree: Boolean = true,
|
||||
predicate: (Rect) -> Boolean,
|
||||
): SemanticsNodeInteraction? {
|
||||
val nodes = semanticsProvider.onAllNodes(matcher, useUnmergedTree = useUnmergedTree)
|
||||
repeat(times = nodes.fetchSemanticsNodes().size) { index ->
|
||||
val node = nodes[index]
|
||||
val matches = runCatching { predicate(node.fetchSemanticsNode().boundsInRoot) }.getOrDefault(false)
|
||||
if (matches) return node
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
val restoringProgressText: KNode = child {
|
||||
|
|
@ -139,12 +202,13 @@ class MainScreenPageObject(private val semanticsProvider: SemanticsNodeInteracti
|
|||
}
|
||||
|
||||
val walletImportedBanner: KNode = child {
|
||||
hasTestTag(WalletNotificationTestTags.ASSETS_DISCOVERY_BANNER)
|
||||
hasTestTag(NotificationTestTags.TITLE)
|
||||
hasText(getResourceString(CoreResR.string.initial_wallet_sync_banner_title))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val walletImportedBannerCheckHereButton: KNode = child {
|
||||
hasAnyAncestor(withTestTag(WalletNotificationTestTags.ASSETS_DISCOVERY_BANNER))
|
||||
hasAnyAncestor(withTestTag(NotificationTestTags.CONTAINER))
|
||||
hasText(getResourceString(CoreResR.string.main_manage_tokens))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
|
@ -201,6 +265,12 @@ class MainScreenPageObject(private val semanticsProvider: SemanticsNodeInteracti
|
|||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val getTangemPayBanner: KNode = child {
|
||||
hasTestTag(NotificationTestTags.TITLE)
|
||||
hasText(getResourceString(CoreResR.string.tangempay_onboarding_banner_title))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val devCardNotificationIcon: KNode = child {
|
||||
hasAnySibling(withText(getResourceString(R.string.warning_developer_card_title)))
|
||||
hasTestTag(NotificationTestTags.ICON)
|
||||
|
|
@ -373,6 +443,23 @@ class MainScreenPageObject(private val semanticsProvider: SemanticsNodeInteracti
|
|||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
/** Collapses the header, scrolls to and clicks the 'Add & manage' button on the on-screen wallet page. */
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
fun clickDisplayedAddAndManageButton() {
|
||||
val container = onScreenPage() ?: error("No on-screen wallet page found")
|
||||
// Best-effort: the button is a footer outside the scrollable list, so performScrollToNode can
|
||||
// throw when it's already visible — that must not abort the click below.
|
||||
runCatching {
|
||||
container.performTouchInput {
|
||||
swipeUp(startY = visibleSize.height * 0.6f, endY = visibleSize.height * 0.1f)
|
||||
}
|
||||
container.performScrollToNode(withTestTag(MainScreenTestTags.ADD_AND_MANAGE_BUTTON))
|
||||
}
|
||||
val button = onScreenPageChild(withTestTag(MainScreenTestTags.ADD_AND_MANAGE_BUTTON))
|
||||
?: error("'Add & manage' button is not displayed on the current wallet page")
|
||||
button.performClick()
|
||||
}
|
||||
|
||||
val searchThroughMarketPlaceholder: KNode = child {
|
||||
hasText(getResourceString(R.string.markets_search_title_placeholder))
|
||||
useUnmergedTree = true
|
||||
|
|
@ -478,6 +565,21 @@ class MainScreenPageObject(private val semanticsProvider: SemanticsNodeInteracti
|
|||
useUnmergedTree = true
|
||||
}.assertIsDisplayed()
|
||||
}
|
||||
|
||||
/**
|
||||
* Token titles displayed on the current wallet page, in visual order. Reads the
|
||||
* [TokenElementsTestTags.TOKEN_TITLE] rows so network group headers (which share the
|
||||
* TOKEN_LIST_ITEM tag) are excluded, keeping the result symmetric with the 'Organize tokens' reader.
|
||||
*/
|
||||
fun getDisplayedTokenTitles(): List<String> =
|
||||
semanticsProvider.onAllNodes(
|
||||
withTestTag(TokenElementsTestTags.TOKEN_TITLE),
|
||||
useUnmergedTree = true,
|
||||
).displayedTextsInVisualOrder()
|
||||
|
||||
private companion object {
|
||||
const val WALLET_SWITCH_ATTEMPTS = 4
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onMainScreen(function: MainScreenPageObject.() -> Unit) =
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import com.tangem.common.R
|
|||
import com.tangem.core.ui.test.BaseButtonTestTags
|
||||
import com.tangem.core.ui.test.BaseSearchBarTestTags
|
||||
import com.tangem.core.ui.test.ManageTokensScreenTestTags
|
||||
import com.tangem.core.ui.test.SearchBarTestTags
|
||||
import com.tangem.core.ui.test.SwitchTestTags
|
||||
import com.tangem.core.ui.test.TopAppBarTestTags
|
||||
import io.github.kakaocup.compose.node.element.ComposeScreen
|
||||
|
|
@ -17,6 +18,7 @@ import androidx.compose.ui.test.hasText as withText
|
|||
import androidx.compose.ui.test.hasAnySibling as withAnySibling
|
||||
import androidx.compose.ui.test.hasAnyDescendant as withAnyDescendant
|
||||
import androidx.compose.ui.test.hasAnyAncestor as withAnyAncestor
|
||||
import com.tangem.core.res.R as CoreResR
|
||||
|
||||
class ManageTokensPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<ManageTokensPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
|
@ -35,11 +37,43 @@ class ManageTokensPageObject(semanticsProvider: SemanticsNodeInteractionsProvide
|
|||
hasTestTag(BaseSearchBarTestTags.SEARCH_BAR)
|
||||
}
|
||||
|
||||
val searchClearButton: KNode = child {
|
||||
hasTestTag(SearchBarTestTags.CLEAR_BUTTON)
|
||||
}
|
||||
|
||||
val addCustomTokenButton: KNode = child {
|
||||
hasTestTag(TopAppBarTestTags.MORE_BUTTON)
|
||||
}
|
||||
|
||||
fun tokenItem(tokenName: String): KNode = child {
|
||||
hasTestTag(ManageTokensScreenTestTags.TOKEN_ITEM)
|
||||
hasText(tokenName)
|
||||
}
|
||||
|
||||
fun networkStandard(networkName: String, standard: String): KNode = child {
|
||||
useUnmergedTree = true
|
||||
addSemanticsMatcher(
|
||||
withText(standard).and(
|
||||
withAnyAncestor(
|
||||
withTestTag(ManageTokensScreenTestTags.NETWORK_NAME)
|
||||
.and(withAnyDescendant(withText(networkName, ignoreCase = true))),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun networkName(networkName: String): KNode = child {
|
||||
useUnmergedTree = true
|
||||
addSemanticsMatcher(
|
||||
withTestTag(ManageTokensScreenTestTags.NETWORK_NAME)
|
||||
.and(withAnyDescendant(withText(networkName))),
|
||||
)
|
||||
}
|
||||
|
||||
val contractAddressCopiedMessage: KNode = child {
|
||||
hasText(getResourceString(CoreResR.string.contract_address_copied_message))
|
||||
}
|
||||
|
||||
fun networkSwitch(networkName: String): KNode = child {
|
||||
useUnmergedTree = true
|
||||
addSemanticsMatcher(
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
package com.tangem.screens
|
||||
|
||||
import androidx.compose.ui.geometry.lerp
|
||||
import androidx.compose.ui.test.SemanticsMatcher
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import androidx.compose.ui.test.hasAnyDescendant
|
||||
import androidx.compose.ui.test.performTouchInput
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.extensions.displayedTextsInVisualOrder
|
||||
import com.tangem.common.extensions.hasLazyListItemPosition
|
||||
import com.tangem.common.utils.LazyListItemNode
|
||||
import com.tangem.core.ui.test.OrganizeTokensScreenTestTags
|
||||
|
|
@ -14,12 +18,13 @@ import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onCompose
|
|||
import io.github.kakaocup.compose.node.element.KNode
|
||||
import io.github.kakaocup.compose.node.element.lazylist.KLazyListNode
|
||||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||
import androidx.compose.ui.test.hasAnyAncestor as withAnyAncestor
|
||||
import androidx.compose.ui.test.hasAnyChild as withAnyChild
|
||||
import androidx.compose.ui.test.hasAnySibling as withAnySibling
|
||||
import androidx.compose.ui.test.hasTestTag as withTestTag
|
||||
import androidx.compose.ui.test.hasText as withText
|
||||
|
||||
class OrganizeTokensPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
class OrganizeTokensPageObject(private val semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<OrganizeTokensPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
// region TopBar
|
||||
|
|
@ -93,6 +98,15 @@ class OrganizeTokensPageObject(semanticsProvider: SemanticsNodeInteractionsProvi
|
|||
}
|
||||
}
|
||||
|
||||
// Read TOKEN_TITLE nodes, not the outer TOKEN_LIST_ITEM: the item container here doesn't merge its
|
||||
// descendants, so the nested title Text never surfaces on the item node's semantics.
|
||||
fun getDisplayedTokenTitles(): List<String> =
|
||||
semanticsProvider.onAllNodes(
|
||||
withTestTag(TokenElementsTestTags.TOKEN_TITLE) and
|
||||
withAnyAncestor(withTestTag(OrganizeTokensScreenTestTags.TOKENS_LAZY_LIST)),
|
||||
useUnmergedTree = true,
|
||||
).displayedTextsInVisualOrder()
|
||||
|
||||
fun tokenDraggableButton(tokenTitle: String): KNode {
|
||||
return lazyList.child {
|
||||
hasTestTag(OrganizeTokensScreenTestTags.DRAGGABLE_IMAGE)
|
||||
|
|
@ -108,6 +122,47 @@ class OrganizeTokensPageObject(semanticsProvider: SemanticsNodeInteractionsProvi
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Reorder by synthesizing a drag on the source row's handle: down, hold, step to the destination
|
||||
// centre, lift — a single `swipe` won't engage the reorder detector. Only valid within one network
|
||||
// group (isValidDropTarget), so source and destination must share a group.
|
||||
fun dragToken(source: String, destination: String) {
|
||||
val sourceHandle = semanticsProvider.onNode(dragHandleMatcher(source), useUnmergedTree = true)
|
||||
val sourceNode = sourceHandle.fetchSemanticsNode()
|
||||
val destinationNode = semanticsProvider
|
||||
.onNode(tokenItemMatcher(destination), useUnmergedTree = true)
|
||||
.fetchSemanticsNode()
|
||||
|
||||
// performTouchInput coordinates are local to the source node; express both endpoints there.
|
||||
val origin = sourceNode.positionInRoot
|
||||
val start = sourceNode.boundsInRoot.center - origin
|
||||
val end = destinationNode.boundsInRoot.center - origin
|
||||
|
||||
sourceHandle.performTouchInput {
|
||||
down(start)
|
||||
advanceEventTime(DRAG_HOLD_MS)
|
||||
repeat(times = DRAG_STEPS) { step ->
|
||||
moveTo(lerp(start = start, stop = end, fraction = (step + 1).toFloat() / DRAG_STEPS))
|
||||
advanceEventTime(DRAG_STEP_MS)
|
||||
}
|
||||
up()
|
||||
}
|
||||
}
|
||||
|
||||
// The drag handle sits under an untagged wrapper inside TOKEN_NON_FIAT_BLOCK, so anchor on the
|
||||
// enclosing TOKEN_LIST_ITEM (one handle + one title per item) instead of the direct parent.
|
||||
private fun dragHandleMatcher(tokenTitle: String): SemanticsMatcher =
|
||||
withTestTag(OrganizeTokensScreenTestTags.DRAGGABLE_IMAGE) and
|
||||
withAnyAncestor(tokenItemMatcher(tokenTitle))
|
||||
|
||||
private fun tokenItemMatcher(tokenTitle: String): SemanticsMatcher =
|
||||
withTestTag(OrganizeTokensScreenTestTags.TOKEN_LIST_ITEM) and hasAnyDescendant(withText(tokenTitle))
|
||||
|
||||
private companion object {
|
||||
const val DRAG_HOLD_MS = 200L
|
||||
const val DRAG_STEPS = 16
|
||||
const val DRAG_STEP_MS = 16L
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onOrganizeTokensScreen(function: OrganizeTokensPageObject.() -> Unit) =
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
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.R
|
||||
import com.tangem.core.ui.test.TokenReceiveAssetsBottomSheetTestTags
|
||||
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
|
||||
|
|
@ -11,10 +13,23 @@ import io.github.kakaocup.kakao.common.utilities.getResourceString
|
|||
class ReceiveAssetsBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<ReceiveAssetsBottomSheetPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
val showQrCodeButton: KNode = child {
|
||||
/** 'Show QR code' button of the address at [index]; both pager cards stay composed, so match by position. */
|
||||
fun showQrCodeButton(index: Int = 0): KNode = child {
|
||||
hasText(getResourceString(R.string.token_receive_show_qr_code_title))
|
||||
hasPosition(index)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val addressesPager: KNode = child {
|
||||
hasTestTag(TokenReceiveAssetsBottomSheetTestTags.ADDRESSES_PAGER)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
/** Pages the addresses carousel to the address of the given [index]. */
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
fun scrollToAddress(index: Int) {
|
||||
addressesPager { performScrollToIndex(index) }
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onReceiveAssetsBottomSheet(function: ReceiveAssetsBottomSheetPageObject.() -> Unit) =
|
||||
|
|
|
|||
|
|
@ -78,6 +78,11 @@ class ReferralProgramPageObject(semanticsProvider: SemanticsNodeInteractionsProv
|
|||
hasTestTag(BaseButtonTestTags.TEXT)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val personalCodeCard: KNode = child {
|
||||
hasTestTag(ReferralProgramScreenTestTags.PERSONAL_CODE_CARD)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onReferralProgramScreen(function: ReferralProgramPageObject.() -> Unit) =
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import com.tangem.core.ui.test.BaseButtonTestTags
|
|||
import com.tangem.core.ui.test.NotificationTestTags
|
||||
import com.tangem.core.ui.test.TokenDetailsScreenTestTags
|
||||
import com.tangem.features.tokendetails.impl.R
|
||||
import com.tangem.core.res.R as CoreResR
|
||||
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
|
||||
|
|
@ -62,6 +63,33 @@ class TokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvide
|
|||
hasText(getResourceString(R.string.staking_enabled))
|
||||
}
|
||||
|
||||
val yieldSupplyAvailableBlock: KNode = child {
|
||||
hasTestTag(TokenDetailsScreenTestTags.YIELD_SUPPLY_AVAILABLE_BLOCK)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val yieldSupplyActiveBlock: KNode = child {
|
||||
hasTestTag(TokenDetailsScreenTestTags.YIELD_SUPPLY_BLOCK)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val earnBlockTitleIcon: KNode = child {
|
||||
hasTestTag(TokenDetailsScreenTestTags.EARN_BLOCK_TITLE_ICON)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val yieldModeConnectedTitle: KNode = child {
|
||||
hasAnyAncestor(withTestTag(TokenDetailsScreenTestTags.YIELD_SUPPLY_BLOCK))
|
||||
hasText(getResourceString(CoreResR.string.yield_module_transaction_enter))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
fun yieldModeApy(apy: String): KNode = child {
|
||||
hasAnyAncestor(withTestTag(TokenDetailsScreenTestTags.YIELD_SUPPLY_BLOCK))
|
||||
hasText(getResourceString(CoreResR.string.yield_module_average_apy, apy))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val title: KNode = child {
|
||||
hasTestTag(TokenDetailsScreenTestTags.TOKEN_TITLE)
|
||||
}
|
||||
|
|
@ -90,6 +118,11 @@ class TokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvide
|
|||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
/** 'Receive' row of the zero-balance actions block (Buy / Swap / Receive), shown instead of the action buttons. */
|
||||
val receiveButton: KNode = child {
|
||||
hasText(getResourceString(R.string.common_receive))
|
||||
}
|
||||
|
||||
fun networkFeeNotificationIcon(feeCurrencyName: String): KNode = child {
|
||||
hasAnySibling(withText(getResourceString(R.string.warning_send_blocked_funds_for_fee_title, feeCurrencyName)))
|
||||
hasTestTag(NotificationTestTags.ICON)
|
||||
|
|
@ -114,6 +147,12 @@ class TokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvide
|
|||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
fun tokenTitle(name: String): KNode = child {
|
||||
hasTestTag(TokenDetailsScreenTestTags.TOKEN_TITLE)
|
||||
hasAnyDescendant(withText(text = name, substring = true))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
fun networkFeeNotificationMessage(
|
||||
currencyName: String,
|
||||
networkName: String,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
package com.tangem.screens
|
||||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.test.TokenMarketBlockTestTags
|
||||
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
|
||||
|
||||
class TokenMarketBlockPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<TokenMarketBlockPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
val block: KNode = child {
|
||||
hasTestTag(TokenMarketBlockTestTags.BLOCK)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val title: KNode = child {
|
||||
hasTestTag(TokenMarketBlockTestTags.TITLE)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val price: KNode = child {
|
||||
hasTestTag(TokenMarketBlockTestTags.PRICE)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val priceChange: KNode = child {
|
||||
hasTestTag(TokenMarketBlockTestTags.PRICE_CHANGE)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val chart: KNode = child {
|
||||
hasTestTag(TokenMarketBlockTestTags.CHART)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onTokenMarketBlock(function: TokenMarketBlockPageObject.() -> Unit) =
|
||||
onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -1,11 +1,14 @@
|
|||
package com.tangem.screens
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import androidx.compose.ui.graphics.asAndroidBitmap
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import androidx.compose.ui.test.captureToImage
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.test.BaseBottomSheetTestTags
|
||||
import com.tangem.core.ui.test.BaseButtonTestTags
|
||||
import com.tangem.core.ui.test.TokenReceiveQrCodeBottomSheetTestTags
|
||||
import com.tangem.wallet.R
|
||||
import io.github.kakaocup.compose.intercept.operation.ComposeOperationType
|
||||
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
|
||||
|
|
@ -40,16 +43,25 @@ class TokenReceiveQrCodeBottomSheetPageObject(semanticsProvider: SemanticsNodeIn
|
|||
}
|
||||
|
||||
val copyButton: KNode = child {
|
||||
hasTestTag(BaseButtonTestTags.TEXT)
|
||||
hasText(getResourceString(R.string.common_copy))
|
||||
useUnmergedTree = true
|
||||
hasClickAction()
|
||||
}
|
||||
|
||||
val shareButton: KNode = child {
|
||||
hasTestTag(BaseButtonTestTags.TEXT)
|
||||
hasText(getResourceString(R.string.common_share))
|
||||
useUnmergedTree = true
|
||||
hasClickAction()
|
||||
}
|
||||
|
||||
/** Captures the QR code node as a bitmap via the Kakao node delegate (no raw composeTestRule access). */
|
||||
fun captureQrCodeBitmap(): Bitmap {
|
||||
lateinit var bitmap: Bitmap
|
||||
qrCode.delegate.perform(QrCodeAction.CAPTURE) {
|
||||
bitmap = captureToImage().asAndroidBitmap()
|
||||
}
|
||||
return bitmap
|
||||
}
|
||||
|
||||
private enum class QrCodeAction : ComposeOperationType { CAPTURE }
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onTokenReceiveQrCodeBottomSheet(function: TokenReceiveQrCodeBottomSheetPageObject.() -> Unit) =
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package com.tangem.screens
|
|||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.test.BaseButtonTestTags
|
||||
import com.tangem.core.ui.test.TokenReceiveWarningBottomSheetTestTags
|
||||
import io.github.kakaocup.compose.node.element.ComposeScreen
|
||||
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
|
||||
|
|
@ -18,8 +17,11 @@ class TokenReceiveWarningBottomSheetPageObject(semanticsProvider: SemanticsNodeI
|
|||
}
|
||||
|
||||
val gotItButton: KNode = child {
|
||||
hasTestTag(BaseButtonTestTags.TEXT)
|
||||
hasText(getResourceString(R.string.common_got_it))
|
||||
}
|
||||
|
||||
fun networkName(name: String): KNode = child {
|
||||
hasText(text = name, substring = true)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,16 @@ class TxHistoryPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
|
|||
hasTestTag(TransactionHistoryItemTestTags.STATUS_CONFIRMED)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
fun transactionUnconfirmedStatus(title: String): KNode = transactionItem(title).child {
|
||||
hasTestTag(TransactionHistoryItemTestTags.STATUS_UNCONFIRMED)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
fun transactionAddress(title: String, address: String): KNode = transactionItem(title).child {
|
||||
hasText(text = address, substring = true)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onTxHistoryScreen(function: TxHistoryPageObject.() -> Unit) =
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
package com.tangem.screens
|
||||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.test.BaseButtonTestTags
|
||||
import com.tangem.core.ui.test.NotificationTestTags
|
||||
import com.tangem.core.ui.test.YieldSupplyTestTags
|
||||
import com.tangem.core.res.R as CoreResR
|
||||
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 YieldSupplyActivePageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<YieldSupplyActivePageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
val stopEarningButton: KNode = child {
|
||||
hasTestTag(YieldSupplyTestTags.STOP_EARNING_BUTTON)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val approveButton: KNode = child {
|
||||
hasTestTag(BaseButtonTestTags.TEXT)
|
||||
hasText(getResourceString(CoreResR.string.yield_module_approve_needed_notification_cta))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
fun notificationTitle(title: String): KNode = child {
|
||||
hasTestTag(NotificationTestTags.TITLE)
|
||||
hasText(title)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onYieldSupplyActiveScreen(function: YieldSupplyActivePageObject.() -> Unit) =
|
||||
onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.screens
|
||||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.test.YieldSupplyTestTags
|
||||
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
|
||||
|
||||
class YieldSupplyApprovePageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<YieldSupplyApprovePageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
val confirmButton: KNode = child {
|
||||
hasTestTag(YieldSupplyTestTags.APPROVE_CONFIRM_BUTTON)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onYieldSupplyApproveScreen(function: YieldSupplyApprovePageObject.() -> Unit) =
|
||||
onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.screens
|
||||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.test.YieldSupplyTestTags
|
||||
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
|
||||
|
||||
class YieldSupplyPromoPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<YieldSupplyPromoPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
val continueButton: KNode = child {
|
||||
hasTestTag(YieldSupplyTestTags.PROMO_CONTINUE_BUTTON)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onYieldSupplyPromoScreen(function: YieldSupplyPromoPageObject.() -> Unit) =
|
||||
onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.screens
|
||||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.test.YieldSupplyTestTags
|
||||
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
|
||||
|
||||
class YieldSupplyStartEarningPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<YieldSupplyStartEarningPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
val startEarningButton: KNode = child {
|
||||
hasTestTag(YieldSupplyTestTags.START_EARNING_BUTTON)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onYieldSupplyStartEarningScreen(function: YieldSupplyStartEarningPageObject.() -> Unit) =
|
||||
onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.screens
|
||||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.test.YieldSupplyTestTags
|
||||
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
|
||||
|
||||
class YieldSupplyStopEarningPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<YieldSupplyStopEarningPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
val confirmButton: KNode = child {
|
||||
hasTestTag(YieldSupplyTestTags.STOP_EARNING_CONFIRM_BUTTON)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onYieldSupplyStopEarningScreen(function: YieldSupplyStopEarningPageObject.() -> Unit) =
|
||||
onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -17,7 +17,7 @@ class TangemPayAddFundsSheetPageObject(semanticsProvider: SemanticsNodeInteracti
|
|||
}
|
||||
|
||||
val receiveOption: KNode = child {
|
||||
hasText(getResourceString(CoreResR.string.common_receive))
|
||||
hasText(getResourceString(CoreResR.string.tangempay_topup_receive_title))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,14 +2,39 @@ package com.tangem.screens.tangempay
|
|||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.res.R as CoreResR
|
||||
import com.tangem.core.ui.test.TangemPayTestTags
|
||||
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 TangemPayCardPagePageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<TangemPayCardPagePageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
val moreButton: KNode = child {
|
||||
hasTestTag(TangemPayTestTags.CARD_PAGE_MORE_BUTTON)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val replaceCardMenuItem: KNode = child {
|
||||
hasText(getResourceString(CoreResR.string.tangempay_card_details_reissue_card))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val cardNumberShort: KNode = child {
|
||||
hasTestTag(TangemPayTestTags.CARD_NUMBER_SHORT)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val reissueInProgressBlock: KNode = child {
|
||||
hasText(
|
||||
text = getResourceString(CoreResR.string.tangempay_reissue_card_in_progress),
|
||||
substring = true,
|
||||
)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val changePinRow: KNode = child {
|
||||
hasTestTag(TangemPayTestTags.CHANGE_PIN_ROW)
|
||||
useUnmergedTree = true
|
||||
|
|
@ -64,6 +89,16 @@ class TangemPayCardPagePageObject(semanticsProvider: SemanticsNodeInteractionsPr
|
|||
hasTestTag(TangemPayTestTags.CARD_DETAILS_COPY_CVC)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val dailyLimitChangeButton: KNode = child {
|
||||
hasTestTag(TangemPayTestTags.DAILY_LIMIT_CHANGE_BUTTON)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val dailyLimitValue: KNode = child {
|
||||
hasTestTag(TangemPayTestTags.DAILY_LIMIT_CURRENT_VALUE)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onTangemPayCardPageScreen(function: TangemPayCardPagePageObject.() -> Unit) =
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
package com.tangem.screens.tangempay
|
||||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.test.SendScreenTestTags
|
||||
import com.tangem.core.ui.test.TangemPayTestTags
|
||||
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
|
||||
|
||||
class TangemPayDailyLimitPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<TangemPayDailyLimitPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
// AmountTextField tags its editable field with SendScreenTestTags.INPUT_TEXT_FIELD; it's the only one here.
|
||||
val amountField: KNode = child {
|
||||
hasTestTag(SendScreenTestTags.INPUT_TEXT_FIELD)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val hint: KNode = child {
|
||||
hasTestTag(TangemPayTestTags.DAILY_LIMIT_HINT)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val setLimitsButton: KNode = child {
|
||||
hasTestTag(TangemPayTestTags.DAILY_LIMIT_SET_BUTTON)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val successTitle: KNode = child {
|
||||
hasTestTag(TangemPayTestTags.DAILY_LIMIT_SUCCESS_TITLE)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val doneButton: KNode = child {
|
||||
hasTestTag(TangemPayTestTags.DAILY_LIMIT_DONE_BUTTON)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
fun presetChip(rawValue: String): KNode = child {
|
||||
hasTestTag(TangemPayTestTags.dailyLimitPresetChip(rawValue))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onTangemPayDailyLimitScreen(function: TangemPayDailyLimitPageObject.() -> Unit) =
|
||||
onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.screens.tangempay
|
||||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.test.WarningBottomSheetTestTags
|
||||
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 androidx.compose.ui.test.hasText as withText
|
||||
|
||||
class TangemPayKycSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<TangemPayKycSheetPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
fun title(text: String): KNode = child {
|
||||
hasTestTag(WarningBottomSheetTestTags.TITLE)
|
||||
hasText(text)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
fun primaryButtonWithText(text: String): KNode = child {
|
||||
hasTestTag(WarningBottomSheetTestTags.BUTTON_PRIMARY)
|
||||
hasAnyDescendant(withText(text))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val closeButton: KNode = child {
|
||||
hasTestTag(WarningBottomSheetTestTags.CLOSE_BUTTON)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onTangemPayKycSheet(function: TangemPayKycSheetPageObject.() -> Unit) =
|
||||
onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -4,6 +4,7 @@ import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
|||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.test.BaseActionButtonsBlockTestTags
|
||||
import com.tangem.core.ui.test.TangemPayTestTags
|
||||
import com.tangem.core.ui.test.TokenDetailsTopBarTestTags
|
||||
import com.tangem.core.res.R as CoreResR
|
||||
import io.github.kakaocup.compose.node.element.ComposeScreen
|
||||
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
|
||||
|
|
@ -19,6 +20,12 @@ class TangemPayMainPageObject(semanticsProvider: SemanticsNodeInteractionsProvid
|
|||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
fun tileWithSubtitle(subtitle: String): KNode = child {
|
||||
hasTestTag(TangemPayTestTags.MAIN_SCREEN_TILE)
|
||||
hasAnyDescendant(withText(subtitle))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val balance: KNode = child {
|
||||
hasTestTag(TangemPayTestTags.PAYMENT_ACCOUNT_BALANCE)
|
||||
useUnmergedTree = true
|
||||
|
|
@ -41,10 +48,27 @@ class TangemPayMainPageObject(semanticsProvider: SemanticsNodeInteractionsProvid
|
|||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val moreActionsButton: KNode = child {
|
||||
hasTestTag(TokenDetailsTopBarTestTags.MORE_BUTTON)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val termsAndFeesMenuItem: KNode = child {
|
||||
hasText(getResourceString(CoreResR.string.tangem_pay_terms_limits))
|
||||
}
|
||||
|
||||
fun transactionRowWithText(text: String): KNode = child {
|
||||
hasText(text)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val reissueInProgressBanner: KNode = child {
|
||||
hasText(
|
||||
text = getResourceString(CoreResR.string.tangempay_reissue_card_in_progress),
|
||||
substring = true,
|
||||
)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onTangemPayMainScreen(function: TangemPayMainPageObject.() -> Unit) =
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.screens.tangempay
|
||||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.res.R as CoreResR
|
||||
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 TangemPayOnboardingPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<TangemPayOnboardingPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
val title: KNode = child {
|
||||
hasText(getResourceString(CoreResR.string.tangempay_onboarding_title))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onTangemPayOnboardingScreen(function: TangemPayOnboardingPageObject.() -> Unit) =
|
||||
onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package com.tangem.screens.tangempay
|
||||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.res.R as CoreResR
|
||||
import com.tangem.core.ui.test.TangemPayTestTags
|
||||
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 TangemPayReissueSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<TangemPayReissueSheetPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
val title: KNode = child {
|
||||
hasText(getResourceString(CoreResR.string.tangempay_reissue_card_title))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val description: KNode = child {
|
||||
hasText(getResourceString(CoreResR.string.tangempay_reissue_card_description))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val feeLabel: KNode = child {
|
||||
hasText(getResourceString(CoreResR.string.tangempay_reissue_card_fee_label))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val feeValue: KNode = child {
|
||||
hasTestTag(TangemPayTestTags.REISSUE_SHEET_FEE_VALUE)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val confirmButton: KNode = child {
|
||||
hasTestTag(TangemPayTestTags.REISSUE_SHEET_CONFIRM_BUTTON)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val feeErrorTitle: KNode = child {
|
||||
hasText(getResourceString(CoreResR.string.tangempay_reissue_card_fee_unreachable_error_title))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val refreshButton: KNode = child {
|
||||
hasText(getResourceString(CoreResR.string.warning_button_refresh))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val insufficientFundsTitle: KNode = child {
|
||||
hasText(getResourceString(CoreResR.string.tangempay_reissue_card_insufficient_funds_title))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val addFundsButton: KNode = child {
|
||||
hasText(getResourceString(CoreResR.string.tangempay_card_details_add_funds))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onTangemPayReissueSheet(function: TangemPayReissueSheetPageObject.() -> Unit) =
|
||||
onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.tangem.screens.tangempay
|
||||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.test.WarningBottomSheetTestTags
|
||||
import com.tangem.core.res.R as CoreResR
|
||||
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 TangemPayServiceUnavailableSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<TangemPayServiceUnavailableSheetPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
val title: KNode = child {
|
||||
hasTestTag(WarningBottomSheetTestTags.TITLE)
|
||||
hasText(getResourceString(CoreResR.string.tangempay_service_unavailable_title))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val description: KNode = child {
|
||||
hasTestTag(WarningBottomSheetTestTags.MESSAGE)
|
||||
hasText(getResourceString(CoreResR.string.tangempay_service_unavailable_description))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
// Merged tree: BUTTON_PRIMARY tags the button, its label is a child — only the merged node has both.
|
||||
val gotItButton: KNode = child {
|
||||
hasTestTag(WarningBottomSheetTestTags.BUTTON_PRIMARY)
|
||||
hasText(getResourceString(CoreResR.string.common_got_it))
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onTangemPayServiceUnavailableSheet(
|
||||
function: TangemPayServiceUnavailableSheetPageObject.() -> Unit,
|
||||
) = onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.screens.tangempay
|
||||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.res.R as CoreResR
|
||||
import com.tangem.core.ui.test.TangemPayTestTags
|
||||
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 TangemPayTransactionDetailsSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<TangemPayTransactionDetailsSheetPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
val feeTitle: KNode = child {
|
||||
hasText(getResourceString(CoreResR.string.tangem_pay_fee_title))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val serviceFeesCategory: KNode = child {
|
||||
hasText(getResourceString(CoreResR.string.tangem_pay_fee_subtitle))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val amount: KNode = child {
|
||||
hasTestTag(TangemPayTestTags.TRANSACTION_DETAILS_AMOUNT)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onTangemPayTransactionDetailsSheet(
|
||||
function: TangemPayTransactionDetailsSheetPageObject.() -> Unit,
|
||||
) = onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -59,15 +59,15 @@ class AppCurrencyTest : BaseTestCase() {
|
|||
onAppSettingsScreen { currencyButton.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Return to 'Details' screen") {
|
||||
waitForIdle()
|
||||
device.uiDevice.pressBack()
|
||||
step("Return to 'Details' screen via 'Back' button") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onAppSettingsScreen { backButton.clickWithAssertion() }
|
||||
onDetailsScreen { screenContainer.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Return to 'Main' screen via 'Back' button") {
|
||||
onDetailsScreen { topAppBarBackButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Main' screen is opened") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onDetailsScreen { topAppBarBackButton.clickWithAssertion() }
|
||||
onMainScreen { screenContainer.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,11 +21,13 @@ 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.Ignore
|
||||
import org.junit.Test
|
||||
|
||||
@HiltAndroidTest
|
||||
class BlockchainTest : BaseTestCase() {
|
||||
|
||||
@Ignore("[REDACTED_JIRA]")
|
||||
@AllureId("3644")
|
||||
@DisplayName("ADA: Checking the min amount/change = 1ADA")
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import com.tangem.common.extensions.clickAndWaitFor
|
|||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.domain.card.ScanFailsRequester
|
||||
import com.tangem.domain.models.scan.ProductType
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
import com.tangem.scenarios.checkFailedTransactionDialog
|
||||
|
|
@ -196,4 +197,68 @@ class FeedbackTest : BaseTestCase() {
|
|||
}
|
||||
}
|
||||
}
|
||||
@AllureId("3960")
|
||||
@DisplayName("Send feedback: Failed card scanning on Details screen")
|
||||
@Test
|
||||
fun sendFeedbackFailedScanningOnDetails() {
|
||||
val gmailText = "Welcome to Gmail"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
device.uiDevice.pressBack()
|
||||
MockProvider.resetEmulateError()
|
||||
}
|
||||
).run {
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Open 'Details screen'") {
|
||||
onMainScreenTopBar { moreButton.clickWithAssertion() }
|
||||
}
|
||||
step("Set scanning error (the next card scan will fail with TagLost)") {
|
||||
MockProvider.setEmulateError(TangemSdkError.TagLost())
|
||||
}
|
||||
step("Click 'Add new wallet' button") {
|
||||
onDetailsScreen { addWalletButton.clickWithAssertion() }
|
||||
}
|
||||
step("Force show 'Scan warning' dialog"){
|
||||
waitForIdle()
|
||||
runOnUiThread {
|
||||
MainScope().launch {
|
||||
scanFailsRequester.show(AnalyticsParam.ScreensSources.Main)
|
||||
}
|
||||
}
|
||||
}
|
||||
step("Check 'Scan warning' dialog") {
|
||||
waitForIdle()
|
||||
checkScanWarningDialog()
|
||||
}
|
||||
step("Click on 'Request support' button") {
|
||||
onScanWarningDialog { requestSupportButton.performClick() }
|
||||
}
|
||||
step("Assert 'Gmail' app is open") {
|
||||
ThirdPartyAppPageObject { assertElementWithTextExists(gmailText) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("3603")
|
||||
@DisplayName("Send feedback: S2C card has Contact support option")
|
||||
@Test
|
||||
fun sendFeedbackForS2CTest() {
|
||||
setupHooks().run {
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen(productType = ProductType.Start2Coin)
|
||||
}
|
||||
step("Open 'Details screen'") {
|
||||
onMainScreenTopBar { moreButton.clickWithAssertion() }
|
||||
}
|
||||
step("Verify 'Contact support' button is displayed") {
|
||||
onDetailsScreen { contactSupportButton.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -23,9 +23,9 @@ class SendTest : BaseTestCase() {
|
|||
val currencyName = "USDC"
|
||||
val feeCurrencyName = "Solana"
|
||||
val feeCurrencySymbol = "SOL"
|
||||
val balanceScenarioName = "solana_balance"
|
||||
val balanceScenarioName = "solana_get_account_info_recipient"
|
||||
val tokensScenarioName = "user_tokens_api"
|
||||
val balanceState = "Empty"
|
||||
val balanceState = "ZeroBalance"
|
||||
val tokensState = "SolanaUSDC"
|
||||
|
||||
setupHooks(
|
||||
|
|
|
|||
|
|
@ -503,6 +503,9 @@ class MainScreenActionButtonsTest : BaseTestCase() {
|
|||
step("Click on 'Swap' button") {
|
||||
onMainScreen { swapButton.performClick() }
|
||||
}
|
||||
step("Skip stories if displayed") {
|
||||
skipSwapStories()
|
||||
}
|
||||
step("Check 'Action is unavailable' dialog") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
||||
checkActionIsUnavailableDialog()
|
||||
|
|
|
|||
|
|
@ -1,25 +1,42 @@
|
|||
package com.tangem.tests.actionButtons
|
||||
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
|
||||
import com.tangem.common.constants.TestConstants.SVS_SEED_PHRASE_12
|
||||
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
|
||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
|
||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||
import com.tangem.common.constants.TestConstants.XRP_RECIPIENT_ADDRESS
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.common.extensions.pullToRefresh
|
||||
import com.tangem.common.utils.resetWireMockScenarioState
|
||||
import com.tangem.common.utils.setWireMockScenarioState
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.scenarios.checkQrCodeBottomSheetScenario
|
||||
import com.tangem.scenarios.enterAmountAndOpenSendConfirm
|
||||
import com.tangem.scenarios.goToQrCodeBottomSheet
|
||||
import com.tangem.scenarios.openMainScreen
|
||||
import com.tangem.scenarios.openSendFromTokenDetails
|
||||
import com.tangem.scenarios.openSendScreenWithHotWallet
|
||||
import com.tangem.scenarios.openSendSuccessScreenViaLongClickOnSendButton
|
||||
import com.tangem.scenarios.readNetworkFeeAmount
|
||||
import com.tangem.scenarios.synchronizeAddresses
|
||||
import com.tangem.scenarios.waitUntilNetworkFeeIsStable
|
||||
import com.tangem.screens.onAddFundsBottomSheet
|
||||
import com.tangem.screens.onDialog
|
||||
import com.tangem.screens.onMainScreen
|
||||
import com.tangem.screens.onSendScreen
|
||||
import com.tangem.screens.onSendSuccessScreen
|
||||
import com.tangem.screens.onSwapStoriesScreen
|
||||
import com.tangem.screens.onSwapTokenScreen
|
||||
import com.tangem.screens.onTokenDetailsScreen
|
||||
import com.tangem.screens.onTransferBottomSheet
|
||||
import com.tangem.screens.onTxHistoryScreen
|
||||
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.Ignore
|
||||
import org.junit.Test
|
||||
|
||||
@HiltAndroidTest
|
||||
|
|
@ -28,6 +45,7 @@ class TokenDetailsScreenActionButtonsTest : BaseTestCase() {
|
|||
@AllureId("594")
|
||||
@DisplayName("Action buttons (token details screen): validate UI")
|
||||
@Test
|
||||
@Ignore("[REDACTED_JIRA]")
|
||||
fun actionButtonsValidateUiTest() {
|
||||
val tokenTitle = "Bitcoin"
|
||||
|
||||
|
|
@ -110,10 +128,13 @@ class TokenDetailsScreenActionButtonsTest : BaseTestCase() {
|
|||
step("Click on 'Add funds' button") {
|
||||
onTokenDetailsScreen { addFundsButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Get $tokenTitle' bottom-sheet is displayed") {
|
||||
onAddFundsBottomSheet { titleWithTokenName(tokenTitle).assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Buy' button in bottom sheet is enabled") {
|
||||
onAddFundsBottomSheet { buyButton.assertIsEnabled() }
|
||||
}
|
||||
step("Assert 'Swap' button in bottom sheet is disabled") {
|
||||
step("Assert 'Swap' button in bottom sheet is not enabled") {
|
||||
onAddFundsBottomSheet { swapButton.assertIsNotEnabled() }
|
||||
}
|
||||
step("Assert 'Receive' button in bottom sheet is enabled") {
|
||||
|
|
@ -257,4 +278,146 @@ class TokenDetailsScreenActionButtonsTest : BaseTestCase() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("4465")
|
||||
@DisplayName("Action buttons (token details screen): 'Send' blocked while a transaction is active, works after completion")
|
||||
@Test
|
||||
fun sendBlockedWhileTransactionActiveTest() {
|
||||
val tokenName = "XRP Ledger"
|
||||
val amount = "1"
|
||||
val userTokensState = "XRPHotWalletSvS"
|
||||
val quotesState = "Ripple"
|
||||
val startedState = "Started"
|
||||
val rippleAccountInfoScenario = "ripple_account_info"
|
||||
val pendingSendMessagePrefix =
|
||||
getResourceString(R.string.token_button_unavailability_reason_pending_transaction_send).substringBefore("%")
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||
resetWireMockScenarioState(rippleAccountInfoScenario)
|
||||
},
|
||||
).run {
|
||||
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$userTokensState'") {
|
||||
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = userTokensState)
|
||||
}
|
||||
step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: '$quotesState'") {
|
||||
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = quotesState)
|
||||
}
|
||||
step("Set WireMock scenario: '$rippleAccountInfoScenario' to state: '$startedState'") {
|
||||
setWireMockScenarioState(scenarioName = rippleAccountInfoScenario, state = startedState)
|
||||
}
|
||||
step("Open the send flow for '$tokenName' on an existing hot wallet") {
|
||||
openSendScreenWithHotWallet(seedPhrase = SVS_SEED_PHRASE_12, tokenName = tokenName)
|
||||
}
|
||||
step("Enter amount '$amount' and open the 'Send confirm' screen") {
|
||||
enterAmountAndOpenSendConfirm(amount = amount, recipientAddress = XRP_RECIPIENT_ADDRESS)
|
||||
}
|
||||
waitUntilNetworkFeeIsStable { readNetworkFeeAmount() }
|
||||
step("Sign, send and open the 'Transaction sent' screen") {
|
||||
openSendSuccessScreenViaLongClickOnSendButton()
|
||||
}
|
||||
step("Click on 'Close' button") {
|
||||
onSendSuccessScreen { closeButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Token details' screen is displayed") {
|
||||
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
|
||||
}
|
||||
step("Open the transfer bottom sheet") {
|
||||
onTokenDetailsScreen { transferButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Send' button is not enabled while the transaction is active") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTransferBottomSheet { sendButton.assertIsNotEnabled() }
|
||||
}
|
||||
}
|
||||
step("Click on 'Send' button") {
|
||||
onTransferBottomSheet { sendButton.performClick() }
|
||||
}
|
||||
step("Assert pending-transaction notification dialog is displayed") {
|
||||
onDialog { containerWithText(pendingSendMessagePrefix).assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Send' screen is not opened") {
|
||||
onSendScreen { amountInputTextField.assertDoesNotExist() }
|
||||
}
|
||||
// Tapping the 'Send' row dismisses the transfer bottom sheet (onActionDispatched) before the dialog shows.
|
||||
step("Close the notification dialog") {
|
||||
onDialog { okButton.clickWithAssertion() }
|
||||
}
|
||||
step("Pull to refresh to complete the active transaction") {
|
||||
pullToRefresh()
|
||||
}
|
||||
step("Open the transfer bottom sheet again") {
|
||||
onTokenDetailsScreen { transferButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Send' button is enabled after the transaction is completed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTransferBottomSheet { sendButton.assertIsEnabled() }
|
||||
}
|
||||
}
|
||||
step("Click on 'Send' button") {
|
||||
onTransferBottomSheet { sendButton.performClick() }
|
||||
}
|
||||
step("Assert 'Send' screen is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onSendScreen { amountInputTextField.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("10209")
|
||||
@DisplayName("Action buttons (token details screen): 'Send' unavailable for a zero-balance token with an active transaction")
|
||||
@Test
|
||||
fun sendUnavailableForZeroBalanceWithActiveTransactionTest() {
|
||||
val tokenName = "Dogecoin"
|
||||
val zeroBalanceState = "ZeroBalance"
|
||||
val activeTxHistoryState = "UnconfirmedOutgoing"
|
||||
val balanceScenarioName = "dogecoin_balance"
|
||||
val txHistoryScenarioName = "dogecoin_tx_history"
|
||||
val sendingTitle = getResourceString(R.string.common_sending)
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||
resetWireMockScenarioState(balanceScenarioName)
|
||||
resetWireMockScenarioState(txHistoryScenarioName)
|
||||
}
|
||||
).run {
|
||||
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$tokenName'") {
|
||||
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = tokenName)
|
||||
}
|
||||
step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: '$tokenName'") {
|
||||
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = tokenName)
|
||||
}
|
||||
step("Set WireMock scenario: '$balanceScenarioName' to state: '$zeroBalanceState'") {
|
||||
setWireMockScenarioState(scenarioName = balanceScenarioName, state = zeroBalanceState)
|
||||
}
|
||||
step("Set WireMock scenario: '$txHistoryScenarioName' to state: '$activeTxHistoryState'") {
|
||||
setWireMockScenarioState(scenarioName = txHistoryScenarioName, state = activeTxHistoryState)
|
||||
}
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Click on token with name: '$tokenName'") {
|
||||
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Token details' screen is displayed") {
|
||||
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert active outgoing '$sendingTitle' transaction block is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTxHistoryScreen { transactionItem(sendingTitle).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Assert 'Transfer' button is not displayed for the zero-balance token") {
|
||||
onTokenDetailsScreen { transferButton.assertIsNotDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package com.tangem.tests.addFunds
|
|||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.extensions.assertTextContainsSafe
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.domain.models.scan.ProductType
|
||||
import com.tangem.scenarios.openMainScreen
|
||||
import com.tangem.scenarios.synchronizeAddresses
|
||||
import com.tangem.screens.onAddTokenBottomSheet
|
||||
|
|
@ -11,6 +12,7 @@ import com.tangem.screens.onBuyTokenDetailsScreen
|
|||
import com.tangem.screens.onMainScreen
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import io.qameta.allure.kotlin.AllureId
|
||||
import io.qameta.allure.kotlin.Issue
|
||||
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||
import org.junit.Test
|
||||
|
||||
|
|
@ -66,8 +68,8 @@ class BuyTest : BaseTestCase() {
|
|||
step("Click on $token in Trending list") {
|
||||
onAddFundsBottomSheet { trendingTokenWithTitle(token).clickWithAssertion() }
|
||||
}
|
||||
step("Click on 'Add' button") {
|
||||
onAddTokenBottomSheet { addButton.clickWithAssertion() }
|
||||
step("Click on 'Confirm' button") {
|
||||
onAddTokenBottomSheet { confirmButton.clickWithAssertion() }
|
||||
}
|
||||
step("Close 'Get token' screen") {
|
||||
onAddFundsBottomSheet { closeButton.clickWithAssertion() }
|
||||
|
|
@ -83,4 +85,24 @@ class BuyTest : BaseTestCase() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("3613")
|
||||
@DisplayName("On-ramp Buy: S2C card doesn't have Buy and Sell options")
|
||||
@Test
|
||||
@Issue("[REDACTED_TASK_KEY]")
|
||||
fun buyAndSellIsNotAvailableForS2CCardTest() {
|
||||
setupHooks().run {
|
||||
step("Open 'Main' screen") {
|
||||
openMainScreen(productType = ProductType.Start2Coin)
|
||||
}
|
||||
step("Verify 'Add funds' button is displayed") {
|
||||
onMainScreen { addFundsButton.assertIsDisplayed() }
|
||||
}
|
||||
step("Verify Buy/Sell action buttons are hidden") {
|
||||
onMainScreen {
|
||||
buyButton.assertDoesNotExist()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -86,9 +86,9 @@ class TotalBalanceUpdateTest : BaseTestCase() {
|
|||
step("Click on 'Add' button in 'Markets' bottom sheet") {
|
||||
onMarketsScreen { addButton.clickWithAssertion() }
|
||||
}
|
||||
step("Click on 'Add' button in 'Add token' bottom sheet") {
|
||||
step("Click on 'Confirm' button in 'Add token' bottom sheet") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
||||
onAddTokenBottomSheet { addButton.performClick() }
|
||||
onAddTokenBottomSheet { confirmButton.performClick() }
|
||||
}
|
||||
}
|
||||
step("Press 'Back' button") {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,274 @@
|
|||
package com.tangem.tests.main
|
||||
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.constants.TestConstants.COINS_API_SCENARIO
|
||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||
import com.tangem.common.extensions.performTextInputInChunks
|
||||
import com.tangem.common.utils.resetWireMockScenarioState
|
||||
import com.tangem.common.utils.setWireMockScenarioState
|
||||
import com.tangem.domain.models.scan.ProductType
|
||||
import com.tangem.scenarios.addCustomTokenWithCustomDerivation
|
||||
import com.tangem.scenarios.assertDerivationPathsInSelector
|
||||
import com.tangem.scenarios.navigateBackToMainFromManageTokens
|
||||
import com.tangem.scenarios.openAddCustomToken
|
||||
import com.tangem.scenarios.openMainScreen
|
||||
import com.tangem.scenarios.synchronizeAddresses
|
||||
import com.tangem.screens.onAddCustomTokenScreen
|
||||
import com.tangem.screens.onMainScreen
|
||||
import com.tangem.tap.domain.sdk.mocks.content.Wallet1LegacyDerivationMockContent
|
||||
import com.tangem.tap.domain.sdk.mocks.content.Wallet2NoEd25519Slip0010MockContent
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import io.qameta.allure.kotlin.AllureId
|
||||
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Test
|
||||
|
||||
@HiltAndroidTest
|
||||
class AddCustomTokenTest : BaseTestCase() {
|
||||
|
||||
private val richState = "ManageTokensRich"
|
||||
private val tokenTitle = "Tether"
|
||||
private val ethereumNetwork = "Ethereum"
|
||||
private val solanaNetwork = "Solana"
|
||||
private val bitcoinNetworkId = "bitcoin"
|
||||
private val ethereumClassicNetworkId = "ethereum-classic"
|
||||
private val ethContract = "0xdac17f958d2ee523a2206206994597c13d831ec7"
|
||||
private val solanaContract = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
|
||||
private val customDerivationPath = "m/44'/60'/0'/0/1"
|
||||
|
||||
@AllureId("772")
|
||||
@DisplayName("Add custom token: added token appears on Main")
|
||||
@Test
|
||||
fun addCustomTokenAppearsOnMainTest() {
|
||||
setupHooks(
|
||||
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
|
||||
).run {
|
||||
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
|
||||
setWireMockScenarioState(COINS_API_SCENARIO, richState)
|
||||
}
|
||||
step("Open 'Main Screen'") { openMainScreen() }
|
||||
step("Synchronize addresses") {
|
||||
flakySafely { onMainScreen { synchronizeAddressesButton.assertIsDisplayed() } }
|
||||
synchronizeAddresses(assertBalance = false)
|
||||
}
|
||||
step("Open 'Add custom token' screen") { openAddCustomToken() }
|
||||
step("Click on network: '$ethereumNetwork'") {
|
||||
flakySafely { onAddCustomTokenScreen { scrollToNetwork(ethereumNetwork) } }
|
||||
onAddCustomTokenScreen { networkRow(ethereumNetwork).performClick() }
|
||||
}
|
||||
step("Enter contract address: '$ethContract'") {
|
||||
flakySafely { onAddCustomTokenScreen { contractAddressField.assertExists() } }
|
||||
// Chunked input spans several validation ticks — the model drops the first sampled form change.
|
||||
onAddCustomTokenScreen { contractAddressField.performTextInputInChunks(ethContract) }
|
||||
}
|
||||
step("Click on 'Add token' button") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { onAddCustomTokenScreen { addTokenButton.assertIsEnabled() } }
|
||||
onAddCustomTokenScreen { addTokenButton.performClick() }
|
||||
}
|
||||
step("Navigate back to 'Main Screen'") { navigateBackToMainFromManageTokens() }
|
||||
step("Assert token: '$tokenTitle' is displayed on Main") {
|
||||
flakySafely { onMainScreen { tokenWithTitleAndAddress(tokenTitle).assertIsDisplayed() } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("775")
|
||||
@DisplayName("Add custom token: custom derivation path is accepted")
|
||||
@Test
|
||||
fun addCustomTokenWithCustomDerivationTest() {
|
||||
setupHooks(
|
||||
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
|
||||
).run {
|
||||
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
|
||||
setWireMockScenarioState(COINS_API_SCENARIO, richState)
|
||||
}
|
||||
step("Open 'Main Screen'") { openMainScreen() }
|
||||
step("Synchronize addresses") {
|
||||
flakySafely { onMainScreen { synchronizeAddressesButton.assertIsDisplayed() } }
|
||||
synchronizeAddresses(assertBalance = false)
|
||||
}
|
||||
step("Open 'Add custom token' screen") { openAddCustomToken() }
|
||||
step("Add custom token with custom derivation on '$ethereumNetwork'") {
|
||||
addCustomTokenWithCustomDerivation(
|
||||
network = ethereumNetwork,
|
||||
contract = ethContract,
|
||||
derivationPath = customDerivationPath,
|
||||
)
|
||||
}
|
||||
step("Assert token: '$tokenTitle' is displayed on Main") {
|
||||
flakySafely { onMainScreen { tokenWithTitleAndAddress(tokenTitle).assertIsDisplayed() } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("771")
|
||||
@DisplayName("Add custom token: custom derivation indicator is shown on Main")
|
||||
@Test
|
||||
fun customDerivationIndicatorOnMainTest() {
|
||||
setupHooks(
|
||||
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
|
||||
).run {
|
||||
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
|
||||
setWireMockScenarioState(COINS_API_SCENARIO, richState)
|
||||
}
|
||||
step("Open 'Main Screen'") { openMainScreen() }
|
||||
step("Synchronize addresses") {
|
||||
flakySafely { onMainScreen { synchronizeAddressesButton.assertIsDisplayed() } }
|
||||
synchronizeAddresses(assertBalance = false)
|
||||
}
|
||||
step("Open 'Add custom token' screen") { openAddCustomToken() }
|
||||
step("Add custom token with custom derivation on '$ethereumNetwork'") {
|
||||
addCustomTokenWithCustomDerivation(
|
||||
network = ethereumNetwork,
|
||||
contract = ethContract,
|
||||
derivationPath = customDerivationPath,
|
||||
)
|
||||
}
|
||||
step("Assert custom derivation indicator for '$tokenTitle' is displayed on Main") {
|
||||
flakySafely { onMainScreen { tokenWithCustomDerivationIcon(tokenTitle).assertIsDisplayed() } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("770")
|
||||
@DisplayName("Add custom token: derivation field is available for a non-EVM network")
|
||||
@Test
|
||||
fun derivationAvailableForNonEvmNetworkTest() {
|
||||
setupHooks(
|
||||
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
|
||||
).run {
|
||||
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
|
||||
setWireMockScenarioState(COINS_API_SCENARIO, richState)
|
||||
}
|
||||
step("Open 'Main Screen'") { openMainScreen() }
|
||||
step("Open 'Add custom token' screen") { openAddCustomToken() }
|
||||
step("Click on network: '$solanaNetwork'") {
|
||||
flakySafely { onAddCustomTokenScreen { scrollToNetwork(solanaNetwork) } }
|
||||
onAddCustomTokenScreen { networkRow(solanaNetwork).performClick() }
|
||||
}
|
||||
step("Assert 'Derivation path' field is available") {
|
||||
flakySafely { onAddCustomTokenScreen { derivationSelectorField.assertExists() } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("769")
|
||||
@DisplayName("Add custom token: Solana token on a modern card shows no unsupported warning")
|
||||
@Test
|
||||
fun solanaTokenModernCardNoWarningTest() {
|
||||
setupHooks(
|
||||
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
|
||||
).run {
|
||||
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
|
||||
setWireMockScenarioState(COINS_API_SCENARIO, richState)
|
||||
}
|
||||
step("Open 'Main Screen'") { openMainScreen() }
|
||||
step("Open 'Add custom token' screen") { openAddCustomToken() }
|
||||
step("Click on network: '$solanaNetwork'") {
|
||||
flakySafely { onAddCustomTokenScreen { scrollToNetwork(solanaNetwork) } }
|
||||
onAddCustomTokenScreen { networkRow(solanaNetwork).performClick() }
|
||||
}
|
||||
step("Enter contract address: '$solanaContract'") {
|
||||
flakySafely { onAddCustomTokenScreen { contractAddressField.assertExists() } }
|
||||
onAddCustomTokenScreen { contractAddressField.performTextInputInChunks(solanaContract) }
|
||||
}
|
||||
step("Assert 'Add token' button is enabled") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { onAddCustomTokenScreen { addTokenButton.assertIsEnabled() } }
|
||||
}
|
||||
step("Assert unsupported-token warning is not displayed") {
|
||||
onAddCustomTokenScreen { warningNotification.assertDoesNotExist() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("777")
|
||||
@DisplayName("Add custom token: network with a missing curve is not offered")
|
||||
@Test
|
||||
fun missingCurveNetworkNotOfferedTest() {
|
||||
val bitcoinNetwork = "Bitcoin"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
|
||||
).run {
|
||||
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
|
||||
setWireMockScenarioState(COINS_API_SCENARIO, richState)
|
||||
}
|
||||
step("Open 'Main Screen'") { openMainScreen(mockContent = Wallet2NoEd25519Slip0010MockContent) }
|
||||
step("Open 'Add custom token' screen") { openAddCustomToken() }
|
||||
step("Assert supported network '$bitcoinNetwork' is displayed") {
|
||||
flakySafely { onAddCustomTokenScreen { scrollToNetwork(bitcoinNetwork) } }
|
||||
}
|
||||
step("Assert '$solanaNetwork' network is not displayed for the missing-curve card") {
|
||||
onAddCustomTokenScreen { selectorList.assertIsDisplayed() }
|
||||
val solanaOffered = runCatching { onAddCustomTokenScreen { scrollToNetwork(solanaNetwork) } }.isSuccess
|
||||
assertFalse("'$solanaNetwork' should not be offered for a missing-curve card", solanaOffered)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("776")
|
||||
@DisplayName("Add custom token: derivation paths match a legacy-batch V1 Wallet card")
|
||||
@Test
|
||||
fun derivationPathsLegacyBatchWalletTest() {
|
||||
setupHooks(
|
||||
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
|
||||
).run {
|
||||
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
|
||||
setWireMockScenarioState(COINS_API_SCENARIO, richState)
|
||||
}
|
||||
step("Open 'Main Screen'") { openMainScreen(mockContent = Wallet1LegacyDerivationMockContent) }
|
||||
step("Open 'Add custom token' screen") { openAddCustomToken() }
|
||||
step("Assert V1 derivation paths are displayed in the selector") {
|
||||
assertDerivationPathsInSelector(
|
||||
ethereumNetwork,
|
||||
bitcoinNetworkId to "m/44'/0'/0'/0/0",
|
||||
ethereumClassicNetworkId to "m/44'/61'/0'/0/0",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("10204")
|
||||
@DisplayName("Add custom token: derivation paths match a V2 Wallet card")
|
||||
@Test
|
||||
fun derivationPathsWalletTest() {
|
||||
setupHooks(
|
||||
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
|
||||
).run {
|
||||
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
|
||||
setWireMockScenarioState(COINS_API_SCENARIO, richState)
|
||||
}
|
||||
step("Open 'Main Screen'") { openMainScreen() }
|
||||
step("Open 'Add custom token' screen") { openAddCustomToken() }
|
||||
step("Assert V2 derivation paths are displayed in the selector") {
|
||||
assertDerivationPathsInSelector(
|
||||
ethereumNetwork,
|
||||
bitcoinNetworkId to "m/44'/0'/0'/0/0",
|
||||
ethereumClassicNetworkId to "m/44'/60'/0'/0/0",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("10205")
|
||||
@DisplayName("Add custom token: derivation paths match a V3 Wallet 2 card")
|
||||
@Test
|
||||
fun derivationPathsWallet2Test() {
|
||||
setupHooks(
|
||||
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
|
||||
).run {
|
||||
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
|
||||
setWireMockScenarioState(COINS_API_SCENARIO, richState)
|
||||
}
|
||||
step("Open 'Main Screen'") { openMainScreen(productType = ProductType.Wallet2) }
|
||||
step("Open 'Add custom token' screen") { openAddCustomToken() }
|
||||
step("Assert V3 derivation paths are displayed in the selector") {
|
||||
assertDerivationPathsInSelector(
|
||||
ethereumNetwork,
|
||||
bitcoinNetworkId to "m/84'/0'/0'/0/0",
|
||||
ethereumClassicNetworkId to "m/44'/61'/0'/0/0",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,250 @@
|
|||
package com.tangem.tests.main
|
||||
|
||||
import androidx.compose.ui.test.longClick
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.constants.TestConstants.COINS_API_SCENARIO
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.common.utils.assertClipboardIsEmpty
|
||||
import com.tangem.common.utils.assertClipboardTextEquals
|
||||
import com.tangem.common.utils.clearClipboard
|
||||
import com.tangem.common.utils.resetWireMockScenarioState
|
||||
import com.tangem.common.utils.setWireMockScenarioState
|
||||
import com.tangem.scenarios.openMainScreen
|
||||
import com.tangem.scenarios.openManageTokens
|
||||
import com.tangem.scenarios.toggleTokenNetworkInManageTokens
|
||||
import com.tangem.screens.onDialog
|
||||
import com.tangem.screens.onManageTokensScreen
|
||||
import com.tangem.tap.domain.sdk.mocks.content.Firmware451MockContent
|
||||
import com.tangem.tap.domain.sdk.mocks.content.Wallet2NoEd25519Slip0010MockContent
|
||||
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
|
||||
import com.tangem.core.res.R as CoreResR
|
||||
|
||||
@HiltAndroidTest
|
||||
class ManageTokensTest : BaseTestCase() {
|
||||
|
||||
private val richState = "ManageTokensRich"
|
||||
private val solanaTokenTitle = "USD Coin"
|
||||
private val solanaNetworkTitle = "SOLANA"
|
||||
private val solanaName = "Solana"
|
||||
|
||||
@AllureId("765")
|
||||
@DisplayName("Manage tokens: network standard labels are shown for token networks")
|
||||
@Test
|
||||
fun networkStandardLabelsTest() {
|
||||
val tokenTitle = "Tether"
|
||||
val ethereum = "Ethereum"
|
||||
val bnbSmartChain = "BNB Smart Chain"
|
||||
val tron = "Tron"
|
||||
val erc20 = "ERC20"
|
||||
val bep20 = "BEP20"
|
||||
val trc20 = "TRC20"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
|
||||
).run {
|
||||
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
|
||||
setWireMockScenarioState(COINS_API_SCENARIO, richState)
|
||||
}
|
||||
step("Open 'Main Screen'") { openMainScreen() }
|
||||
step("Open 'Manage tokens' screen") { openManageTokens() }
|
||||
step("Click on token: '$tokenTitle'") {
|
||||
flakySafely { onManageTokensScreen { tokenItem(tokenTitle).assertIsDisplayed() } }
|
||||
onManageTokensScreen { tokenItem(tokenTitle).performClick() }
|
||||
}
|
||||
step("Assert '$ethereum' network standard '$erc20' is displayed") {
|
||||
flakySafely {
|
||||
onManageTokensScreen { networkStandard(networkName = ethereum, standard = erc20).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Assert '$bnbSmartChain' network standard '$bep20' is displayed") {
|
||||
flakySafely {
|
||||
onManageTokensScreen {
|
||||
networkStandard(networkName = bnbSmartChain, standard = bep20).assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
step("Assert '$tron' network standard '$trc20' is displayed") {
|
||||
flakySafely {
|
||||
onManageTokensScreen { networkStandard(networkName = tron, standard = trc20).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("667")
|
||||
@DisplayName("Manage tokens: search by name and ticker filters the list")
|
||||
@Test
|
||||
fun searchByNameAndTickerTest() {
|
||||
val tokenTitle = "Tether"
|
||||
val nameQuery = "Tether"
|
||||
val tickerQuery = "USDT"
|
||||
val emptyQuery = "Zzqnotoken"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
|
||||
).run {
|
||||
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
|
||||
setWireMockScenarioState(COINS_API_SCENARIO, richState)
|
||||
}
|
||||
step("Open 'Main Screen'") { openMainScreen() }
|
||||
step("Open 'Manage tokens' screen") { openManageTokens() }
|
||||
step("Search by name: '$nameQuery'") {
|
||||
onManageTokensScreen {
|
||||
searchField.performClick()
|
||||
searchField.performTextInput(nameQuery)
|
||||
}
|
||||
}
|
||||
step("Assert token: '$tokenTitle' is displayed") {
|
||||
flakySafely { onManageTokensScreen { tokenItem(tokenTitle).assertIsDisplayed() } }
|
||||
}
|
||||
step("Clear search field") {
|
||||
onManageTokensScreen { searchClearButton.clickWithAssertion() }
|
||||
}
|
||||
step("Search by ticker: '$tickerQuery'") {
|
||||
onManageTokensScreen { searchField.performTextInput(tickerQuery) }
|
||||
}
|
||||
step("Assert token: '$tokenTitle' is displayed") {
|
||||
flakySafely { onManageTokensScreen { tokenItem(tokenTitle).assertIsDisplayed() } }
|
||||
}
|
||||
step("Clear search field") {
|
||||
onManageTokensScreen { searchClearButton.clickWithAssertion() }
|
||||
}
|
||||
step("Search by unknown query: '$emptyQuery'") {
|
||||
onManageTokensScreen { searchField.performTextInput(emptyQuery) }
|
||||
}
|
||||
step("Assert token: '$tokenTitle' is not displayed") {
|
||||
flakySafely { onManageTokensScreen { tokenItem(tokenTitle).assertDoesNotExist() } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("763")
|
||||
@DisplayName("Manage tokens: enabling Solana network on a modern card shows no warning")
|
||||
@Test
|
||||
fun solanaNetworkNoWarningTest() {
|
||||
setupHooks(
|
||||
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
|
||||
).run {
|
||||
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
|
||||
setWireMockScenarioState(COINS_API_SCENARIO, richState)
|
||||
}
|
||||
step("Open 'Main Screen'") { openMainScreen() }
|
||||
step("Open 'Manage tokens' screen") { openManageTokens() }
|
||||
step("Enable '$solanaNetworkTitle' network for token: '$solanaTokenTitle'") {
|
||||
toggleTokenNetworkInManageTokens(tokenTitle = solanaTokenTitle, networkTitle = solanaNetworkTitle)
|
||||
}
|
||||
step("Assert hide-token alert is not displayed") {
|
||||
waitForIdle()
|
||||
onDialog { dialogContainer.assertDoesNotExist() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("719")
|
||||
@DisplayName("Manage tokens: long tap on a token network copies the contract address, main network does not")
|
||||
@Test
|
||||
fun copyContractAddressOnNetworkLongTapTest() {
|
||||
val tokenTitle = "Tether"
|
||||
val tokenNetworkTitle = "ETHEREUM"
|
||||
val coinTitle = "Bitcoin"
|
||||
val coinNetworkTitle = "BITCOIN"
|
||||
val contractAddress = "0xdac17f958d2ee523a2206206994597c13d831ec7"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
|
||||
).run {
|
||||
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
|
||||
setWireMockScenarioState(COINS_API_SCENARIO, richState)
|
||||
}
|
||||
step("Open 'Main Screen'") { openMainScreen() }
|
||||
step("Open 'Manage tokens' screen") { openManageTokens() }
|
||||
step("Click on token: '$tokenTitle'") {
|
||||
flakySafely { onManageTokensScreen { tokenItem(tokenTitle).assertIsDisplayed() } }
|
||||
onManageTokensScreen { tokenItem(tokenTitle).performClick() }
|
||||
}
|
||||
step("Long click on '$tokenNetworkTitle' network row") {
|
||||
flakySafely { onManageTokensScreen { networkName(tokenNetworkTitle).assertIsDisplayed() } }
|
||||
onManageTokensScreen {
|
||||
networkName(tokenNetworkTitle).performTouchInput {
|
||||
longClick(position = center, durationMillis = 1000L)
|
||||
}
|
||||
}
|
||||
}
|
||||
step("Assert contract-address-copied message is displayed") {
|
||||
flakySafely { onManageTokensScreen { contractAddressCopiedMessage.assertIsDisplayed() } }
|
||||
}
|
||||
step("Assert clipboard contains '$tokenTitle' contract address") {
|
||||
assertClipboardTextEquals(contractAddress)
|
||||
}
|
||||
step("Clear clipboard") { clearClipboard() }
|
||||
step("Click on token: '$coinTitle'") {
|
||||
flakySafely { onManageTokensScreen { tokenItem(coinTitle).assertIsDisplayed() } }
|
||||
onManageTokensScreen { tokenItem(coinTitle).performClick() }
|
||||
}
|
||||
step("Long click on '$coinNetworkTitle' main network row") {
|
||||
flakySafely { onManageTokensScreen { networkName(coinNetworkTitle).assertIsDisplayed() } }
|
||||
onManageTokensScreen {
|
||||
networkName(coinNetworkTitle).performTouchInput {
|
||||
longClick(position = center, durationMillis = 1000L)
|
||||
}
|
||||
}
|
||||
}
|
||||
step("Assert clipboard is empty after long click on main network") {
|
||||
waitForIdle()
|
||||
assertClipboardIsEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("737")
|
||||
@DisplayName("Manage tokens: enabling Solana on an old-firmware card shows firmware-limitation warning")
|
||||
@Test
|
||||
fun solanaFirmwareLimitationWarningTest() {
|
||||
val warningMessage = getResourceString(CoreResR.string.alert_manage_tokens_unsupported_message, solanaName)
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
|
||||
).run {
|
||||
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
|
||||
setWireMockScenarioState(COINS_API_SCENARIO, richState)
|
||||
}
|
||||
step("Open 'Main Screen'") { openMainScreen(mockContent = Firmware451MockContent) }
|
||||
step("Open 'Manage tokens' screen") { openManageTokens() }
|
||||
step("Enable '$solanaNetworkTitle' network for token: '$solanaTokenTitle'") {
|
||||
toggleTokenNetworkInManageTokens(tokenTitle = solanaTokenTitle, networkTitle = solanaNetworkTitle)
|
||||
}
|
||||
step("Assert firmware-limitation warning is displayed") {
|
||||
flakySafely { onDialog { containerWithText(warningMessage).assertIsDisplayed() } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("767")
|
||||
@DisplayName("Manage tokens: enabling Solana on a card missing its curve shows unsupported-curve warning")
|
||||
@Test
|
||||
fun solanaUnsupportedCurveWarningTest() {
|
||||
val warningMessage =
|
||||
getResourceString(CoreResR.string.alert_manage_tokens_unsupported_curve_message, solanaName)
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = { resetWireMockScenarioState(COINS_API_SCENARIO) },
|
||||
).run {
|
||||
step("Set WireMock scenario: '$COINS_API_SCENARIO' to state: '$richState'") {
|
||||
setWireMockScenarioState(COINS_API_SCENARIO, richState)
|
||||
}
|
||||
step("Open 'Main Screen'") { openMainScreen(mockContent = Wallet2NoEd25519Slip0010MockContent) }
|
||||
step("Open 'Manage tokens' screen") { openManageTokens() }
|
||||
step("Enable '$solanaNetworkTitle' network for token: '$solanaTokenTitle'") {
|
||||
toggleTokenNetworkInManageTokens(tokenTitle = solanaTokenTitle, networkTitle = solanaNetworkTitle)
|
||||
}
|
||||
step("Assert unsupported-curve warning is displayed") {
|
||||
flakySafely { onDialog { containerWithText(warningMessage).assertIsDisplayed() } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
package com.tangem.tests.main
|
||||
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.scenarios.addNewCardWalletWithoutSync
|
||||
import com.tangem.scenarios.assertOrganizeTokensMatch
|
||||
import com.tangem.scenarios.getMainScreenTokensOrder
|
||||
import com.tangem.scenarios.openMainScreen
|
||||
import com.tangem.scenarios.switchToPreviousWallet
|
||||
import com.tangem.scenarios.synchronizeAddresses
|
||||
import com.tangem.screens.onAddAndManageBottomSheet
|
||||
import com.tangem.screens.onMainScreen
|
||||
import com.tangem.screens.onOrganizeTokensScreen
|
||||
import com.tangem.tap.domain.sdk.mocks.content.Wallet2MockContent
|
||||
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 OrganizeTokensTest : BaseTestCase() {
|
||||
|
||||
@AllureId("71")
|
||||
@DisplayName("Organize tokens: Correct tokens list displaying for current wallet")
|
||||
@Test
|
||||
fun organizeTokensCorrectTokensListDisplaying() {
|
||||
setupHooks().run {
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Add a second card wallet") {
|
||||
addNewCardWalletWithoutSync(Wallet2MockContent)
|
||||
}
|
||||
step("Switch to first wallet Main screen") {
|
||||
switchToPreviousWallet()
|
||||
}
|
||||
|
||||
val firstWalletTokens = getMainScreenTokensOrder()
|
||||
assertOrganizeTokensMatch(firstWalletTokens)
|
||||
|
||||
step("Switch to second wallet Main screen") {
|
||||
onMainScreen { swipeToAdjacentWallet(toPrevious = false) }
|
||||
}
|
||||
|
||||
val secondWalletTokens = getMainScreenTokensOrder()
|
||||
assertOrganizeTokensMatch(secondWalletTokens)
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("2753")
|
||||
@DisplayName("Organize tokens: Tokens order changing")
|
||||
@Test
|
||||
fun organizeTokensOrderChanging() {
|
||||
setupHooks().run{
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Open 'Organize tokens' bottom-sheet") {
|
||||
onMainScreen { clickDisplayedAddAndManageButton() }
|
||||
onAddAndManageBottomSheet { organizeTokensButton.clickWithAssertion() }
|
||||
}
|
||||
step("Drag the 3rd token onto the 2nd position and assert the new order") {
|
||||
onOrganizeTokensScreen {
|
||||
val sourceIndex = 2
|
||||
val destinationIndex = 1
|
||||
val before = getDisplayedTokenTitles()
|
||||
require(before.size > sourceIndex && before.size > destinationIndex) {
|
||||
"Expected at least ${maxOf(sourceIndex, destinationIndex) + 1} tokens to reorder, but got ${before.size}: $before"
|
||||
}
|
||||
dragToken(source = before[sourceIndex], destination = before[destinationIndex])
|
||||
val expected = before.toMutableList().apply {
|
||||
add(destinationIndex, removeAt(sourceIndex))
|
||||
}
|
||||
assertEquals(expected, getDisplayedTokenTitles())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,12 +4,17 @@ import com.tangem.common.BaseTestCase
|
|||
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
|
||||
import com.tangem.common.utils.resetWireMockScenarioState
|
||||
import com.tangem.common.utils.setWireMockScenarioState
|
||||
import com.tangem.scenarios.addNewCardWalletWithoutSync
|
||||
import com.tangem.scenarios.getMainScreenTokensOrder
|
||||
import com.tangem.scenarios.openMainScreen
|
||||
import com.tangem.scenarios.synchronizeAddresses
|
||||
import com.tangem.screens.onMainScreen
|
||||
import com.tangem.tap.domain.sdk.mocks.content.Wallet2MockContent
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import io.qameta.allure.kotlin.AllureId
|
||||
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
@HiltAndroidTest
|
||||
|
|
@ -48,4 +53,54 @@ class TokenListTest : BaseTestCase() {
|
|||
}
|
||||
}
|
||||
|
||||
@AllureId("177")
|
||||
@DisplayName("Main: token list differs after switching to a second wallet")
|
||||
@Test
|
||||
fun tokenListChangedAfterSwitchingWalletTest() {
|
||||
val userTokensState = "ReducedTokens"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||
}
|
||||
).run {
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
|
||||
val firstWalletTokens = getMainScreenTokensOrder()
|
||||
|
||||
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$userTokensState'") {
|
||||
setWireMockScenarioState(USER_TOKENS_API_SCENARIO, userTokensState)
|
||||
}
|
||||
step("Add a second card wallet") {
|
||||
addNewCardWalletWithoutSync(Wallet2MockContent)
|
||||
}
|
||||
|
||||
val secondWalletTokens = getMainScreenTokensOrder()
|
||||
|
||||
step("Assert both wallets exposed a non-empty token list") {
|
||||
assertTrue(
|
||||
"First wallet token list should not be empty",
|
||||
firstWalletTokens.isNotEmpty(),
|
||||
)
|
||||
assertTrue(
|
||||
"Second wallet token list should not be empty",
|
||||
secondWalletTokens.isNotEmpty(),
|
||||
)
|
||||
}
|
||||
step("Assert the two wallets show different token lists") {
|
||||
assertNotEquals(
|
||||
"Expected the second wallet's token list to differ from the first " +
|
||||
"(first=$firstWalletTokens, second=$secondWalletTokens)",
|
||||
firstWalletTokens,
|
||||
secondWalletTokens,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -27,11 +27,9 @@ class WarningsTest : BaseTestCase() {
|
|||
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||
}
|
||||
).run {
|
||||
|
||||
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$scenarioState'") {
|
||||
setWireMockScenarioState(USER_TOKENS_API_SCENARIO, scenarioState)
|
||||
}
|
||||
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 displayed") {
|
||||
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 displayed") {
|
||||
onMainScreen { assertTokenDoesNotExist(token) }
|
||||
}
|
||||
step("Verify network $tokenNetwork is displayed") {
|
||||
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 is not displayed") {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ import org.junit.Test
|
|||
@HiltAndroidTest
|
||||
class SendConfirmScreenTest : BaseTestCase() {
|
||||
|
||||
@Ignore("[REDACTED_JIRA]")
|
||||
@AllureId("4003")
|
||||
@DisplayName("Send (Confirm screen): change sending amount")
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -132,9 +132,9 @@ class GaslessSendTest : BaseTestCase() {
|
|||
@DisplayName("Gasless: completed gasless transaction is shown in token transaction history")
|
||||
@Test
|
||||
fun checkGaslessTransactionInHistoryTest() {
|
||||
val sentAmount = "1.00"
|
||||
val operationAmount = "1.00"
|
||||
val gaslessFeeAmount = "0.10"
|
||||
val sentTitle = getResourceString(R.string.common_sent)
|
||||
val operationTitle = getResourceString(R.string.transaction_history_operation)
|
||||
val gaslessFeeTitle = getResourceString(R.string.gasless_transaction_fee)
|
||||
|
||||
setupHooks(
|
||||
|
|
@ -166,13 +166,13 @@ class GaslessSendTest : BaseTestCase() {
|
|||
onTxHistoryScreen { transactionItem(gaslessFeeTitle).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Assert '$sentTitle' transaction is displayed") {
|
||||
onTxHistoryScreen { transactionItem(sentTitle).assertIsDisplayed() }
|
||||
step("Assert '$operationTitle' transaction is displayed") {
|
||||
onTxHistoryScreen { transactionItem(operationTitle).assertIsDisplayed() }
|
||||
}
|
||||
step("Assert '$sentTitle' amount '$sentAmount' is displayed in '$currencySymbol'") {
|
||||
step("Assert '$operationTitle' amount '$operationAmount' is displayed in '$currencySymbol'") {
|
||||
onTxHistoryScreen {
|
||||
transactionAmount(sentTitle).assertTextContains(sentAmount, substring = true)
|
||||
transactionCurrency(sentTitle).assertTextEquals(currencySymbol)
|
||||
transactionAmount(operationTitle).assertTextContains(operationAmount, substring = true)
|
||||
transactionCurrency(operationTitle).assertTextEquals(currencySymbol)
|
||||
}
|
||||
}
|
||||
step("Assert gasless '$gaslessFeeTitle' amount '$gaslessFeeAmount' is displayed in '$currencySymbol'") {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,123 @@
|
|||
package com.tangem.tests.send.reasonBlock
|
||||
|
||||
import com.tangem.common.BaseTestCase
|
||||
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.openMainScreen
|
||||
import com.tangem.scenarios.synchronizeAddresses
|
||||
import com.tangem.screens.onDialog
|
||||
import com.tangem.screens.onMainScreen
|
||||
import com.tangem.screens.onTokenDetailsScreen
|
||||
import com.tangem.screens.onTransferBottomSheet
|
||||
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 ReasonBlockTest : BaseTestCase() {
|
||||
|
||||
@AllureId("3616")
|
||||
@DisplayName("Reason block: Send is unavailable if user has pending transaction")
|
||||
@Test
|
||||
fun reasonBlockSendUnavailableWithPendingTransactionTest() {
|
||||
val txHistoryScenarioName = "dogecoin_tx_history"
|
||||
val txHistoryState = "EmptyWithPendingTransaction"
|
||||
val walletsScenarioName = "user_tokens_api"
|
||||
val walletsState = "Dogecoin"
|
||||
val token = "Dogecoin"
|
||||
val reasonText = getResourceString(R.string.token_button_unavailability_reason_pending_transaction_send)
|
||||
.substringBefore("%s")
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(txHistoryScenarioName)
|
||||
resetWireMockScenarioState(walletsScenarioName)
|
||||
}
|
||||
).run {
|
||||
step("Set Wiremock scenario: $txHistoryScenarioName to state $txHistoryState") {
|
||||
setWireMockScenarioState(scenarioName = txHistoryScenarioName, state = txHistoryState)
|
||||
}
|
||||
step("Set Wiremock scenario: $walletsScenarioName to state $walletsState") {
|
||||
setWireMockScenarioState(scenarioName = walletsScenarioName, state = walletsState)
|
||||
}
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Click on token with name $token") {
|
||||
onMainScreen { tokenWithTitleAndAddress(token).clickWithAssertion() }
|
||||
}
|
||||
step("Click on 'Transfer' button") {
|
||||
onTokenDetailsScreen { transferButton.clickWithAssertion() }
|
||||
}
|
||||
step("Verify 'Send' button is disabled") {
|
||||
onTransferBottomSheet { sendButton.assertIsNotEnabled() }
|
||||
}
|
||||
step("Click on 'Send' button") {
|
||||
onTransferBottomSheet { sendButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert pending-transaction reason dialog is displayed") {
|
||||
onDialog { containerWithText(reasonText).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("3615")
|
||||
@DisplayName("Reason block: Token withdrawal is unavailable if there are no fee coverage")
|
||||
@Test
|
||||
fun reasonBlockTokenWithdrawalUnavailableWithoutFeeCoverage() {
|
||||
val userWalletsScenarioName = "user_tokens_api"
|
||||
val userWalletsState = "SolanaUSDC"
|
||||
val solBalanceScenarioName = "solana_get_account_info_recipient"
|
||||
val solBalanceState = "ZeroBalance"
|
||||
val token = "USDC"
|
||||
val feeCurrencyName = "Solana"
|
||||
val feeCurrencySymbol = "SOL"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(userWalletsScenarioName)
|
||||
resetWireMockScenarioState(solBalanceScenarioName)
|
||||
}
|
||||
).run {
|
||||
step("Set Wiremock scenario: $userWalletsScenarioName to state $userWalletsState") {
|
||||
setWireMockScenarioState(scenarioName = userWalletsScenarioName, state = userWalletsState)
|
||||
}
|
||||
step("Set Wiremock scenario: $solBalanceScenarioName to state $solBalanceState") {
|
||||
setWireMockScenarioState(scenarioName = solBalanceScenarioName, state = solBalanceState)
|
||||
}
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Click on token with name $token") {
|
||||
onMainScreen { tokenWithTitleAndAddress(token).clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Insufficient $feeCurrencySymbol for fee' notification is displayed") {
|
||||
onTokenDetailsScreen {
|
||||
networkFeeNotificationTitle(feeCurrencyName).assertIsDisplayed()
|
||||
networkFeeNotificationMessage(
|
||||
currencyName = token,
|
||||
networkName = feeCurrencyName,
|
||||
feeCurrencyName = feeCurrencyName,
|
||||
feeCurrencySymbol = feeCurrencySymbol,
|
||||
).assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
step("Click on 'Go to $feeCurrencySymbol' button") {
|
||||
onTokenDetailsScreen { goToBuyCurrencyButton(feeCurrencySymbol).clickWithAssertion() }
|
||||
}
|
||||
step("Assert $feeCurrencyName token screen is opened") {
|
||||
onTokenDetailsScreen { tokenTitle(feeCurrencyName).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -155,7 +155,11 @@ class GaslessSendViaSwapTest : BaseTestCase() {
|
|||
enterSwapAmountAndOpenSendConfirm(amount = tokenAmount, recipientAddress = BITCOIN_RECIPIENT_ADDRESS)
|
||||
}
|
||||
step("Pay the network fee with '$tokenName' via the fee selector") {
|
||||
selectStablecoinAsFeeToken(coinName = nativeTokenName, tokenName = tokenName)
|
||||
selectStablecoinAsFeeToken(
|
||||
coinName = nativeTokenName,
|
||||
tokenName = tokenName,
|
||||
expectApplyEnabled = false,
|
||||
)
|
||||
}
|
||||
step("Assert 'Not enough funds' error is displayed in the fee selector") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import com.tangem.common.extensions.extractText
|
|||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.common.utils.resetWireMockScenarioState
|
||||
import com.tangem.common.utils.setWireMockScenarioState
|
||||
import com.tangem.domain.models.scan.ProductType
|
||||
import com.tangem.scenarios.*
|
||||
import com.tangem.screens.*
|
||||
import com.tangem.tap.domain.sdk.mocks.content.Wallet2WithDerivationsMockContent
|
||||
|
|
@ -825,4 +826,112 @@ class SendViaSwapTest : BaseTestCase() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("3972")
|
||||
@DisplayName("Send via Swap: single-currency card has no 'Convert' button in Send")
|
||||
@Test
|
||||
fun singleCurrencyCardNoConvertButtonInSendTest() {
|
||||
val cardType = ProductType.Note
|
||||
val tokenTitle = "Dogecoin"
|
||||
|
||||
setupHooks().run {
|
||||
step("Open 'Main Screen' on '${cardType.name}' card") {
|
||||
openMainScreen(cardType)
|
||||
}
|
||||
step("Assert 'Add funds' button is displayed") {
|
||||
onMainScreen { addFundsButton.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Transfer' button is displayed") {
|
||||
onMainScreen { transferButton.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Swap' button is not displayed") {
|
||||
onMainScreen { swapButton.assertIsNotDisplayed() }
|
||||
}
|
||||
step("Click on 'Transfer' button") {
|
||||
onMainScreen { transferButton.performClick() }
|
||||
}
|
||||
step("Click on token: '$tokenTitle'") {
|
||||
onChooseTokenBottomSheet { tokenWithTitle(tokenTitle).clickWithAssertion() }
|
||||
}
|
||||
step("Click on 'Send' button in bottom sheet") {
|
||||
onTransferBottomSheet { sendButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert amount input text field is displayed") {
|
||||
flakySafely { onSendScreen { amountInputTextField.assertIsDisplayed() } }
|
||||
}
|
||||
step("Assert 'Swap to another token' button is not displayed") {
|
||||
onSendScreen { swapToAnotherTokenButton.assertDoesNotExist() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("10196")
|
||||
@DisplayName("Send via Swap: memo-restricted network is not selectable as a receive token")
|
||||
@Test
|
||||
fun memoRestrictedNetworkNotSelectableAsReceiveTokenTest() {
|
||||
val tokenName = "Bitcoin"
|
||||
val algorand = "Algorand"
|
||||
val bitcoinBalanceScenarioName = "bitcoin_utxo"
|
||||
val bitcoinBalanceScenarioState = "Balance"
|
||||
val assetsScenarioName = "express_api_assets"
|
||||
val assetsScenarioState = "BitcoinExchangeEnabled"
|
||||
val userTokensScenarioState = "Wallet2"
|
||||
val coinsScenarioName = "coins_api"
|
||||
val coinsScenarioState = "WithAlgorand"
|
||||
val warningTitle = getResourceString(R.string.express_swap_not_supported_title, algorand)
|
||||
val warningMessage = getResourceString(R.string.express_swap_not_supported_text)
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(bitcoinBalanceScenarioName)
|
||||
resetWireMockScenarioState(assetsScenarioName)
|
||||
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||
resetWireMockScenarioState(coinsScenarioName)
|
||||
}
|
||||
).run {
|
||||
|
||||
step("Set WireMock scenario: '$bitcoinBalanceScenarioName' to state: '$bitcoinBalanceScenarioState'") {
|
||||
setWireMockScenarioState(scenarioName = bitcoinBalanceScenarioName, state = bitcoinBalanceScenarioState)
|
||||
}
|
||||
step("Set WireMock scenario: '$assetsScenarioName' to state: '$assetsScenarioState'") {
|
||||
setWireMockScenarioState(scenarioName = assetsScenarioName, state = assetsScenarioState)
|
||||
}
|
||||
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$userTokensScenarioState'") {
|
||||
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = userTokensScenarioState)
|
||||
}
|
||||
step("Set WireMock scenario: '$coinsScenarioName' to state: '$coinsScenarioState'") {
|
||||
setWireMockScenarioState(scenarioName = coinsScenarioName, state = coinsScenarioState)
|
||||
}
|
||||
step("Open 'Send' screen") {
|
||||
openSendScreen(tokenName, mockContent = Wallet2WithDerivationsMockContent)
|
||||
}
|
||||
step("Click on 'Swap to another token' button") {
|
||||
onSendScreen { swapToAnotherTokenButton.performClick() }
|
||||
}
|
||||
step("Type '$algorand' in search text field") {
|
||||
onSendViaSwapScreen {
|
||||
searchField.performClick()
|
||||
searchField.performTextInput(algorand)
|
||||
}
|
||||
}
|
||||
step("Click on token: '$algorand'") {
|
||||
onSendViaSwapScreen { tokenItem(algorand).performClick() }
|
||||
}
|
||||
step("Assert warning bottom sheet icon is displayed") {
|
||||
onWarningBottomSheet { icon.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert warning bottom sheet title is displayed") {
|
||||
onWarningBottomSheet { title(warningTitle).assertIsDisplayed() }
|
||||
}
|
||||
step("Assert warning bottom sheet message is displayed") {
|
||||
onWarningBottomSheet { message(warningMessage).assertIsDisplayed() }
|
||||
}
|
||||
step("Click on 'Ok, Got it!' button") {
|
||||
onWarningBottomSheet { okGotItButton.performClick() }
|
||||
}
|
||||
step("Assert 'Swap to another token' screen is displayed") {
|
||||
onSendViaSwapScreen { tokenItem(algorand).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,9 +4,11 @@ import com.tangem.common.BaseTestCase
|
|||
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
|
||||
import com.tangem.common.constants.TestConstants.SOLANA_RECIPIENT_ADDRESS
|
||||
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
|
||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.common.utils.resetWireMockScenarioState
|
||||
import com.tangem.scenarios.checkSendWarning
|
||||
import com.tangem.scenarios.openSendConfirmScreenViaNextButton
|
||||
import com.tangem.scenarios.openSendScreen
|
||||
import com.tangem.screens.onSendAddressScreen
|
||||
import com.tangem.screens.onSendScreen
|
||||
|
|
@ -20,7 +22,7 @@ import org.junit.Test
|
|||
@HiltAndroidTest
|
||||
class SolanaWarningsTest : BaseTestCase() {
|
||||
private val tokenName = "Solana"
|
||||
private val amountToLeaveLessThanRent = "0.0016941"
|
||||
private val amountToLeaveLessThanRent = "0.0372"
|
||||
private val amountToLeaveGreaterThanRent = "0.0000941"
|
||||
private val amountToLeaveRentOnly = "0.001689338"
|
||||
private val rentAmount = "SOL 0.00089088"
|
||||
|
|
@ -90,8 +92,10 @@ class SolanaWarningsTest : BaseTestCase() {
|
|||
step("Type address in input text field") {
|
||||
onSendAddressScreen { addressTextField.performTextReplacement(SOLANA_RECIPIENT_ADDRESS) }
|
||||
}
|
||||
step("Click on 'Next' button") {
|
||||
onSendAddressScreen { nextButton.clickWithAssertion() }
|
||||
step("Open 'Send confirm' screen via 'Next' button") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
openSendConfirmScreenViaNextButton()
|
||||
}
|
||||
}
|
||||
step("Assert 'Invalid amount warning' is not displayed") {
|
||||
checkSendWarning(
|
||||
|
|
|
|||
|
|
@ -58,8 +58,8 @@ class SearchAndSwapTest : BaseTestCase() {
|
|||
onSwapSelectTokenScreen { marketsTokenWithName(receiveTokenName).clickWithAssertion() }
|
||||
}
|
||||
}
|
||||
step("Click on 'Add' button") {
|
||||
onAddTokenBottomSheet { addButton.performClick() }
|
||||
step("Click on 'Confirm' button") {
|
||||
onAddTokenBottomSheet { confirmButton.performClick() }
|
||||
}
|
||||
step("Assert swap token symbol: '$swapTokenSymbol' is displayed") {
|
||||
onSwapTokenScreen { swapTokenSymbol(swapTokenSymbol).assertIsDisplayed() }
|
||||
|
|
@ -106,8 +106,8 @@ class SearchAndSwapTest : BaseTestCase() {
|
|||
onSwapSelectTokenScreen { marketsTokenWithName(receiveTokenName).clickWithAssertion() }
|
||||
}
|
||||
}
|
||||
step("Click on 'Add' button") {
|
||||
onAddTokenBottomSheet { addButton.performClick() }
|
||||
step("Click on 'Confirm' button") {
|
||||
onAddTokenBottomSheet { confirmButton.performClick() }
|
||||
}
|
||||
step("Assert swap token symbol: '$swapTokenSymbol' is displayed") {
|
||||
onSwapTokenScreen { swapTokenSymbol(swapTokenSymbol).assertIsDisplayed() }
|
||||
|
|
@ -195,8 +195,8 @@ class SearchAndSwapTest : BaseTestCase() {
|
|||
onSwapSelectTokenScreen { marketsTokenWithName(receiveTokenName).clickWithAssertion() }
|
||||
}
|
||||
}
|
||||
step("Click on 'Add' button") {
|
||||
onAddTokenBottomSheet { addButton.performClick() }
|
||||
step("Click on 'Confirm' button") {
|
||||
onAddTokenBottomSheet { confirmButton.performClick() }
|
||||
}
|
||||
step("Assert warning title '$warningTitle' is displayed") {
|
||||
onSwapTokenScreen { warningTitle(warningTitle).assertIsDisplayed() }
|
||||
|
|
@ -251,8 +251,8 @@ class SearchAndSwapTest : BaseTestCase() {
|
|||
onSwapSelectTokenScreen { marketsTokenWithName(swapTokenName).performClick() }
|
||||
}
|
||||
}
|
||||
step("Click on 'Add' button") {
|
||||
onAddTokenBottomSheet { addButton.performClick() }
|
||||
step("Click on 'Confirm' button") {
|
||||
onAddTokenBottomSheet { confirmButton.performClick() }
|
||||
}
|
||||
step("Assert swap token symbol: '$swapTokenSymbol' is displayed") {
|
||||
onSwapTokenScreen { swapTokenSymbol(swapTokenSymbol).assertIsDisplayed() }
|
||||
|
|
|
|||
|
|
@ -84,8 +84,8 @@ class SwapTokenScreenWarningsTest : BaseTestCase() {
|
|||
val tokenTitle = "USDC"
|
||||
val inputAmount = "1000"
|
||||
val tokensScenarioState = "SolanaUSDC"
|
||||
val balanceScenarioName = "solana_balance"
|
||||
val balanceScenarioState = "Empty"
|
||||
val balanceScenarioName = "solana_get_account_info_recipient"
|
||||
val balanceScenarioState = "ZeroBalance"
|
||||
val pairsScenarioName = "solana_from_pairs"
|
||||
val pairsScenarioState = "DexProvider"
|
||||
val networkName = "Solana"
|
||||
|
|
@ -554,7 +554,7 @@ class SwapTokenScreenWarningsTest : BaseTestCase() {
|
|||
fun solanaRemainingBalanceLessThanRentAmountTest() {
|
||||
val tokenTitle = "Solana"
|
||||
val receiveTokenName = "USDC"
|
||||
val inputAmount = "0.0016941"
|
||||
val inputAmount = "0.0372"
|
||||
val tokensScenarioState = "SolanaUSDC"
|
||||
val rentAmount = "SOL 0.00089088"
|
||||
val notificationTitle = getResourceString(R.string.send_notification_invalid_amount_title)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,204 @@
|
|||
package com.tangem.tests.tangempay
|
||||
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.constants.TestConstants.TANGEM_PAY_ELIGIBILITY_SCENARIO
|
||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||
import com.tangem.common.extensions.assertTextContainsSafe
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.common.utils.resetWireMockScenarioState
|
||||
import com.tangem.common.utils.resetWireMockScenarios
|
||||
import com.tangem.common.utils.setWireMockScenarioState
|
||||
import com.tangem.core.res.R as CoreResR
|
||||
import com.tangem.scenarios.openTangemPayDailyLimitSetup
|
||||
import com.tangem.screens.onDialog
|
||||
import com.tangem.screens.tangempay.onTangemPayCardPageScreen
|
||||
import com.tangem.screens.tangempay.onTangemPayDailyLimitScreen
|
||||
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 TangemPayDailyLimitTest : BaseTestCase() {
|
||||
|
||||
private val dailyLimitScenario = "tangem_pay_daily_limit"
|
||||
private val highLimitState = "HighLimit"
|
||||
private val setErrorState = "SetError"
|
||||
private val eligibilityState = "PaeraCustomer"
|
||||
|
||||
@AllureId("9726")
|
||||
@DisplayName("Tangem Pay: daily limit screen is displayed correctly")
|
||||
@Test
|
||||
fun dailyLimitScreenIsDisplayedCorrectlyTest() {
|
||||
setupHooks(
|
||||
additionalBeforeSection = {
|
||||
resetWireMockScenarios()
|
||||
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO, eligibilityState)
|
||||
setWireMockScenarioState(dailyLimitScenario, highLimitState)
|
||||
},
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO)
|
||||
resetWireMockScenarioState(dailyLimitScenario)
|
||||
},
|
||||
).run {
|
||||
step("Open daily limit setup screen") { openTangemPayDailyLimitSetup() }
|
||||
step("Assert amount field is displayed") {
|
||||
onTangemPayDailyLimitScreen { amountField.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert hint is displayed") {
|
||||
onTangemPayDailyLimitScreen { hint.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Set limits' button is displayed") {
|
||||
onTangemPayDailyLimitScreen { setLimitsButton.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert quick value preset '1' is displayed") {
|
||||
onTangemPayDailyLimitScreen { presetChip("1").assertIsDisplayed() }
|
||||
}
|
||||
step("Assert quick value preset '5000' is displayed") {
|
||||
onTangemPayDailyLimitScreen { presetChip("5000").assertIsDisplayed() }
|
||||
}
|
||||
step("Assert quick value preset '10000' is displayed") {
|
||||
onTangemPayDailyLimitScreen { presetChip("10000").assertIsDisplayed() }
|
||||
}
|
||||
step("Assert quick value preset '25000' is displayed") {
|
||||
onTangemPayDailyLimitScreen { presetChip("25000").assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("9724")
|
||||
@DisplayName("Tangem Pay: selected quick value is applied to the amount field")
|
||||
@Test
|
||||
fun selectedQuickValueIsAppliedToAmountFieldTest() {
|
||||
val presetValue = "5000"
|
||||
|
||||
setupHooks(
|
||||
additionalBeforeSection = {
|
||||
resetWireMockScenarios()
|
||||
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO, eligibilityState)
|
||||
setWireMockScenarioState(dailyLimitScenario, highLimitState)
|
||||
},
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO)
|
||||
resetWireMockScenarioState(dailyLimitScenario)
|
||||
},
|
||||
).run {
|
||||
step("Open daily limit setup screen") { openTangemPayDailyLimitSetup() }
|
||||
step("Click on '$presetValue' quick value preset") {
|
||||
onTangemPayDailyLimitScreen { presetChip(presetValue).clickWithAssertion() }
|
||||
}
|
||||
step("Assert amount field contains '$presetValue'") {
|
||||
onTangemPayDailyLimitScreen { amountField.assertTextContainsSafe(presetValue) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("9725")
|
||||
@DisplayName("Tangem Pay: 'Set limits' is disabled when the amount is above the limit")
|
||||
@Test
|
||||
fun setLimitsIsDisabledWhenAmountAboveLimitTest() {
|
||||
val amountAboveLimit = "300000"
|
||||
|
||||
setupHooks(
|
||||
additionalBeforeSection = {
|
||||
resetWireMockScenarios()
|
||||
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO, eligibilityState)
|
||||
setWireMockScenarioState(dailyLimitScenario, highLimitState)
|
||||
},
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO)
|
||||
resetWireMockScenarioState(dailyLimitScenario)
|
||||
},
|
||||
).run {
|
||||
step("Open daily limit setup screen") { openTangemPayDailyLimitSetup() }
|
||||
step("Enter amount '$amountAboveLimit' above the limit") {
|
||||
onTangemPayDailyLimitScreen { amountField.performTextReplacement(amountAboveLimit) }
|
||||
}
|
||||
step("Assert 'Set limits' button is not enabled") {
|
||||
onTangemPayDailyLimitScreen { setLimitsButton.assertIsNotEnabled() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("9739")
|
||||
@DisplayName("Tangem Pay: successful daily limit setup")
|
||||
@Test
|
||||
fun successfulDailyLimitSetupTest() {
|
||||
val newLimit = "5000"
|
||||
val displayedLimit = "5,000"
|
||||
|
||||
setupHooks(
|
||||
additionalBeforeSection = {
|
||||
resetWireMockScenarios()
|
||||
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO, eligibilityState)
|
||||
setWireMockScenarioState(dailyLimitScenario, highLimitState)
|
||||
},
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO)
|
||||
resetWireMockScenarioState(dailyLimitScenario)
|
||||
},
|
||||
).run {
|
||||
step("Open daily limit setup screen") { openTangemPayDailyLimitSetup() }
|
||||
step("Enter amount '$newLimit'") {
|
||||
onTangemPayDailyLimitScreen { amountField.performTextReplacement(newLimit) }
|
||||
}
|
||||
step("Click on 'Set limits' button") {
|
||||
onTangemPayDailyLimitScreen { setLimitsButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert daily limit success screen is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTangemPayDailyLimitScreen { successTitle.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Click on 'Done' button") {
|
||||
onTangemPayDailyLimitScreen { doneButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert daily limit value contains '$displayedLimit'") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTangemPayCardPageScreen {
|
||||
dailyLimitValue.assertTextContainsSafe(displayedLimit, substring = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("9727")
|
||||
@DisplayName("Tangem Pay: error is shown when the limit change fails")
|
||||
@Test
|
||||
fun errorIsShownWhenLimitChangeFailsTest() {
|
||||
val newLimit = "5000"
|
||||
val errorTitle = getResourceString(CoreResR.string.common_something_went_wrong)
|
||||
|
||||
setupHooks(
|
||||
additionalBeforeSection = {
|
||||
resetWireMockScenarios()
|
||||
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO, eligibilityState)
|
||||
setWireMockScenarioState(dailyLimitScenario, setErrorState)
|
||||
},
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO)
|
||||
resetWireMockScenarioState(dailyLimitScenario)
|
||||
},
|
||||
).run {
|
||||
step("Open daily limit setup screen") { openTangemPayDailyLimitSetup() }
|
||||
step("Enter amount '$newLimit'") {
|
||||
onTangemPayDailyLimitScreen { amountField.performTextReplacement(newLimit) }
|
||||
}
|
||||
step("Click on 'Set limits' button") {
|
||||
onTangemPayDailyLimitScreen { setLimitsButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert limit change error dialog is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onDialog {
|
||||
title.assertTextContainsSafe(errorTitle)
|
||||
}
|
||||
}
|
||||
}
|
||||
step("Click on 'OK' button") {
|
||||
onDialog { okButton.clickWithAssertion() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,220 @@
|
|||
package com.tangem.tests.tangempay
|
||||
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.constants.TestConstants.SVS_SEED_PHRASE_12
|
||||
import com.tangem.common.constants.TestConstants.TANGEM_PAY_ACCESS_CODE
|
||||
import com.tangem.common.constants.TestConstants.TANGEM_PAY_ELIGIBILITY_CHANNELS_SCENARIO
|
||||
import com.tangem.common.constants.TestConstants.TANGEM_PAY_ELIGIBILITY_SCENARIO
|
||||
import com.tangem.common.constants.TestConstants.TANGEM_PAY_KYC_STATUS_SCENARIO
|
||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_SHORT
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.common.utils.resetWireMockScenarioState
|
||||
import com.tangem.common.utils.setWireMockScenarioState
|
||||
import com.tangem.scenarios.openMainScreenWithExistingHotWallet
|
||||
import com.tangem.screens.onDetailsScreen
|
||||
import com.tangem.screens.onMainScreen
|
||||
import com.tangem.screens.onMainScreenTopBar
|
||||
import com.tangem.screens.tangempay.onTangemPayKycSheet
|
||||
import com.tangem.screens.tangempay.onTangemPayMainScreen
|
||||
import com.tangem.screens.tangempay.onTangemPayOnboardingScreen
|
||||
import com.tangem.core.res.R as CoreResR
|
||||
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 TangemPayOnboardingKycTest : BaseTestCase() {
|
||||
|
||||
@AllureId("9630")
|
||||
@DisplayName("Tangem Pay: existing customer authorization shows the tile on Main")
|
||||
@Test
|
||||
fun existingUserAuthorizationShowsTangemPayTileOnMainTest() {
|
||||
val paeraCustomerState = "PaeraCustomer"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO)
|
||||
},
|
||||
).run {
|
||||
step("Set WireMock scenario '$TANGEM_PAY_ELIGIBILITY_SCENARIO' to '$paeraCustomerState'") {
|
||||
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO, paeraCustomerState)
|
||||
}
|
||||
step("Open 'Main' screen with existing hot wallet") {
|
||||
openMainScreenWithExistingHotWallet(SVS_SEED_PHRASE_12, accessCode = TANGEM_PAY_ACCESS_CODE)
|
||||
}
|
||||
step("Assert Tangem Pay tile is displayed on Main") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTangemPayMainScreen { mainScreenTile.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("9632")
|
||||
@DisplayName("Tangem Pay: Get Tangem Pay banner is shown on Main for the BANNER channel")
|
||||
@Test
|
||||
fun getTangemPayBannerShownOnMainWhenBannerChannelReceivedTest() {
|
||||
val eligibilityStartedState = "Started"
|
||||
val channelsBannerState = "Banner"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO)
|
||||
resetWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_CHANNELS_SCENARIO)
|
||||
},
|
||||
).run {
|
||||
step("Set WireMock scenario '$TANGEM_PAY_ELIGIBILITY_SCENARIO' to '$eligibilityStartedState'") {
|
||||
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO, eligibilityStartedState)
|
||||
}
|
||||
step("Set WireMock scenario '$TANGEM_PAY_ELIGIBILITY_CHANNELS_SCENARIO' to '$channelsBannerState'") {
|
||||
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_CHANNELS_SCENARIO, channelsBannerState)
|
||||
}
|
||||
step("Open 'Main' screen with existing hot wallet") {
|
||||
openMainScreenWithExistingHotWallet(SVS_SEED_PHRASE_12, accessCode = TANGEM_PAY_ACCESS_CODE)
|
||||
}
|
||||
step("Assert 'Get Tangem Pay' banner is displayed on Main") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onMainScreen { getTangemPayBanner.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("9673")
|
||||
@DisplayName("Tangem Pay: onboarding opens from app Details for the DETAILS channel")
|
||||
@Test
|
||||
fun onboardingOpensFromAppDetailsWhenDetailsChannelReceivedTest() {
|
||||
val eligibilityStartedState = "Started"
|
||||
val channelsBannerAndDetailsState = "BannerAndDetails"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO)
|
||||
resetWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_CHANNELS_SCENARIO)
|
||||
},
|
||||
).run {
|
||||
step("Set WireMock scenario '$TANGEM_PAY_ELIGIBILITY_SCENARIO' to '$eligibilityStartedState'") {
|
||||
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO, eligibilityStartedState)
|
||||
}
|
||||
step("Set WireMock scenario '$TANGEM_PAY_ELIGIBILITY_CHANNELS_SCENARIO' to '$channelsBannerAndDetailsState'") {
|
||||
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_CHANNELS_SCENARIO, channelsBannerAndDetailsState)
|
||||
}
|
||||
step("Open 'Main' screen with existing hot wallet") {
|
||||
openMainScreenWithExistingHotWallet(SVS_SEED_PHRASE_12, accessCode = TANGEM_PAY_ACCESS_CODE)
|
||||
}
|
||||
step("Click on 'More' button") {
|
||||
onMainScreenTopBar { moreButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Get Tangem Pay' row is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onDetailsScreen { getTangemPayRow.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Click on 'Get Tangem Pay' row") {
|
||||
onDetailsScreen { getTangemPayRow.clickWithAssertion() }
|
||||
}
|
||||
step("Assert Tangem Pay onboarding screen is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTangemPayOnboardingScreen { title.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("9643")
|
||||
@DisplayName("Tangem Pay: KYC in progress sheet closes and the status remains on Main")
|
||||
@Test
|
||||
fun kycInProgressSheetClosesAndStatusRemainsOnMainTest() {
|
||||
val paeraCustomerState = "PaeraCustomer"
|
||||
val kycInProgressState = "InProgress"
|
||||
val kycInProgressText = getResourceString(CoreResR.string.tangempay_kyc_in_progress)
|
||||
val viewStatusText = getResourceString(CoreResR.string.tangempay_kyc_in_progress_notification_button)
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO)
|
||||
resetWireMockScenarioState(TANGEM_PAY_KYC_STATUS_SCENARIO)
|
||||
},
|
||||
).run {
|
||||
step("Set WireMock scenario '$TANGEM_PAY_ELIGIBILITY_SCENARIO' to '$paeraCustomerState'") {
|
||||
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO, paeraCustomerState)
|
||||
}
|
||||
step("Set WireMock scenario '$TANGEM_PAY_KYC_STATUS_SCENARIO' to '$kycInProgressState'") {
|
||||
setWireMockScenarioState(TANGEM_PAY_KYC_STATUS_SCENARIO, kycInProgressState)
|
||||
}
|
||||
step("Open 'Main' screen with existing hot wallet") {
|
||||
openMainScreenWithExistingHotWallet(SVS_SEED_PHRASE_12, accessCode = TANGEM_PAY_ACCESS_CODE)
|
||||
}
|
||||
step("Assert Tangem Pay tile with '$kycInProgressText' is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTangemPayMainScreen { tileWithSubtitle(kycInProgressText).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Click on Tangem Pay tile") {
|
||||
onTangemPayMainScreen { mainScreenTile.clickWithAssertion() }
|
||||
}
|
||||
step("Assert KYC in progress sheet is displayed") {
|
||||
onTangemPayKycSheet {
|
||||
title(kycInProgressText).assertIsDisplayed()
|
||||
primaryButtonWithText(viewStatusText).assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
step("Click on 'Close' button") {
|
||||
onTangemPayKycSheet { closeButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert KYC in progress sheet is not displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_SHORT) {
|
||||
onTangemPayKycSheet { primaryButtonWithText(viewStatusText).assertDoesNotExist() }
|
||||
}
|
||||
}
|
||||
step("Assert Tangem Pay tile with '$kycInProgressText' is still displayed") {
|
||||
onTangemPayMainScreen { tileWithSubtitle(kycInProgressText).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("9517")
|
||||
@DisplayName("Tangem Pay: KYC rejected status shown on Main and in the sheet")
|
||||
@Test
|
||||
fun kycRejectedStatusShownOnMainAndInSheetTest() {
|
||||
val paeraCustomerState = "PaeraCustomer"
|
||||
val kycDeclinedState = "Declined"
|
||||
val kycRejectedTileText = getResourceString(CoreResR.string.tangempay_kyc_has_failed)
|
||||
val rejectedSheetTitle = getResourceString(CoreResR.string.tangempay_kyc_rejected)
|
||||
val goToSupportText = getResourceString(CoreResR.string.tangempay_go_to_support)
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO)
|
||||
resetWireMockScenarioState(TANGEM_PAY_KYC_STATUS_SCENARIO)
|
||||
},
|
||||
).run {
|
||||
step("Set WireMock scenario '$TANGEM_PAY_ELIGIBILITY_SCENARIO' to '$paeraCustomerState'") {
|
||||
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO, paeraCustomerState)
|
||||
}
|
||||
step("Set WireMock scenario '$TANGEM_PAY_KYC_STATUS_SCENARIO' to '$kycDeclinedState'") {
|
||||
setWireMockScenarioState(TANGEM_PAY_KYC_STATUS_SCENARIO, kycDeclinedState)
|
||||
}
|
||||
step("Open 'Main' screen with existing hot wallet") {
|
||||
openMainScreenWithExistingHotWallet(SVS_SEED_PHRASE_12, accessCode = TANGEM_PAY_ACCESS_CODE)
|
||||
}
|
||||
step("Assert Tangem Pay tile with '$kycRejectedTileText' is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTangemPayMainScreen { tileWithSubtitle(kycRejectedTileText).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Click on Tangem Pay tile") {
|
||||
onTangemPayMainScreen { mainScreenTile.clickWithAssertion() }
|
||||
}
|
||||
step("Assert KYC rejected sheet is displayed") {
|
||||
onTangemPayKycSheet {
|
||||
title(rejectedSheetTitle).assertIsDisplayed()
|
||||
primaryButtonWithText(goToSupportText).assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,299 @@
|
|||
package com.tangem.tests.tangempay
|
||||
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.constants.TestConstants.TANGEM_PAY_ELIGIBILITY_SCENARIO
|
||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
|
||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_VERY_LONG
|
||||
import com.tangem.common.extensions.assertTextContainsSafe
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.common.extensions.extractText
|
||||
import com.tangem.common.utils.resetWireMockScenarioState
|
||||
import com.tangem.common.utils.resetWireMockScenarios
|
||||
import com.tangem.common.utils.setWireMockScenarioState
|
||||
import com.tangem.scenarios.*
|
||||
import com.tangem.screens.onTokenReceiveWarningBottomSheet
|
||||
import com.tangem.screens.tangempay.*
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import io.qameta.allure.kotlin.AllureId
|
||||
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
|
||||
@HiltAndroidTest
|
||||
class TangemPayReissueTest : BaseTestCase() {
|
||||
|
||||
private val eligibilityState = "PaeraCustomer"
|
||||
private val balanceScenario = "tangem_pay_balance_update"
|
||||
private val balanceInitialState = "InitialBalance"
|
||||
private val cardReissueScenario = "tangem_pay_card_reissue"
|
||||
private val cardReissueStartedState = "Started"
|
||||
private val cardReissueFeeErrorState = "FeeError"
|
||||
private val reissueOrderScenario = "tangem_pay_reissue_order"
|
||||
private val reissueOrderStartedState = "Started"
|
||||
private val reissueOrderCompletedState = "Completed"
|
||||
private val reissueScenario = "tangem_pay_reissue"
|
||||
private val reissueCompletedState = "Completed"
|
||||
private val historyScenario = "tangem_pay_transaction_history"
|
||||
private val historyInitialEmptyState = "InitialEmpty"
|
||||
private val historyAfterReissueFeeState = "AfterReissueFee"
|
||||
|
||||
@AllureId("9749")
|
||||
@DisplayName("Tangem Pay: card reissue bottom sheet displays replace card details")
|
||||
@Test
|
||||
fun reissueBottomSheetDisplaysReplaceCardDetailsTest() {
|
||||
setupHooks(
|
||||
additionalBeforeSection = {
|
||||
resetWireMockScenarios()
|
||||
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO, eligibilityState)
|
||||
setWireMockScenarioState(balanceScenario, balanceInitialState)
|
||||
setWireMockScenarioState(cardReissueScenario, cardReissueStartedState)
|
||||
},
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO)
|
||||
resetWireMockScenarioState(balanceScenario)
|
||||
resetWireMockScenarioState(cardReissueScenario)
|
||||
},
|
||||
).run {
|
||||
step("Open Tangem Pay card page") { openTangemPayCardPage() }
|
||||
step("Open the 'Replace card' reissue bottom sheet") { openReissueSheet() }
|
||||
step("Assert reissue sheet title, description and fee label are displayed") {
|
||||
onTangemPayReissueSheet {
|
||||
title.assertIsDisplayed()
|
||||
description.assertIsDisplayed()
|
||||
feeLabel.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
step("Assert replacement fee value is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
||||
onTangemPayReissueSheet { feeValue.assertTextContainsSafe("1.00", substring = true) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("9833")
|
||||
@DisplayName("Tangem Pay: error is shown when reissue cost is not fetched")
|
||||
@Test
|
||||
fun reissueFeeErrorShowsErrorWhenFeeRequestFailsTest() {
|
||||
setupHooks(
|
||||
additionalBeforeSection = {
|
||||
resetWireMockScenarios()
|
||||
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO, eligibilityState)
|
||||
setWireMockScenarioState(cardReissueScenario, cardReissueFeeErrorState)
|
||||
},
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO)
|
||||
resetWireMockScenarioState(cardReissueScenario)
|
||||
},
|
||||
).run {
|
||||
step("Open Tangem Pay card page") { openTangemPayCardPage() }
|
||||
step("Open the 'Replace card' reissue bottom sheet") { openReissueSheet() }
|
||||
step("Assert fee unreachable error and 'Refresh' button are displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
||||
onTangemPayReissueSheet {
|
||||
feeErrorTitle.assertIsDisplayed()
|
||||
refreshButton.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("9752")
|
||||
@DisplayName("Tangem Pay: reissue fee transaction appears in history and details")
|
||||
@Test
|
||||
fun reissueFeeTransactionAppearsInHistoryAndDetailsTest() {
|
||||
val feeMerchantName = "Card replacement fee"
|
||||
|
||||
setupHooks(
|
||||
additionalBeforeSection = {
|
||||
resetWireMockScenarios()
|
||||
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO, eligibilityState)
|
||||
setWireMockScenarioState(balanceScenario, balanceInitialState)
|
||||
setWireMockScenarioState(cardReissueScenario, cardReissueStartedState)
|
||||
setWireMockScenarioState(reissueOrderScenario, reissueOrderStartedState)
|
||||
setWireMockScenarioState(historyScenario, historyInitialEmptyState)
|
||||
},
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO)
|
||||
resetWireMockScenarioState(balanceScenario)
|
||||
resetWireMockScenarioState(cardReissueScenario)
|
||||
resetWireMockScenarioState(reissueOrderScenario)
|
||||
resetWireMockScenarioState(historyScenario)
|
||||
},
|
||||
).run {
|
||||
step("Open Tangem Pay card page") { openTangemPayCardPage() }
|
||||
step("Open the 'Replace card' reissue bottom sheet") { openReissueSheet() }
|
||||
step("Wait for replacement fee to load") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
||||
onTangemPayReissueSheet { feeValue.assertTextContainsSafe("1.00", substring = true) }
|
||||
}
|
||||
}
|
||||
step("Click on 'Replace card' confirm button") {
|
||||
onTangemPayReissueSheet { confirmButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert reissue started and the sheet is dismissed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
||||
onTangemPayCardPageScreen { reissueInProgressBlock.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Return to the payment account screen") {
|
||||
device.uiDevice.pressBack()
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
||||
onTangemPayMainScreen { balance.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Switch WireMock scenario '$historyScenario' to '$historyAfterReissueFeeState'") {
|
||||
setWireMockScenarioState(historyScenario, historyAfterReissueFeeState)
|
||||
}
|
||||
step("Pull to refresh Tangem Pay") { pullToRefreshTangemPay() }
|
||||
step("Assert '$feeMerchantName' transaction is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
||||
onTangemPayMainScreen { transactionRowWithText(feeMerchantName).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Click on '$feeMerchantName' transaction row") {
|
||||
onTangemPayMainScreen { transactionRowWithText(feeMerchantName).performClick() }
|
||||
}
|
||||
step("Assert fee transaction details are displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
||||
onTangemPayTransactionDetailsSheet {
|
||||
feeTitle.assertIsDisplayed()
|
||||
serviceFeesCategory.assertIsDisplayed()
|
||||
amount.assertTextContainsSafe("1.00", substring = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("9743")
|
||||
@DisplayName("Tangem Pay: card reissue end-to-end replaces card with new details")
|
||||
@Test
|
||||
fun reissueEndToEndReplacesCardWithNewDetailsTest() {
|
||||
val reissuedCardLastDigits = "5353"
|
||||
val balanceAfterWithdrawState = "AfterWithdraw"
|
||||
|
||||
// AfterWithdraw funds the fee check without a customer/me stub, so reissued customer/me wins.
|
||||
setupHooks(
|
||||
additionalBeforeSection = {
|
||||
resetWireMockScenarios()
|
||||
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO, eligibilityState)
|
||||
setWireMockScenarioState(balanceScenario, balanceAfterWithdrawState)
|
||||
setWireMockScenarioState(cardReissueScenario, cardReissueStartedState)
|
||||
setWireMockScenarioState(reissueOrderScenario, reissueOrderStartedState)
|
||||
},
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO)
|
||||
resetWireMockScenarioState(balanceScenario)
|
||||
resetWireMockScenarioState(cardReissueScenario)
|
||||
resetWireMockScenarioState(reissueOrderScenario)
|
||||
resetWireMockScenarioState(reissueScenario)
|
||||
},
|
||||
).run {
|
||||
var oldCardNumber = ""
|
||||
step("Open Tangem Pay card page") { openTangemPayCardPage() }
|
||||
step("Read the current card number") {
|
||||
onTangemPayCardPageScreen { cardNumberShort.assertIsDisplayed() }
|
||||
onTangemPayCardPageScreen { oldCardNumber = cardNumberShort.extractText() }
|
||||
}
|
||||
step("Open the 'Replace card' reissue bottom sheet") { openReissueSheet() }
|
||||
step("Wait for replacement fee to load") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
||||
onTangemPayReissueSheet { feeValue.assertTextContainsSafe("1.00", substring = true) }
|
||||
}
|
||||
}
|
||||
step("Click on 'Replace card' confirm button") {
|
||||
onTangemPayReissueSheet { confirmButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert reissue started (sheet dismissed, card is replacing)") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
||||
onTangemPayReissueSheet { confirmButton.assertDoesNotExist() }
|
||||
onTangemPayCardPageScreen { reissueInProgressBlock.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Switch WireMock scenario '$reissueOrderScenario' to '$reissueOrderCompletedState'") {
|
||||
setWireMockScenarioState(reissueOrderScenario, reissueOrderCompletedState)
|
||||
}
|
||||
step("Switch WireMock scenario '$reissueScenario' to '$reissueCompletedState'") {
|
||||
setWireMockScenarioState(reissueScenario, reissueCompletedState)
|
||||
}
|
||||
step("Return to the payment account screen") {
|
||||
device.uiDevice.pressBack()
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
||||
onTangemPayMainScreen { balance.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Refresh until the card leaves the replacing state") {
|
||||
pullToRefreshTangemPay()
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_VERY_LONG) {
|
||||
onTangemPayMainScreen { reissueInProgressBanner.assertDoesNotExist() }
|
||||
}
|
||||
}
|
||||
step("Reopen the reissued card") {
|
||||
onTangemPayMainScreen { cardButton.clickWithAssertion() }
|
||||
}
|
||||
var newCardNumber = ""
|
||||
step("Read the new card number") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
||||
onTangemPayCardPageScreen {
|
||||
cardNumberShort.assertTextContainsSafe(reissuedCardLastDigits, substring = true)
|
||||
}
|
||||
}
|
||||
onTangemPayCardPageScreen { newCardNumber = cardNumberShort.extractText() }
|
||||
}
|
||||
step("Assert the card number changed after reissue") {
|
||||
Assert.assertNotEquals(oldCardNumber, newCardNumber)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("9746")
|
||||
@DisplayName("Tangem Pay: top up via card reissue when unable to cover fee")
|
||||
@Test
|
||||
fun reissueTopUpUnableToCoverFeeAddFundsReceiveTest() {
|
||||
val depositNetworkName = "Polygon"
|
||||
|
||||
setupHooks(
|
||||
additionalBeforeSection = {
|
||||
resetWireMockScenarios()
|
||||
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO, eligibilityState)
|
||||
setWireMockScenarioState(cardReissueScenario, cardReissueStartedState)
|
||||
},
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO)
|
||||
resetWireMockScenarioState(cardReissueScenario)
|
||||
},
|
||||
).run {
|
||||
step("Open Tangem Pay card page") { openTangemPayCardPage() }
|
||||
step("Open the 'Replace card' reissue bottom sheet") { openReissueSheet() }
|
||||
step("Assert 'Unable to cover fee' state is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
||||
onTangemPayReissueSheet { insufficientFundsTitle.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Click on 'Add funds' button") {
|
||||
onTangemPayReissueSheet { addFundsButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Add funds' sheet shows Swap and Receive options") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
||||
onTangemPayAddFundsSheet {
|
||||
swapOption.assertIsDisplayed()
|
||||
receiveOption.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
step("Click on 'Receive' option") {
|
||||
onTangemPayAddFundsSheet { receiveOption.clickWithAssertion() }
|
||||
}
|
||||
step("Assert deposit info for the '$depositNetworkName' network is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
||||
onTokenReceiveWarningBottomSheet {
|
||||
bottomSheet.assertIsDisplayed()
|
||||
networkName(depositNetworkName).assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,12 @@
|
|||
package com.tangem.tests.tangempay
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Instrumentation.ActivityResult
|
||||
import android.content.Intent
|
||||
import androidx.test.espresso.intent.Intents.intended
|
||||
import androidx.test.espresso.intent.Intents.intending
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers.hasData
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.constants.TestConstants.TANGEM_PAY_ELIGIBILITY_SCENARIO
|
||||
|
|
@ -11,11 +18,14 @@ import com.tangem.common.utils.assertClipboardTextEquals
|
|||
import com.tangem.common.utils.resetWireMockScenarioState
|
||||
import com.tangem.common.utils.resetWireMockScenarios
|
||||
import com.tangem.common.utils.setWireMockScenarioState
|
||||
import com.tangem.features.tangempay.TangemPayConstants
|
||||
import com.tangem.scenarios.*
|
||||
import com.tangem.screens.tangempay.*
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import io.qameta.allure.kotlin.AllureId
|
||||
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||
import org.hamcrest.CoreMatchers.allOf
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
|
||||
@HiltAndroidTest
|
||||
|
|
@ -240,4 +250,75 @@ class TangemPayTest : BaseTestCase() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("9592")
|
||||
@DisplayName("Tangem Pay: Terms and fees opens the tariffs document")
|
||||
@Test
|
||||
fun termsAndFeesOpensTariffsFromMoreActionsMenuTest() {
|
||||
val eligibilityState = "PaeraCustomer"
|
||||
|
||||
setupHooks(
|
||||
additionalBeforeSection = {
|
||||
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO, eligibilityState)
|
||||
},
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO)
|
||||
},
|
||||
).run {
|
||||
step("Open Tangem Pay") { openTangemPay() }
|
||||
step("Click on 'More actions' button") {
|
||||
onTangemPayMainScreen { moreActionsButton.clickWithAssertion() }
|
||||
}
|
||||
step("Stub external browser so the tariffs document is not actually opened") {
|
||||
intending(hasAction(Intent.ACTION_VIEW))
|
||||
.respondWith(ActivityResult(Activity.RESULT_OK, null))
|
||||
}
|
||||
step("Click on 'Terms and fees' menu item") {
|
||||
onTangemPayMainScreen { termsAndFeesMenuItem.clickWithAssertion() }
|
||||
}
|
||||
step("Assert the tariffs document is opened in an external browser") {
|
||||
intended(allOf(hasAction(Intent.ACTION_VIEW), hasData(TangemPayConstants.TERMS_AND_LIMITS_LINK)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// iOS-only: Android has no 'Service temporarily unavailable' sheet and no empty-depositAddress guard in Add funds.
|
||||
@Ignore("[REDACTED_JIRA]")
|
||||
@AllureId("9557")
|
||||
@DisplayName("Tangem Pay: service unavailable error when customer has no deposit address")
|
||||
@Test
|
||||
fun addFundsShowsServiceUnavailableErrorWhenNoDepositAddressTest() {
|
||||
val depositAddressScenario = "tangem_pay_deposit_address"
|
||||
val noDepositAddressState = "NoDepositAddress"
|
||||
val eligibilityState = "PaeraCustomer"
|
||||
|
||||
setupHooks(
|
||||
additionalBeforeSection = {
|
||||
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO, eligibilityState)
|
||||
setWireMockScenarioState(depositAddressScenario, noDepositAddressState)
|
||||
},
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO)
|
||||
resetWireMockScenarioState(depositAddressScenario)
|
||||
},
|
||||
).run {
|
||||
step("Open Tangem Pay") { openTangemPay() }
|
||||
step("Click on 'Add funds' button") {
|
||||
onTangemPayMainScreen { topUpButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Service temporarily unavailable' sheet is displayed") {
|
||||
onTangemPayServiceUnavailableSheet {
|
||||
title.assertIsDisplayed()
|
||||
description.assertIsDisplayed()
|
||||
gotItButton.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
step("Click on 'Got it' button") {
|
||||
onTangemPayServiceUnavailableSheet { gotItButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Service temporarily unavailable' sheet is not displayed") {
|
||||
onTangemPayServiceUnavailableSheet { title.assertDoesNotExist() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,268 @@
|
|||
package com.tangem.tests.tokenDetails
|
||||
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.constants.TestConstants.QUOTES_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.scenarios.assertQrCodeEncodesDisplayedAddress
|
||||
import com.tangem.scenarios.assertQrCodesMatchForBothAddressTypes
|
||||
import com.tangem.scenarios.goToQrCodeBottomSheet
|
||||
import com.tangem.scenarios.openReceiveViaAddFunds
|
||||
import com.tangem.scenarios.openTokenDetails
|
||||
import com.tangem.screens.onTokenDetailsScreen
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import io.qameta.allure.kotlin.AllureId
|
||||
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||
import org.junit.Test
|
||||
|
||||
@HiltAndroidTest
|
||||
class TokenDetailsAddressesTest : BaseTestCase() {
|
||||
|
||||
@AllureId("4947")
|
||||
@DisplayName("Token details (address): QR code encodes the displayed address (Bitcoin, 2 address types)")
|
||||
@Test
|
||||
fun qrCodeEncodesDisplayedAddressBitcoinTest() {
|
||||
val tokenName = "Bitcoin"
|
||||
|
||||
setupHooks().run {
|
||||
step("Open token details for '$tokenName'") {
|
||||
openTokenDetails(tokenName)
|
||||
}
|
||||
step("Open receive via 'Add funds'") {
|
||||
openReceiveViaAddFunds()
|
||||
}
|
||||
step("Assert QR codes match for both address types") {
|
||||
assertQrCodesMatchForBothAddressTypes()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("10218")
|
||||
@DisplayName("Token details (address): QR code encodes the displayed address (Cosmos)")
|
||||
@Test
|
||||
fun qrCodeEncodesDisplayedAddressCosmosTest() {
|
||||
val tokenName = "Cosmos"
|
||||
val networksProvidersScenario = "networks_providers"
|
||||
val appTransfersNetworksState = "AppTransfersNetworks"
|
||||
|
||||
setupHooks(
|
||||
additionalBeforeAppLaunchSection = {
|
||||
setWireMockScenarioState(networksProvidersScenario, appTransfersNetworksState)
|
||||
},
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||
resetWireMockScenarioState(networksProvidersScenario)
|
||||
},
|
||||
).run {
|
||||
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$tokenName'") {
|
||||
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = tokenName)
|
||||
}
|
||||
step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: '$appTransfersNetworksState'") {
|
||||
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = appTransfersNetworksState)
|
||||
}
|
||||
|
||||
step("Open token details for '$tokenName'") {
|
||||
openTokenDetails(tokenName)
|
||||
}
|
||||
step("Open receive via 'Add funds'") {
|
||||
openReceiveViaAddFunds()
|
||||
}
|
||||
step("Go to QR code bottom sheet") {
|
||||
goToQrCodeBottomSheet()
|
||||
}
|
||||
step("Assert QR code encodes the displayed address") {
|
||||
assertQrCodeEncodesDisplayedAddress()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("10219")
|
||||
@DisplayName("Token details (address): QR code encodes the displayed address (Kaspa)")
|
||||
@Test
|
||||
fun qrCodeEncodesDisplayedAddressKaspaTest() {
|
||||
val tokenName = "Kaspa"
|
||||
val networksProvidersScenario = "networks_providers"
|
||||
val appTransfersNetworksState = "AppTransfersNetworks"
|
||||
val quotesKaspaState = "Kaspa"
|
||||
val kaspaUtxoScenario = "kaspa_utxo"
|
||||
val kaspaUtxoState = "more_than_84_android"
|
||||
|
||||
setupHooks(
|
||||
additionalBeforeAppLaunchSection = {
|
||||
setWireMockScenarioState(networksProvidersScenario, appTransfersNetworksState)
|
||||
},
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||
resetWireMockScenarioState(networksProvidersScenario)
|
||||
resetWireMockScenarioState(kaspaUtxoScenario)
|
||||
},
|
||||
).run {
|
||||
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$tokenName'") {
|
||||
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = tokenName)
|
||||
}
|
||||
step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: '$quotesKaspaState'") {
|
||||
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = quotesKaspaState)
|
||||
}
|
||||
step("Set WireMock scenario: '$kaspaUtxoScenario' to state: '$kaspaUtxoState'") {
|
||||
setWireMockScenarioState(scenarioName = kaspaUtxoScenario, state = kaspaUtxoState)
|
||||
}
|
||||
|
||||
step("Open token details for '$tokenName'") {
|
||||
openTokenDetails(tokenName)
|
||||
}
|
||||
step("Open receive via 'Add funds'") {
|
||||
openReceiveViaAddFunds()
|
||||
}
|
||||
step("Go to QR code bottom sheet") {
|
||||
goToQrCodeBottomSheet()
|
||||
}
|
||||
step("Assert QR code encodes the displayed address") {
|
||||
assertQrCodeEncodesDisplayedAddress()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("10215")
|
||||
@DisplayName("Token details (address): QR code encodes the displayed address (Litecoin, 2 address types)")
|
||||
@Test
|
||||
fun qrCodeEncodesDisplayedAddressLitecoinTest() {
|
||||
val tokenName = "Litecoin"
|
||||
val userTokensState = "Litecoin"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = { resetWireMockScenarioState(USER_TOKENS_API_SCENARIO) },
|
||||
).run {
|
||||
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$userTokensState'") {
|
||||
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = userTokensState)
|
||||
}
|
||||
|
||||
step("Open token details for '$tokenName'") {
|
||||
openTokenDetails(tokenName)
|
||||
}
|
||||
step("Click on 'Receive' button") {
|
||||
onTokenDetailsScreen { receiveButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert QR codes match for both address types") {
|
||||
assertQrCodesMatchForBothAddressTypes()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("10220")
|
||||
@DisplayName("Token details (address): QR code encodes the displayed address (XDC Network, 2 address types)")
|
||||
@Test
|
||||
fun qrCodeEncodesDisplayedAddressXdcTest() {
|
||||
val tokenName = "XDC Network"
|
||||
val userTokensState = "XDC"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = { resetWireMockScenarioState(USER_TOKENS_API_SCENARIO) },
|
||||
).run {
|
||||
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$userTokensState'") {
|
||||
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = userTokensState)
|
||||
}
|
||||
|
||||
step("Open token details for '$tokenName'") {
|
||||
openTokenDetails(tokenName)
|
||||
}
|
||||
step("Click on 'Receive' button") {
|
||||
onTokenDetailsScreen { receiveButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert QR codes match for both address types") {
|
||||
assertQrCodesMatchForBothAddressTypes()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("10216")
|
||||
@DisplayName("Token details (address): QR code encodes the displayed address (Hedera)")
|
||||
@Test
|
||||
fun qrCodeEncodesDisplayedAddressHederaTest() {
|
||||
val tokenName = "Hedera"
|
||||
val networksProvidersScenario = "networks_providers"
|
||||
val appTransfersNetworksState = "AppTransfersNetworks"
|
||||
|
||||
setupHooks(
|
||||
additionalBeforeAppLaunchSection = {
|
||||
setWireMockScenarioState(networksProvidersScenario, appTransfersNetworksState)
|
||||
},
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||
resetWireMockScenarioState(networksProvidersScenario)
|
||||
},
|
||||
).run {
|
||||
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$tokenName'") {
|
||||
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = tokenName)
|
||||
}
|
||||
step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: '$appTransfersNetworksState'") {
|
||||
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = appTransfersNetworksState)
|
||||
}
|
||||
|
||||
step("Open token details for '$tokenName'") {
|
||||
openTokenDetails(tokenName)
|
||||
}
|
||||
step("Open receive via 'Add funds'") {
|
||||
openReceiveViaAddFunds()
|
||||
}
|
||||
step("Go to QR code bottom sheet") {
|
||||
goToQrCodeBottomSheet()
|
||||
}
|
||||
step("Assert QR code encodes the displayed address") {
|
||||
assertQrCodeEncodesDisplayedAddress()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("10217")
|
||||
@DisplayName("Token details (address): QR code encodes the displayed address (Ethereum)")
|
||||
@Test
|
||||
fun qrCodeEncodesDisplayedAddressEthereumTest() {
|
||||
val tokenName = "Ethereum"
|
||||
|
||||
setupHooks().run {
|
||||
step("Open token details for '$tokenName'") {
|
||||
openTokenDetails(tokenName)
|
||||
}
|
||||
step("Open receive via 'Add funds'") {
|
||||
openReceiveViaAddFunds()
|
||||
}
|
||||
step("Go to QR code bottom sheet") {
|
||||
goToQrCodeBottomSheet()
|
||||
}
|
||||
step("Assert QR code encodes the displayed address") {
|
||||
assertQrCodeEncodesDisplayedAddress()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("10214")
|
||||
@DisplayName("Token details (address): QR code encodes the displayed address (Decimal Smart Chain, 2 address types)")
|
||||
@Test
|
||||
fun qrCodeEncodesDisplayedAddressDecimalTest() {
|
||||
val tokenName = "Decimal Smart Chain"
|
||||
val userTokensState = "Decimal"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = { resetWireMockScenarioState(USER_TOKENS_API_SCENARIO) },
|
||||
).run {
|
||||
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$userTokensState'") {
|
||||
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = userTokensState)
|
||||
}
|
||||
|
||||
step("Open token details for '$tokenName'") {
|
||||
openTokenDetails(tokenName)
|
||||
}
|
||||
step("Click on 'Receive' action") {
|
||||
onTokenDetailsScreen { receiveButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert QR codes match for both address types") {
|
||||
assertQrCodesMatchForBothAddressTypes()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
package com.tangem.tests.tokenDetails
|
||||
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
|
||||
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
|
||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||
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.openMainScreen
|
||||
import com.tangem.scenarios.synchronizeAddresses
|
||||
import com.tangem.screens.onMainScreen
|
||||
import com.tangem.screens.onTokenDetailsScreen
|
||||
import com.tangem.screens.onTokenMarketBlock
|
||||
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 TokenDetailsMarketPriceTests : BaseTestCase() {
|
||||
|
||||
@AllureId("301")
|
||||
@DisplayName("Token details: Market Price block data")
|
||||
@Test
|
||||
fun marketPriceBlockDataTest() {
|
||||
val tokenName = "Dogecoin"
|
||||
val marketPriceTitle = getResourceString(R.string.markets_common_market_price)
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||
}
|
||||
).run {
|
||||
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$tokenName'") {
|
||||
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = tokenName)
|
||||
}
|
||||
step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: '$tokenName'") {
|
||||
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = tokenName)
|
||||
}
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Click on token with name: '$tokenName'") {
|
||||
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Token details' screen is displayed") {
|
||||
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Market Price' block is displayed with title '$marketPriceTitle'") {
|
||||
onTokenMarketBlock {
|
||||
block.assertIsDisplayed()
|
||||
title.assertTextEquals(marketPriceTitle)
|
||||
}
|
||||
}
|
||||
step("Assert price rate is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTokenMarketBlock { price.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Assert 24h price change is displayed") {
|
||||
onTokenMarketBlock { priceChange.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert mini chart is displayed") {
|
||||
onTokenMarketBlock { chart.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
package com.tangem.tests.tokenDetails
|
||||
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.constants.TestConstants.DOGECOIN_RECIPIENT_ADDRESS
|
||||
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
|
||||
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
|
||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||
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.openMainScreen
|
||||
import com.tangem.scenarios.synchronizeAddresses
|
||||
import com.tangem.screens.onMainScreen
|
||||
import com.tangem.screens.onTokenDetailsScreen
|
||||
import com.tangem.screens.onTxHistoryScreen
|
||||
import com.tangem.utils.toBriefAddressFormat
|
||||
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 TokenDetailsTests : BaseTestCase() {
|
||||
|
||||
private val txHistoryScenarioName = "dogecoin_tx_history"
|
||||
|
||||
@AllureId("304")
|
||||
@DisplayName("Token details: active outgoing transaction block")
|
||||
@Test
|
||||
fun activeOutgoingTransactionBlockTest() {
|
||||
val tokenName = "Dogecoin"
|
||||
val currencySymbol = "DOGE"
|
||||
val txHistoryScenarioState = "UnconfirmedOutgoing"
|
||||
val sendingTitle = getResourceString(R.string.common_sending)
|
||||
val recipientBriefAddress = DOGECOIN_RECIPIENT_ADDRESS.toBriefAddressFormat()
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||
resetWireMockScenarioState(txHistoryScenarioName)
|
||||
}
|
||||
).run {
|
||||
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$tokenName'") {
|
||||
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = tokenName)
|
||||
}
|
||||
step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: '$tokenName'") {
|
||||
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = tokenName)
|
||||
}
|
||||
step("Set WireMock scenario: '$txHistoryScenarioName' to state: '$txHistoryScenarioState'") {
|
||||
setWireMockScenarioState(scenarioName = txHistoryScenarioName, state = txHistoryScenarioState)
|
||||
}
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Click on token with name: '$tokenName'") {
|
||||
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Token details' screen is displayed") {
|
||||
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert active outgoing '$sendingTitle' transaction is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTxHistoryScreen { transactionItem(sendingTitle).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Assert active outgoing transaction status is unconfirmed") {
|
||||
onTxHistoryScreen { transactionUnconfirmedStatus(sendingTitle).assertIsDisplayed() }
|
||||
}
|
||||
step("Assert active outgoing transaction amount is displayed in '$currencySymbol'") {
|
||||
onTxHistoryScreen {
|
||||
transactionAmount(sendingTitle).assertIsDisplayed()
|
||||
transactionCurrency(sendingTitle).assertTextEquals(currencySymbol)
|
||||
}
|
||||
}
|
||||
step("Assert active outgoing transaction recipient address '$recipientBriefAddress' is displayed") {
|
||||
onTxHistoryScreen { transactionAddress(sendingTitle, recipientBriefAddress).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -19,7 +19,6 @@ import com.tangem.tap.domain.sdk.mocks.content.Wallet2WithDerivationsMockContent
|
|||
import com.tangem.tap.domain.sdk.mocks.content.WalletMockContent
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||
import io.qameta.allure.kotlin.Allure.step
|
||||
import io.qameta.allure.kotlin.AllureId
|
||||
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||
import org.junit.Ignore
|
||||
|
|
|
|||
|
|
@ -0,0 +1,548 @@
|
|||
package com.tangem.tests.yield
|
||||
|
||||
import androidx.compose.ui.test.ExperimentalTestApi
|
||||
import androidx.compose.ui.test.longClick
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.constants.TestConstants.HOLD_DURATION_MS
|
||||
import com.tangem.common.constants.TestConstants.SVS_SEED_PHRASE_12
|
||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.common.extensions.pullToRefresh
|
||||
import com.tangem.common.utils.resetWireMockScenarioState
|
||||
import com.tangem.common.utils.setWireMockScenarioState
|
||||
import com.tangem.scenarios.openMainScreenWithExistingHotWallet
|
||||
import com.tangem.screens.onMainScreen
|
||||
import com.tangem.screens.onTokenDetailsScreen
|
||||
import com.tangem.screens.onTokenDetailsTopBar
|
||||
import com.tangem.screens.onTxHistoryScreen
|
||||
import com.tangem.screens.onYieldSupplyActiveScreen
|
||||
import com.tangem.screens.onYieldSupplyApproveScreen
|
||||
import com.tangem.screens.onYieldSupplyPromoScreen
|
||||
import com.tangem.screens.onYieldSupplyStartEarningScreen
|
||||
import com.tangem.screens.onYieldSupplyStopEarningScreen
|
||||
import com.tangem.core.res.R as CoreResR
|
||||
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
|
||||
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
@HiltAndroidTest
|
||||
class YieldModeTest : BaseTestCase() {
|
||||
|
||||
@AllureId("7957")
|
||||
@DisplayName("Yield mode: first-time landing activation with zero balance")
|
||||
@Test
|
||||
fun firstTimeLandingActivationZeroBalanceTest() {
|
||||
val tokenTitle = "USDC"
|
||||
val apy = "5.24"
|
||||
|
||||
val portfolioScenario = "user_tokens_api"
|
||||
val portfolioState = "YieldUSDCEthereumZeroBalance"
|
||||
val balancesScenario = "moralis_evm_token_balances_api"
|
||||
val balancesState = "ZeroUsdcEvmBalances"
|
||||
val yieldScenario = "yield_supply_status"
|
||||
val yieldNotActiveState = "NotActive"
|
||||
val yieldActiveState = "Active"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(portfolioScenario)
|
||||
resetWireMockScenarioState(balancesScenario)
|
||||
resetWireMockScenarioState(yieldScenario)
|
||||
}
|
||||
).run {
|
||||
|
||||
step("Set WireMock scenario: '$portfolioScenario' to state: '$portfolioState'") {
|
||||
setWireMockScenarioState(scenarioName = portfolioScenario, state = portfolioState)
|
||||
}
|
||||
step("Set WireMock scenario: '$balancesScenario' to state: '$balancesState'") {
|
||||
setWireMockScenarioState(scenarioName = balancesScenario, state = balancesState)
|
||||
}
|
||||
step("Set WireMock scenario: '$yieldScenario' to state: '$yieldNotActiveState'") {
|
||||
setWireMockScenarioState(scenarioName = yieldScenario, state = yieldNotActiveState)
|
||||
}
|
||||
|
||||
step("Open 'Main Screen' with existing hot wallet") {
|
||||
openMainScreenWithExistingHotWallet(seedPhrase = SVS_SEED_PHRASE_12)
|
||||
}
|
||||
|
||||
step("Click on token with name: '$tokenTitle'") {
|
||||
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Token details' screen is displayed") {
|
||||
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Available yield block' is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTokenDetailsScreen { yieldSupplyAvailableBlock.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
|
||||
step("Click on 'Available yield block'") {
|
||||
onTokenDetailsScreen { yieldSupplyAvailableBlock.clickWithAssertion() }
|
||||
}
|
||||
step("Click on 'Continue' button") {
|
||||
onYieldSupplyPromoScreen { continueButton.clickWithAssertion() }
|
||||
}
|
||||
step("Hold 'Start earning' button to confirm activation") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onYieldSupplyStartEarningScreen {
|
||||
startEarningButton.performTouchInput { longClick(durationMillis = HOLD_DURATION_MS) }
|
||||
startEarningButton.assertIsNotDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
step("Set WireMock scenario: '$yieldScenario' to state: '$yieldActiveState'") {
|
||||
setWireMockScenarioState(scenarioName = yieldScenario, state = yieldActiveState)
|
||||
}
|
||||
step("Assert 'Yield mode enabled' block is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTokenDetailsScreen { yieldModeConnectedTitle.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Assert 'Average APY' is displayed in yield block") {
|
||||
onTokenDetailsScreen { yieldModeApy(apy).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("4938")
|
||||
@DisplayName("Yield mode: first-time landing activation")
|
||||
@Test
|
||||
fun firstTimeLandingActivationTest() {
|
||||
val tokenTitle = "USDC"
|
||||
val apy = "5.24"
|
||||
val enterTransactionTitle = getResourceString(CoreResR.string.yield_module_transaction_enter)
|
||||
|
||||
val portfolioScenario = "user_tokens_api"
|
||||
val portfolioState = "YieldUSDCEthereum"
|
||||
val balancesScenario = "moralis_evm_token_balances_api"
|
||||
val balancesState = "NonZeroEvmBalances"
|
||||
val yieldScenario = "yield_supply_status"
|
||||
val yieldNotActiveState = "NotActive"
|
||||
val yieldActiveState = "Active"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(portfolioScenario)
|
||||
resetWireMockScenarioState(balancesScenario)
|
||||
resetWireMockScenarioState(yieldScenario)
|
||||
}
|
||||
).run {
|
||||
|
||||
step("Set WireMock scenario: '$portfolioScenario' to state: '$portfolioState'") {
|
||||
setWireMockScenarioState(scenarioName = portfolioScenario, state = portfolioState)
|
||||
}
|
||||
step("Set WireMock scenario: '$balancesScenario' to state: '$balancesState'") {
|
||||
setWireMockScenarioState(scenarioName = balancesScenario, state = balancesState)
|
||||
}
|
||||
step("Set WireMock scenario: '$yieldScenario' to state: '$yieldNotActiveState'") {
|
||||
setWireMockScenarioState(scenarioName = yieldScenario, state = yieldNotActiveState)
|
||||
}
|
||||
|
||||
step("Open 'Main Screen' with existing hot wallet") {
|
||||
openMainScreenWithExistingHotWallet(seedPhrase = SVS_SEED_PHRASE_12)
|
||||
}
|
||||
|
||||
step("Click on token with name: '$tokenTitle'") {
|
||||
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Token details' screen is displayed") {
|
||||
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Available yield block' is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTokenDetailsScreen { yieldSupplyAvailableBlock.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
|
||||
step("Click on 'Available yield block'") {
|
||||
onTokenDetailsScreen { yieldSupplyAvailableBlock.clickWithAssertion() }
|
||||
}
|
||||
step("Click on 'Continue' button") {
|
||||
onYieldSupplyPromoScreen { continueButton.clickWithAssertion() }
|
||||
}
|
||||
step("Hold 'Start earning' button to confirm activation") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onYieldSupplyStartEarningScreen {
|
||||
startEarningButton.performTouchInput { longClick(durationMillis = HOLD_DURATION_MS) }
|
||||
startEarningButton.assertIsNotDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
step("Set WireMock scenario: '$yieldScenario' to state: '$yieldActiveState'") {
|
||||
setWireMockScenarioState(scenarioName = yieldScenario, state = yieldActiveState)
|
||||
}
|
||||
step("Perform pull to refresh") {
|
||||
pullToRefresh()
|
||||
}
|
||||
step("Assert 'Yield mode enabled' block is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTokenDetailsScreen { yieldModeConnectedTitle.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Assert 'Average APY' is displayed in yield block") {
|
||||
onTokenDetailsScreen { yieldModeApy(apy).assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Yield mode enabled' transaction is displayed in history") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTxHistoryScreen {
|
||||
transactionItem(enterTransactionTitle).assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("5473")
|
||||
@DisplayName("Yield mode: top-up for active landing")
|
||||
@Test
|
||||
fun topUpActiveLandingTest() {
|
||||
val tokenTitle = "USDC"
|
||||
val topUpAmount = "600.00"
|
||||
val receivedTransactionTitle = getResourceString(CoreResR.string.common_received)
|
||||
val topUpTransactionTitle = getResourceString(CoreResR.string.yield_module_transaction_topup)
|
||||
val supplyingNotificationTitle = getResourceString(
|
||||
CoreResR.string.yield_module_amount_not_transfered_to_aave_title,
|
||||
topUpAmount,
|
||||
tokenTitle,
|
||||
)
|
||||
|
||||
val portfolioScenario = "user_tokens_api"
|
||||
val portfolioState = "YieldUSDCEthereum"
|
||||
val balancesScenario = "moralis_evm_token_balances_api"
|
||||
val balancesState = "NonZeroEvmBalances"
|
||||
val yieldScenario = "yield_supply_status"
|
||||
val yieldTopUpState = "TopUp"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(portfolioScenario)
|
||||
resetWireMockScenarioState(balancesScenario)
|
||||
resetWireMockScenarioState(yieldScenario)
|
||||
}
|
||||
).run {
|
||||
|
||||
step("Set WireMock scenario: '$portfolioScenario' to state: '$portfolioState'") {
|
||||
setWireMockScenarioState(scenarioName = portfolioScenario, state = portfolioState)
|
||||
}
|
||||
step("Set WireMock scenario: '$balancesScenario' to state: '$balancesState'") {
|
||||
setWireMockScenarioState(scenarioName = balancesScenario, state = balancesState)
|
||||
}
|
||||
step("Set WireMock scenario: '$yieldScenario' to state: '$yieldTopUpState'") {
|
||||
setWireMockScenarioState(scenarioName = yieldScenario, state = yieldTopUpState)
|
||||
}
|
||||
|
||||
step("Open 'Main Screen' with existing hot wallet") {
|
||||
openMainScreenWithExistingHotWallet(seedPhrase = SVS_SEED_PHRASE_12)
|
||||
}
|
||||
|
||||
step("Click on token with name: '$tokenTitle'") {
|
||||
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Token details' screen is displayed") {
|
||||
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Not supplied' info icon is displayed in yield block") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTokenDetailsScreen { earnBlockTitleIcon.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Assert 'Received' transaction is displayed in history") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTxHistoryScreen { transactionItem(receivedTransactionTitle).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Assert 'Supply to Aave' transaction is displayed in history") {
|
||||
onTxHistoryScreen { transactionItem(topUpTransactionTitle).assertIsDisplayed() }
|
||||
}
|
||||
|
||||
step("Click on 'Yield mode enabled' block") {
|
||||
onTokenDetailsScreen { yieldSupplyActiveBlock.clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Supplying to Aave' notification is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onYieldSupplyActiveScreen { notificationTitle(supplyingNotificationTitle).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("7960")
|
||||
@DisplayName("Yield mode: granting approval")
|
||||
@Test
|
||||
fun grantApprovalTest() {
|
||||
val tokenTitle = "USDC"
|
||||
val approveNeededTitle = getResourceString(CoreResR.string.yield_module_approve_needed_notification_title)
|
||||
|
||||
val portfolioScenario = "user_tokens_api"
|
||||
val portfolioState = "YieldUSDCEthereum"
|
||||
val balancesScenario = "moralis_evm_token_balances_api"
|
||||
val balancesState = "NonZeroEvmBalances"
|
||||
val yieldScenario = "yield_supply_status"
|
||||
val yieldApproveNeededState = "Active"
|
||||
val yieldApproveGrantedState = "ApproveGranted"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(portfolioScenario)
|
||||
resetWireMockScenarioState(balancesScenario)
|
||||
resetWireMockScenarioState(yieldScenario)
|
||||
}
|
||||
).run {
|
||||
|
||||
step("Set WireMock scenario: '$portfolioScenario' to state: '$portfolioState'") {
|
||||
setWireMockScenarioState(scenarioName = portfolioScenario, state = portfolioState)
|
||||
}
|
||||
step("Set WireMock scenario: '$balancesScenario' to state: '$balancesState'") {
|
||||
setWireMockScenarioState(scenarioName = balancesScenario, state = balancesState)
|
||||
}
|
||||
step("Set WireMock scenario: '$yieldScenario' to state: '$yieldApproveNeededState'") {
|
||||
setWireMockScenarioState(scenarioName = yieldScenario, state = yieldApproveNeededState)
|
||||
}
|
||||
|
||||
step("Open 'Main Screen' with existing hot wallet") {
|
||||
openMainScreenWithExistingHotWallet(seedPhrase = SVS_SEED_PHRASE_12)
|
||||
}
|
||||
|
||||
step("Click on token with name: '$tokenTitle'") {
|
||||
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Token details' screen is displayed") {
|
||||
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Approve needed' info icon is displayed in yield block") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTokenDetailsScreen { earnBlockTitleIcon.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
|
||||
step("Click on 'Yield mode enabled' block") {
|
||||
onTokenDetailsScreen { yieldSupplyActiveBlock.clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Approve needed' notification is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onYieldSupplyActiveScreen { notificationTitle(approveNeededTitle).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Click on 'Approve' button") {
|
||||
onYieldSupplyActiveScreen { approveButton.clickWithAssertion() }
|
||||
}
|
||||
step("Hold 'Confirm' button to confirm approval") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onYieldSupplyApproveScreen {
|
||||
confirmButton.performTouchInput { longClick(durationMillis = HOLD_DURATION_MS) }
|
||||
confirmButton.assertIsNotDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
step("Set WireMock scenario: '$yieldScenario' to state: '$yieldApproveGrantedState'") {
|
||||
setWireMockScenarioState(scenarioName = yieldScenario, state = yieldApproveGrantedState)
|
||||
}
|
||||
step("Perform pull to refresh") {
|
||||
pullToRefresh()
|
||||
}
|
||||
step("Assert 'Approve needed' info icon is not displayed in yield block") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTokenDetailsScreen { earnBlockTitleIcon.assertIsNotDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("4940")
|
||||
@DisplayName("Yield mode: reopening landing")
|
||||
@Test
|
||||
fun reopenLandingActivationTest() {
|
||||
val tokenTitle = "USDC"
|
||||
val nativeCoinTitle = "Ethereum"
|
||||
val apy = "5.24"
|
||||
val reactivateTransactionTitle = getResourceString(CoreResR.string.yield_module_transaction_reactivate)
|
||||
val enterTransactionTitle = getResourceString(CoreResR.string.yield_module_transaction_enter)
|
||||
val portfolioScenario = "user_tokens_api"
|
||||
val portfolioState = "YieldUSDCEthereum"
|
||||
val balancesScenario = "moralis_evm_token_balances_api"
|
||||
val balancesState = "NonZeroEvmBalances"
|
||||
val yieldScenario = "yield_supply_status"
|
||||
val yieldNotActiveState = "NotActive"
|
||||
val yieldActiveState = "Active"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(portfolioScenario)
|
||||
resetWireMockScenarioState(balancesScenario)
|
||||
resetWireMockScenarioState(yieldScenario)
|
||||
}
|
||||
).run {
|
||||
|
||||
step("Set WireMock scenario: '$portfolioScenario' to state: '$portfolioState'") {
|
||||
setWireMockScenarioState(scenarioName = portfolioScenario, state = portfolioState)
|
||||
}
|
||||
step("Set WireMock scenario: '$balancesScenario' to state: '$balancesState'") {
|
||||
setWireMockScenarioState(scenarioName = balancesScenario, state = balancesState)
|
||||
}
|
||||
step("Set WireMock scenario: '$yieldScenario' to state: '$yieldNotActiveState'") {
|
||||
setWireMockScenarioState(scenarioName = yieldScenario, state = yieldNotActiveState)
|
||||
}
|
||||
|
||||
step("Open 'Main Screen' with existing hot wallet") {
|
||||
openMainScreenWithExistingHotWallet(seedPhrase = SVS_SEED_PHRASE_12)
|
||||
}
|
||||
|
||||
step("Click on token with name: '$tokenTitle'") {
|
||||
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Token details' screen is displayed") {
|
||||
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Available yield block' is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTokenDetailsScreen { yieldSupplyAvailableBlock.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
|
||||
step("Click on 'Available yield block'") {
|
||||
onTokenDetailsScreen { yieldSupplyAvailableBlock.clickWithAssertion() }
|
||||
}
|
||||
step("Click on 'Continue' button") {
|
||||
onYieldSupplyPromoScreen { continueButton.clickWithAssertion() }
|
||||
}
|
||||
step("Hold 'Start earning' button to confirm activation") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onYieldSupplyStartEarningScreen {
|
||||
startEarningButton.performTouchInput { longClick(durationMillis = HOLD_DURATION_MS) }
|
||||
startEarningButton.assertIsNotDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
step("Set WireMock scenario: '$yieldScenario' to state: '$yieldActiveState'") {
|
||||
setWireMockScenarioState(scenarioName = yieldScenario, state = yieldActiveState)
|
||||
}
|
||||
step("Perform pull to refresh") {
|
||||
pullToRefresh()
|
||||
}
|
||||
step("Assert 'Yield mode enabled' block is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTokenDetailsScreen { yieldModeConnectedTitle.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Assert 'Average APY' is displayed in yield block") {
|
||||
onTokenDetailsScreen { yieldModeApy(apy).assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Yield mode enabled' transaction is displayed in history") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTxHistoryScreen { transactionItem(enterTransactionTitle).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
|
||||
step("Click 'Back' button to return to 'Main Screen'") {
|
||||
onTokenDetailsTopBar { backButton.clickWithAssertion() }
|
||||
}
|
||||
step("Click on native coin with name: '$nativeCoinTitle'") {
|
||||
onMainScreen { tokenWithTitleAndAddress(nativeCoinTitle).clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Token details' screen is displayed") {
|
||||
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Reactivate Token' transaction is displayed in history") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTxHistoryScreen { transactionItem(reactivateTransactionTitle).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Assert 'Enter protocol' transaction is displayed in history") {
|
||||
onTxHistoryScreen { transactionItem(enterTransactionTitle).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("4937")
|
||||
@DisplayName("Yield mode: closing active landing")
|
||||
@Test
|
||||
fun closeActiveLandingTest() {
|
||||
val tokenTitle = "USDC"
|
||||
val nativeCoinTitle = "Ethereum"
|
||||
val exitTransactionTitle = getResourceString(CoreResR.string.yield_module_transaction_exit)
|
||||
val portfolioScenario = "user_tokens_api"
|
||||
val portfolioState = "YieldUSDCEthereum"
|
||||
val balancesScenario = "moralis_evm_token_balances_api"
|
||||
val balancesState = "NonZeroEvmBalances"
|
||||
val yieldScenario = "yield_supply_status"
|
||||
val yieldActiveState = "Active"
|
||||
val yieldExitedState = "Exited"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(portfolioScenario)
|
||||
resetWireMockScenarioState(balancesScenario)
|
||||
resetWireMockScenarioState(yieldScenario)
|
||||
}
|
||||
).run {
|
||||
|
||||
step("Set WireMock scenario: '$portfolioScenario' to state: '$portfolioState'") {
|
||||
setWireMockScenarioState(scenarioName = portfolioScenario, state = portfolioState)
|
||||
}
|
||||
step("Set WireMock scenario: '$balancesScenario' to state: '$balancesState'") {
|
||||
setWireMockScenarioState(scenarioName = balancesScenario, state = balancesState)
|
||||
}
|
||||
step("Set WireMock scenario: '$yieldScenario' to state: '$yieldActiveState'") {
|
||||
setWireMockScenarioState(scenarioName = yieldScenario, state = yieldActiveState)
|
||||
}
|
||||
|
||||
step("Open 'Main Screen' with existing hot wallet") {
|
||||
openMainScreenWithExistingHotWallet(seedPhrase = SVS_SEED_PHRASE_12)
|
||||
}
|
||||
|
||||
step("Click on token with name: '$tokenTitle'") {
|
||||
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Token details' screen is displayed") {
|
||||
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Yield mode enabled' block is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTokenDetailsScreen { yieldModeConnectedTitle.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
|
||||
step("Click on 'Yield mode enabled' block") {
|
||||
onTokenDetailsScreen { yieldSupplyActiveBlock.clickWithAssertion() }
|
||||
}
|
||||
step("Click on 'Disable Yield Mode' button") {
|
||||
onYieldSupplyActiveScreen { stopEarningButton.clickWithAssertion() }
|
||||
}
|
||||
step("Hold 'Confirm' button to confirm exit") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onYieldSupplyStopEarningScreen {
|
||||
confirmButton.performTouchInput { longClick(durationMillis = HOLD_DURATION_MS) }
|
||||
confirmButton.assertIsNotDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
step("Set WireMock scenario: '$yieldScenario' to state: '$yieldExitedState'") {
|
||||
setWireMockScenarioState(scenarioName = yieldScenario, state = yieldExitedState)
|
||||
}
|
||||
|
||||
step("Assert 'Available yield block' is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTokenDetailsScreen { yieldSupplyAvailableBlock.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Click 'Back' button to return to 'Main Screen'") {
|
||||
onTokenDetailsTopBar { backButton.clickWithAssertion() }
|
||||
}
|
||||
step("Click on native coin with name: '$nativeCoinTitle'") {
|
||||
onMainScreen { tokenWithTitleAndAddress(nativeCoinTitle).clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Token details' screen is displayed") {
|
||||
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Yield mode disabled' transaction is displayed in history") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTxHistoryScreen { transactionItem(exitTransactionTitle).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,11 +12,13 @@ import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
|||
import com.tangem.lib.auth.AuthFeatureToggles
|
||||
import com.tangem.lib.auth.session.DeviceRegistrar
|
||||
import com.tangem.domain.apptheme.GetAppThemeModeUseCase
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler
|
||||
import com.tangem.tap.common.analytics.handlers.appsflyer.AppsFlyerClient
|
||||
import com.tangem.tap.common.log.TangemLoggingInitializer
|
||||
import com.tangem.tap.domain.walletregistration.WalletRegistrationLauncher
|
||||
import dagger.hilt.EntryPoint
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
|
|
@ -58,4 +60,8 @@ interface ApplicationEntryPoint {
|
|||
fun getDeviceRegistrar(): DeviceRegistrar
|
||||
|
||||
fun getAuthFeatureToggles(): AuthFeatureToggles
|
||||
|
||||
fun getWalletRegistrationLauncher(): WalletRegistrationLauncher
|
||||
|
||||
fun getUserWalletsListRepository(): UserWalletsListRepository
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import android.annotation.SuppressLint
|
|||
import android.content.Intent
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.res.Configuration
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
|
|
@ -28,9 +29,11 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
|||
import androidx.lifecycle.flowWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import arrow.core.getOrElse
|
||||
import com.appsflyer.AppsFlyerLib
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.WEBLINK_KEY
|
||||
import com.tangem.common.routing.deeplink.PayloadToDeeplinkConverter
|
||||
import com.tangem.common.routing.deeplink.PushDeeplinkPolicy
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.di.RootAppComponentContext
|
||||
|
|
@ -215,6 +218,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
|
|||
|
||||
if (BuildConfig.TESTER_MENU_ENABLED) {
|
||||
lifecycle.addObserver(testerMenuLauncher.launchOnKeyEventObserver)
|
||||
testerMenuLauncher.registerTesterMenuShortcut()
|
||||
}
|
||||
|
||||
if (intent != null) {
|
||||
|
|
@ -367,6 +371,9 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
|
|||
super.onNewIntent(intent)
|
||||
TangemLogger.i("onNewIntent: data=${intent.data}, extras=${intent.extras?.keySet()}")
|
||||
|
||||
// Warm start: let the AppsFlyer SDK resolve a OneLink delivered while the app is already running.
|
||||
AppsFlyerLib.getInstance().performOnDeepLinking(intent, this)
|
||||
|
||||
val isFromPush = intent.extras?.containsKey(OPENED_FROM_GCM_PUSH) == true
|
||||
if (isFromPush) {
|
||||
analyticsEventsHandler.send(Push.PushNotificationOpened())
|
||||
|
|
@ -410,25 +417,33 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
|
|||
}
|
||||
|
||||
private fun handleDeepLink(intent: Intent, isFromOnNewIntent: Boolean) {
|
||||
val deepLinkExtras = PayloadToDeeplinkConverter.convertBundle(intent.extras)?.toUri()
|
||||
val externalDeepLink = intent.data
|
||||
val extrasDeepLink = PayloadToDeeplinkConverter.convertBundle(intent.extras)?.toUri()
|
||||
val webLink = intent.getStringExtra(WEBLINK_KEY)
|
||||
|
||||
val receivedDeepLink = intent.data ?: deepLinkExtras
|
||||
|
||||
when {
|
||||
receivedDeepLink != null -> {
|
||||
deeplinkFactory.handleDeeplink(
|
||||
deeplinkUri = receivedDeepLink,
|
||||
coroutineScope = lifecycleScope,
|
||||
isFromOnNewIntent = isFromOnNewIntent,
|
||||
)
|
||||
}
|
||||
// External deep links (browser / universal links) arrive via intent.data — never gated.
|
||||
externalDeepLink != null -> launchDeepLink(externalDeepLink, isFromOnNewIntent)
|
||||
// A deep link reconstructed from a notification payload must pass the push policy. If it is
|
||||
// present but blocked, stop — do not fall back to another payload-supplied field (e.g. webLink).
|
||||
extrasDeepLink != null ->
|
||||
if (PushDeeplinkPolicy.isOpenableFromPush(scheme = extrasDeepLink.scheme, host = extrasDeepLink.host)) {
|
||||
launchDeepLink(extrasDeepLink, isFromOnNewIntent)
|
||||
}
|
||||
webLink?.uriValidate() == true -> {
|
||||
urlOpener.openUrl(webLink)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchDeepLink(deeplinkUri: Uri, isFromOnNewIntent: Boolean) {
|
||||
deeplinkFactory.handleDeeplink(
|
||||
deeplinkUri = deeplinkUri,
|
||||
coroutineScope = lifecycleScope,
|
||||
isFromOnNewIntent = isFromOnNewIntent,
|
||||
)
|
||||
}
|
||||
|
||||
private fun sendStakingUnsubmittedHashes() {
|
||||
lifecycleScope.launch {
|
||||
sendUnsubmittedHashesUseCase.invoke()
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
|
|||
import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
||||
import com.tangem.domain.apptheme.GetAppThemeModeUseCase
|
||||
import com.tangem.domain.common.LogConfig
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import com.tangem.lib.auth.AuthFeatureToggles
|
||||
import com.tangem.lib.auth.devicekey.DeviceKeyManager
|
||||
|
|
@ -34,6 +35,8 @@ import com.tangem.tap.common.analytics.handlers.customerio.CustomerIoAnalyticsHa
|
|||
import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler
|
||||
import com.tangem.tap.common.images.createCoilImageLoader
|
||||
import com.tangem.tap.common.log.TangemLoggingInitializer
|
||||
import com.tangem.tap.domain.walletregistration.WalletRegistrationLauncher
|
||||
import com.tangem.utils.coroutines.runSuspendCatching
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import com.tangem.wallet.BuildConfig
|
||||
import dagger.hilt.EntryPoints
|
||||
|
|
@ -104,6 +107,12 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
|
|||
private val authFeatureToggles: AuthFeatureToggles
|
||||
get() = entryPoint.getAuthFeatureToggles()
|
||||
|
||||
private val walletRegistrationLauncher: WalletRegistrationLauncher
|
||||
get() = entryPoint.getWalletRegistrationLauncher()
|
||||
|
||||
private val userWalletsListRepository: UserWalletsListRepository
|
||||
get() = entryPoint.getUserWalletsListRepository()
|
||||
|
||||
// endregion
|
||||
|
||||
private val appScope = MainScope()
|
||||
|
|
@ -152,6 +161,18 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
|
|||
deviceKeyManager.generateIfMissing()
|
||||
deviceRegistrar.register()
|
||||
.onLeft { error -> TangemLogger.w("Device registration deferred: $error") }
|
||||
.onRight {
|
||||
// Safety net: register any MOBILE wallets that missed it (created before the
|
||||
// toggle was on, or after a transient failure). Requires the DPoP session from
|
||||
// device registration, so it runs only once that succeeded. Best-effort —
|
||||
// loading the wallet list (userWalletsSync) can throw, which must not crash
|
||||
// startup, so the whole retry is guarded.
|
||||
runSuspendCatching {
|
||||
walletRegistrationLauncher.retryMobileRegistrations(
|
||||
userWalletsListRepository.userWalletsSync(),
|
||||
)
|
||||
}.onFailure { error -> TangemLogger.e("Mobile wallet registration retry failed", error) }
|
||||
}
|
||||
}
|
||||
}
|
||||
walletsRepository = entryPoint.getWalletsRepository()
|
||||
|
|
|
|||
|
|
@ -13,17 +13,9 @@ class AppsFlyerDeepLinkListener @Inject constructor(
|
|||
|
||||
override fun onDeepLinking(p0: DeepLinkResult) {
|
||||
when (p0.status) {
|
||||
DeepLinkResult.Status.FOUND -> {
|
||||
referralParamsHandler.handleDeeplink(deepLink = p0.deepLink)
|
||||
}
|
||||
DeepLinkResult.Status.NOT_FOUND -> {
|
||||
referralParamsHandler.handleNoDeeplink()
|
||||
TangemLogger.i("No deep link found")
|
||||
}
|
||||
DeepLinkResult.Status.ERROR -> {
|
||||
referralParamsHandler.handleNoDeeplink()
|
||||
TangemLogger.e("Deep link error: ${p0.error}")
|
||||
}
|
||||
DeepLinkResult.Status.FOUND -> referralParamsHandler.handleDeeplink(deepLink = p0.deepLink)
|
||||
DeepLinkResult.Status.NOT_FOUND -> TangemLogger.i("No deep link found")
|
||||
DeepLinkResult.Status.ERROR -> TangemLogger.e("Deep link error: ${p0.error}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
package com.tangem.tap.common.analytics.appsflyer
|
||||
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
|
||||
import com.tangem.domain.appsflyer.AppsFlyerDeeplink
|
||||
import com.tangem.domain.appsflyer.usecase.ClearAppsFlyerDeeplinkUseCase
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Reactively routes AppsFlyer deep links persisted by [AppsFlyerReferralParamsHandler].
|
||||
*
|
||||
* To add a deep link: add it to [AppsFlyerDeeplink] and a branch in [onDeeplinkPending] (the `when` is exhaustive).
|
||||
*/
|
||||
@Singleton
|
||||
class AppsFlyerDeeplinkRouter @Inject constructor(
|
||||
private val appsFlyerStore: AppsFlyerStore,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val clearAppsFlyerDeeplinkUseCase: ClearAppsFlyerDeeplinkUseCase,
|
||||
private val appRouter: AppRouter,
|
||||
) {
|
||||
|
||||
fun observe(scope: CoroutineScope, currentRoute: Flow<AppRoute?>) {
|
||||
combine(
|
||||
appsFlyerStore.observeNavigationDeeplink(),
|
||||
currentRoute.distinctUntilChanged(),
|
||||
) { deepLinkValue, route ->
|
||||
if (deepLinkValue != null && route != null) deepLinkValue to route else null
|
||||
}
|
||||
.filterNotNull()
|
||||
.onEach { (deepLinkValue, route) -> onDeeplinkPending(deepLinkValue, route) }
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
private suspend fun onDeeplinkPending(deepLinkValue: String, currentRoute: AppRoute) {
|
||||
when (AppsFlyerDeeplink.from(deepLinkValue)) {
|
||||
AppsFlyerDeeplink.TangemPayMobileOnboarding -> routeTangemPayOnboarding(currentRoute)
|
||||
AppsFlyerDeeplink.Referral -> routeReferral(currentRoute)
|
||||
null -> TangemLogger.i("Ignoring unknown AppsFlyer deep link value: $deepLinkValue")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun routeTangemPayOnboarding(currentRoute: AppRoute) {
|
||||
// Not on an idle entry screen yet: keep the deep link pending, re-evaluate on next route change.
|
||||
if (!isIdleEntryPoint(currentRoute)) return
|
||||
|
||||
if (userWalletsListRepository.userWalletsSync().isNotEmpty()) {
|
||||
TangemLogger.i("[TangemPay][HWO] Routing AppsFlyer deep link to Tangem Pay onboarding")
|
||||
// Authorized: push onto the wallet screen so Back returns to it.
|
||||
appRouter.push(AppRoute.TangemPayOnboarding(AppRoute.TangemPayOnboarding.Mode.MobileOnboardingDeeplink))
|
||||
} else {
|
||||
TangemLogger.i("[TangemPay][HWO] Routing AppsFlyer deep link to hot wallet onboarding")
|
||||
// Not authorized: open onboarding as the root, skipping Home/stories (original cold-start flow).
|
||||
appRouter.replaceAll(AppRoute.TangemPayHotWalletOnboarding)
|
||||
}
|
||||
// One-shot: consume the deep link once routed so the user isn't forced back here on relaunch.
|
||||
clearAppsFlyerDeeplinkUseCase()
|
||||
}
|
||||
|
||||
private suspend fun routeReferral(currentRoute: AppRoute) {
|
||||
// Referral targets fresh installs: from an idle entry screen, go straight to hot wallet creation
|
||||
// (skips stories). The deep link is NOT cleared here — it stays as referral attribution (read by
|
||||
// IsReferralInstallUseCase, cleared on wallet creation); replaceAll keeps it off the back stack.
|
||||
if (!isIdleEntryPoint(currentRoute)) return
|
||||
if (userWalletsListRepository.userWalletsSync().isNotEmpty()) return
|
||||
|
||||
TangemLogger.i("[Referral] Routing AppsFlyer referral deep link to hot wallet creation")
|
||||
appRouter.replaceAll(AppRoute.CreateWalletStart(mode = AppRoute.CreateWalletStart.Mode.HotWallet))
|
||||
}
|
||||
}
|
||||
|
||||
// Route only from idle entry screens so an in-progress flow (scan, KYC, onboarding…) isn't interrupted.
|
||||
internal fun isIdleEntryPoint(currentRoute: AppRoute?): Boolean =
|
||||
currentRoute is AppRoute.Home || currentRoute is AppRoute.Stories || currentRoute is AppRoute.Wallet
|
||||
|
|
@ -1,12 +1,11 @@
|
|||
package com.tangem.tap.common.analytics.appsflyer
|
||||
|
||||
import com.appsflyer.deeplink.DeepLink
|
||||
import com.tangem.datasource.local.appsflyer.AppsFlyerDeeplinkSource
|
||||
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
|
||||
import com.tangem.domain.appsflyer.AppsFlyerDeeplink
|
||||
import com.tangem.domain.wallets.models.AppsFlyerConversionData
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
|
@ -19,8 +18,6 @@ class AppsFlyerReferralParamsHandler @Inject constructor(
|
|||
private val coroutineScope: AppCoroutineScope,
|
||||
) {
|
||||
|
||||
private val deepLinkDeferred = CompletableDeferred<String?>()
|
||||
|
||||
fun handle(params: Map<String?, Any?>) {
|
||||
handle(
|
||||
deepLinkValue = params[DEEP_LINK_VALUE] as? String,
|
||||
|
|
@ -35,40 +32,15 @@ class AppsFlyerReferralParamsHandler @Inject constructor(
|
|||
deepLinkSub1 = deepLink.getStringValue(DEEP_LINK_SUB_1),
|
||||
deepLinkSub2 = deepLink.getStringValue(DEEP_LINK_SUB_2),
|
||||
)
|
||||
deepLinkDeferred.complete(deepLink.deepLinkValue)
|
||||
}
|
||||
|
||||
fun handleNoDeeplink() {
|
||||
deepLinkDeferred.complete(null)
|
||||
}
|
||||
|
||||
suspend fun waitForDeeplink(deeplinkSource: AppsFlyerDeeplinkSource): String? {
|
||||
appsFlyerStore.getDeeplink(deeplinkSource)?.let { return it }
|
||||
|
||||
val expectedValue = when (deeplinkSource) {
|
||||
AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding -> TANGEM_PAY_HOT_WALLET_ONBOARDING_DEEP_LINK_VALUE
|
||||
AppsFlyerDeeplinkSource.Referral -> REFERRAL_DEEP_LINK_VALUE
|
||||
}
|
||||
val resolvedValue = deepLinkDeferred.await().takeIf { it == expectedValue }
|
||||
|
||||
// The deep link may have been persisted to the store while we were awaiting (e.g. from
|
||||
// conversion-data handling, which stores the deep link but doesn't complete the deferred).
|
||||
return resolvedValue ?: appsFlyerStore.getDeeplink(deeplinkSource)
|
||||
}
|
||||
|
||||
private fun handle(deepLinkValue: String?, deepLinkSub1: String?, deepLinkSub2: String?) {
|
||||
TangemLogger.i("AppsFlyer deeplink received: value=$deepLinkValue")
|
||||
when (deepLinkValue) {
|
||||
REFERRAL_DEEP_LINK_VALUE -> handleReferral(deepLinkSub1, deepLinkSub2)
|
||||
TANGEM_PAY_HOT_WALLET_ONBOARDING_DEEP_LINK_VALUE -> handleTangemPayHotWalletOnboarding(deepLinkValue)
|
||||
else -> TangemLogger.i("Ignoring deep link with value: ${deepLinkValue ?: "null"}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleTangemPayHotWalletOnboarding(deepLinkValue: String) {
|
||||
coroutineScope.launch {
|
||||
appsFlyerStore.storeDeeplink(AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding, deepLinkValue)
|
||||
TangemLogger.i("[TangemPay][HWO] Deep link stored")
|
||||
when (AppsFlyerDeeplink.from(deepLinkValue)) {
|
||||
AppsFlyerDeeplink.Referral -> handleReferral(deepLinkSub1, deepLinkSub2)
|
||||
AppsFlyerDeeplink.TangemPayMobileOnboarding ->
|
||||
storeNavigationDeeplink(AppsFlyerDeeplink.TangemPayMobileOnboarding.deepLinkValue)
|
||||
null -> TangemLogger.i("Ignoring deep link with value: ${deepLinkValue ?: "null"}")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -76,9 +48,7 @@ class AppsFlyerReferralParamsHandler @Inject constructor(
|
|||
@Suppress("NullableToStringCall")
|
||||
TangemLogger.i("refcode=$deepLinkSub1\ncampaign=$deepLinkSub2")
|
||||
|
||||
coroutineScope.launch {
|
||||
appsFlyerStore.storeDeeplink(AppsFlyerDeeplinkSource.Referral, REFERRAL_DEEP_LINK_VALUE)
|
||||
}
|
||||
storeNavigationDeeplink(AppsFlyerDeeplink.Referral.deepLinkValue)
|
||||
|
||||
if (!isValidParam(deepLinkSub1)) {
|
||||
TangemLogger.e("Deeplink conversion data is invalid")
|
||||
|
|
@ -97,6 +67,13 @@ class AppsFlyerReferralParamsHandler @Inject constructor(
|
|||
return value != null && value.isNotBlank() && !value.equals("null", ignoreCase = true)
|
||||
}
|
||||
|
||||
private fun storeNavigationDeeplink(deepLinkValue: String) {
|
||||
coroutineScope.launch {
|
||||
appsFlyerStore.storeNavigationDeeplink(deepLinkValue)
|
||||
TangemLogger.i("AppsFlyer navigation deep link stored: $deepLinkValue")
|
||||
}
|
||||
}
|
||||
|
||||
private fun storeConversionData(refcode: String, campaign: String?) {
|
||||
coroutineScope.launch {
|
||||
appsFlyerStore.storeIfAbsent(
|
||||
|
|
@ -107,9 +84,6 @@ class AppsFlyerReferralParamsHandler @Inject constructor(
|
|||
|
||||
private companion object {
|
||||
|
||||
const val REFERRAL_DEEP_LINK_VALUE = "referral"
|
||||
const val TANGEM_PAY_HOT_WALLET_ONBOARDING_DEEP_LINK_VALUE = "tpay_mobileonboard"
|
||||
|
||||
const val DEEP_LINK_VALUE = "deep_link_value"
|
||||
const val DEEP_LINK_SUB_1 = "deep_link_sub1"
|
||||
const val DEEP_LINK_SUB_2 = "deep_link_sub2"
|
||||
|
|
|
|||
|
|
@ -36,9 +36,9 @@ class AppsFlyerClient @AssistedInject constructor(
|
|||
setAppId(context.packageName)
|
||||
setDebugLog(true)
|
||||
|
||||
subscribeForDeepLink(appsFlyerDeepLinkListener)
|
||||
|
||||
init(apiKey, tangemAFConversionListener, context)
|
||||
subscribeForDeepLink(appsFlyerDeepLinkListener)
|
||||
setOneLinkCustomDomain(BRANDED_ONELINK_DOMAIN)
|
||||
|
||||
TangemLogger.i("Starting AppsFlyer SDK")
|
||||
start(context, apiKey, InitializationListener)
|
||||
|
|
@ -100,4 +100,9 @@ class AppsFlyerClient @AssistedInject constructor(
|
|||
interface Factory {
|
||||
fun create(apiKey: String): AppsFlyerClient
|
||||
}
|
||||
|
||||
private companion object {
|
||||
// Branded AppsFlyer OneLink domain (matches the join.tangem.com App Link filter in AndroidManifest).
|
||||
const val BRANDED_ONELINK_DOMAIN = "join.tangem.com"
|
||||
}
|
||||
}
|
||||
|
|
@ -34,18 +34,11 @@ class CustomerIoAnalyticsHandler(
|
|||
class Builder : AnalyticsHandlerBuilder {
|
||||
override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsHandler? {
|
||||
val cdpApiKey = data.config.customerIoCdpApiKey
|
||||
return if (data.logConfig.isCustomerIoLogEnabled) {
|
||||
CustomerIoAnalyticsHandler(client = CustomerIoLogClient())
|
||||
} else if (!cdpApiKey.isNullOrBlank()) {
|
||||
CustomerIoAnalyticsHandler(
|
||||
client = CustomerIoClient(
|
||||
application = data.application,
|
||||
cdpApiKey = cdpApiKey,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
if (cdpApiKey.isNullOrBlank()) return null
|
||||
|
||||
return CustomerIoAnalyticsHandler(
|
||||
client = CustomerIoClient(application = data.application, cdpApiKey = cdpApiKey),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
package com.tangem.tap.common.analytics.handlers.customerio
|
||||
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
|
||||
/**
|
||||
* Log client for Customer.io (used in debug mode).
|
||||
*
|
||||
* Logs all operations to Timber instead of sending them to Customer.io.
|
||||
*/
|
||||
internal class CustomerIoLogClient : CustomerIoAnalyticsClient {
|
||||
|
||||
private var userId: String? = null
|
||||
|
||||
override fun setUserId(userId: String) {
|
||||
this.userId = userId
|
||||
TangemLogger.withTag(CustomerIoAnalyticsHandler.ID).d("identify: userId=$userId")
|
||||
}
|
||||
|
||||
override fun clearUserId() {
|
||||
TangemLogger.withTag(CustomerIoAnalyticsHandler.ID).d("clearIdentify: previous userId=$userId")
|
||||
this.userId = null
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import android.net.Uri
|
|||
import androidx.core.net.toUri
|
||||
import com.tangem.common.routing.DeepLinkRoute
|
||||
import com.tangem.common.routing.deeplink.PayloadToDeeplinkConverter
|
||||
import com.tangem.common.routing.deeplink.PushDeeplinkPolicy
|
||||
import com.tangem.utils.extensions.uriValidate
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -21,7 +22,9 @@ internal class PushMessageHandler @Inject constructor(
|
|||
) {
|
||||
|
||||
fun onMessageReceived(data: Map<String, String>) {
|
||||
val deeplink = PayloadToDeeplinkConverter.convert(data)?.toUri() ?: return
|
||||
val deeplink = PayloadToDeeplinkConverter.convert(data)?.toUri()
|
||||
?.takeIf { PushDeeplinkPolicy.isOpenableFromPush(scheme = it.scheme, host = it.host) }
|
||||
?: return
|
||||
val queryParams = deeplink.getQueryParams()
|
||||
when (deeplink.host) {
|
||||
DeepLinkRoute.TokenDetails.host -> tokenDetailsPushHandler.handle(queryParams)
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
package com.tangem.tap.core.ui
|
||||
|
||||
import com.tangem.core.configtoggle.FeatureToggles
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.core.ui.DesignFeatureToggles
|
||||
import javax.inject.Inject
|
||||
|
||||
class DefaultDesignFeatureToggles @Inject constructor(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : DesignFeatureToggles {
|
||||
|
||||
override val isRedesignEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.APP_REDESIGN_ENABLED)
|
||||
|
||||
override val isWarningsRefactoringEnabled: Boolean
|
||||
get() = isRedesignEnabled &&
|
||||
featureTogglesManager.isFeatureEnabled(FeatureToggles.AND_14829_WARNINGS_REFACTORING_ENABLED)
|
||||
}
|
||||
|
|
@ -1,13 +1,17 @@
|
|||
package com.tangem.tap.data
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import com.tangem.utils.info.AppInfoProvider
|
||||
import com.tangem.wallet.BuildConfig
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultAppInfoProvider @Inject constructor() : AppInfoProvider {
|
||||
internal class DefaultAppInfoProvider @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
) : AppInfoProvider {
|
||||
override val platform: String
|
||||
get() = "Android"
|
||||
override val device: String
|
||||
|
|
@ -18,6 +22,8 @@ internal class DefaultAppInfoProvider @Inject constructor() : AppInfoProvider {
|
|||
get() = Build.VERSION.SDK_INT
|
||||
override val language: String
|
||||
get() = Locale.getDefault().toLanguageTag()
|
||||
override val deviceScale: Float
|
||||
get() = context.resources.displayMetrics.density
|
||||
override val timezone: String
|
||||
get() = TimeZone.getDefault().id
|
||||
override val appVersion: String = BuildConfig.VERSION_NAME
|
||||
|
|
|
|||
|
|
@ -4,15 +4,16 @@ import androidx.datastore.core.DataStore
|
|||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.offramp.model.PendingOfframp
|
||||
import com.tangem.domain.offramp.model.PendingOfframp.Companion.EXPIRY_MS
|
||||
import com.tangem.domain.offramp.repository.OfframpRepository
|
||||
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
|
||||
import com.tangem.tap.data.converter.PendingOfframpEntryConverter
|
||||
import com.tangem.tap.data.model.PendingOfframpEntry
|
||||
import com.tangem.tap.network.exchangeServices.SellService
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Default implementation of [OfframpRepository].
|
||||
|
|
@ -27,7 +28,7 @@ internal class DefaultOfframpRepository(
|
|||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : OfframpRepository {
|
||||
|
||||
private val pendingOfframpConverter = PendingOfframpEntryConverter()
|
||||
private val converter = PendingOfframpEntryConverter()
|
||||
|
||||
override fun getOfframpUrl(
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
|
|
@ -71,13 +72,17 @@ internal class DefaultOfframpRepository(
|
|||
entry.requestId == requestId &&
|
||||
entry.userWalletId == userWalletId.stringValue &&
|
||||
entry.currencyId == currencyId &&
|
||||
now - entry.createdAt < EXPIRY_MS
|
||||
!entry.isExpired(now)
|
||||
}
|
||||
// Keep the matched record so the same redirect can be followed again until it expires; only prune the
|
||||
// expired ones. The record is dropped naturally once it ages past EXPIRY_MS.
|
||||
stored.filterNotExpired(now)
|
||||
}
|
||||
matched?.let(pendingOfframpConverter::convert)
|
||||
matched?.let(converter::convert)
|
||||
}
|
||||
|
||||
override suspend fun getAllStoredOfframps(): List<PendingOfframp> = withContext(dispatchers.io) {
|
||||
pendingOfframpStore.data.first().map(converter::convert)
|
||||
}
|
||||
|
||||
// Returns the same instance when nothing is expired, so DataStore.updateData sees an unchanged value and skips
|
||||
|
|
@ -85,7 +90,5 @@ internal class DefaultOfframpRepository(
|
|||
private fun List<PendingOfframpEntry>.filterNotExpired(now: Long): List<PendingOfframpEntry> =
|
||||
if (none { now - it.createdAt >= EXPIRY_MS }) this else filter { now - it.createdAt < EXPIRY_MS }
|
||||
|
||||
private companion object {
|
||||
val EXPIRY_MS: Long = TimeUnit.HOURS.toMillis(1)
|
||||
}
|
||||
private fun PendingOfframpEntry.isExpired(now: Long): Boolean = converter.convert(this).isExpired(now)
|
||||
}
|
||||
|
|
@ -1,305 +0,0 @@
|
|||
package com.tangem.tap.data
|
||||
|
||||
import android.content.Context
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import com.tangem.data.pay.entity.WithdrawStoreData
|
||||
import com.tangem.data.pay.util.WithdrawStateConverter
|
||||
import com.tangem.data.pay.util.WithdrawStoreDataConverter
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectMapSync
|
||||
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
|
||||
import com.tangem.datasource.local.preferences.utils.getSyncOrNull
|
||||
import com.tangem.datasource.local.preferences.utils.store
|
||||
import com.tangem.datasource.local.visa.TangemPayStorage
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.TangemPayWithdrawState
|
||||
import com.tangem.domain.visa.model.TangemPayAuthTokens
|
||||
import com.tangem.sdk.storage.AndroidSecureStorageV2
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
private const val AUTH_TOKENS_DEFAULT_KEY = "tangem_pay_default_key"
|
||||
private const val WITHDRAW_ORDER_ID_KEY = "tangem_pay_withdraw_order_id_key"
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
@Singleton
|
||||
internal class DefaultTangemPayStorage @Inject constructor(
|
||||
@ApplicationContext applicationContext: Context,
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
private val dispatcherProvider: CoroutineDispatcherProvider,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
) : TangemPayStorage {
|
||||
|
||||
private val secureStorage by lazy {
|
||||
AndroidSecureStorageV2(
|
||||
appContext = applicationContext,
|
||||
useStrongBox = false,
|
||||
name = "tangem_pay_storage",
|
||||
)
|
||||
}
|
||||
|
||||
private val tokensAdapter by lazy { moshi.adapter(TangemPayAuthTokens::class.java) }
|
||||
private val withdrawStoreDataConverter by lazy { WithdrawStoreDataConverter() }
|
||||
private val withdrawStateConverter by lazy { WithdrawStateConverter() }
|
||||
|
||||
private val listType by lazy {
|
||||
Types.newParameterizedType(List::class.java, WithdrawStoreData::class.java)
|
||||
}
|
||||
private val mapType by lazy {
|
||||
Types.newParameterizedType(Map::class.java, String::class.java, listType)
|
||||
}
|
||||
private val adapter: JsonAdapter<Map<String, List<WithdrawStoreData>>> by lazy {
|
||||
appPreferencesStore.moshi.adapter(mapType)
|
||||
}
|
||||
|
||||
override suspend fun storeCustomerWalletAddress(userWalletId: UserWalletId, customerWalletAddress: String) {
|
||||
appPreferencesStore
|
||||
.store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), customerWalletAddress)
|
||||
}
|
||||
|
||||
override suspend fun getCustomerWalletAddress(userWalletId: UserWalletId): String? {
|
||||
return appPreferencesStore.getSyncOrNull(
|
||||
key = PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId),
|
||||
)
|
||||
.takeIf { !it.isNullOrEmpty() }
|
||||
}
|
||||
|
||||
override suspend fun clearCustomerWalletAddress(userWalletId: UserWalletId) {
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), "")
|
||||
}
|
||||
|
||||
override suspend fun storeAuthTokens(customerWalletAddress: String, tokens: TangemPayAuthTokens) =
|
||||
withContext(dispatcherProvider.io) {
|
||||
val json = tokensAdapter.toJson(tokens)
|
||||
|
||||
secureStorage.store(
|
||||
json.encodeToByteArray(throwOnInvalidSequence = true),
|
||||
createAuthTokensKey(customerWalletAddress),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getAuthTokens(customerWalletAddress: String): TangemPayAuthTokens? {
|
||||
return withContext(dispatcherProvider.io) {
|
||||
val authTokens = secureStorage.get(createAuthTokensKey(customerWalletAddress))
|
||||
?.decodeToString(throwOnInvalidSequence = true)
|
||||
?.let(tokensAdapter::fromJson)
|
||||
|
||||
authTokens?.let { tokens ->
|
||||
if (tokens.idempotencyKey == null) {
|
||||
val newAuthTokens = tokens.copy(idempotencyKey = UUID.randomUUID().toString())
|
||||
storeAuthTokens(customerWalletAddress, newAuthTokens)
|
||||
newAuthTokens
|
||||
} else {
|
||||
tokens
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun clearAuthTokens(customerWalletAddress: String) {
|
||||
withContext(dispatcherProvider.io) {
|
||||
secureStorage.delete(createAuthTokensKey(customerWalletAddress))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun storeOrderId(customerWalletAddress: String, orderId: String) {
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress), orderId)
|
||||
}
|
||||
|
||||
override suspend fun getOrderId(customerWalletAddress: String): String? {
|
||||
return appPreferencesStore.getSyncOrNull(key = PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress))
|
||||
.takeIf { !it.isNullOrEmpty() }
|
||||
}
|
||||
|
||||
override suspend fun getAddToWalletDone(customerWalletAddress: String): Boolean {
|
||||
return appPreferencesStore.getSyncOrNull(
|
||||
key = PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress),
|
||||
) == true
|
||||
}
|
||||
|
||||
override suspend fun storeAddToWalletDone(customerWalletAddress: String, isDone: Boolean) {
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), isDone)
|
||||
}
|
||||
|
||||
override suspend fun clearOrderId(customerWalletAddress: String) {
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress), "")
|
||||
}
|
||||
|
||||
override suspend fun storeVirtualAccountOrderId(customerWalletAddress: String, vaOrderId: String) {
|
||||
appPreferencesStore.store(
|
||||
key = PreferencesKeys.getTangemPayVirtualAccountOrderIdKey(customerWalletAddress),
|
||||
value = vaOrderId,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getVirtualAccountOrderId(customerWalletAddress: String): String? {
|
||||
return appPreferencesStore.getSyncOrNull(
|
||||
key = PreferencesKeys.getTangemPayVirtualAccountOrderIdKey(customerWalletAddress),
|
||||
).takeIf { !it.isNullOrEmpty() }
|
||||
}
|
||||
|
||||
override suspend fun clearVirtualAccountOrderId(customerWalletAddress: String) {
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayVirtualAccountOrderIdKey(customerWalletAddress), "")
|
||||
}
|
||||
|
||||
override suspend fun storeCheckCustomerWalletResult(userWalletId: UserWalletId, isPaeraCustomer: Boolean) {
|
||||
appPreferencesStore.store(
|
||||
PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId),
|
||||
isPaeraCustomer,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun checkCustomerWalletResult(userWalletId: UserWalletId): Boolean? {
|
||||
return appPreferencesStore.getSyncOrNull(PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId))
|
||||
}
|
||||
|
||||
override suspend fun storeActiveWithdrawOrderId(userWalletId: UserWalletId, orderId: String) {
|
||||
appPreferencesStore.editData { mutablePreferences ->
|
||||
val orders = mutablePreferences.getObjectMap<String>(
|
||||
PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY,
|
||||
)
|
||||
.plus(createWithdrawOrderIdKey(userWalletId) to orderId)
|
||||
mutablePreferences.setObjectMap(
|
||||
key = PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY,
|
||||
value = orders,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun storeWithdrawOrder(userWalletId: UserWalletId, data: TangemPayWithdrawState) {
|
||||
appPreferencesStore.editData { prefs ->
|
||||
val walletKey = createWithdrawOrderIdKey(userWalletId)
|
||||
val currentMap = prefs[PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY]
|
||||
?.let(adapter::fromJson)
|
||||
.orEmpty()
|
||||
val newItem = withdrawStoreDataConverter.convert(data)
|
||||
val currentList = currentMap[walletKey].orEmpty()
|
||||
val updatedList = buildList(currentList.size + 1) {
|
||||
for (item in currentList) { if (item.orderId != newItem.orderId) add(item) }
|
||||
add(newItem)
|
||||
}
|
||||
prefs[PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY] =
|
||||
adapter.toJson(currentMap + (walletKey to updatedList))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getActiveWithdrawOrderId(userWalletId: UserWalletId): String? {
|
||||
val orders = appPreferencesStore.getObjectMapSync<String>(
|
||||
PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY,
|
||||
)
|
||||
return orders[createWithdrawOrderIdKey(userWalletId)]
|
||||
}
|
||||
|
||||
override suspend fun getWithdrawOrders(userWalletId: UserWalletId): List<TangemPayWithdrawState> {
|
||||
val map = appPreferencesStore.data.firstOrNull()
|
||||
?.get(PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY)?.let(adapter::fromJson).orEmpty()
|
||||
return map[createWithdrawOrderIdKey(userWalletId)].orEmpty().map(withdrawStateConverter::convert)
|
||||
}
|
||||
|
||||
override suspend fun deleteActiveWithdrawOrder(userWalletId: UserWalletId) {
|
||||
appPreferencesStore.editData { mutablePreferences ->
|
||||
val orders = mutablePreferences.getObjectMap<String>(
|
||||
PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY,
|
||||
)
|
||||
.minus(createWithdrawOrderIdKey(userWalletId))
|
||||
mutablePreferences.setObjectMap(
|
||||
key = PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY,
|
||||
value = orders,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun deleteWithdrawOrder(userWalletId: UserWalletId, orderId: String) {
|
||||
appPreferencesStore.editData { prefs ->
|
||||
val walletKey = createWithdrawOrderIdKey(userWalletId)
|
||||
val currentMap = prefs[PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY]?.let(adapter::fromJson)
|
||||
.orEmpty()
|
||||
val currentList = currentMap[walletKey].orEmpty()
|
||||
val updatedList = buildList(currentList.size) {
|
||||
for (item in currentList) {
|
||||
if (item.orderId != orderId) add(item)
|
||||
}
|
||||
}
|
||||
val updatedMap = if (updatedList.isEmpty()) {
|
||||
currentMap - walletKey
|
||||
} else {
|
||||
currentMap + (walletKey to updatedList)
|
||||
}
|
||||
prefs[PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY] = adapter.toJson(updatedMap)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun storeHideOnboardingBanner(userWalletId: UserWalletId, hide: Boolean) {
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), hide)
|
||||
}
|
||||
|
||||
override suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean {
|
||||
return appPreferencesStore.getSyncOrNull(
|
||||
key = PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId),
|
||||
) == true
|
||||
}
|
||||
|
||||
override suspend fun storeTangemPayEligibility(eligibility: Set<String>) {
|
||||
withContext(dispatcherProvider.io) {
|
||||
appPreferencesStore.store(
|
||||
key = PreferencesKeys.TANGEM_PAY_ELIGIBILITY_KEY,
|
||||
value = eligibility,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getTangemPayEligibility(): Set<String> {
|
||||
return withContext(dispatcherProvider.io) {
|
||||
appPreferencesStore.getSyncOrDefault(
|
||||
key = PreferencesKeys.TANGEM_PAY_ELIGIBILITY_KEY,
|
||||
default = emptySet(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun storeIsTangemPayDeactivated(userWalletId: UserWalletId) {
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayDeactivatedKey(userWalletId), true)
|
||||
}
|
||||
|
||||
override suspend fun isTangemPayDeactivated(userWalletId: UserWalletId): Boolean {
|
||||
val key = PreferencesKeys.getTangemPayDeactivatedKey(userWalletId)
|
||||
return appPreferencesStore.getSyncOrNull(key) == true
|
||||
}
|
||||
|
||||
override suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String) {
|
||||
withContext(dispatcherProvider.io) {
|
||||
secureStorage.delete(createAuthTokensKey(customerWalletAddress))
|
||||
}
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId), false)
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), "")
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress), "")
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), false)
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), false)
|
||||
// Clear the withdraw order hints together with the rest of the cache.
|
||||
deleteActiveWithdrawOrder(userWalletId)
|
||||
clearWithdrawOrders(userWalletId)
|
||||
}
|
||||
|
||||
private suspend fun clearWithdrawOrders(userWalletId: UserWalletId) {
|
||||
appPreferencesStore.editData { prefs ->
|
||||
val walletKey = createWithdrawOrderIdKey(userWalletId)
|
||||
val currentMap = prefs[PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY]?.let(adapter::fromJson)
|
||||
.orEmpty()
|
||||
val updatedMap = currentMap - walletKey
|
||||
prefs[PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY] = adapter.toJson(updatedMap)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createAuthTokensKey(address: String): String = "${AUTH_TOKENS_DEFAULT_KEY}_$address"
|
||||
|
||||
private fun createWithdrawOrderIdKey(userWalletId: UserWalletId): String = "${WITHDRAW_ORDER_ID_KEY}_$userWalletId"
|
||||
}
|
||||
|
|
@ -13,6 +13,8 @@ import com.tangem.tap.domain.tasks.visa.TangemPayGenerateAddressAndSignChallenge
|
|||
import com.tangem.tap.domain.tasks.visa.TangemPayGenerateVirtualAccountAddressTask
|
||||
import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask
|
||||
import com.tangem.tap.domain.visa.VisaCardScanHandler
|
||||
import com.tangem.tap.domain.walletregistration.WalletRegistrationLauncher
|
||||
import dagger.Lazy
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -36,6 +38,7 @@ internal class TangemSdkManagerModule {
|
|||
onboardingV2FeatureToggles: OnboardingV2FeatureToggles,
|
||||
analyticsErrorHandler: AnalyticsErrorHandler,
|
||||
cardRepository: CardRepository,
|
||||
walletRegistrationLauncher: Lazy<WalletRegistrationLauncher>,
|
||||
): TangemSdkManager {
|
||||
return if (BuildConfig.MOCK_DATA_SOURCE) {
|
||||
MockTangemSdkManager(resources = context.resources)
|
||||
|
|
@ -50,6 +53,7 @@ internal class TangemSdkManagerModule {
|
|||
onboardingV2FeatureToggles = onboardingV2FeatureToggles,
|
||||
analyticsErrorHandler = analyticsErrorHandler,
|
||||
cardRepository = cardRepository,
|
||||
walletRegistrationLauncher = walletRegistrationLauncher,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import androidx.compose.material3.SnackbarHostState
|
|||
import com.tangem.core.decompose.di.GlobalUiMessageSender
|
||||
import com.tangem.core.decompose.ui.DefaultUiMessageSender
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.DesignFeatureToggles
|
||||
import com.tangem.core.ui.UiDependencies
|
||||
import com.tangem.core.ui.components.snackbar.TangemTopSnackbarHostState
|
||||
import com.tangem.core.ui.haptic.VibratorHapticManager
|
||||
|
|
@ -25,7 +24,6 @@ internal object UiDependenciesModule {
|
|||
fun provideUiDependencies(
|
||||
vibratorHapticManager: VibratorHapticManager,
|
||||
appThemeModeHolder: AppThemeModeHolder,
|
||||
designFeatureToggles: DesignFeatureToggles,
|
||||
): UiDependencies {
|
||||
return object : UiDependencies {
|
||||
override val vibratorHapticManager = vibratorHapticManager
|
||||
|
|
@ -33,7 +31,6 @@ internal object UiDependenciesModule {
|
|||
override val globalSnackbarHostState: SnackbarHostState = SnackbarHostState()
|
||||
override val globalTopSnackbarHostState: TangemTopSnackbarHostState = TangemTopSnackbarHostState()
|
||||
override val eventMessageHandler: EventMessageHandler = EventMessageHandler()
|
||||
override val designFeatureToggles: DesignFeatureToggles = designFeatureToggles
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import com.tangem.core.navigation.deeplink.DeeplinkLauncher
|
|||
import com.tangem.core.navigation.finisher.AppFinisher
|
||||
import com.tangem.core.navigation.settings.SettingsManager
|
||||
import com.tangem.core.navigation.share.ShareManager
|
||||
import com.tangem.core.navigation.url.AppStoreOpener
|
||||
import com.tangem.core.navigation.url.DefaultAppStoreOpener
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.tap.common.finisher.AndroidAppFinisher
|
||||
|
|
@ -35,6 +37,10 @@ internal interface UtilsModule {
|
|||
@Singleton
|
||||
fun bindAppInfoProvider(impl: DefaultAppInfoProvider): AppInfoProvider
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindAppStoreOpener(impl: DefaultAppStoreOpener): AppStoreOpener
|
||||
|
||||
companion object {
|
||||
|
||||
@Provides
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue