Skip to content

Commit 88cee2a

Browse files
committed
test: add CashScreen, Swap, and OnRamp ViewModel state tests
Add 56 tests across 3 new test files for high-use flows: - CashScreenViewModelStateTest (13 tests): state reducers, computed props (canGive, maxAvailableForGive, isError) - SwapViewModelStateTest (22 tests): state reducers, computed props (canTransact, sellFee, netTransferAmount, transactionLimit, isError), all progress states - OnRampViewModelStateTest (21 tests): state reducers, provider selection logic (canChangeCurrency), AmountEntryState computed props, minimumPurchaseAmount
1 parent 0089460 commit 88cee2a

2 files changed

Lines changed: 362 additions & 0 deletions

File tree

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package com.flipcash.app.cash.internal
2+
3+
import com.getcode.opencode.model.financial.CurrencyCode
4+
import com.getcode.opencode.model.financial.Fiat
5+
import com.getcode.solana.keys.Mint
6+
import com.getcode.ui.components.text.AmountAnimatedInputUiModel
7+
import kotlin.test.Test
8+
import kotlin.test.assertEquals
9+
import kotlin.test.assertFalse
10+
import kotlin.test.assertNull
11+
import kotlin.test.assertTrue
12+
13+
class CashScreenViewModelStateTest {
14+
15+
private val reduce = CashScreenViewModel.Companion.updateStateForEvent
16+
private fun mint() = Mint(ByteArray(32) { 1 }.toList())
17+
18+
// --- Default state ---
19+
20+
@Test
21+
fun `default state has null token and address`() {
22+
val state = CashScreenViewModel.State()
23+
assertNull(state.selectedTokenAddress)
24+
assertNull(state.token)
25+
assertNull(state.limits)
26+
assertNull(state.maxForGive)
27+
assertFalse(state.canGive)
28+
}
29+
30+
// --- State reducers ---
31+
32+
@Test
33+
fun `OnTokenSelected sets selectedTokenAddress`() {
34+
val mint = mint()
35+
val updated = reduce(CashScreenViewModel.Event.OnTokenSelected(mint))(CashScreenViewModel.State())
36+
assertEquals(mint, updated.selectedTokenAddress)
37+
}
38+
39+
@Test
40+
fun `OnAmountChanged updates amountAnimatedModel`() {
41+
val model = AmountAnimatedInputUiModel(lastPressedBackspace = true)
42+
val updated = reduce(CashScreenViewModel.Event.OnAmountChanged(model))(CashScreenViewModel.State())
43+
assertEquals(model, updated.amountAnimatedModel)
44+
}
45+
46+
@Test
47+
fun `UpdateLoadingState sets loading`() {
48+
val updated = reduce(
49+
CashScreenViewModel.Event.UpdateLoadingState(loading = true)
50+
)(CashScreenViewModel.State())
51+
assertTrue(updated.generatingBill.loading)
52+
assertFalse(updated.generatingBill.success)
53+
}
54+
55+
@Test
56+
fun `UpdateLoadingState sets success`() {
57+
val updated = reduce(
58+
CashScreenViewModel.Event.UpdateLoadingState(loading = false, success = true)
59+
)(CashScreenViewModel.State())
60+
assertTrue(updated.generatingBill.success)
61+
assertFalse(updated.generatingBill.loading)
62+
}
63+
64+
@Test
65+
fun `OnMaxDetermined sets maxForGive`() {
66+
val updated = reduce(
67+
CashScreenViewModel.Event.OnMaxDetermined(max = 50.0, currencyCode = CurrencyCode.USD)
68+
)(CashScreenViewModel.State())
69+
assertEquals(50.0 to CurrencyCode.USD, updated.maxForGive)
70+
}
71+
72+
@Test
73+
fun `OnLimitsChanged sets limits`() {
74+
val updated = reduce(
75+
CashScreenViewModel.Event.OnLimitsChanged(null)
76+
)(CashScreenViewModel.State())
77+
assertNull(updated.limits)
78+
}
79+
80+
// --- No-op events ---
81+
82+
@Test
83+
fun `no-op events return state unchanged`() {
84+
val state = CashScreenViewModel.State(selectedTokenAddress = mint())
85+
val noOpEvents = listOf(
86+
CashScreenViewModel.Event.InitializeToken(null),
87+
CashScreenViewModel.Event.OnBackspace,
88+
CashScreenViewModel.Event.OnGive,
89+
CashScreenViewModel.Event.OnEnteredNumberChanged(),
90+
CashScreenViewModel.Event.OnNumberPressed(5),
91+
CashScreenViewModel.Event.OnDecimalPressed,
92+
CashScreenViewModel.Event.AddCashToWallet(Fiat.Zero),
93+
CashScreenViewModel.Event.OpenScreen(com.flipcash.app.core.AppRoute.Loading),
94+
)
95+
noOpEvents.forEach { event ->
96+
assertEquals(state, reduce(event)(state), "Event $event should be no-op")
97+
}
98+
}
99+
100+
// --- Computed: canGive ---
101+
102+
@Test
103+
fun `canGive is false when amount is zero`() {
104+
assertFalse(CashScreenViewModel.State().canGive)
105+
}
106+
107+
// --- Computed: maxAvailableForGive ---
108+
109+
@Test
110+
fun `maxAvailableForGive is empty when maxForGive is null`() {
111+
val state = CashScreenViewModel.State(maxForGive = null)
112+
assertEquals("", state.maxAvailableForGive)
113+
}
114+
115+
@Test
116+
fun `maxAvailableForGive is formatted when maxForGive is set`() {
117+
val state = CashScreenViewModel.State(maxForGive = 100.0 to CurrencyCode.USD)
118+
assertTrue(state.maxAvailableForGive.isNotEmpty())
119+
}
120+
121+
// --- Computed: isError ---
122+
123+
@Test
124+
fun `isError is true when maxForGive is null`() {
125+
// Default amountData has "0" text (not empty), and maxForGive is null → error
126+
assertTrue(CashScreenViewModel.State().isError)
127+
}
128+
129+
@Test
130+
fun `isError is false when amount within maxForGive`() {
131+
// Default amount 0.0 <= 100.0
132+
val state = CashScreenViewModel.State(maxForGive = 100.0 to CurrencyCode.USD)
133+
assertFalse(state.isError)
134+
}
135+
}
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
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

Comments
 (0)