Updated on 2026-08-14
This commit is contained in:
parent
0c4d14650d
commit
2561a7eef6
11 changed files with 389 additions and 4 deletions
|
|
@ -63,6 +63,17 @@ sealed interface OnChainTx : TxHistoryInfo {
|
|||
*/
|
||||
fun TxInfo.identityKey(): String = "$txHash|$type"
|
||||
|
||||
/**
|
||||
* Hash of the matched on-chain leg, used to open the row in a block explorer; `null` when there is no
|
||||
* blockchain tx to link to — an [ExpressTx] whose on-chain leg has not matched yet. The express `txId`
|
||||
* must never stand in here: it is not an on-chain hash and would build a broken explorer URL.
|
||||
*/
|
||||
inline val TxHistoryInfo.explorerHash: String?
|
||||
get() = when (this) {
|
||||
is OnChainTx.BSDK -> txInfo.txHash
|
||||
is ExpressTx -> matchHash
|
||||
}
|
||||
|
||||
/**
|
||||
* A history row backed by an express operation. It is a thin wrapper over the standalone express
|
||||
* model ([ExchangeTransaction] / [OnrampTransaction]), adding only the history-view concerns:
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ dependencies {
|
|||
|
||||
/** Domain models */
|
||||
api(projects.domain.models)
|
||||
api(projects.domain.txhistory)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.features.txhistory.component
|
||||
|
||||
import com.tangem.domain.txhistory.model.TxHistoryInfo
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Activation config for the transaction-details child slot owned by the host (e.g. token-details).
|
||||
*
|
||||
|
||||
* with `serializer = null` — the details sheet is intentionally not restored after process death.
|
||||
*/
|
||||
data class TxHistoryDetailsSlotConfig(val txHistoryInfo: Flow<TxHistoryInfo>)
|
||||
|
|
@ -67,6 +67,7 @@ dependencies {
|
|||
|
||||
/* Tests */
|
||||
testImplementation(projects.common.test)
|
||||
testImplementation(projects.test.core)
|
||||
testImplementation(projects.domain.onramp.models)
|
||||
testImplementation(deps.test.junit5)
|
||||
testImplementation(deps.test.mockk)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
package com.tangem.features.txhistory.converter
|
||||
|
||||
import com.tangem.core.ui.components.transactions.state.TransactionItemUM.Content.Status
|
||||
import com.tangem.domain.express.models.ExpressExchangeStatus
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
/**
|
||||
* Collapses the typed swap status into a UI [Status] bucket: the single success state
|
||||
* ([Finished][ExpressExchangeStatus.Finished]), the failure/return states ([Failed][ExpressExchangeStatus.Failed]/
|
||||
* [TxFailed][ExpressExchangeStatus.TxFailed]/[Refunded][ExpressExchangeStatus.Refunded]/
|
||||
* [Expired][ExpressExchangeStatus.Expired]/[Unknown][ExpressExchangeStatus.Unknown]) → [Failed][Status.Failed],
|
||||
* everything in flight (incl. [Verifying][ExpressExchangeStatus.Verifying] and [Paused][ExpressExchangeStatus.Paused])
|
||||
* → [Unconfirmed][Status.Unconfirmed].
|
||||
*/
|
||||
internal class ExpressExchangeStatusToUiStatusConverter : Converter<ExpressExchangeStatus, Status> {
|
||||
|
||||
override fun convert(value: ExpressExchangeStatus): Status = when (value) {
|
||||
ExpressExchangeStatus.Finished -> Status.Confirmed
|
||||
ExpressExchangeStatus.Failed,
|
||||
ExpressExchangeStatus.TxFailed,
|
||||
ExpressExchangeStatus.Refunded,
|
||||
ExpressExchangeStatus.Expired,
|
||||
ExpressExchangeStatus.Unknown,
|
||||
-> Status.Failed
|
||||
ExpressExchangeStatus.Preview,
|
||||
ExpressExchangeStatus.Created,
|
||||
ExpressExchangeStatus.ExchangeTxSent,
|
||||
ExpressExchangeStatus.Waiting,
|
||||
ExpressExchangeStatus.WaitingTxHash,
|
||||
ExpressExchangeStatus.Confirming,
|
||||
ExpressExchangeStatus.Exchanging,
|
||||
ExpressExchangeStatus.Sending,
|
||||
ExpressExchangeStatus.Verifying,
|
||||
ExpressExchangeStatus.Paused,
|
||||
-> Status.Unconfirmed
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.tangem.features.txhistory.converter
|
||||
|
||||
import com.tangem.core.ui.components.transactions.state.TransactionItemUM.Content.Status
|
||||
import com.tangem.domain.express.models.ExpressOnrampStatus
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
/**
|
||||
* Collapses the typed onramp status into a UI [Status] bucket: the single success state
|
||||
* ([Finished][ExpressOnrampStatus.Finished]), the failure states ([Failed][ExpressOnrampStatus.Failed]/
|
||||
* [Expired][ExpressOnrampStatus.Expired]/[Unknown][ExpressOnrampStatus.Unknown]) → [Failed][Status.Failed],
|
||||
* everything in flight → [Unconfirmed][Status.Unconfirmed].
|
||||
*/
|
||||
internal class ExpressOnrampStatusToUiStatusConverter : Converter<ExpressOnrampStatus, Status> {
|
||||
|
||||
override fun convert(value: ExpressOnrampStatus): Status = when (value) {
|
||||
ExpressOnrampStatus.Finished -> Status.Confirmed
|
||||
ExpressOnrampStatus.Failed,
|
||||
ExpressOnrampStatus.Expired,
|
||||
ExpressOnrampStatus.Unknown,
|
||||
-> Status.Failed
|
||||
ExpressOnrampStatus.Created,
|
||||
ExpressOnrampStatus.WaitingForPayment,
|
||||
ExpressOnrampStatus.PaymentProcessing,
|
||||
ExpressOnrampStatus.Verifying,
|
||||
ExpressOnrampStatus.Paid,
|
||||
ExpressOnrampStatus.Sending,
|
||||
ExpressOnrampStatus.Paused,
|
||||
-> Status.Unconfirmed
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,230 @@
|
|||
package com.tangem.features.txhistory.converter
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import com.tangem.common.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.components.transactions.state.TransactionItemUM.Content.Status
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
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.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.utils.DateTimeFormatters
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.TxInfo
|
||||
import com.tangem.domain.models.network.TxInfo.TransactionType
|
||||
import com.tangem.domain.txhistory.model.ExpressTx
|
||||
import com.tangem.domain.txhistory.model.OnChainTx
|
||||
import com.tangem.domain.txhistory.model.TxHistoryInfo
|
||||
import com.tangem.features.txhistory.entity.TxHistoryDetailsUM
|
||||
import com.tangem.features.txhistory.entity.TxHistoryDetailsUM.StatusBannerUM.Severity
|
||||
import com.tangem.features.txhistory.impl.R
|
||||
import com.tangem.utils.StringsSigns
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.utils.extensions.isZero
|
||||
import com.tangem.utils.toBriefAddressFormat
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import org.joda.time.DateTime
|
||||
|
||||
/**
|
||||
* Converts a [TxHistoryInfo] row to a [TxHistoryDetailsUM] for the in-app transaction details card.
|
||||
*
|
||||
* The dispatch mirrors the row converters: an [OnChainTx.BSDK] always renders as [TxHistoryDetailsUM.SingleAsset]
|
||||
* (a two-asset swap surfaces as [ExpressTx.Swap], handled separately), while an [ExpressTx] (swap / onramp) currently
|
||||
* produces a header-only [TxHistoryDetailsUM.TwoAssets] with the express status banner. The express legs (`from`/`to`
|
||||
* amounts, currencies, fiat) are populated in a follow-up ([REDACTED_TASK_KEY]).
|
||||
*/
|
||||
internal class TxHistoryInfoToTxHistoryDetailsUMConverter(
|
||||
private val currency: CryptoCurrency,
|
||||
private val onCopyAddress: (String) -> Unit,
|
||||
) : Converter<TxHistoryInfo, TxHistoryDetailsUM> {
|
||||
|
||||
private val iconStateConverter = CryptoCurrencyToIconStateConverter()
|
||||
private val exchangeStatusConverter = ExpressExchangeStatusToUiStatusConverter()
|
||||
private val onrampStatusConverter = ExpressOnrampStatusToUiStatusConverter()
|
||||
|
||||
override fun convert(value: TxHistoryInfo): TxHistoryDetailsUM = when (value) {
|
||||
is OnChainTx.BSDK -> convertOnChain(value.txInfo)
|
||||
is ExpressTx.Swap -> convertExpressSwap(value)
|
||||
is ExpressTx.Onramp -> convertExpressOnramp(value)
|
||||
}
|
||||
|
||||
// region On-chain (TxInfo)
|
||||
|
||||
/**
|
||||
* Every on-chain row renders as [TxHistoryDetailsUM.SingleAsset]. A two-asset swap always surfaces as
|
||||
* [ExpressTx.Swap] (handled separately); an on-chain `TxInfo` of type `Swap` (e.g. a DEX swap with no express
|
||||
* record) carries no legs, so it falls back to the single amount it does have rather than an empty two-asset card.
|
||||
*/
|
||||
private fun convertOnChain(value: TxInfo): TxHistoryDetailsUM = TxHistoryDetailsUM.SingleAsset(
|
||||
header = value.toHeaderUM(),
|
||||
amountBlock = value.toAmountBlockUM(),
|
||||
counterparty = value.toCounterpartyUM(),
|
||||
// TODO: TxInfo has no network fee / rate yet — empty until those fields are added to TxInfo.
|
||||
rows = persistentListOf(),
|
||||
)
|
||||
|
||||
private fun TxInfo.toHeaderUM(): TxHistoryDetailsUM.HeaderUM = TxHistoryDetailsUM.HeaderUM(
|
||||
iconRes = headerIcon(),
|
||||
status = status.toUiStatus(),
|
||||
title = headerTitle(),
|
||||
subtitle = headerSubtitle(timestampInMillis),
|
||||
)
|
||||
|
||||
private fun TxInfo.toAmountBlockUM(): TxHistoryDetailsUM.AmountBlockUM = TxHistoryDetailsUM.AmountBlockUM(
|
||||
currencyIcon = iconStateConverter.convert(currency),
|
||||
amount = stringReference(signedAmount(currency)),
|
||||
// TODO: TxInfo has no fiat amount yet — empty until the fiat field is added to TxInfo; a hardcoded
|
||||
// placeholder would show a misleading value.
|
||||
fiatAmount = TextReference.EMPTY,
|
||||
isFailed = status is TxInfo.TransactionStatus.Failed,
|
||||
)
|
||||
|
||||
/**
|
||||
* Counterparty card ("Recipient" / "From"). Currently only the external-address avatar is produced — built from
|
||||
* the `User` interaction address (the same source the history list uses for its external-address subtitle).
|
||||
*
|
||||
* The own-account / own-wallet avatars require the address->owner lookup the list assembles in
|
||||
* `TxHistoryLookupContext`; wiring that into the detail model is a follow-up, so for now a counterparty that is not
|
||||
* a plain external `User` address yields no card (`null`).
|
||||
*/
|
||||
private fun TxInfo.toCounterpartyUM(): TxHistoryDetailsUM.CounterpartyUM? {
|
||||
val address = (interactionAddressType as? TxInfo.InteractionAddressType.User)?.address ?: return null
|
||||
return TxHistoryDetailsUM.CounterpartyUM(
|
||||
label = counterpartyLabel(),
|
||||
title = stringReference(address.toBriefAddressFormat()),
|
||||
avatar = TxHistoryDetailsUM.CounterpartyAvatar.Address(rawAddress = address),
|
||||
onCopyClick = { onCopyAddress(address) },
|
||||
)
|
||||
}
|
||||
|
||||
/** Section label above the counterparty: "Recipient" for outgoing transfers, "From" for incoming. */
|
||||
private fun TxInfo.counterpartyLabel(): TextReference =
|
||||
if (isOutgoing) resourceReference(R.string.send_recipient) else resourceReference(R.string.common_from)
|
||||
|
||||
// endregion
|
||||
|
||||
// region Express (swap / onramp)
|
||||
|
||||
private fun convertExpressSwap(swap: ExpressTx.Swap): TxHistoryDetailsUM.TwoAssets {
|
||||
val status = exchangeStatusConverter.convert(swap.tx.status)
|
||||
return TxHistoryDetailsUM.TwoAssets(
|
||||
header = TxHistoryDetailsUM.HeaderUM(
|
||||
iconRes = R.drawable.ic_exchange_vertical_24,
|
||||
status = status,
|
||||
title = status.statusAwareTitle(R.string.common_swapping, R.string.common_swapped),
|
||||
subtitle = headerSubtitle(swap.timestampMillis),
|
||||
),
|
||||
statusBanner = status.toStatusBannerUM(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertExpressOnramp(onramp: ExpressTx.Onramp): TxHistoryDetailsUM.TwoAssets {
|
||||
val status = onrampStatusConverter.convert(onramp.tx.status)
|
||||
return TxHistoryDetailsUM.TwoAssets(
|
||||
header = TxHistoryDetailsUM.HeaderUM(
|
||||
iconRes = R.drawable.ic_tangem_card_24,
|
||||
status = status,
|
||||
title = status.statusAwareTitle(
|
||||
R.string.tx_history_onramp_top_up,
|
||||
R.string.tx_history_onramp_topped_up,
|
||||
),
|
||||
subtitle = headerSubtitle(onramp.timestampMillis),
|
||||
),
|
||||
statusBanner = status.toStatusBannerUM(),
|
||||
)
|
||||
}
|
||||
|
||||
// endregion
|
||||
}
|
||||
|
||||
// region Status helpers
|
||||
|
||||
/**
|
||||
* Express status plaque under the two-asset block, keyed on the collapsed UI [Status] bucket.
|
||||
*
|
||||
* A stopgap shared by on-chain swaps and express ops — [Severity.Warning] (verification) is not reachable here yet.
|
||||
* [REDACTED_TODO_COMMENT]
|
||||
*/
|
||||
private fun Status.toStatusBannerUM(): TxHistoryDetailsUM.StatusBannerUM = when (this) {
|
||||
is Status.Unconfirmed -> TxHistoryDetailsUM.StatusBannerUM(
|
||||
severity = Severity.Info,
|
||||
title = resourceReference(R.string.express_exchange_status_receiving_active),
|
||||
isLoading = true,
|
||||
)
|
||||
is Status.Confirmed -> TxHistoryDetailsUM.StatusBannerUM(
|
||||
severity = Severity.Success,
|
||||
title = resourceReference(R.string.express_exchange_status_exchanged),
|
||||
isLoading = false,
|
||||
)
|
||||
is Status.Failed -> TxHistoryDetailsUM.StatusBannerUM(
|
||||
severity = Severity.Error,
|
||||
title = resourceReference(R.string.express_exchange_status_failed),
|
||||
subtitle = resourceReference(R.string.express_exchange_notification_failed_text),
|
||||
isLoading = false,
|
||||
)
|
||||
}
|
||||
|
||||
private fun Status.statusAwareTitle(@StringRes pending: Int, @StringRes confirmed: Int): TextReference = when (this) {
|
||||
is Status.Failed -> resourceReference(R.string.common_action_failed, wrappedList(resourceReference(pending)))
|
||||
is Status.Unconfirmed -> resourceReference(pending)
|
||||
is Status.Confirmed -> resourceReference(confirmed)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Amount building helpers
|
||||
|
||||
/**
|
||||
* Signed crypto amount with inline symbol, e.g. `+ 350.31 USDT` / `- 350.31 USDT`. The sign is `-` for outgoing, `+`
|
||||
* otherwise, and is dropped for zero amounts and for the failed state (a failed tx moved nothing) — the UI then only
|
||||
* strikes the amount through and dims it via [TxHistoryDetailsUM.AmountBlockUM.isFailed].
|
||||
*/
|
||||
private fun TxInfo.signedAmount(currency: CryptoCurrency): String {
|
||||
val formatted = amount.format { crypto(cryptoCurrency = currency, ignoreSymbolPosition = true) }
|
||||
val prefix = when {
|
||||
status is TxInfo.TransactionStatus.Failed -> ""
|
||||
amount.isZero() -> ""
|
||||
isOutgoing -> "${StringsSigns.MINUS} "
|
||||
else -> "${StringsSigns.PLUS} "
|
||||
}
|
||||
return (prefix + formatted).trim()
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Header building helpers
|
||||
|
||||
/** Type glyph. Unlike the history list, the failed state keeps the type glyph (only the color changes). */
|
||||
private fun TxInfo.headerIcon(): Int = when (type) {
|
||||
is TransactionType.Swap -> R.drawable.ic_exchange_vertical_24
|
||||
else -> if (isOutgoing) R.drawable.ic_arrow_up_24 else R.drawable.ic_arrow_down_24
|
||||
}
|
||||
|
||||
private fun TxInfo.headerTitle(): TextReference = when (type) {
|
||||
is TransactionType.Swap -> statusAwareTitle(R.string.common_swapping, R.string.common_swapped)
|
||||
is TransactionType.Transfer -> statusAwareTitle(R.string.common_transfer, R.string.common_transferred)
|
||||
else -> stringReference(type.toString())
|
||||
}
|
||||
|
||||
private fun headerSubtitle(timestampMillis: Long): TextReference {
|
||||
val dateTime = DateTime(timestampMillis)
|
||||
val date = DateTimeFormatters.dateMMMdYYYY.print(dateTime)
|
||||
val time = DateTimeFormatters.timeFormatter.print(dateTime)
|
||||
return stringReference("$date, $time")
|
||||
}
|
||||
|
||||
private fun TxInfo.statusAwareTitle(@StringRes pending: Int, @StringRes confirmed: Int): TextReference = when (status) {
|
||||
is TxInfo.TransactionStatus.Failed ->
|
||||
resourceReference(R.string.common_action_failed, wrappedList(resourceReference(pending)))
|
||||
is TxInfo.TransactionStatus.Unconfirmed -> resourceReference(pending)
|
||||
is TxInfo.TransactionStatus.Confirmed -> resourceReference(confirmed)
|
||||
}
|
||||
|
||||
private fun TxInfo.TransactionStatus.toUiStatus(): Status = when (this) {
|
||||
TxInfo.TransactionStatus.Confirmed -> Status.Confirmed
|
||||
TxInfo.TransactionStatus.Failed -> Status.Failed
|
||||
TxInfo.TransactionStatus.Unconfirmed -> Status.Unconfirmed
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
|
@ -89,8 +89,9 @@ internal class TxInfoToTxHistoryDetailsUMConverter(
|
|||
private fun TxInfo.toAmountBlockUM(): TxHistoryDetailsUM.AmountBlockUM = TxHistoryDetailsUM.AmountBlockUM(
|
||||
currencyIcon = iconStateConverter.convert(currency),
|
||||
amount = stringReference(signedAmount(currency)),
|
||||
// TODO: TxInfo has no fiat amount yet — placeholder until the fiat field is added to TxInfo.
|
||||
fiatAmount = stringReference("\$0.00"),
|
||||
// TODO: TxInfo has no fiat amount yet — empty until the fiat field is added to TxInfo; a hardcoded
|
||||
// placeholder would show a misleading value.
|
||||
fiatAmount = TextReference.EMPTY,
|
||||
isFailed = status is TxInfo.TransactionStatus.Failed,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
* UI model for the in-app transaction details ("Operation") card.
|
||||
*
|
||||
* One model for all transaction types; the layout family is chosen from the transaction type by
|
||||
* `TxInfoToTxHistoryDetailsUMConverter`:
|
||||
* `TxHistoryInfoToTxHistoryDetailsUMConverter`:
|
||||
* - [SingleAsset] — Receive / Send / Transfer
|
||||
* - [TwoAssets] — Swap / Onramp
|
||||
*/
|
||||
|
|
@ -131,7 +131,7 @@ internal sealed interface TxHistoryDetailsUM : TangemBottomSheetConfigContent {
|
|||
*
|
||||
* The layout is identical across counterparty kinds; the only variance is the [avatar] (see [CounterpartyAvatar])
|
||||
* and whether copy is offered. Only the [CounterpartyAvatar.Address] kind is currently produced by
|
||||
* [com.tangem.features.txhistory.converter.TxInfoToTxHistoryDetailsUMConverter]; the own-account / own-wallet
|
||||
* [com.tangem.features.txhistory.converter.TxHistoryInfoToTxHistoryDetailsUMConverter]; the own-account / own-wallet
|
||||
* avatars are populated in a follow-up, once the detail model assembles the same address->owner lookup the list
|
||||
* uses (`TxHistoryLookupContext`).
|
||||
*
|
||||
|
|
|
|||
|
|
@ -52,6 +52,18 @@ internal class HistoryTxListManager @AssistedInject constructor(
|
|||
|
||||
val paginationStatus: Flow<PaginationStatus<*>> = state.map { it.status }.distinctUntilChanged()
|
||||
|
||||
/**
|
||||
* Reactive stream of a single row tracked by its [TxHistoryInfo.txId], for the in-app details sheet.
|
||||
*
|
||||
* Seeded with the tapped [item] so the sheet always has an immediate snapshot, then re-emits the matching row from
|
||||
* the live merged list as its status changes. The seed also covers rows not present in [items] yet (e.g. a pending
|
||||
* tx surfaced from the currency status), which would otherwise never resolve.
|
||||
*/
|
||||
fun txHistoryInfoFlow(item: TxHistoryInfo): Flow<TxHistoryInfo> = items
|
||||
.mapNotNull { list -> list.firstOrNull { it.txId == item.txId } }
|
||||
.onStart { emit(item) }
|
||||
.distinctUntilChanged()
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
suspend fun init() {
|
||||
coroutineScope {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
package com.tangem.features.txhistory.converter
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.core.ui.components.transactions.state.TransactionItemUM.Content.Status
|
||||
import com.tangem.domain.express.models.ExpressOnrampStatus
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class ExpressOnrampStatusToUiStatusConverterTest {
|
||||
|
||||
private val converter = ExpressOnrampStatusToUiStatusConverter()
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun convert(model: Model) {
|
||||
// Act
|
||||
val actual = converter.convert(model.status)
|
||||
|
||||
// Assert
|
||||
assertThat(actual).isEqualTo(model.expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN every onramp status WHEN listing test models THEN all enum entries are covered`() {
|
||||
// Asserts the mapping table below stays in lockstep with the enum, so a newly added status
|
||||
// can never silently fall through with an untested UI bucket.
|
||||
val covered = provideTestModels().map { it.status }.toSet()
|
||||
|
||||
assertThat(covered).containsExactlyElementsIn(ExpressOnrampStatus.entries)
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
Model(status = ExpressOnrampStatus.Finished, expected = Status.Confirmed),
|
||||
Model(status = ExpressOnrampStatus.Failed, expected = Status.Failed),
|
||||
Model(status = ExpressOnrampStatus.Expired, expected = Status.Failed),
|
||||
Model(status = ExpressOnrampStatus.Unknown, expected = Status.Failed),
|
||||
Model(status = ExpressOnrampStatus.Created, expected = Status.Unconfirmed),
|
||||
Model(status = ExpressOnrampStatus.WaitingForPayment, expected = Status.Unconfirmed),
|
||||
Model(status = ExpressOnrampStatus.PaymentProcessing, expected = Status.Unconfirmed),
|
||||
Model(status = ExpressOnrampStatus.Verifying, expected = Status.Unconfirmed),
|
||||
Model(status = ExpressOnrampStatus.Paid, expected = Status.Unconfirmed),
|
||||
Model(status = ExpressOnrampStatus.Sending, expected = Status.Unconfirmed),
|
||||
Model(status = ExpressOnrampStatus.Paused, expected = Status.Unconfirmed),
|
||||
)
|
||||
|
||||
internal data class Model(val status: ExpressOnrampStatus, val expected: Status)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue