Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-06 15:51:03 +03:00
parent 97f2810845
commit 91200c4973
20 changed files with 843 additions and 5 deletions

View file

@ -201,4 +201,8 @@ Delete anything explaining WHAT a step does.
- **`reference/running-and-debugging.md`** — read when building, installing, running tests (orchestrator
vs. raw `am instrument`), running against a local WireMock, interpreting CLI/Allure output, using
`@Ignore`, or driving WireMock scenarios. Includes how to find app-side root causes when the UI fails
silently (the app log in `files/log.txt`, and the WireMock journal).
silently (the app log in `files/log.txt`, and the WireMock journal).
- **`reference/yield-mode.md`** — read before writing any **Yield Mode (yield-supply / "Earning")** test.
Covers the hot-wallet activation flow, why Ethereum (not Polygon — gasless), the full mock set +
scenarios, the `isActive`-semantics / lowercase-address / hold-timing / Pill-testTag gotchas, and the
production testTags already added. Mirror `tests/yield/YieldModeTest.kt`.

View file

@ -0,0 +1,96 @@
# Yield Mode (yield-supply) UI tests
Hard-won specifics for testing the Yield Mode feature (`features/yield-supply`, analytics category
"Earning"). The first test (`app/.../tests/yield/YieldModeTest.kt`, case #4938 "first-time landing
activation") is the reference — mirror it. Read this before writing any yield test.
## What the feature is
Depositing a stablecoin (USDC/USDT) into a DeFi protocol (Aave) from the app to earn APY. Distinct from
Staking (`domain/staking`) but both render the shared `EarnBlock` (`common/ui/.../earn/`) on token
details. Activation = a **real signed transaction** (approve + enter), so it needs a **hot wallet**
(mock card can't sign). Use `openMainScreenWithExistingHotWallet(SVS_SEED_PHRASE_12)`.
## Network: use Ethereum, NOT Polygon
- **Ethereum USDC** activation is a **native ETH** transaction → the standard send path works.
- **Polygon USDC** activation routes through the **gasless** flow (`GASLESS_APPROVAL_ENABLED=true` +
Polygon USDC is gasless-eligible) → `gaslessTransaction()`; the SDK's `EthereumTransactionValidator`
throws `FailedToSendException` synchronously and the send never completes. Avoid Polygon for yield
send/activation tests unless you specifically mock the whole gasless-v2 stack.
- Wallet (SVS_SEED_PHRASE_12) EVM address: `0x3369554b994908d249d307b105f8e5e3115615c2`.
- Ethereum USDC contract: `0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48` (chainId 1, decimals 6).
## Running (same as the general doc, repeated because it bit us)
Run **via the orchestrator**, never raw `am instrument` (with `am instrument` the fresh-wallet portfolio
silently never loads):
```bash
curl -s -X POST http://localhost:8081/__admin/mappings/reset # after editing mocks
./gradlew :app:connectedGoogleMockedAndroidTest \
-Pandroid.testInstrumentationRunnerArguments.class=com.tangem.tests.yield.YieldModeTest \
-Pandroid.testInstrumentationRunnerArguments.wiremockBaseUrl=http://10.0.2.2:8081
```
WireMock on 8081 is a docker container bind-mounting the `tangem-api-mocks` working tree — `mappings/reset`
reloads from disk, no rebuild. App-side logs (`TangemLogger`) also reach logcat; the failure semantics
tree is dumped to logcat (`ComposeTree`).
## Mock set (in tangem-api-mocks, branch `feature/AND-16082_yield_mode_mocks`)
- **Yield API** (`mappings/yield-api/api/v1/`): `yield/markets`, `yield/token/1/{usdc}` (+ `/chart`),
`module/activate`, `module/deactivate`. Base URL is `wiremock.tests-d.com` (see infra fix below) so the
path is `/api/v1/yield/...`.
- **On-chain eth_call** (`mappings/providers/ethereum/eth-call-yield.json`, priority 1 to beat the generic
`eth-call.json`): factory `getModule` (to=`0xd8972a45...`), processor service-fee (to=`0x4ff6178b...`),
yield status `0xf8e8be9c` + balances `0x16a398f7`/`0x5002bb7e` (to = module `0x1111…`), allowance
`0xdd62ed3e` (to = USDC) → 0. Status/balances gated by scenario `yield_supply_status` (NotActive=zeros,
Active=`…01`,`…01`,`…8ac7230489e80000`).
- **History** (`mappings/eth-blockbook.nownodes.io/`): NowNodes BlockBook v2. Ethereum mainnet tx history
is **NowNodes eth-blockbook**, NOT Etherscan and NOT `/v2/transaction-events` (the latter is a
push-dedup POST). The host is redirected by the interceptor (infra fix below).
- **Portfolio**: scenario `user_tokens_api` state `YieldUSDCEthereum` (accounts API; native ETH + USDC),
balances via `moralis_evm_token_balances_api=NonZeroEvmBalances`.
## Scenarios the test drives
`user_tokens_api=YieldUSDCEthereum`, `moralis_evm_token_balances_api=NonZeroEvmBalances`, and
`yield_supply_status` **NotActive → (after activation) Active**. The Active flip is what turns the
EarnBlock from "available" into the active "Yield Mode enabled / Average APY" state AND surfaces the
history row.
## Gotchas that cost real time
1. **`isActive` in `yield/markets` & `yield/token` means "market is available", not "user activated".**
It MUST be `true` or `YieldSupplyTokenStatusSuccessTransformer` returns `Unavailable` and the block
never renders. User activation is tracked separately by the on-chain `eth_call` status flip.
2. **`yieldSupplyKey` matching is case-sensitive string equality** (`"${backendId}_$tokenAddress"` vs
`"${network.rawId}_$contractAddress"`). All addresses in mocks must be **lowercase** — every existing
mock is. A checksummed address yields `yieldSupplyApy size=0` and no available block.
3. **Active vs available is driven by `CryptoCurrencyStatus.value.yieldSupplyStatus.isActive`** (on-chain
eth_call `0xf8e8be9c`), not the API `isActive`. Flip the `yield_supply_status` scenario + pull-to-refresh.
4. **Infra fix (production, already applied):** `YieldSupply` ApiConfig MOCK base URL was `yield.tests-d.com`
(NOT redirected by `WireMockRedirectInterceptor`) → fixed to `wiremock.tests-d.com`. And
`eth-blockbook.nownodes.io` was added to `REDIRECTABLE_THIRD_PARTY_HOSTS`. Without these the yield/history
requests bypass WireMock entirely.
5. **Hold-to-confirm must wait for the fee.** The "Start earning" `HoldToConfirmButton` is disabled
(`holdToConfirmGestures(enabled=false)` swallows the gesture) until the fee is calculated. Don't gate on
the high-fee notification (it's absent on cheap chains) — use the fee-agnostic retry: a single step that
`flakySafely`-retries `longClick(HOLD_DURATION_MS)` then asserts the sheet closed
(`startEarningButton.assertIsNotDisplayed()`). `state.isConfirmed` makes re-holds no-ops, so no double-send.
A hold step that finishes in ~270 ms instead of ~2 s = the gesture was swallowed (button disabled).
6. **The yield-enter history row is a `TransactionItemUM.Pill`** (converter maps `YieldSupply.Enter`
Pill, like Approve/Staking), rendered by `TransactionStatusPill`. It now carries
`TransactionHistoryItemTestTags.ITEM` (added for parity with `ContentItem`) so `transactionItem(title)`
finds it. Title string = `yield_module_transaction_enter` ("Yield Mode enabled" / "Режим доходности
подключен"). The row opens the explorer on click.
7. **"Nothing to add to TxHistory"** in logs is NOT an error — it refers to recent/pending txs, separate
from the API history.
## testTags added to production (reuse, don't re-add)
`TokenDetailsScreenTestTags.YIELD_SUPPLY_BLOCK` / `YIELD_SUPPLY_AVAILABLE_BLOCK`; `YieldSupplyTestTags`
(`PROMO_CONTINUE_BUTTON`, `START_EARNING_BUTTON`); `TransactionHistoryItemTestTags.ITEM` now on
`TransactionStatusPill`. Page objects: yield locators in `TokenDetailsPageObject`,
`YieldSupplyPromoPageObject`, `YieldSupplyStartEarningPageObject`.

View file

@ -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)
}

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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() }
}
}
}
}
}

