diff --git a/.claude/skills/write-ui-test/SKILL.md b/.claude/skills/write-ui-test/SKILL.md index f03145b922..bcdffcc8f8 100644 --- a/.claude/skills/write-ui-test/SKILL.md +++ b/.claude/skills/write-ui-test/SKILL.md @@ -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). \ No newline at end of file + 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`. \ No newline at end of file diff --git a/.claude/skills/write-ui-test/reference/yield-mode.md b/.claude/skills/write-ui-test/reference/yield-mode.md new file mode 100644 index 0000000000..1791b264a9 --- /dev/null +++ b/.claude/skills/write-ui-test/reference/yield-mode.md @@ -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`. \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/TokenDetailsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/TokenDetailsPageObject.kt index 8a7f71afee..804d998508 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/TokenDetailsPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/TokenDetailsPageObject.kt @@ -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) } diff --git a/app/src/androidTest/kotlin/com/tangem/screens/YieldSupplyActivePageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/YieldSupplyActivePageObject.kt new file mode 100644 index 0000000000..7d96fcf444 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/YieldSupplyActivePageObject.kt @@ -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(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) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/YieldSupplyApprovePageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/YieldSupplyApprovePageObject.kt new file mode 100644 index 0000000000..ca056c3415 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/YieldSupplyApprovePageObject.kt @@ -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(semanticsProvider = semanticsProvider) { + + val confirmButton: KNode = child { + hasTestTag(YieldSupplyTestTags.APPROVE_CONFIRM_BUTTON) + useUnmergedTree = true + } +} + +internal fun BaseTestCase.onYieldSupplyApproveScreen(function: YieldSupplyApprovePageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/YieldSupplyPromoPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/YieldSupplyPromoPageObject.kt new file mode 100644 index 0000000000..24d8ba73f0 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/YieldSupplyPromoPageObject.kt @@ -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(semanticsProvider = semanticsProvider) { + + val continueButton: KNode = child { + hasTestTag(YieldSupplyTestTags.PROMO_CONTINUE_BUTTON) + useUnmergedTree = true + } +} + +internal fun BaseTestCase.onYieldSupplyPromoScreen(function: YieldSupplyPromoPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/YieldSupplyStartEarningPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/YieldSupplyStartEarningPageObject.kt new file mode 100644 index 0000000000..51d001122c --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/YieldSupplyStartEarningPageObject.kt @@ -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(semanticsProvider = semanticsProvider) { + + val startEarningButton: KNode = child { + hasTestTag(YieldSupplyTestTags.START_EARNING_BUTTON) + useUnmergedTree = true + } +} + +internal fun BaseTestCase.onYieldSupplyStartEarningScreen(function: YieldSupplyStartEarningPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/YieldSupplyStopEarningPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/YieldSupplyStopEarningPageObject.kt new file mode 100644 index 0000000000..f9e466634a --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/YieldSupplyStopEarningPageObject.kt @@ -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(semanticsProvider = semanticsProvider) { + + val confirmButton: KNode = child { + hasTestTag(YieldSupplyTestTags.STOP_EARNING_CONFIRM_BUTTON) + useUnmergedTree = true + } +} + +internal fun BaseTestCase.onYieldSupplyStopEarningScreen(function: YieldSupplyStopEarningPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/yield/YieldModeTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/yield/YieldModeTest.kt new file mode 100644 index 0000000000..1ef3d1b6a5 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/yield/YieldModeTest.kt @@ -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() } + } + } + } + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/earn/EarnBlock.kt b/common/ui/src/main/java/com/tangem/common/ui/earn/EarnBlock.kt index c4be2f621e..ebd2c78785 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/earn/EarnBlock.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/earn/EarnBlock.kt @@ -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), ) } } diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/WireMockRedirectInterceptor.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/WireMockRedirectInterceptor.kt index 98c99ba21a..78d1278bcf 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/utils/WireMockRedirectInterceptor.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/utils/WireMockRedirectInterceptor.kt @@ -47,6 +47,7 @@ class WireMockRedirectInterceptor : Interceptor { "deep-index.moralis.io", "solana-gateway.moralis.io", "api.etherscan.io", + "eth-blockbook.nownodes.io", ) /** diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionStatusPill.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionStatusPill.kt index ea7e43035d..0e373fea22 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionStatusPill.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionStatusPill.kt @@ -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, diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/TokenDetailsScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/TokenDetailsScreenTestTags.kt index 26e0a85af7..fe52d8e649 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/TokenDetailsScreenTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/TokenDetailsScreenTestTags.kt @@ -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" diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/YieldSupplyTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/YieldSupplyTestTags.kt new file mode 100644 index 0000000000..9c2129cf91 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/YieldSupplyTestTags.kt @@ -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" +} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/DefaultYieldSupplyActiveComponent.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/DefaultYieldSupplyActiveComponent.kt index dc62882ed0..28d83b8379 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/DefaultYieldSupplyActiveComponent.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/DefaultYieldSupplyActiveComponent.kt @@ -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), ) } diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/DefaultYieldSupplyComponent.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/DefaultYieldSupplyComponent.kt index 15c94cd9ac..0c59c78f6a 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/DefaultYieldSupplyComponent.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/DefaultYieldSupplyComponent.kt @@ -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) diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/promo/ui/YieldSupplyPromoContent.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/promo/ui/YieldSupplyPromoContent.kt index 325ecc9c1e..63a32e18f2 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/promo/ui/YieldSupplyPromoContent.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/promo/ui/YieldSupplyPromoContent.kt @@ -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), ) } } diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/approve/YieldSupplyApproveComponent.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/approve/YieldSupplyApproveComponent.kt index dc2ffd7777..38fe76b13d 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/approve/YieldSupplyApproveComponent.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/approve/YieldSupplyApproveComponent.kt @@ -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( diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/YieldSupplyStartEarningComponent.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/YieldSupplyStartEarningComponent.kt index c561bab7ca..a40a92a98d 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/YieldSupplyStartEarningComponent.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/YieldSupplyStartEarningComponent.kt @@ -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( diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/YieldSupplyStopEarningComponent.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/YieldSupplyStopEarningComponent.kt index 58c0a5e000..118ad352db 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/YieldSupplyStopEarningComponent.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/YieldSupplyStopEarningComponent.kt @@ -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(