|
| 1 | +package com.flipcash.app.tokens.ui |
| 2 | + |
| 3 | +import com.flipcash.app.core.tokens.SwapPurpose |
| 4 | +import com.getcode.opencode.exchange.VerifiedFiat |
| 5 | +import com.getcode.opencode.model.financial.CurrencyCode |
| 6 | +import com.getcode.opencode.model.financial.Fiat |
| 7 | +import com.getcode.opencode.model.financial.LocalFiat |
| 8 | +import com.getcode.solana.keys.Mint |
| 9 | +import com.getcode.ui.components.text.AmountAnimatedInputUiModel |
| 10 | +import com.getcode.view.LoadingSuccessState |
| 11 | +import kotlin.test.Test |
| 12 | +import kotlin.test.assertEquals |
| 13 | +import kotlin.test.assertFalse |
| 14 | +import kotlin.test.assertNull |
| 15 | +import kotlin.test.assertTrue |
| 16 | + |
| 17 | +class SwapViewModelStateTest { |
| 18 | + |
| 19 | + private val reduce = SwapViewModel.Companion.updateStateForEvent |
| 20 | + private fun mint() = Mint(ByteArray(32) { 1 }.toList()) |
| 21 | + |
| 22 | + // --- Default state --- |
| 23 | + |
| 24 | + @Test |
| 25 | + fun `default state`() { |
| 26 | + val state = SwapViewModel.State() |
| 27 | + assertNull(state.purpose) |
| 28 | + assertNull(state.tokenWithBalance) |
| 29 | + assertNull(state.reservesWithBalance) |
| 30 | + assertNull(state.swapId) |
| 31 | + assertNull(state.confirmedNetTransferAmount) |
| 32 | + assertFalse(state.loading) |
| 33 | + assertFalse(state.canTransact) |
| 34 | + } |
| 35 | + |
| 36 | + // --- State reducers --- |
| 37 | + |
| 38 | + @Test |
| 39 | + fun `OnPurposeChanged sets purpose`() { |
| 40 | + val purpose = SwapPurpose.Buy(mint()) |
| 41 | + val updated = reduce(SwapViewModel.Event.OnPurposeChanged(purpose))(SwapViewModel.State()) |
| 42 | + assertEquals(purpose, updated.purpose) |
| 43 | + } |
| 44 | + |
| 45 | + @Test |
| 46 | + fun `OnAmountChanged updates amount model in entry state`() { |
| 47 | + val model = AmountAnimatedInputUiModel(lastPressedBackspace = true) |
| 48 | + val updated = reduce(SwapViewModel.Event.OnAmountChanged(model))(SwapViewModel.State()) |
| 49 | + assertEquals(model, updated.amountEntryState.amountAnimatedModel) |
| 50 | + } |
| 51 | + |
| 52 | + @Test |
| 53 | + fun `OnAmountAccepted sets selectedAmount and confirmedNetTransferAmount`() { |
| 54 | + val amount = VerifiedFiat(LocalFiat.Zero, null) |
| 55 | + val net = Fiat(10.0, CurrencyCode.USD) |
| 56 | + val updated = reduce( |
| 57 | + SwapViewModel.Event.OnAmountAccepted(amount, net) |
| 58 | + )(SwapViewModel.State()) |
| 59 | + assertEquals(amount, updated.amountEntryState.selectedAmount) |
| 60 | + assertEquals(net, updated.confirmedNetTransferAmount) |
| 61 | + } |
| 62 | + |
| 63 | + @Test |
| 64 | + fun `OnMaxDetermined sets maxToAdd`() { |
| 65 | + val updated = reduce( |
| 66 | + SwapViewModel.Event.OnMaxDetermined(max = 500.0, currencyCode = CurrencyCode.EUR) |
| 67 | + )(SwapViewModel.State()) |
| 68 | + assertEquals(500.0 to CurrencyCode.EUR, updated.amountEntryState.maxToAdd) |
| 69 | + } |
| 70 | + |
| 71 | + @Test |
| 72 | + fun `UpdateBuyState sets buy loading and success`() { |
| 73 | + val loading = reduce( |
| 74 | + SwapViewModel.Event.UpdateBuyState(loading = true) |
| 75 | + )(SwapViewModel.State()) |
| 76 | + assertTrue(loading.buyProgress.loading) |
| 77 | + assertFalse(loading.buyProgress.success) |
| 78 | + |
| 79 | + val success = reduce( |
| 80 | + SwapViewModel.Event.UpdateBuyState(loading = false, success = true) |
| 81 | + )(SwapViewModel.State()) |
| 82 | + assertTrue(success.buyProgress.success) |
| 83 | + assertFalse(success.buyProgress.loading) |
| 84 | + } |
| 85 | + |
| 86 | + @Test |
| 87 | + fun `UpdateSellState sets sell loading and success`() { |
| 88 | + val loading = reduce( |
| 89 | + SwapViewModel.Event.UpdateSellState(loading = true) |
| 90 | + )(SwapViewModel.State()) |
| 91 | + assertTrue(loading.sellProgress.loading) |
| 92 | + |
| 93 | + val success = reduce( |
| 94 | + SwapViewModel.Event.UpdateSellState(loading = false, success = true) |
| 95 | + )(SwapViewModel.State()) |
| 96 | + assertTrue(success.sellProgress.success) |
| 97 | + } |
| 98 | + |
| 99 | + @Test |
| 100 | + fun `UpdateProcessingState sets loading, success, and error`() { |
| 101 | + val error = reduce( |
| 102 | + SwapViewModel.Event.UpdateProcessingState(error = true) |
| 103 | + )(SwapViewModel.State()) |
| 104 | + assertTrue(error.processingProgress.error) |
| 105 | + assertFalse(error.processingProgress.loading) |
| 106 | + } |
| 107 | + |
| 108 | + @Test |
| 109 | + fun `OnLimitsChanged sets limits in entry state`() { |
| 110 | + val updated = reduce( |
| 111 | + SwapViewModel.Event.OnLimitsChanged(null) |
| 112 | + )(SwapViewModel.State()) |
| 113 | + assertNull(updated.amountEntryState.limits) |
| 114 | + } |
| 115 | + |
| 116 | + // --- No-op events --- |
| 117 | + |
| 118 | + @Test |
| 119 | + fun `no-op events return state unchanged`() { |
| 120 | + val state = SwapViewModel.State(purpose = SwapPurpose.Buy(mint())) |
| 121 | + val noOpEvents = listOf( |
| 122 | + SwapViewModel.Event.OnAmountConfirmed, |
| 123 | + SwapViewModel.Event.OnBackspace, |
| 124 | + SwapViewModel.Event.OnDecimalPressed, |
| 125 | + SwapViewModel.Event.OnEnteredNumberChanged(), |
| 126 | + SwapViewModel.Event.OnNumberPressed(3), |
| 127 | + SwapViewModel.Event.OnSellConfirmed, |
| 128 | + SwapViewModel.Event.ProceedWithPurchase(VerifiedFiat(LocalFiat.Zero, null)), |
| 129 | + SwapViewModel.Event.ProceedWithSale(VerifiedFiat(LocalFiat.Zero, null)), |
| 130 | + SwapViewModel.Event.OnTransactionSuccessful, |
| 131 | + SwapViewModel.Event.ShowSellReceipt, |
| 132 | + SwapViewModel.Event.Exit, |
| 133 | + ) |
| 134 | + noOpEvents.forEach { event -> |
| 135 | + assertEquals(state, reduce(event)(state), "Event $event should be no-op") |
| 136 | + } |
| 137 | + } |
| 138 | + |
| 139 | + // --- Computed: tokenBalance --- |
| 140 | + |
| 141 | + @Test |
| 142 | + fun `tokenBalance is Zero when tokenWithBalance is null`() { |
| 143 | + assertEquals(Fiat.Zero, SwapViewModel.State().tokenBalance) |
| 144 | + } |
| 145 | + |
| 146 | + // --- Computed: reservesBalance --- |
| 147 | + |
| 148 | + @Test |
| 149 | + fun `reservesBalance is Zero when reservesWithBalance is null`() { |
| 150 | + assertEquals(Fiat.Zero, SwapViewModel.State().reservesBalance) |
| 151 | + } |
| 152 | + |
| 153 | + // --- Computed: canTransact --- |
| 154 | + |
| 155 | + @Test |
| 156 | + fun `canTransact is false when amount is zero`() { |
| 157 | + assertFalse(SwapViewModel.State().canTransact) |
| 158 | + } |
| 159 | + |
| 160 | + @Test |
| 161 | + fun `canTransact is false when buyProgress is loading`() { |
| 162 | + val state = SwapViewModel.State( |
| 163 | + buyProgress = LoadingSuccessState(loading = true) |
| 164 | + ) |
| 165 | + assertFalse(state.canTransact) |
| 166 | + } |
| 167 | + |
| 168 | + @Test |
| 169 | + fun `canTransact is false when sellProgress is loading`() { |
| 170 | + val state = SwapViewModel.State( |
| 171 | + sellProgress = LoadingSuccessState(loading = true) |
| 172 | + ) |
| 173 | + assertFalse(state.canTransact) |
| 174 | + } |
| 175 | + |
| 176 | + @Test |
| 177 | + fun `canTransact is false when processingProgress is loading`() { |
| 178 | + val state = SwapViewModel.State( |
| 179 | + processingProgress = LoadingSuccessState(loading = true) |
| 180 | + ) |
| 181 | + assertFalse(state.canTransact) |
| 182 | + } |
| 183 | + |
| 184 | + // --- Computed: sellFee --- |
| 185 | + |
| 186 | + @Test |
| 187 | + fun `sellFee is null when tokenWithBalance is null`() { |
| 188 | + assertNull(SwapViewModel.State().sellFee) |
| 189 | + } |
| 190 | + |
| 191 | + // --- Computed: tokenName --- |
| 192 | + |
| 193 | + @Test |
| 194 | + fun `tokenName is empty when tokenWithBalance is null`() { |
| 195 | + assertEquals("", SwapViewModel.State().tokenName) |
| 196 | + } |
| 197 | + |
| 198 | + // --- Computed: maxAvailableToSwap --- |
| 199 | + |
| 200 | + @Test |
| 201 | + fun `maxAvailableToSwap is empty when purpose is null`() { |
| 202 | + assertEquals("", SwapViewModel.State().maxAvailableToSwap) |
| 203 | + } |
| 204 | + |
| 205 | + // --- Computed: netTransferAmount --- |
| 206 | + |
| 207 | + @Test |
| 208 | + fun `netTransferAmount uses confirmedNetTransferAmount when set`() { |
| 209 | + val confirmed = Fiat(42.0, CurrencyCode.USD) |
| 210 | + val state = SwapViewModel.State(confirmedNetTransferAmount = confirmed) |
| 211 | + assertEquals(confirmed, state.netTransferAmount) |
| 212 | + } |
| 213 | + |
| 214 | + // --- Computed: isError --- |
| 215 | + |
| 216 | + @Test |
| 217 | + fun `isError is false when amount is empty`() { |
| 218 | + assertFalse(SwapViewModel.State().isError) |
| 219 | + } |
| 220 | + |
| 221 | + // --- Computed: transactionLimit --- |
| 222 | + |
| 223 | + @Test |
| 224 | + fun `transactionLimit is Zero when purpose is null`() { |
| 225 | + assertEquals(Fiat.Zero, SwapViewModel.State().transactionLimit) |
| 226 | + } |
| 227 | +} |
0 commit comments