View file

@ -304,7 +304,9 @@ private fun EarnBlockTitle(titleUM: EarnBlockUM.TitleUM, type: Type, modifier: M
iconRes = icon.tone.iconRes(),
tintReference = { icon.tone.tint() },
),
modifier = Modifier.size(TangemTheme.dimens2.x4),
modifier = Modifier
.size(TangemTheme.dimens2.x4)
.testTag(TokenDetailsScreenTestTags.EARN_BLOCK_TITLE_ICON),
)
}
}

View file

@ -47,6 +47,7 @@ class WireMockRedirectInterceptor : Interceptor {
"deep-index.moralis.io",
"solana-gateway.moralis.io",
"api.etherscan.io",
"eth-blockbook.nownodes.io",
)
/**

View file

@ -21,9 +21,11 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.test.TransactionHistoryItemTestTags
import com.tangem.core.ui.components.icons.identicon.IdentIcon
import com.tangem.core.ui.components.transactions.state.TransactionItemUM
import com.tangem.core.ui.components.transactions.state.TransactionItemUM.Content.Status
@ -45,6 +47,7 @@ internal fun TransactionStatusPill(
Row(
modifier = modifier
.fillMaxWidth()
.testTag(TransactionHistoryItemTestTags.ITEM)
.clickable(onClick = state.onClick)
.padding(horizontal = TangemTheme.dimens2.x4, vertical = TangemTheme.dimens2.x2),
horizontalArrangement = Arrangement.Center,

View file

@ -17,6 +17,10 @@ object TokenDetailsScreenTestTags {
const val STAKING_REWARD_VALUE = "TOKEN_DETAILS_SCREEN_STAKING_REWARD_VALUE"
const val STAKING_CHEVRON_ICON = "TOKEN_DETAILS_SCREEN_STAKING_CHEVRON_ICON"
const val YIELD_SUPPLY_BLOCK = "TOKEN_DETAILS_SCREEN_YIELD_SUPPLY_BLOCK"
const val YIELD_SUPPLY_AVAILABLE_BLOCK = "TOKEN_DETAILS_SCREEN_YIELD_SUPPLY_AVAILABLE_BLOCK"
const val EARN_BLOCK_TITLE_ICON = "TOKEN_DETAILS_SCREEN_EARN_BLOCK_TITLE_ICON"
const val EXPRESS_STATUS_ITEM = "TOKEN_DETAILS_SCREEN_EXPRESS_STATUS_ITEM"
const val EXPRESS_STATUS_ITEM_TITLE = "TOKEN_DETAILS_SCREEN_EXPRESS_STATUS_ITEM_TITLE"
const val EXPRESS_STATUS_ITEM_FROM_ICON = "TOKEN_DETAILS_SCREEN_EXPRESS_STATUS_ITEM_FROM_ICON"

View file

@ -0,0 +1,9 @@
package com.tangem.core.ui.test
object YieldSupplyTestTags {
const val PROMO_CONTINUE_BUTTON = "YIELD_SUPPLY_PROMO_CONTINUE_BUTTON"
const val START_EARNING_BUTTON = "YIELD_SUPPLY_START_EARNING_BUTTON"
const val STOP_EARNING_BUTTON = "YIELD_SUPPLY_STOP_EARNING_BUTTON"
const val STOP_EARNING_CONFIRM_BUTTON = "YIELD_SUPPLY_STOP_EARNING_CONFIRM_BUTTON"
const val APPROVE_CONFIRM_BUTTON = "YIELD_SUPPLY_APPROVE_CONFIRM_BUTTON"
}

View file

@ -6,6 +6,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.decompose.extensions.compose.subscribeAsState
@ -20,6 +21,7 @@ import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.YieldSupplyTestTags
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.yield.supply.api.YieldSupplyActiveComponent
import com.tangem.features.yield.supply.impl.R
@ -101,7 +103,8 @@ internal class DefaultYieldSupplyActiveComponent @AssistedInject constructor(
start = 16.dp,
end = 16.dp,
bottom = 16.dp,
),
)
.testTag(YieldSupplyTestTags.STOP_EARNING_BUTTON),
)
}

View file

@ -3,11 +3,14 @@ package com.tangem.features.yield.supply.impl.main
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.common.ui.earn.EarnBlock
import com.tangem.common.ui.earn.EarnBlockUM
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.test.TokenDetailsScreenTestTags
import com.tangem.features.yield.supply.api.YieldSupplyComponent
import com.tangem.features.yield.supply.impl.main.model.YieldSupplyModel
import com.tangem.features.yield.supply.impl.main.ui.YieldSupplyBlockContentLegacy
@ -26,7 +29,16 @@ internal class DefaultYieldSupplyComponent @AssistedInject constructor(
override fun Content(modifier: Modifier) {
if (LocalRedesignEnabled.current) {
val earnBlockUM by model.uiState.collectAsStateWithLifecycle()
earnBlockUM?.let { EarnBlock(state = it, modifier = modifier) }
earnBlockUM?.let { blockUM ->
val tag = if (blockUM is EarnBlockUM.Content &&
blockUM.backgroundUM is EarnBlockUM.BackgroundUM.AccentSoft
) {
TokenDetailsScreenTestTags.YIELD_SUPPLY_AVAILABLE_BLOCK
} else {
TokenDetailsScreenTestTags.YIELD_SUPPLY_BLOCK
}
EarnBlock(state = blockUM, modifier = modifier.testTag(tag))
}
} else {
val yieldSupplyUM by model.uiStateLegacy.collectAsStateWithLifecycle()
YieldSupplyBlockContentLegacy(yieldSupplyUM = yieldSupplyUM, modifier = modifier)

View file

@ -19,6 +19,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.SpanStyle
@ -37,6 +38,7 @@ import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.YieldSupplyTestTags
import com.tangem.features.yield.supply.impl.R
import com.tangem.features.yield.supply.impl.promo.entity.YieldSupplyPromoUM
import com.tangem.features.yield.supply.impl.promo.model.YieldSupplyPromoClickIntents
@ -75,7 +77,8 @@ internal fun YieldSupplyPromoContent(
end = 16.dp,
bottom = 8.dp,
)
.fillMaxWidth(),
.fillMaxWidth()
.testTag(YieldSupplyTestTags.PROMO_CONTINUE_BUTTON),
)
}
}

View file

@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@ -27,6 +28,7 @@ import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetWi
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.YieldSupplyTestTags
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.features.yield.supply.impl.R
@ -78,6 +80,7 @@ internal class YieldSupplyApproveComponent(
val modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.testTag(YieldSupplyTestTags.APPROVE_CONFIRM_BUTTON)
if (state.isHoldToConfirmEnabled) {
HoldToConfirmButton(

View file

@ -10,6 +10,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@ -24,6 +25,7 @@ import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.decompose.ComposableModularContentComponent
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.YieldSupplyTestTags
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.yield.supply.impl.R
@ -110,6 +112,7 @@ internal class YieldSupplyStartEarningComponent(
val modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.testTag(YieldSupplyTestTags.START_EARNING_BUTTON)
if (state.isHoldToConfirmEnabled) {
HoldToConfirmButton(

View file

@ -12,6 +12,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@ -28,6 +29,7 @@ import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetWi
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.YieldSupplyTestTags
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.features.yield.supply.impl.R
@ -82,6 +84,7 @@ internal class YieldSupplyStopEarningComponent(
val modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.testTag(YieldSupplyTestTags.STOP_EARNING_CONFIRM_BUTTON)
if (state.isHoldToConfirmEnabled) {
HoldToConfirmButton(