Updated on 2026-08-14
This commit is contained in:
parent
2c11ec0405
commit
a561e0f16c
5 changed files with 423 additions and 276 deletions
|
|
@ -3,21 +3,19 @@ package com.tangem.features.foryou.impl.model
|
|||
import arrow.core.right
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.common.test.domain.wallet.MockUserWalletFactory
|
||||
import com.tangem.common.ui.userwallet.converter.WalletIconUMConverter
|
||||
import com.tangem.core.ui.ds.image.DeviceIconUM
|
||||
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
|
||||
import com.tangem.domain.account.models.AccountStatusList
|
||||
import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.TotalFiatBalance
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletIcon
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.wallets.usecase.GetWalletIconUseCase
|
||||
import com.tangem.features.foryou.impl.components.state.MarketChartUM
|
||||
import com.tangem.features.foryou.impl.entity.PortfolioReviewUM
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.every
|
||||
|
|
@ -36,24 +34,18 @@ import org.junit.jupiter.api.Test
|
|||
import java.math.BigDecimal
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Suppress("LargeClass")
|
||||
internal class ForYouModelTest {
|
||||
|
||||
private val userWalletsListRepository: UserWalletsListRepository = mockk()
|
||||
private val multiAccountStatusListSupplier: MultiAccountStatusListSupplier = mockk()
|
||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase = mockk()
|
||||
private val walletIconUMConverter: WalletIconUMConverter = mockk {
|
||||
every { convert(any()) } returns DeviceIconUM.Stub(cardsCount = 1)
|
||||
}
|
||||
private val getWalletIconUseCase: GetWalletIconUseCase = mockk()
|
||||
|
||||
private var model: ForYouModel? = null
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
every { getWalletIconUseCase.invoke(any()) } returns UserWalletIcon.Stub(cardsCount = 1)
|
||||
// Default: a real, non-empty emission so the model's `getOrElse { Default }` mapping path is
|
||||
// actually exercised in every test, not bypassed by an empty flow. Individual tests may override.
|
||||
// actually exercised in every test, not bypassed by an empty flow.
|
||||
every { getSelectedAppCurrencyUseCase() } returns flowOf(AppCurrency.Default.right())
|
||||
}
|
||||
|
||||
|
|
@ -67,21 +59,19 @@ internal class ForYouModelTest {
|
|||
inner class InitialState {
|
||||
|
||||
@Test
|
||||
fun `GIVEN model created WHEN not yet advanced THEN uiState is Loading`() = runTest {
|
||||
fun `GIVEN model created WHEN not yet advanced THEN uiState is Loading with skeleton rows`() = runTest {
|
||||
// Arrange
|
||||
every { userWalletsListRepository.userWallets } returns MutableStateFlow(null)
|
||||
every { userWalletsListRepository.selectedUserWallet } returns MutableStateFlow(null)
|
||||
every { multiAccountStatusListSupplier.invokeAsMap() } returns flowOf(linkedMapOf())
|
||||
every { getSelectedAppCurrencyUseCase() } returns flowOf(AppCurrency.Default.right())
|
||||
|
||||
// Act
|
||||
val model = createModel(testScope = this)
|
||||
|
||||
// Assert — before advancing, the model exposes skeleton placeholder rows
|
||||
val loading = model.uiState.value.portfolioReviewUM as PortfolioReviewUM.Loading
|
||||
|
||||
assertThat(loading.tokenList).hasSize(4)
|
||||
assertThat(loading.tokenList.all { it.tokenRowUM is TangemTokenRowUM.Loading }).isTrue()
|
||||
assertThat(loading.marketChartUM).isEqualTo(MarketChartUM.NoData)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -89,107 +79,42 @@ internal class ForYouModelTest {
|
|||
inner class ContentState {
|
||||
|
||||
@Test
|
||||
fun `GIVEN wallets and account statuses emitted WHEN advanced THEN uiState becomes Content with tabs`() =
|
||||
fun `GIVEN selected wallet and statuses emitted WHEN advanced THEN uiState becomes Content`() = runTest {
|
||||
// Arrange
|
||||
val currency = createCoin(rawCurrencyId = "btc", symbol = "BTC")
|
||||
stubSelectedWallet(
|
||||
currencies = listOf(createStatus(currency, loadedValue(BigDecimal("100")))),
|
||||
totalFiatBalance = BigDecimal("100"),
|
||||
)
|
||||
|
||||
// Act
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
val content = model.uiState.value.portfolioReviewUM as PortfolioReviewUM.Content
|
||||
assertThat(content.tokenList.map { it.tokenRowUM.id }).containsExactly("btc")
|
||||
assertThat(content.marketChartUM).isInstanceOf(MarketChartUM.Loaded::class.java)
|
||||
assertThat(model.uiState.value.notifications).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN total balance from outdated source WHEN advanced THEN outdated-data notification is shown`() =
|
||||
runTest {
|
||||
// Arrange
|
||||
val walletOne = MockUserWalletFactory.create().copy(walletId = UserWalletId("01"), name = "Wallet 1")
|
||||
val walletTwo = MockUserWalletFactory.create().copy(walletId = UserWalletId("02"), name = "Wallet 2")
|
||||
every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(walletOne, walletTwo))
|
||||
every { userWalletsListRepository.selectedUserWallet } returns MutableStateFlow(walletOne)
|
||||
|
||||
val currency = createCoin(rawCurrencyId = "btc", symbol = "BTC")
|
||||
val accountStatusList = createAccountStatusList(
|
||||
userWalletId = walletOne.walletId,
|
||||
stubSelectedWallet(
|
||||
currencies = listOf(createStatus(currency, loadedValue(BigDecimal("100")))),
|
||||
totalFiatBalance = BigDecimal("100"),
|
||||
source = StatusSource.ONLY_CACHE,
|
||||
)
|
||||
every { multiAccountStatusListSupplier.invokeAsMap() } returns flowOf(
|
||||
linkedMapOf(walletOne.walletId to accountStatusList),
|
||||
)
|
||||
every { getSelectedAppCurrencyUseCase() } returns flowOf()
|
||||
|
||||
// Act
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
val content = model.uiState.value.portfolioReviewUM as PortfolioReviewUM.Content
|
||||
assertThat(content.assetCount).isNotNull()
|
||||
assertThat(model.uiState.value.walletListUM.items).hasSize(2)
|
||||
assertThat(model.uiState.value.walletListUM.items.map { it.isSelected }).containsExactly(true, false)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN exactly one wallet WHEN advanced THEN walletListUM items is empty`() = runTest {
|
||||
// Arrange
|
||||
val wallet = MockUserWalletFactory.create().copy(walletId = UserWalletId("01"), name = "Wallet 1")
|
||||
every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(wallet))
|
||||
every { userWalletsListRepository.selectedUserWallet } returns MutableStateFlow(wallet)
|
||||
|
||||
val accountStatusList = createAccountStatusList(
|
||||
userWalletId = wallet.walletId,
|
||||
currencies = emptyList(),
|
||||
totalFiatBalance = BigDecimal.ZERO,
|
||||
)
|
||||
every { multiAccountStatusListSupplier.invokeAsMap() } returns flowOf(
|
||||
linkedMapOf(wallet.walletId to accountStatusList),
|
||||
)
|
||||
every { getSelectedAppCurrencyUseCase() } returns flowOf()
|
||||
|
||||
// Act
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert — the "tabs.size != 1" rule: a single wallet shows no tabs
|
||||
assertThat(model.uiState.value.walletListUM.items).isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class TabClick {
|
||||
|
||||
@Test
|
||||
fun `GIVEN two wallets WHEN onTabClick THEN locally selected wallet switches and currencies rederive`() =
|
||||
runTest {
|
||||
// Arrange
|
||||
val walletOne = MockUserWalletFactory.create().copy(walletId = UserWalletId("01"), name = "Wallet 1")
|
||||
val walletTwo = MockUserWalletFactory.create().copy(walletId = UserWalletId("02"), name = "Wallet 2")
|
||||
every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(walletOne, walletTwo))
|
||||
every { userWalletsListRepository.selectedUserWallet } returns MutableStateFlow(walletOne)
|
||||
|
||||
val btc = createCoin(rawCurrencyId = "btc", symbol = "BTC")
|
||||
val eth = createCoin(rawCurrencyId = "eth", symbol = "ETH")
|
||||
val statusOne = createAccountStatusList(
|
||||
userWalletId = walletOne.walletId,
|
||||
currencies = listOf(createStatus(btc, loadedValue(BigDecimal("100")))),
|
||||
totalFiatBalance = BigDecimal("100"),
|
||||
)
|
||||
val statusTwo = createAccountStatusList(
|
||||
userWalletId = walletTwo.walletId,
|
||||
currencies = listOf(createStatus(eth, loadedValue(BigDecimal("50")))),
|
||||
totalFiatBalance = BigDecimal("50"),
|
||||
)
|
||||
every { multiAccountStatusListSupplier.invokeAsMap() } returns flowOf(
|
||||
linkedMapOf(walletOne.walletId to statusOne, walletTwo.walletId to statusTwo),
|
||||
)
|
||||
every { getSelectedAppCurrencyUseCase() } returns flowOf()
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Act — click the second wallet's tab
|
||||
model.uiState.value.walletListUM.items[1].onClick()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
assertThat(model.uiState.value.walletListUM.items.map { it.isSelected }).containsExactly(
|
||||
false,
|
||||
true,
|
||||
).inOrder()
|
||||
val content = model.uiState.value.portfolioReviewUM as PortfolioReviewUM.Content
|
||||
val assetRow = content.tokenList.single().tokenRowUM as TangemTokenRowUM.Content
|
||||
val titleUM = assetRow.titleUM as TangemTokenRowUM.TitleUM.Content
|
||||
assertThat(titleUM.text).isEqualTo(com.tangem.core.ui.extensions.stringReference("ETH"))
|
||||
assertThat(model.uiState.value.notifications).containsExactly(ForYouNotification.UsedOutdatedData)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -199,29 +124,18 @@ internal class ForYouModelTest {
|
|||
@Test
|
||||
fun `GIVEN asset row clicked WHEN clicked again THEN isExpanded toggles back to false`() = runTest {
|
||||
// Arrange
|
||||
val wallet = MockUserWalletFactory.create().copy(walletId = UserWalletId("01"), name = "Wallet 1")
|
||||
every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(wallet))
|
||||
every { userWalletsListRepository.selectedUserWallet } returns MutableStateFlow(wallet)
|
||||
|
||||
val currency = createCoin(rawCurrencyId = "btc", symbol = "BTC")
|
||||
val accountStatusList = createAccountStatusList(
|
||||
userWalletId = wallet.walletId,
|
||||
stubSelectedWallet(
|
||||
currencies = listOf(createStatus(currency, loadedValue(BigDecimal("100")))),
|
||||
totalFiatBalance = BigDecimal("100"),
|
||||
)
|
||||
every { multiAccountStatusListSupplier.invokeAsMap() } returns flowOf(
|
||||
linkedMapOf(wallet.walletId to accountStatusList),
|
||||
)
|
||||
every { getSelectedAppCurrencyUseCase() } returns flowOf()
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
val initialContent = model.uiState.value.portfolioReviewUM as PortfolioReviewUM.Content
|
||||
assertThat(initialContent.tokenList.single().isExpanded).isFalse()
|
||||
|
||||
// Act — click once to expand
|
||||
val assetRow = initialContent.tokenList.single().tokenRowUM as TangemTokenRowUM.Content
|
||||
assetRow.onItemClick?.invoke()
|
||||
initialContent.assetRow().onItemClick?.invoke()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
|
|
@ -229,8 +143,7 @@ internal class ForYouModelTest {
|
|||
assertThat(expandedContent.tokenList.single().isExpanded).isTrue()
|
||||
|
||||
// Act — click again to collapse
|
||||
val expandedRow = expandedContent.tokenList.single().tokenRowUM as TangemTokenRowUM.Content
|
||||
expandedRow.onItemClick?.invoke()
|
||||
expandedContent.assetRow().onItemClick?.invoke()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
|
|
@ -246,26 +159,15 @@ internal class ForYouModelTest {
|
|||
fun `GIVEN Content state WHEN period clicked THEN initialSelectedItem updates without resetting rest`() =
|
||||
runTest {
|
||||
// Arrange
|
||||
val wallet = MockUserWalletFactory.create().copy(walletId = UserWalletId("01"), name = "Wallet 1")
|
||||
every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(wallet))
|
||||
every { userWalletsListRepository.selectedUserWallet } returns MutableStateFlow(wallet)
|
||||
|
||||
val currency = createCoin(rawCurrencyId = "btc", symbol = "BTC")
|
||||
val accountStatusList = createAccountStatusList(
|
||||
userWalletId = wallet.walletId,
|
||||
stubSelectedWallet(
|
||||
currencies = listOf(createStatus(currency, loadedValue(BigDecimal("100")))),
|
||||
totalFiatBalance = BigDecimal("100"),
|
||||
)
|
||||
every { multiAccountStatusListSupplier.invokeAsMap() } returns flowOf(
|
||||
linkedMapOf(wallet.walletId to accountStatusList),
|
||||
)
|
||||
every { getSelectedAppCurrencyUseCase() } returns flowOf()
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
val contentBefore = model.uiState.value.portfolioReviewUM as PortfolioReviewUM.Content
|
||||
val weekItem = contentBefore.periodPickerUM.items[1]
|
||||
val assetCountBefore = contentBefore.assetCount
|
||||
|
||||
// Act
|
||||
contentBefore.onPeriodClick(weekItem)
|
||||
|
|
@ -273,19 +175,34 @@ internal class ForYouModelTest {
|
|||
// Assert
|
||||
val contentAfter = model.uiState.value.portfolioReviewUM as PortfolioReviewUM.Content
|
||||
assertThat(contentAfter.periodPickerUM.initialSelectedItem).isEqualTo(weekItem)
|
||||
assertThat(contentAfter.assetCount).isEqualTo(assetCountBefore)
|
||||
assertThat(contentAfter.tokenList).isEqualTo(contentBefore.tokenList)
|
||||
}
|
||||
}
|
||||
|
||||
private fun PortfolioReviewUM.Content.assetRow(): TangemTokenRowUM.Content =
|
||||
tokenList.single().tokenRowUM as TangemTokenRowUM.Content
|
||||
|
||||
/** Wires the repository + supplier so the model derives Content from a single selected wallet. */
|
||||
private fun stubSelectedWallet(
|
||||
currencies: List<CryptoCurrencyStatus>,
|
||||
totalFiatBalance: BigDecimal,
|
||||
source: StatusSource = StatusSource.ACTUAL,
|
||||
) {
|
||||
val wallet = MockUserWalletFactory.create().copy(walletId = UserWalletId("01"))
|
||||
every { userWalletsListRepository.selectedUserWallet } returns MutableStateFlow(wallet)
|
||||
every { multiAccountStatusListSupplier.invokeAsMap() } returns flowOf(
|
||||
linkedMapOf(
|
||||
wallet.walletId to createAccountStatusList(currencies, totalFiatBalance, source),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createModel(testScope: TestScope): ForYouModel {
|
||||
return ForYouModel(
|
||||
userWalletsListRepository = userWalletsListRepository,
|
||||
multiAccountStatusListSupplier = multiAccountStatusListSupplier,
|
||||
dispatchers = testScope.createTestingCoroutineDispatcherProvider(),
|
||||
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
|
||||
walletIconUMConverter = walletIconUMConverter,
|
||||
getWalletIconUseCase = getWalletIconUseCase,
|
||||
).also { model = it }
|
||||
}
|
||||
|
||||
|
|
@ -301,15 +218,14 @@ internal class ForYouModelTest {
|
|||
}
|
||||
|
||||
private fun createAccountStatusList(
|
||||
userWalletId: UserWalletId,
|
||||
currencies: List<CryptoCurrencyStatus>,
|
||||
totalFiatBalance: BigDecimal,
|
||||
source: StatusSource = StatusSource.ACTUAL,
|
||||
): AccountStatusList = mockk {
|
||||
every { this@mockk.userWalletId } returns userWalletId
|
||||
every { flattenCurrencies() } returns currencies
|
||||
every { this@mockk.totalFiatBalance } returns TotalFiatBalance.Loaded(
|
||||
amount = totalFiatBalance,
|
||||
source = com.tangem.domain.models.StatusSource.ACTUAL,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -322,6 +238,7 @@ internal class ForYouModelTest {
|
|||
every { amount } returns BigDecimal.ONE
|
||||
every { this@mockk.fiatAmount } returns fiatAmount
|
||||
every { isError } returns false
|
||||
every { sources } returns CryptoCurrencyStatus.Sources()
|
||||
}
|
||||
|
||||
private fun createCoin(rawCurrencyId: String, symbol: String): CryptoCurrency.Coin {
|
||||
|
|
@ -337,6 +254,7 @@ internal class ForYouModelTest {
|
|||
return mockk<CryptoCurrency.Coin> {
|
||||
every { this@mockk.id } returns currencyId
|
||||
every { this@mockk.symbol } returns symbol
|
||||
every { this@mockk.name } returns symbol
|
||||
every { this@mockk.network } returns network
|
||||
every { this@mockk.decimals } returns 8
|
||||
every { isCustom } returns false
|
||||
|
|
|
|||
|
|
@ -1,11 +1,6 @@
|
|||
package com.tangem.features.foryou.impl.model.converter
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import io.mockk.every
|
||||
|
|
@ -16,8 +11,6 @@ import java.math.BigDecimal
|
|||
|
||||
internal class ForYouPortfolioFormattersTest {
|
||||
|
||||
private val appCurrency: AppCurrency = AppCurrency.Default
|
||||
|
||||
@Nested
|
||||
inner class ForYouGroupKey {
|
||||
|
||||
|
|
@ -62,102 +55,66 @@ internal class ForYouPortfolioFormattersTest {
|
|||
}
|
||||
|
||||
@Nested
|
||||
inner class ToForYouFiatText {
|
||||
inner class ToForYouPercent {
|
||||
|
||||
@Test
|
||||
fun `GIVEN a fiat amount WHEN toForYouFiatText THEN delegates to fiat formatting`() {
|
||||
// Arrange
|
||||
val amount = BigDecimal("1234.5")
|
||||
|
||||
// Act
|
||||
val result = amount.toForYouFiatText(appCurrency)
|
||||
|
||||
// Assert
|
||||
val expected = stringReference(
|
||||
amount.format { fiat(fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol) },
|
||||
)
|
||||
assertThat(result).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN null amount WHEN toForYouFiatText THEN renders dash text`() {
|
||||
fun `GIVEN null amount WHEN toForYouPercent THEN returns null`() {
|
||||
// Arrange
|
||||
val amount: BigDecimal? = null
|
||||
|
||||
// Act
|
||||
val result = amount.toForYouFiatText(appCurrency)
|
||||
val result = amount.toForYouPercent(BigDecimal("100"))
|
||||
|
||||
// Assert
|
||||
val expected = stringReference(
|
||||
amount.format { fiat(fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol) },
|
||||
)
|
||||
assertThat(result).isEqualTo(expected)
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class ToForYouPercentText {
|
||||
|
||||
@Test
|
||||
fun `GIVEN null amount WHEN toForYouPercentText THEN returns EMPTY`() {
|
||||
// Arrange
|
||||
val amount: BigDecimal? = null
|
||||
|
||||
// Act
|
||||
val result = amount.toForYouPercentText(BigDecimal("100"))
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEqualTo(TextReference.EMPTY)
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN zero total WHEN toForYouPercentText THEN returns EMPTY`() {
|
||||
fun `GIVEN zero total WHEN toForYouPercent THEN returns null`() {
|
||||
// Arrange
|
||||
val amount = BigDecimal("10")
|
||||
|
||||
// Act
|
||||
val result = amount.toForYouPercentText(BigDecimal.ZERO)
|
||||
val result = amount.toForYouPercent(BigDecimal.ZERO)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEqualTo(TextReference.EMPTY)
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN zero amount WHEN toForYouPercentText THEN returns EMPTY`() {
|
||||
fun `GIVEN zero amount WHEN toForYouPercent THEN returns null`() {
|
||||
// Arrange
|
||||
val amount = BigDecimal.ZERO
|
||||
|
||||
// Act
|
||||
val result = amount.toForYouPercentText(BigDecimal("100"))
|
||||
val result = amount.toForYouPercent(BigDecimal("100"))
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEqualTo(TextReference.EMPTY)
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN non-zero amount and total WHEN toForYouPercentText THEN returns rounded percent share`() {
|
||||
// Arrange
|
||||
val amount = BigDecimal("25.00")
|
||||
val total = BigDecimal("100")
|
||||
fun `GIVEN non-zero amount and total WHEN toForYouPercent THEN returns the share as a ratio`() {
|
||||
// Arrange — 50.00 / 200 = 0.25 (ratio, scaled to the amount's scale)
|
||||
val amount = BigDecimal("50.00")
|
||||
|
||||
// Act
|
||||
val result = amount.toForYouPercentText(total)
|
||||
val result = amount.toForYouPercent(BigDecimal("200"))
|
||||
|
||||
// Assert — 25.00 / 100 = 0.25 -> 25.00%
|
||||
assertThat(result).isEqualTo(stringReference("25.00%"))
|
||||
// Assert
|
||||
assertThat(result).isEqualTo(BigDecimal("0.25"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN a share requiring rounding WHEN toForYouPercentText THEN applies HALF_UP rounding`() {
|
||||
// Arrange — 1.0000 / 3 = 0.3333... -> rounds to 33.33%
|
||||
fun `GIVEN a share requiring rounding WHEN toForYouPercent THEN applies HALF_UP rounding`() {
|
||||
// Arrange — 1.0000 / 3 = 0.3333... rounds HALF_UP to the amount's scale (4)
|
||||
val amount = BigDecimal("1.0000")
|
||||
val total = BigDecimal("3")
|
||||
|
||||
// Act
|
||||
val result = amount.toForYouPercentText(total)
|
||||
val result = amount.toForYouPercent(BigDecimal("3"))
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEqualTo(stringReference("33.33%"))
|
||||
assertThat(result).isEqualTo(BigDecimal("0.3333"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.features.foryou.impl.model.converter
|
|||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
|
||||
import com.tangem.core.ui.extensions.pluralReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
|
|
@ -77,7 +78,7 @@ internal class ForYouTokenListConverterTest {
|
|||
val item = result.single()
|
||||
val row = item.tokenRowUM as TangemTokenRowUM.Content
|
||||
val subtitle = row.subtitleUM as TangemTokenRowUM.SubtitleUM.Content
|
||||
assertThat(subtitle.text).isEqualTo(stringReference("2 networks"))
|
||||
assertThat(subtitle.text).isEqualTo(pluralReference(R.plurals.common_networks_count, count = 2))
|
||||
assertThat(item.tokenList).hasSize(2)
|
||||
}
|
||||
|
||||
|
|
@ -115,13 +116,13 @@ internal class ForYouTokenListConverterTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN otherAssetCount is zero WHEN convert THEN no Other row is appended`() {
|
||||
fun `GIVEN no other assets WHEN convert THEN no Other row is appended`() {
|
||||
// Arrange
|
||||
val currency = createCoin(rawCurrencyId = "bitcoin", symbol = "BTC", networkId = "bitcoin")
|
||||
val status = createStatus(currency, loadedValue(BigDecimal("1"), BigDecimal("100")))
|
||||
val converter = createConverter(
|
||||
totalFiatBalance = BigDecimal("100"),
|
||||
otherAssetCount = 0,
|
||||
otherAssets = emptyList(),
|
||||
)
|
||||
|
||||
// Act
|
||||
|
|
@ -132,14 +133,13 @@ internal class ForYouTokenListConverterTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN otherAssetCount is one WHEN convert THEN Other row subtitle is singular`() {
|
||||
fun `GIVEN a single other asset WHEN convert THEN Other row subtitle is singular`() {
|
||||
// Arrange
|
||||
val currency = createCoin(rawCurrencyId = "bitcoin", symbol = "BTC", networkId = "bitcoin")
|
||||
val status = createStatus(currency, loadedValue(BigDecimal("1"), BigDecimal("100")))
|
||||
val converter = createConverter(
|
||||
totalFiatBalance = BigDecimal("100"),
|
||||
otherAssetCount = 1,
|
||||
otherFiatBalance = BigDecimal("50"),
|
||||
otherAssets = listOf(otherAsset(BigDecimal("50"))),
|
||||
)
|
||||
|
||||
// Act
|
||||
|
|
@ -149,18 +149,21 @@ internal class ForYouTokenListConverterTest {
|
|||
val otherRow = result.last().tokenRowUM as TangemTokenRowUM.Content
|
||||
assertThat(otherRow.id).isEqualTo("for_you_other_assets")
|
||||
val subtitle = otherRow.subtitleUM as TangemTokenRowUM.SubtitleUM.Content
|
||||
assertThat(subtitle.text).isEqualTo(stringReference("1 asset"))
|
||||
assertThat(subtitle.text).isEqualTo(pluralReference(R.plurals.common_assets, count = 1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN otherAssetCount is more than one WHEN convert THEN Other row subtitle is plural`() {
|
||||
fun `GIVEN more than one other asset WHEN convert THEN Other row subtitle is plural`() {
|
||||
// Arrange
|
||||
val currency = createCoin(rawCurrencyId = "bitcoin", symbol = "BTC", networkId = "bitcoin")
|
||||
val status = createStatus(currency, loadedValue(BigDecimal("1"), BigDecimal("100")))
|
||||
val converter = createConverter(
|
||||
totalFiatBalance = BigDecimal("100"),
|
||||
otherAssetCount = 3,
|
||||
otherFiatBalance = BigDecimal("50"),
|
||||
otherAssets = listOf(
|
||||
otherAsset(BigDecimal("30")),
|
||||
otherAsset(BigDecimal("15")),
|
||||
otherAsset(BigDecimal("5")),
|
||||
),
|
||||
)
|
||||
|
||||
// Act
|
||||
|
|
@ -169,7 +172,7 @@ internal class ForYouTokenListConverterTest {
|
|||
// Assert
|
||||
val otherRow = result.last().tokenRowUM as TangemTokenRowUM.Content
|
||||
val subtitle = otherRow.subtitleUM as TangemTokenRowUM.SubtitleUM.Content
|
||||
assertThat(subtitle.text).isEqualTo(stringReference("3 assets"))
|
||||
assertThat(subtitle.text).isEqualTo(pluralReference(R.plurals.common_assets, count = 3))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -210,17 +213,22 @@ internal class ForYouTokenListConverterTest {
|
|||
private fun createConverter(
|
||||
totalFiatBalance: BigDecimal,
|
||||
expandedAssetIds: Set<String> = emptySet(),
|
||||
otherAssetCount: Int = 0,
|
||||
otherFiatBalance: BigDecimal = BigDecimal.ZERO,
|
||||
otherAssets: List<Pair<List<CryptoCurrencyStatus>, BigDecimal>> = emptyList(),
|
||||
): ForYouTokenListConverter = ForYouTokenListConverter(
|
||||
appCurrency = appCurrency,
|
||||
totalFiatBalance = totalFiatBalance,
|
||||
expandedAssetIds = expandedAssetIds,
|
||||
expandClick = {},
|
||||
otherAssetCount = otherAssetCount,
|
||||
otherFiatBalance = otherFiatBalance,
|
||||
otherAssets = otherAssets,
|
||||
)
|
||||
|
||||
/**
|
||||
* Builds an "other" asset entry — only its summed [balance] and the number of entries drive the
|
||||
* collapsed "Other" row, so the currency list is left empty.
|
||||
*/
|
||||
private fun otherAsset(balance: BigDecimal): Pair<List<CryptoCurrencyStatus>, BigDecimal> =
|
||||
emptyList<CryptoCurrencyStatus>() to balance
|
||||
|
||||
private fun createStatus(currency: CryptoCurrency, value: CryptoCurrencyStatus.Value) = CryptoCurrencyStatus(
|
||||
currency = currency,
|
||||
value = value,
|
||||
|
|
@ -230,6 +238,7 @@ internal class ForYouTokenListConverterTest {
|
|||
every { this@mockk.amount } returns amount
|
||||
every { this@mockk.fiatAmount } returns fiatAmount
|
||||
every { isError } returns false
|
||||
every { sources } returns CryptoCurrencyStatus.Sources()
|
||||
}
|
||||
|
||||
private fun createCoin(rawCurrencyId: String, symbol: String, networkId: String): CryptoCurrency.Coin {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,13 @@ package com.tangem.features.foryou.impl.model.converter
|
|||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.percent
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
|
|
@ -49,8 +55,8 @@ internal class ForYouTokenRowConverterTest {
|
|||
assertThat(result.id).isEqualTo("coin-eth")
|
||||
val topEnd = result.topEndContentUM as TangemTokenRowUM.EndContentUM.Content
|
||||
val bottomEnd = result.bottomEndContentUM as TangemTokenRowUM.EndContentUM.Content
|
||||
assertThat(topEnd.text).isEqualTo(BigDecimal("400").toForYouFiatText(appCurrency))
|
||||
assertThat(bottomEnd.text).isEqualTo(BigDecimal("400").toForYouPercentText(BigDecimal("1000")))
|
||||
assertThat(topEnd.text).isEqualTo(BigDecimal("400").expectedFiatText())
|
||||
assertThat(bottomEnd.text).isEqualTo(BigDecimal("400").expectedPercentText(BigDecimal("1000")))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -68,7 +74,7 @@ internal class ForYouTokenRowConverterTest {
|
|||
|
||||
// Assert
|
||||
val topEnd = result.topEndContentUM as TangemTokenRowUM.EndContentUM.Content
|
||||
assertThat(topEnd.text).isEqualTo(BigDecimal("600").toForYouFiatText(appCurrency))
|
||||
assertThat(topEnd.text).isEqualTo(BigDecimal("600").expectedFiatText())
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -87,6 +93,124 @@ internal class ForYouTokenRowConverterTest {
|
|||
// Assert
|
||||
assertThat(result).isInstanceOf(TangemTokenRowUM.Content::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN loaded status from cache WHEN convertNetworkGroup THEN content flickers`() {
|
||||
// Arrange
|
||||
val currency = createCurrency(id = "coin-eth", symbol = "ETH", networkName = "Ethereum")
|
||||
val statuses = listOf(
|
||||
createStatus(
|
||||
currency,
|
||||
loadedValue(amount = BigDecimal("1"), fiatAmount = BigDecimal("100"), source = StatusSource.CACHE),
|
||||
),
|
||||
)
|
||||
val converter = createConverter(totalFiatBalance = BigDecimal("1000"))
|
||||
|
||||
// Act
|
||||
val result = converter.convertNetworkGroup(statuses) as TangemTokenRowUM.Content
|
||||
|
||||
// Assert
|
||||
val topEnd = result.topEndContentUM as TangemTokenRowUM.EndContentUM.Content
|
||||
val bottomEnd = result.bottomEndContentUM as TangemTokenRowUM.EndContentUM.Content
|
||||
assertThat(topEnd.isFlickering).isTrue()
|
||||
assertThat(bottomEnd.isFlickering).isTrue()
|
||||
assertThat(topEnd.startIcons).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN loaded status only-cache WHEN convertNetworkGroup THEN error-sync start icon shown`() {
|
||||
// Arrange
|
||||
val currency = createCurrency(id = "coin-eth", symbol = "ETH", networkName = "Ethereum")
|
||||
val statuses = listOf(
|
||||
createStatus(
|
||||
currency,
|
||||
loadedValue(
|
||||
amount = BigDecimal("1"),
|
||||
fiatAmount = BigDecimal("100"),
|
||||
source = StatusSource.ONLY_CACHE,
|
||||
),
|
||||
),
|
||||
)
|
||||
val converter = createConverter(totalFiatBalance = BigDecimal("1000"))
|
||||
|
||||
// Act
|
||||
val result = converter.convertNetworkGroup(statuses) as TangemTokenRowUM.Content
|
||||
|
||||
// Assert
|
||||
val topEnd = result.topEndContentUM as TangemTokenRowUM.EndContentUM.Content
|
||||
assertThat(topEnd.isFlickering).isFalse()
|
||||
assertThat(topEnd.startIcons).hasSize(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN missed derivation status WHEN convertNetworkGroup THEN no-address treatment`() {
|
||||
// Arrange
|
||||
val currency = createCurrency(id = "coin-eth", symbol = "ETH", networkName = "Ethereum")
|
||||
val statuses = listOf(createStatus(currency, missedDerivationValue()))
|
||||
val converter = createConverter(totalFiatBalance = BigDecimal("1000"))
|
||||
|
||||
// Act
|
||||
val result = converter.convertNetworkGroup(statuses) as TangemTokenRowUM.Content
|
||||
|
||||
// Assert — top-end is a dash, bottom-end carries the attention "no address" icon
|
||||
val topEnd = result.topEndContentUM as TangemTokenRowUM.EndContentUM.Content
|
||||
val bottomEnd = result.bottomEndContentUM as TangemTokenRowUM.EndContentUM.Content
|
||||
assertThat(topEnd.endIcons).isEmpty()
|
||||
assertThat(bottomEnd.endIcons).hasSize(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN unreachable status WHEN convertNetworkGroup THEN dash on top and attention icon on bottom`() {
|
||||
// Arrange
|
||||
val currency = createCurrency(id = "coin-eth", symbol = "ETH", networkName = "Ethereum")
|
||||
val statuses = listOf(createStatus(currency, unreachableValue()))
|
||||
val converter = createConverter(totalFiatBalance = BigDecimal("1000"))
|
||||
|
||||
// Act
|
||||
val result = converter.convertNetworkGroup(statuses) as TangemTokenRowUM.Content
|
||||
|
||||
// Assert — top-end is a bare dash, the attention "unreachable" icon lives on the bottom end
|
||||
val topEnd = result.topEndContentUM as TangemTokenRowUM.EndContentUM.Content
|
||||
val bottomEnd = result.bottomEndContentUM as TangemTokenRowUM.EndContentUM.Content
|
||||
assertThat(topEnd.endIcons).isEmpty()
|
||||
assertThat(bottomEnd.endIcons).hasSize(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN mixed Loaded and Unreachable WHEN convertNetworkGroup THEN collapses to unreachable`() {
|
||||
// Arrange — one account resolved, another unreachable: the row must surface the error state
|
||||
val currency = createCurrency(id = "coin-eth", symbol = "ETH", networkName = "Ethereum")
|
||||
val statuses = listOf(
|
||||
createStatus(currency, loadedValue(amount = BigDecimal("1"), fiatAmount = BigDecimal("100"))),
|
||||
createStatus(currency, unreachableValue()),
|
||||
)
|
||||
val converter = createConverter(totalFiatBalance = BigDecimal("1000"))
|
||||
|
||||
// Act
|
||||
val result = converter.convertNetworkGroup(statuses) as TangemTokenRowUM.Content
|
||||
|
||||
// Assert — the unreachable treatment (attention icon on the bottom end) wins over the loaded amount
|
||||
val bottomEnd = result.bottomEndContentUM as TangemTokenRowUM.EndContentUM.Content
|
||||
assertThat(bottomEnd.endIcons).hasSize(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN mixed MissedDerivation and Unreachable WHEN convertNetworkGroup THEN missed-derivation wins`() {
|
||||
// Arrange — missed derivation is the most severe terminal state and dominates
|
||||
val currency = createCurrency(id = "coin-eth", symbol = "ETH", networkName = "Ethereum")
|
||||
val statuses = listOf(
|
||||
createStatus(currency, unreachableValue()),
|
||||
createStatus(currency, missedDerivationValue()),
|
||||
)
|
||||
val converter = createConverter(totalFiatBalance = BigDecimal("1000"))
|
||||
|
||||
// Act
|
||||
val result = converter.convertNetworkGroup(statuses) as TangemTokenRowUM.Content
|
||||
|
||||
// Assert — top-end is a dash (no-address treatment), not an unreachable label
|
||||
val topEnd = result.topEndContentUM as TangemTokenRowUM.EndContentUM.Content
|
||||
assertThat(topEnd.endIcons).isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createConverter(totalFiatBalance: BigDecimal) = ForYouTokenRowConverter(
|
||||
|
|
@ -94,15 +218,46 @@ internal class ForYouTokenRowConverterTest {
|
|||
totalFiatBalance = totalFiatBalance,
|
||||
)
|
||||
|
||||
/** Mirrors the production fiat rendering used by [ForYouTokenRowConverter] for a resolved row. */
|
||||
private fun BigDecimal.expectedFiatText(): TextReference = stringReference(
|
||||
format { fiat(fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol) },
|
||||
)
|
||||
|
||||
/** Mirrors the production percent-share rendering used by [ForYouTokenRowConverter] for a resolved row. */
|
||||
private fun BigDecimal.expectedPercentText(total: BigDecimal): TextReference = stringReference(
|
||||
toForYouPercent(total).format { percent() },
|
||||
)
|
||||
|
||||
private fun createStatus(currency: CryptoCurrency, value: CryptoCurrencyStatus.Value) = CryptoCurrencyStatus(
|
||||
currency = currency,
|
||||
value = value,
|
||||
)
|
||||
|
||||
private fun loadedValue(amount: BigDecimal, fiatAmount: BigDecimal): CryptoCurrencyStatus.Loaded = mockk {
|
||||
private fun loadedValue(
|
||||
amount: BigDecimal,
|
||||
fiatAmount: BigDecimal,
|
||||
source: StatusSource = StatusSource.ACTUAL,
|
||||
): CryptoCurrencyStatus.Loaded = mockk {
|
||||
every { this@mockk.amount } returns amount
|
||||
every { this@mockk.fiatAmount } returns fiatAmount
|
||||
every { isError } returns false
|
||||
every { sources } returns CryptoCurrencyStatus.Sources(
|
||||
networkSource = source,
|
||||
quoteSource = source,
|
||||
stakingBalanceSource = source,
|
||||
)
|
||||
}
|
||||
|
||||
private fun missedDerivationValue(): CryptoCurrencyStatus.MissedDerivation = mockk {
|
||||
every { amount } returns null
|
||||
every { fiatAmount } returns null
|
||||
every { isError } returns true
|
||||
}
|
||||
|
||||
private fun unreachableValue(): CryptoCurrencyStatus.Unreachable = mockk {
|
||||
every { amount } returns null
|
||||
every { fiatAmount } returns null
|
||||
every { isError } returns true
|
||||
}
|
||||
|
||||
private fun createCurrency(
|
||||
|
|
|
|||
|
|
@ -2,14 +2,18 @@ package com.tangem.features.foryou.impl.model.transformer
|
|||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.account.models.AccountStatusList
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.TotalFiatBalance
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.features.commonfeatures.api.choosetoken.model.WalletListUM
|
||||
import com.tangem.features.foryou.impl.components.state.MarketChartUM
|
||||
import com.tangem.features.foryou.impl.entity.ForYouTokenListItemUM
|
||||
import com.tangem.features.foryou.impl.entity.ForYouUM
|
||||
import com.tangem.features.foryou.impl.entity.PortfolioReviewUM
|
||||
import com.tangem.features.foryou.impl.model.ForYouNotification
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
@ -20,13 +24,12 @@ import java.math.BigDecimal
|
|||
internal class SetPortfolioReviewTransformerTest {
|
||||
|
||||
private val appCurrency: AppCurrency = AppCurrency.Default
|
||||
private val walletListUM = WalletListUM(items = persistentListOf())
|
||||
|
||||
@Nested
|
||||
inner class Transform {
|
||||
inner class TokenList {
|
||||
|
||||
@Test
|
||||
fun `GIVEN currency with zero fiat balance WHEN transform THEN it is dropped from asset count`() {
|
||||
fun `GIVEN currency with resolved zero fiat balance WHEN transform THEN it is dropped from the list`() {
|
||||
// Arrange
|
||||
val zeroBalance = createCurrency(rawCurrencyId = "btc", symbol = "BTC")
|
||||
val nonZeroBalance = createCurrency(rawCurrencyId = "eth", symbol = "ETH")
|
||||
|
|
@ -34,18 +37,37 @@ internal class SetPortfolioReviewTransformerTest {
|
|||
createStatus(zeroBalance, loadedValue(BigDecimal.ZERO)),
|
||||
createStatus(nonZeroBalance, loadedValue(BigDecimal("100"))),
|
||||
)
|
||||
val transformer = createTransformer(currencies = currencies, totalFiatBalance = BigDecimal("100"))
|
||||
val transformer = createTransformer(accountStatusList(currencies, loaded(BigDecimal("100"))))
|
||||
|
||||
// Act
|
||||
val result = transformer.transform(loadingState()).portfolioReviewUM as PortfolioReviewUM.Content
|
||||
|
||||
// Assert
|
||||
assertThat(result.assetCount).isEqualTo(stringReference("1 assets"))
|
||||
// Assert — only the ETH asset survives; the zero-fiat BTC is dropped
|
||||
assertThat(result.tokenList.map { it.tokenRowUM.id }).containsExactly("eth")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN assets across networks WHEN transform THEN they are aggregated and ranked by summed fiat`() {
|
||||
// Arrange — same asset (rawCurrencyId "usdc") on two networks aggregates into one asset
|
||||
fun `GIVEN non-content status with null fiat WHEN transform THEN it is kept not dropped`() {
|
||||
// Arrange — a non-content status (Unreachable) carries a null fiatAmount, not a resolved zero;
|
||||
// it must still be shown so the user sees the token they hold, with the appropriate treatment.
|
||||
val unreachable = createCurrency(rawCurrencyId = "btc", symbol = "BTC")
|
||||
val loaded = createCurrency(rawCurrencyId = "eth", symbol = "ETH")
|
||||
val currencies = listOf(
|
||||
createStatus(unreachable, unreachableValue()),
|
||||
createStatus(loaded, loadedValue(BigDecimal("100"))),
|
||||
)
|
||||
val transformer = createTransformer(accountStatusList(currencies, loaded(BigDecimal("100"))))
|
||||
|
||||
// Act
|
||||
val result = transformer.transform(loadingState()).portfolioReviewUM as PortfolioReviewUM.Content
|
||||
|
||||
// Assert — both assets kept, ranked by summed fiat (eth 100 > btc 0)
|
||||
assertThat(result.tokenList.map { it.tokenRowUM.id }).containsExactly("eth", "btc").inOrder()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN same asset across networks WHEN transform THEN aggregated into one asset ranked by summed fiat`() {
|
||||
// Arrange — the same asset (shared rawCurrencyId "usdc") aggregates into one asset
|
||||
val onEth = createCurrency(rawCurrencyId = "usdc", symbol = "USDC")
|
||||
val onSol = createCurrency(rawCurrencyId = "usdc", symbol = "USDC")
|
||||
val other = createCurrency(rawCurrencyId = "btc", symbol = "BTC")
|
||||
|
|
@ -54,13 +76,13 @@ internal class SetPortfolioReviewTransformerTest {
|
|||
createStatus(onSol, loadedValue(BigDecimal("60"))),
|
||||
createStatus(other, loadedValue(BigDecimal("10"))),
|
||||
)
|
||||
val transformer = createTransformer(currencies = currencies, totalFiatBalance = BigDecimal("120"))
|
||||
val transformer = createTransformer(accountStatusList(currencies, loaded(BigDecimal("120"))))
|
||||
|
||||
// Act
|
||||
val result = transformer.transform(loadingState()).portfolioReviewUM as PortfolioReviewUM.Content
|
||||
|
||||
// Assert — 2 ranked assets: usdc (110 total) and btc (10)
|
||||
assertThat(result.assetCount).isEqualTo(stringReference("2 assets"))
|
||||
// Assert — 2 ranked assets: usdc (110 total) ahead of btc (10)
|
||||
assertThat(result.tokenList.map { it.tokenRowUM.id }).containsExactly("usdc", "btc").inOrder()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -72,12 +94,12 @@ internal class SetPortfolioReviewTransformerTest {
|
|||
loadedValue(BigDecimal(100 - index)),
|
||||
)
|
||||
}
|
||||
val transformer = createTransformer(currencies = currencies, totalFiatBalance = BigDecimal("470"))
|
||||
val transformer = createTransformer(accountStatusList(currencies, loaded(BigDecimal("470"))))
|
||||
|
||||
// Act
|
||||
val result = transformer.transform(loadingState()).portfolioReviewUM as PortfolioReviewUM.Content
|
||||
|
||||
// Assert — tokenList has 4 top asset rows + 1 "Other" row = 5 items
|
||||
// Assert — 4 top asset rows + 1 "Other" row
|
||||
assertThat(result.tokenList).hasSize(5)
|
||||
assertThat(result.tokenList.last().tokenRowUM.id).isEqualTo("for_you_other_assets")
|
||||
}
|
||||
|
|
@ -91,7 +113,7 @@ internal class SetPortfolioReviewTransformerTest {
|
|||
loadedValue(BigDecimal(100 - index)),
|
||||
)
|
||||
}
|
||||
val transformer = createTransformer(currencies = currencies, totalFiatBalance = BigDecimal("394"))
|
||||
val transformer = createTransformer(accountStatusList(currencies, loaded(BigDecimal("394"))))
|
||||
|
||||
// Act
|
||||
val result = transformer.transform(loadingState()).portfolioReviewUM as PortfolioReviewUM.Content
|
||||
|
|
@ -101,39 +123,76 @@ internal class SetPortfolioReviewTransformerTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN total and top balance non-zero WHEN transform THEN topHoldingPercent is computed`() {
|
||||
// Arrange — a single asset means top balance == total balance == 100%
|
||||
val currency = createCurrency(rawCurrencyId = "btc", symbol = "BTC")
|
||||
val currencies = listOf(createStatus(currency, loadedValue(BigDecimal("100"))))
|
||||
val transformer = createTransformer(currencies = currencies, totalFiatBalance = BigDecimal("100"))
|
||||
fun `GIVEN null account status list WHEN transform THEN token list is empty`() {
|
||||
// Arrange
|
||||
val transformer = createTransformer(accountStatusList = null)
|
||||
|
||||
// Act
|
||||
val result = transformer.transform(loadingState()).portfolioReviewUM as PortfolioReviewUM.Content
|
||||
|
||||
// Assert
|
||||
assertThat(result.topHoldingPercent).isEqualTo(stringReference("Top holding 100.00%"))
|
||||
assertThat(result.tokenList).isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class MarketChart {
|
||||
|
||||
@Test
|
||||
fun `GIVEN loaded total balance WHEN transform THEN market chart is Loaded with one segment per top asset`() {
|
||||
// Arrange
|
||||
val currencies = listOf(
|
||||
createStatus(createCurrency(rawCurrencyId = "btc", symbol = "BTC"), loadedValue(BigDecimal("70"))),
|
||||
createStatus(createCurrency(rawCurrencyId = "eth", symbol = "ETH"), loadedValue(BigDecimal("30"))),
|
||||
)
|
||||
val transformer = createTransformer(accountStatusList(currencies, loaded(BigDecimal("100"))))
|
||||
|
||||
// Act
|
||||
val result = transformer.transform(loadingState()).portfolioReviewUM as PortfolioReviewUM.Content
|
||||
|
||||
// Assert
|
||||
val marketChart = result.marketChartUM as MarketChartUM.Loaded
|
||||
assertThat(marketChart.assetCount).isEqualTo(2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN zero total fiat balance WHEN transform THEN topHoldingPercent is DASH_SIGN`() {
|
||||
fun `GIVEN non-loaded total balance WHEN transform THEN market chart is NoData`() {
|
||||
// Arrange
|
||||
val currency = createCurrency(rawCurrencyId = "btc", symbol = "BTC")
|
||||
val currencies = listOf(createStatus(currency, loadedValue(BigDecimal.ZERO)))
|
||||
val transformer = createTransformer(currencies = currencies, totalFiatBalance = BigDecimal.ZERO)
|
||||
val currencies = listOf(
|
||||
createStatus(createCurrency(rawCurrencyId = "btc", symbol = "BTC"), loadedValue(BigDecimal("100"))),
|
||||
)
|
||||
val transformer = createTransformer(accountStatusList(currencies, TotalFiatBalance.Loading))
|
||||
|
||||
// Act
|
||||
val result = transformer.transform(loadingState()).portfolioReviewUM as PortfolioReviewUM.Content
|
||||
|
||||
// Assert
|
||||
assertThat(result.topHoldingPercent).isEqualTo(stringReference("Top holding —"))
|
||||
assertThat(result.marketChartUM).isEqualTo(MarketChartUM.NoData)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN null account status list WHEN transform THEN market chart is NoData`() {
|
||||
// Arrange
|
||||
val transformer = createTransformer(accountStatusList = null)
|
||||
|
||||
// Act
|
||||
val result = transformer.transform(loadingState()).portfolioReviewUM as PortfolioReviewUM.Content
|
||||
|
||||
// Assert
|
||||
assertThat(result.marketChartUM).isEqualTo(MarketChartUM.NoData)
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class PeriodPicker {
|
||||
|
||||
@Test
|
||||
fun `GIVEN prev state is Loading WHEN transform THEN period picker is freshly created with Day selected`() {
|
||||
// Arrange
|
||||
val currency = createCurrency(rawCurrencyId = "btc", symbol = "BTC")
|
||||
val currencies = listOf(createStatus(currency, loadedValue(BigDecimal("10"))))
|
||||
val transformer = createTransformer(currencies = currencies, totalFiatBalance = BigDecimal("10"))
|
||||
val currencies = listOf(
|
||||
createStatus(createCurrency(rawCurrencyId = "btc", symbol = "BTC"), loadedValue(BigDecimal("10"))),
|
||||
)
|
||||
val transformer = createTransformer(accountStatusList(currencies, loaded(BigDecimal("10"))))
|
||||
|
||||
// Act
|
||||
val result = transformer.transform(loadingState()).portfolioReviewUM as PortfolioReviewUM.Content
|
||||
|
|
@ -150,15 +209,17 @@ internal class SetPortfolioReviewTransformerTest {
|
|||
@Test
|
||||
fun `GIVEN prev state is Content WHEN transform THEN period picker selection is preserved`() {
|
||||
// Arrange
|
||||
val currency = createCurrency(rawCurrencyId = "btc", symbol = "BTC")
|
||||
val currencies = listOf(createStatus(currency, loadedValue(BigDecimal("10"))))
|
||||
val transformer = createTransformer(currencies = currencies, totalFiatBalance = BigDecimal("10"))
|
||||
val prevContentState = transformer.transform(loadingState()).portfolioReviewUM as PortfolioReviewUM.Content
|
||||
val weekItem = prevContentState.periodPickerUM.items[1]
|
||||
val prevWithWeekSelected = prevContentState.copy(
|
||||
periodPickerUM = prevContentState.periodPickerUM.copy(initialSelectedItem = weekItem),
|
||||
val currencies = listOf(
|
||||
createStatus(createCurrency(rawCurrencyId = "btc", symbol = "BTC"), loadedValue(BigDecimal("10"))),
|
||||
)
|
||||
val transformer = createTransformer(accountStatusList(currencies, loaded(BigDecimal("10"))))
|
||||
val prevContent = transformer.transform(loadingState()).portfolioReviewUM as PortfolioReviewUM.Content
|
||||
val weekItem = prevContent.periodPickerUM.items[1]
|
||||
val prevState = loadingState().copy(
|
||||
portfolioReviewUM = prevContent.copy(
|
||||
periodPickerUM = prevContent.periodPickerUM.copy(initialSelectedItem = weekItem),
|
||||
),
|
||||
)
|
||||
val prevState = ForYouUM(walletListUM = walletListUM, portfolioReviewUM = prevWithWeekSelected)
|
||||
|
||||
// Act
|
||||
val result = transformer.transform(prevState).portfolioReviewUM as PortfolioReviewUM.Content
|
||||
|
|
@ -166,48 +227,86 @@ internal class SetPortfolioReviewTransformerTest {
|
|||
// Assert
|
||||
assertThat(result.periodPickerUM.initialSelectedItem).isEqualTo(weekItem)
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class Notifications {
|
||||
|
||||
@Test
|
||||
fun `GIVEN new state WHEN transform THEN walletListUM is applied from constructor`() {
|
||||
fun `GIVEN total balance from outdated source WHEN transform THEN outdated-data notification is emitted`() {
|
||||
// Arrange
|
||||
val currency = createCurrency(rawCurrencyId = "btc", symbol = "BTC")
|
||||
val currencies = listOf(createStatus(currency, loadedValue(BigDecimal("10"))))
|
||||
val newWalletListUM = WalletListUM(items = persistentListOf())
|
||||
val transformer = SetPortfolioReviewTransformer(
|
||||
walletListUM = newWalletListUM,
|
||||
currencies = currencies,
|
||||
totalFiatBalance = BigDecimal("10"),
|
||||
appCurrency = appCurrency,
|
||||
expandedAssetIds = emptySet(),
|
||||
expandClick = {},
|
||||
onPeriodClick = {},
|
||||
val currencies = listOf(
|
||||
createStatus(createCurrency(rawCurrencyId = "btc", symbol = "BTC"), loadedValue(BigDecimal("10"))),
|
||||
)
|
||||
val transformer = createTransformer(
|
||||
accountStatusList(currencies, loaded(BigDecimal("10"), source = StatusSource.ONLY_CACHE)),
|
||||
)
|
||||
|
||||
// Act
|
||||
val result = transformer.transform(loadingState())
|
||||
|
||||
// Assert
|
||||
assertThat(result.walletListUM).isSameInstanceAs(newWalletListUM)
|
||||
assertThat(result.notifications).containsExactly(ForYouNotification.UsedOutdatedData)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN total balance from actual source WHEN transform THEN no notification is emitted`() {
|
||||
// Arrange
|
||||
val currencies = listOf(
|
||||
createStatus(createCurrency(rawCurrencyId = "btc", symbol = "BTC"), loadedValue(BigDecimal("10"))),
|
||||
)
|
||||
val transformer = createTransformer(
|
||||
accountStatusList(currencies, loaded(BigDecimal("10"), source = StatusSource.ACTUAL)),
|
||||
)
|
||||
|
||||
// Act
|
||||
val result = transformer.transform(loadingState())
|
||||
|
||||
// Assert
|
||||
assertThat(result.notifications).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN null account status list WHEN transform THEN no notification is emitted`() {
|
||||
// Arrange
|
||||
val transformer = createTransformer(accountStatusList = null)
|
||||
|
||||
// Act
|
||||
val result = transformer.transform(loadingState())
|
||||
|
||||
// Assert
|
||||
assertThat(result.notifications).isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createTransformer(
|
||||
currencies: List<CryptoCurrencyStatus>,
|
||||
totalFiatBalance: BigDecimal,
|
||||
accountStatusList: AccountStatusList?,
|
||||
expandedAssetIds: Set<String> = emptySet(),
|
||||
) = SetPortfolioReviewTransformer(
|
||||
walletListUM = walletListUM,
|
||||
currencies = currencies,
|
||||
totalFiatBalance = totalFiatBalance,
|
||||
accountStatusList = accountStatusList,
|
||||
appCurrency = appCurrency,
|
||||
expandedAssetIds = expandedAssetIds,
|
||||
expandClick = {},
|
||||
onPeriodClick = {},
|
||||
)
|
||||
|
||||
private fun accountStatusList(
|
||||
currencies: List<CryptoCurrencyStatus>,
|
||||
totalFiatBalance: TotalFiatBalance,
|
||||
): AccountStatusList = mockk {
|
||||
every { flattenCurrencies() } returns currencies
|
||||
every { this@mockk.totalFiatBalance } returns totalFiatBalance
|
||||
}
|
||||
|
||||
private fun loaded(amount: BigDecimal, source: StatusSource = StatusSource.ACTUAL): TotalFiatBalance.Loaded =
|
||||
TotalFiatBalance.Loaded(amount = amount, source = source)
|
||||
|
||||
private fun loadingState(): ForYouUM = ForYouUM(
|
||||
walletListUM = walletListUM,
|
||||
portfolioReviewUM = PortfolioReviewUM.Loading(tokenList = persistentListOf<ForYouTokenListItemUM>()),
|
||||
portfolioReviewUM = PortfolioReviewUM.Loading(
|
||||
tokenList = persistentListOf<ForYouTokenListItemUM>(),
|
||||
marketChartUM = MarketChartUM.NoData,
|
||||
),
|
||||
notifications = persistentListOf(),
|
||||
)
|
||||
|
||||
private fun createStatus(currency: CryptoCurrency, value: CryptoCurrencyStatus.Value) = CryptoCurrencyStatus(
|
||||
|
|
@ -219,8 +318,16 @@ internal class SetPortfolioReviewTransformerTest {
|
|||
every { amount } returns BigDecimal.ONE
|
||||
every { this@mockk.fiatAmount } returns fiatAmount
|
||||
every { isError } returns false
|
||||
every { sources } returns CryptoCurrencyStatus.Sources()
|
||||
}
|
||||
|
||||
/** A non-content status: carries a null fiatAmount (unknown balance), not a resolved zero. */
|
||||
private fun unreachableValue(): CryptoCurrencyStatus.Unreachable = CryptoCurrencyStatus.Unreachable(
|
||||
priceChange = null,
|
||||
fiatRate = null,
|
||||
networkAddress = null,
|
||||
)
|
||||
|
||||
private fun createCurrency(rawCurrencyId: String, symbol: String): CryptoCurrency.Coin {
|
||||
val network: Network = mockk {
|
||||
every { name } returns "Network"
|
||||
|
|
@ -234,6 +341,7 @@ internal class SetPortfolioReviewTransformerTest {
|
|||
return mockk<CryptoCurrency.Coin> {
|
||||
every { this@mockk.id } returns currencyId
|
||||
every { this@mockk.symbol } returns symbol
|
||||
every { this@mockk.name } returns symbol
|
||||
every { this@mockk.network } returns network
|
||||
every { this@mockk.decimals } returns 8
|
||||
every { isCustom } returns false
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue