Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-06 18:40:51 +05:00
parent fcb3722ca0
commit df10b4fd21
17 changed files with 559 additions and 38 deletions

View file

@ -73,7 +73,7 @@ object MockYieldDTOFactory {
type = "type",
rewardSchedule = YieldDTO.MetadataDTO.RewardScheduleDTO.DAY,
cooldownPeriod = null,
warmupPeriod = YieldDTO.MetadataDTO.PeriodDTO(1),
warmupPeriod = YieldDTO.MetadataDTO.PeriodDTO(1, seconds = null),
rewardClaiming = YieldDTO.MetadataDTO.RewardClaimingDTO.AUTO,
defaultValidator = null,
minimumStake = null,

View file

@ -150,6 +150,8 @@ data class YieldDTO(
data class PeriodDTO(
@Json(name = "days")
val days: Int?,
@Json(name = "seconds")
val seconds: Int?,
)
@JsonClass(generateAdapter = true)

View file

@ -326,6 +326,12 @@
<string name="common_hide">Скрыть</string>
<string name="common_hold_to">Удерживайте, чтобы %s</string>
<string name="common_hour">час</string>
<plurals name="common_hours">
<item quantity="one">%d час</item>
<item quantity="few">%d часa</item>
<item quantity="many">%d часов</item>
<item quantity="other">%d часа</item>
</plurals>
<plurals name="common_hours_time_ago">
<item quantity="one">%dч назад</item>
<item quantity="few">%dч назад</item>

View file

@ -217,6 +217,7 @@
<string name="common_access_denied">Access denied</string>
<string name="common_account">Account</string>
<string name="common_accounts">Accounts</string>
<string name="common_action_failed">%s failed</string>
<string name="common_activate">Activate</string>
<string name="common_add">Add</string>
<string name="common_add_funds">Add funds</string>
@ -233,6 +234,8 @@
<string name="common_apply">Apply</string>
<string name="common_approval">Approval</string>
<string name="common_approve">Approve</string>
<string name="common_approved">Approved</string>
<string name="common_approving">Approving</string>
<string name="common_attention">Attention</string>
<string name="common_available_networks">Available networks</string>
<string name="common_backup">Backup</string>
@ -315,6 +318,10 @@
<string name="common_hide">Hide</string>
<string name="common_hold_to">Hold to %s</string>
<string name="common_hour">hour</string>
<plurals name="common_hours">
<item quantity="one">%d hour</item>
<item quantity="other">%d hours</item>
</plurals>
<plurals name="common_hours_time_ago">
<item quantity="one">%dh ago</item>
<item quantity="other">%dh ago</item>
@ -364,6 +371,8 @@
<string name="common_range_with_space">%1$s — %2$s</string>
<string name="common_read_more">Read more</string>
<string name="common_receive">Receive</string>
<string name="common_received">Received</string>
<string name="common_receiving">Receiving</string>
<string name="common_recommended">Recommended</string>
<string name="common_reject">Reject</string>
<string name="common_reload">Reload</string>
@ -382,6 +391,8 @@
<string name="common_send">Send</string>
<string name="common_send_colon">Send:</string>
<string name="common_send_tx_error">Failed to send transaction</string>
<string name="common_sending">Sending</string>
<string name="common_sent">Sent</string>
<string name="common_server_unavailable">The server is not available, please try again later</string>
<string name="common_share">Share</string>
<string name="common_share_link">Share Link</string>
@ -392,6 +403,7 @@
<string name="common_skip">Skip</string>
<string name="common_something_went_wrong">Something went wrong</string>
<string name="common_stake">Stake</string>
<string name="common_staked">Staked</string>
<string name="common_staking">Staking</string>
<string name="common_start">Start</string>
<string name="common_submit">Submit</string>
@ -399,6 +411,8 @@
<string name="common_support">Support</string>
<string name="common_supported_networks">Supported networks</string>
<string name="common_swap">Swap</string>
<string name="common_swapped">Swapped</string>
<string name="common_swapping">Swapping</string>
<string name="common_tangem">Tangem</string>
<string name="common_tangem_wallet">Tangem Wallet</string>
<string name="common_tap_and_hold_hint">Tap and hold</string>
@ -416,6 +430,7 @@
<string name="common_transaction_status">Transaction status</string>
<string name="common_transactions">Transactions</string>
<string name="common_transfer">Transfer</string>
<string name="common_transferred">Transferred</string>
<string name="common_unable_to_load">Unable to load data…</string>
<string name="common_understand">I understand</string>
<string name="common_understand_continue">I understand, continue</string>
@ -425,10 +440,12 @@
<string name="common_unstake">Unstake</string>
<string name="common_utxo_validate_withdrawal_message_warning">Due to %1$s limitations only %2$d UTXOs can fit in a single transaction. This means you can only send %3$s or less. You need to reduce the amount.</string>
<string name="common_value_copied">Value copied</string>
<string name="common_voting">Voting</string>
<string name="common_wallets">Wallets</string>
<string name="common_warning">Warning</string>
<string name="common_week">week</string>
<string name="common_with">with</string>
<string name="common_withdrawing">Withdrawing</string>
<string name="common_yes">Yes</string>
<string name="common_yield_mode">Yield Mode</string>
<string name="contract_address_copied_message">Contract address copied!</string>
@ -590,6 +607,7 @@
<string name="express_provider_best_rate">Best rate</string>
<string name="express_provider_fca_warning_list">FCA Warning List</string>
<string name="express_provider_fixed_rate_is_unavailable">Fixed rate is unavailable</string>
<string name="express_provider_for_swap">Provider for swap</string>
<string name="express_provider_great_rate">Competitive rate</string>
<string name="express_provider_in_fca_warning_list">Provider in FCA warning list</string>
<string name="express_provider_max_amount">Available up to %s</string>
@ -1334,6 +1352,8 @@
<string name="send_notification_invalid_reserve_amount_text">Target account is not created. Please change the amount to send.</string>
<string name="send_notification_invalid_reserve_amount_title">The amount to send must be at least %s</string>
<string name="send_notification_leave_button">Leave %s</string>
<string name="send_notification_no_trustline_text">A trustline for %s is required first.</string>
<string name="send_notification_no_trustline_title">Can\'t receive token</string>
<string name="send_notification_reduce_by">Reduce by %s</string>
<string name="send_notification_reduce_to">Reduce to %s</string>
<string name="send_notification_transaction_delay_text">Kindly be aware that your transaction may experience delays under specific fee settings</string>
@ -1820,12 +1840,21 @@
<string name="tokens_list_unavailable_to_sell_header">Unavailable to sell</string>
<string name="tokens_list_unavailable_to_swap_header">Unavailable for swap from %s</string>
<string name="tokens_list_unavailable_to_swap_source_header">Unavailable for swap</string>
<string name="transaction_history_claiming_reward">Claiming reward</string>
<string name="transaction_history_contract_address">contract: %s</string>
<string name="transaction_history_disabling_yield_mode">Disabling Yield mode</string>
<string name="transaction_history_earned_from_stake">Earned from stake</string>
<string name="transaction_history_empty_transactions">You don\'t have any transactions yet</string>
<string name="transaction_history_error_failed_to_load">Failed to load transaction history.\nClick on reload button to update the information.</string>
<string name="transaction_history_from_inline_address">from: %%image%% %s</string>
<string name="transaction_history_multiple_addresses">Multiple addresses</string>
<string name="transaction_history_not_supported_description">Transaction history is currently not supported for this blockchain. But don\'t worry, we\'re working on it! In the meantime you can check it in the explorer.</string>
<string name="transaction_history_operation">Operation</string>
<string name="transaction_history_pending">Pending</string>
<string name="transaction_history_rewards_restaked">Rewards restaked</string>
<string name="transaction_history_rewards_restaking">Rewards restaking</string>
<string name="transaction_history_staking_reward">Staking reward</string>
<string name="transaction_history_to_inline_address">to: %%image%% %s</string>
<string name="transaction_history_transaction_for_address">for: %s</string>
<string name="transaction_history_transaction_from_address">from: %s</string>
<string name="transaction_history_transaction_to_address">to: %s</string>

View file

@ -8,6 +8,8 @@ import org.joda.time.DateTimeZone
import org.joda.time.LocalDate
import org.joda.time.format.DateTimeFormatter
const val SECONDS_IN_HOUR = 3600
/**
* If [this] timestamp is today or yesterday, returns relative date,
* otherwise returns formatting date.

View file

@ -117,6 +117,7 @@ internal object YieldConverter : Converter<YieldDTO, Yield> {
private fun convertPeriod(periodDTO: YieldDTO.MetadataDTO.PeriodDTO): Yield.Metadata.Period {
return Yield.Metadata.Period(
days = periodDTO.days.asMandatory("days"),
seconds = periodDTO.seconds,
)
}

View file

@ -114,6 +114,7 @@ data class Yield(
@Serializable
data class Period(
val days: Int,
val seconds: Int?,
)
@Serializable

View file

@ -2,7 +2,7 @@ package com.tangem.domain.staking.model
sealed class CooldownPeriod {
data class Fixed(val days: Int) : CooldownPeriod()
data class Fixed(val period: Period) : CooldownPeriod()
data class Range(val minDays: Int, val maxDays: Int) : CooldownPeriod()
}

View file

@ -62,7 +62,7 @@ class P2PEthPoolIntegration(
// Metadata
override val warmupPeriodDays: Int = 0
override val warmupPeriod: Period = Period.Days(0)
override val cooldownPeriod: CooldownPeriod = CooldownPeriod.Range(
minDays = MIN_COOLDOWN_DAYS,

View file

@ -0,0 +1,14 @@
package com.tangem.domain.staking.model
sealed class Period {
abstract val value: Int
data class Days(
override val value: Int,
) : Period()
data class Seconds(
override val value: Int,
) : Period()
}

View file

@ -49,10 +49,22 @@ class StakeKitIntegration(
// Metadata
override val warmupPeriodDays: Int = yield.metadata.warmupPeriod.days
override val warmupPeriod: Period = yield.metadata.warmupPeriod.let { period ->
period.seconds?.let {
Period.Seconds(it)
} ?: period.days.let {
Period.Days(it)
}
}
override val cooldownPeriod: CooldownPeriod? = yield.metadata.cooldownPeriod?.days?.let {
CooldownPeriod.Fixed(it)
override val cooldownPeriod: CooldownPeriod? = yield.metadata.cooldownPeriod?.let { period ->
CooldownPeriod.Fixed(
period.seconds?.let {
Period.Seconds(it)
} ?: period.days.let {
Period.Days(it)
},
)
}
override val rewardSchedule: RewardSchedule = yield.metadata.rewardSchedule.toRewardSchedule()

View file

@ -43,7 +43,7 @@ sealed interface StakingIntegration {
// Metadata
val warmupPeriodDays: Int
val warmupPeriod: Period
val cooldownPeriod: CooldownPeriod?

View file

@ -0,0 +1,206 @@
package com.tangem.domain.staking.model
import com.google.common.truth.Truth.assertThat
import com.tangem.domain.models.staking.NetworkType
import com.tangem.domain.models.staking.YieldToken
import com.tangem.domain.staking.model.stakekit.AddressArgument
import com.tangem.domain.staking.model.stakekit.Yield
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import java.math.BigDecimal
/**
* Tests for [StakeKitIntegration] specifically the Period/CooldownPeriod mapping from [Yield.Metadata].
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class StakeKitIntegrationTest {
// region helpers
private val dummyToken = YieldToken(
name = "Test Token",
network = NetworkType.SOLANA,
symbol = "SOL",
decimals = 9,
address = null,
coinGeckoId = null,
logoURI = null,
isPoints = false,
)
private val dummyEnter = Yield.Args.Enter(
addresses = Yield.Args.Enter.Addresses(
address = AddressArgument(required = false),
),
args = emptyMap(),
)
private val dummyArgs = Yield.Args(enter = dummyEnter, exit = null)
private val dummyStatus = Yield.Status(enter = true, exit = null)
private val dummyEnabled = Yield.Metadata.Enabled(enabled = true)
private fun buildYield(
warmupPeriod: Yield.Metadata.Period,
cooldownPeriod: Yield.Metadata.Period?,
): Yield {
return Yield(
id = "test-integration",
token = dummyToken,
tokens = emptyList(),
args = dummyArgs,
status = dummyStatus,
apy = BigDecimal("5.0"),
rewardRate = 5.0,
rewardType = com.tangem.domain.staking.model.common.RewardType.APY,
metadata = Yield.Metadata(
name = "Test Staking",
logoUri = "https://example.com/logo.png",
description = "Test staking integration",
documentation = null,
gasFeeToken = dummyToken,
token = dummyToken,
tokens = emptyList(),
type = "liquid",
rewardSchedule = Yield.Metadata.RewardSchedule.DAY,
cooldownPeriod = cooldownPeriod,
warmupPeriod = warmupPeriod,
rewardClaiming = Yield.Metadata.RewardClaiming.AUTO,
defaultValidator = null,
minimumStake = null,
supportsMultipleValidators = false,
revshare = dummyEnabled,
fee = dummyEnabled,
),
validators = emptyList(),
isAvailable = true,
)
}
private fun buildIntegration(
warmupPeriod: Yield.Metadata.Period,
cooldownPeriod: Yield.Metadata.Period?,
): StakeKitIntegration {
return StakeKitIntegration(
integrationId = StakingIntegrationID.StakeKit.Coin.Solana,
yield = buildYield(warmupPeriod = warmupPeriod, cooldownPeriod = cooldownPeriod),
)
}
// endregion
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class `warmupPeriod mapping` {
@Test
fun `should produce Period Seconds when seconds is non-null`() {
// given
val warmup = Yield.Metadata.Period(days = 3, seconds = 7200)
// when
val integration = buildIntegration(warmupPeriod = warmup, cooldownPeriod = null)
// then
assertThat(integration.warmupPeriod).isEqualTo(Period.Seconds(7200))
}
@Test
fun `should produce Period Days when seconds is null`() {
// given
val warmup = Yield.Metadata.Period(days = 5, seconds = null)
// when
val integration = buildIntegration(warmupPeriod = warmup, cooldownPeriod = null)
// then
assertThat(integration.warmupPeriod).isEqualTo(Period.Days(5))
}
@Test
fun `should prefer seconds over days when both are present`() {
// given — days is non-zero but seconds takes priority
val warmup = Yield.Metadata.Period(days = 10, seconds = 3600)
// when
val integration = buildIntegration(warmupPeriod = warmup, cooldownPeriod = null)
// then
assertThat(integration.warmupPeriod).isInstanceOf(Period.Seconds::class.java)
assertThat((integration.warmupPeriod as Period.Seconds).value).isEqualTo(3600)
}
@Test
fun `should produce Period Days with zero value when days is zero and seconds is null`() {
// given
val warmup = Yield.Metadata.Period(days = 0, seconds = null)
// when
val integration = buildIntegration(warmupPeriod = warmup, cooldownPeriod = null)
// then
assertThat(integration.warmupPeriod).isEqualTo(Period.Days(0))
}
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class `cooldownPeriod mapping` {
@Test
fun `should be null when yield cooldownPeriod is null`() {
// given
val warmup = Yield.Metadata.Period(days = 1, seconds = null)
// when
val integration = buildIntegration(warmupPeriod = warmup, cooldownPeriod = null)
// then
assertThat(integration.cooldownPeriod).isNull()
}
@Test
fun `should produce Fixed Period Seconds when cooldown seconds is non-null`() {
// given
val warmup = Yield.Metadata.Period(days = 1, seconds = null)
val cooldown = Yield.Metadata.Period(days = 2, seconds = 86400)
// when
val integration = buildIntegration(warmupPeriod = warmup, cooldownPeriod = cooldown)
// then
assertThat(integration.cooldownPeriod).isEqualTo(CooldownPeriod.Fixed(Period.Seconds(86400)))
}
@Test
fun `should produce Fixed Period Days when cooldown seconds is null`() {
// given
val warmup = Yield.Metadata.Period(days = 1, seconds = null)
val cooldown = Yield.Metadata.Period(days = 3, seconds = null)
// when
val integration = buildIntegration(warmupPeriod = warmup, cooldownPeriod = cooldown)
// then
assertThat(integration.cooldownPeriod).isEqualTo(CooldownPeriod.Fixed(Period.Days(3)))
}
@Test
fun `should prefer seconds over days in cooldown when both are present`() {
// given
val warmup = Yield.Metadata.Period(days = 1, seconds = null)
val cooldown = Yield.Metadata.Period(days = 7, seconds = 604800)
// when
val integration = buildIntegration(warmupPeriod = warmup, cooldownPeriod = cooldown)
// then
val period = integration.cooldownPeriod
assertThat(period).isInstanceOf(CooldownPeriod.Fixed::class.java)
assertThat((period as CooldownPeriod.Fixed).period).isInstanceOf(Period.Seconds::class.java)
assertThat((period.period as Period.Seconds).value).isEqualTo(604800)
}
}
}

View file

@ -1,23 +1,16 @@
package com.tangem.features.staking.impl.presentation.state.converters
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.pluralReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.utils.SECONDS_IN_HOUR
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.staking.BalanceType
import com.tangem.domain.models.staking.PendingAction
import com.tangem.domain.models.staking.StakingBalance
import com.tangem.domain.models.staking.StakingBalanceEntry
import com.tangem.domain.models.staking.StakingEntryActions
import com.tangem.domain.models.staking.StakingEntryType
import com.tangem.domain.models.staking.*
import com.tangem.domain.models.staking.action.StakingActionType
import com.tangem.domain.staking.model.Period
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.model.StakingTarget
import com.tangem.features.staking.impl.R
@ -125,12 +118,26 @@ internal class StakingBalanceEntryConverter(
}
}
StakingEntryType.PREPARING -> {
val warmupPeriod = integration.warmupPeriodDays
val warmupPeriod = integration.warmupPeriod
TextReference.Combined(
wrappedList(
resourceReference(R.string.staking_details_warmup_period),
stringReference(" "),
pluralReference(R.plurals.common_days, warmupPeriod, wrappedList(warmupPeriod)),
when (warmupPeriod) {
is Period.Days -> pluralReference(
id = R.plurals.common_days,
count = warmupPeriod.value,
formatArgs = wrappedList(warmupPeriod.value),
)
is Period.Seconds -> {
val hours = warmupPeriod.value / SECONDS_IN_HOUR
pluralReference(
id = R.plurals.common_hours,
count = hours,
formatArgs = wrappedList(hours),
)
}
},
),
)
}

View file

@ -7,18 +7,20 @@ import com.tangem.common.ui.amountScreen.converters.AmountStateConverter
import com.tangem.common.ui.amountScreen.models.AmountParameters
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
import com.tangem.common.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
import com.tangem.core.ui.components.list.RoundedListWithDividersItemData
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.core.ui.utils.SECONDS_IN_HOUR
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.staking.StakingBalanceEntry
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.staking.model.Period
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.model.StakingTarget
import com.tangem.domain.staking.model.common.RewardClaiming
@ -199,17 +201,27 @@ internal class SetInitialDataStateTransformer(
}
private fun createWarmupPeriodItem(): RoundedListWithDividersItemData? {
val warmupPeriodDays = integration.warmupPeriodDays
if (warmupPeriodDays == 0) return null
val warmupPeriod = integration.warmupPeriod
if (warmupPeriod.value == 0) return null
return RoundedListWithDividersItemData(
id = R.string.staking_details_warmup_period,
startText = TextReference.Res(R.string.staking_details_warmup_period),
endText = pluralReference(
id = R.plurals.common_days,
count = warmupPeriodDays,
formatArgs = wrappedList(warmupPeriodDays),
),
endText = when (warmupPeriod) {
is Period.Days -> pluralReference(
id = R.plurals.common_days,
count = warmupPeriod.value,
formatArgs = wrappedList(warmupPeriod.value),
)
is Period.Seconds -> {
val hours = warmupPeriod.value / SECONDS_IN_HOUR
pluralReference(
id = R.plurals.common_hours,
count = hours,
formatArgs = wrappedList(hours),
)
}
},
iconClick = { clickIntents.onInfoClick(InfoType.WARMUP_PERIOD) },
)
}

View file

@ -1,22 +1,30 @@
package com.tangem.features.staking.impl.presentation.state.utils
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.combinedReference
import com.tangem.core.ui.extensions.pluralReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.utils.SECONDS_IN_HOUR
import com.tangem.domain.staking.model.CooldownPeriod
import com.tangem.domain.staking.model.Period
import com.tangem.features.staking.impl.R
import com.tangem.utils.StringsSigns.MINUS
import com.tangem.utils.StringsSigns.NON_BREAKING_SPACE
internal fun CooldownPeriod.toTextReference(): TextReference {
return when (this) {
is CooldownPeriod.Fixed -> pluralReference(
id = R.plurals.common_days,
count = days,
formatArgs = wrappedList(days),
)
is CooldownPeriod.Fixed -> when (period) {
is Period.Days -> pluralReference(
id = R.plurals.common_days,
count = period.value,
formatArgs = wrappedList(period.value),
)
is Period.Seconds -> {
val hours = period.value / SECONDS_IN_HOUR
pluralReference(
id = R.plurals.common_hours,
count = hours,
formatArgs = wrappedList(hours),
)
}
}
is CooldownPeriod.Range -> combinedReference(
stringReference("$minDays$MINUS$maxDays$NON_BREAKING_SPACE"),
pluralReference(

View file

@ -0,0 +1,221 @@
package com.tangem.features.staking.impl.presentation.state.utils
import com.google.common.truth.Truth.assertThat
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.combinedReference
import com.tangem.core.ui.extensions.pluralReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.utils.SECONDS_IN_HOUR
import com.tangem.domain.staking.model.CooldownPeriod
import com.tangem.domain.staking.model.Period
import com.tangem.features.staking.impl.R
import com.tangem.utils.StringsSigns.MINUS
import com.tangem.utils.StringsSigns.NON_BREAKING_SPACE
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
/**
* Tests for [CooldownPeriod.toTextReference] extension function.
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class CooldownPeriodUtilsTest {
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class `Fixed with Days` {
@Test
fun `should return plural days reference for Fixed Period Days`() {
// given
val cooldown = CooldownPeriod.Fixed(Period.Days(7))
// when
val result = cooldown.toTextReference()
// then
val expected = pluralReference(
id = R.plurals.common_days,
count = 7,
formatArgs = wrappedList(7),
)
assertThat(result).isEqualTo(expected)
}
@Test
fun `should return plural days with zero for Fixed Period Days zero`() {
// given
val cooldown = CooldownPeriod.Fixed(Period.Days(0))
// when
val result = cooldown.toTextReference()
// then
val expected = pluralReference(
id = R.plurals.common_days,
count = 0,
formatArgs = wrappedList(0),
)
assertThat(result).isEqualTo(expected)
}
@Test
fun `should pass day count as both count and format arg`() {
// given
val days = 14
val cooldown = CooldownPeriod.Fixed(Period.Days(days))
// when
val result = cooldown.toTextReference()
// then
assertThat(result).isInstanceOf(TextReference.PluralRes::class.java)
val plural = result as TextReference.PluralRes
assertThat(plural.count).isEqualTo(days)
assertThat(plural.formatArgs.first()).isEqualTo(days)
}
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class `Fixed with Seconds` {
@Test
fun `should return plural hours reference for Fixed Period Seconds`() {
// given — 2 hours worth of seconds
val cooldown = CooldownPeriod.Fixed(Period.Seconds(2 * SECONDS_IN_HOUR))
// when
val result = cooldown.toTextReference()
// then
val expectedHours = 2
val expected = pluralReference(
id = R.plurals.common_hours,
count = expectedHours,
formatArgs = wrappedList(expectedHours),
)
assertThat(result).isEqualTo(expected)
}
@Test
fun `should convert exactly one hour worth of seconds to 1 hour`() {
// given
val cooldown = CooldownPeriod.Fixed(Period.Seconds(SECONDS_IN_HOUR))
// when
val result = cooldown.toTextReference()
// then
assertThat(result).isInstanceOf(TextReference.PluralRes::class.java)
val plural = result as TextReference.PluralRes
assertThat(plural.count).isEqualTo(1)
assertThat(plural.formatArgs.first()).isEqualTo(1)
}
@Test
fun `should floor to 1 hour when seconds are not divisible evenly`() {
// given — 5000 seconds = 1.388... hours → integer division → 1
val cooldown = CooldownPeriod.Fixed(Period.Seconds(5000))
// when
val result = cooldown.toTextReference()
// then
assertThat(result).isInstanceOf(TextReference.PluralRes::class.java)
val plural = result as TextReference.PluralRes
assertThat(plural.count).isEqualTo(1)
}
@Test
fun `should return 0 hours for zero seconds`() {
// given
val cooldown = CooldownPeriod.Fixed(Period.Seconds(0))
// when
val result = cooldown.toTextReference()
// then
assertThat(result).isInstanceOf(TextReference.PluralRes::class.java)
val plural = result as TextReference.PluralRes
assertThat(plural.count).isEqualTo(0)
assertThat(plural.formatArgs.first()).isEqualTo(0)
}
@Test
fun `should use R plurals common_hours resource id`() {
// given
val cooldown = CooldownPeriod.Fixed(Period.Seconds(SECONDS_IN_HOUR))
// when
val result = cooldown.toTextReference()
// then
assertThat(result).isInstanceOf(TextReference.PluralRes::class.java)
assertThat((result as TextReference.PluralRes).id).isEqualTo(R.plurals.common_hours)
}
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class Range {
@Test
fun `should return combined reference for Range`() {
// given
val minDays = 2
val maxDays = 5
val cooldown = CooldownPeriod.Range(minDays = minDays, maxDays = maxDays)
// when
val result = cooldown.toTextReference()
// then
val expected = combinedReference(
stringReference("$minDays$MINUS$maxDays$NON_BREAKING_SPACE"),
pluralReference(
id = R.plurals.common_days_no_param,
count = maxDays,
),
)
assertThat(result).isEqualTo(expected)
}
@Test
fun `should use maxDays as count for plural in Range`() {
// given
val maxDays = 10
val cooldown = CooldownPeriod.Range(minDays = 3, maxDays = maxDays)
// when
val result = cooldown.toTextReference()
// then
assertThat(result).isInstanceOf(TextReference.Combined::class.java)
val combined = result as TextReference.Combined
val pluralPart = combined.refs[1]
assertThat(pluralPart).isInstanceOf(TextReference.PluralRes::class.java)
assertThat((pluralPart as TextReference.PluralRes).count).isEqualTo(maxDays)
}
@Test
fun `should include minDays and maxDays with minus and non-breaking-space in string part`() {
// given
val minDays = 1
val maxDays = 7
val cooldown = CooldownPeriod.Range(minDays = minDays, maxDays = maxDays)
// when
val result = cooldown.toTextReference()
// then
assertThat(result).isInstanceOf(TextReference.Combined::class.java)
val combined = result as TextReference.Combined
val stringPart = combined.refs[0]
assertThat(stringPart).isInstanceOf(TextReference.Str::class.java)
assertThat((stringPart as TextReference.Str).value)
.isEqualTo("$minDays$MINUS$maxDays$NON_BREAKING_SPACE")
}
}
}