Skip to content

Commit d08ebb4

Browse files
committed
test(study-options): add unit tests for StudyOptionsViewModel
1 parent 2014cf2 commit d08ebb4

1 file changed

Lines changed: 205 additions & 0 deletions

File tree

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
/*
2+
* Copyright (c) 2026 Ashish Yadav <mailtoashish693@gmail.com>
3+
*
4+
* This program is free software; you can redistribute it and/or modify it under
5+
* the terms of the GNU General Public License as published by the Free Software
6+
* Foundation; either version 3 of the License, or (at your option) any later
7+
* version.
8+
*
9+
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
10+
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
11+
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
12+
*
13+
* You should have received a copy of the GNU General Public License along with
14+
* this program. If not, see <http://www.gnu.org/licenses/>.
15+
*/
16+
package com.ichi2.anki
17+
18+
import androidx.test.ext.junit.runners.AndroidJUnit4
19+
import anki.scheduler.CardAnswer.Rating
20+
import app.cash.turbine.test
21+
import com.ichi2.anki.CollectionManager.withCol
22+
import com.ichi2.testutils.ensureOpsExecuted
23+
import org.hamcrest.MatcherAssert.assertThat
24+
import org.hamcrest.Matchers.instanceOf
25+
import org.junit.Test
26+
import org.junit.runner.RunWith
27+
import kotlin.test.assertEquals
28+
import kotlin.test.assertFalse
29+
import kotlin.test.assertIs
30+
import kotlin.test.assertTrue
31+
32+
@RunWith(AndroidJUnit4::class)
33+
class StudyOptionsViewModelTest : RobolectricTest() {
34+
private val viewModel = StudyOptionsViewModel()
35+
36+
@Test
37+
fun `initial state is Loading`() {
38+
assertThat(viewModel.state.value, instanceOf(StudyOptionsState.Loading::class.java))
39+
}
40+
41+
@Test
42+
fun `refreshData - empty deck shows Empty state`() =
43+
runTest {
44+
col
45+
viewModel.refreshData().join()
46+
assertIs<StudyOptionsState.Empty>(viewModel.state.value)
47+
}
48+
49+
@Test
50+
fun `refreshData - deck with due cards shows StudyOptions state`() =
51+
runTest {
52+
addBasicNote("Front", "Back")
53+
54+
viewModel.refreshData().join()
55+
56+
val state = viewModel.state.value
57+
assertIs<StudyOptionsState.StudyOptions>(state)
58+
assertEquals(1, state.data.newCardsToday)
59+
assertEquals(1, state.data.numberOfCardsInDeck)
60+
assertEquals(1, state.data.totalNewCards)
61+
assertEquals(0, state.data.lrnCardsToday)
62+
assertEquals(0, state.data.revCardsToday)
63+
}
64+
65+
@Test
66+
fun `refreshData - regular deck is not filtered`() =
67+
runTest {
68+
addBasicNote()
69+
70+
viewModel.refreshData().join()
71+
72+
assertFalse(viewModel.isFilteredDeck.value)
73+
}
74+
75+
@Test
76+
fun `refreshData - congrats state when no cards due`() =
77+
runTest {
78+
addBasicNote()
79+
withCol {
80+
while (sched.card != null) {
81+
val card = sched.card!!
82+
sched.answerCard(card, Rating.EASY)
83+
}
84+
}
85+
86+
viewModel.refreshData().join()
87+
88+
assertIs<StudyOptionsState.Congrats>(viewModel.state.value)
89+
}
90+
91+
@Test
92+
fun `refreshData - state flow emits updates`() =
93+
runTest {
94+
col
95+
viewModel.state.test {
96+
assertIs<StudyOptionsState.Loading>(awaitItem())
97+
98+
viewModel.refreshData().join()
99+
assertIs<StudyOptionsState.Empty>(awaitItem())
100+
101+
addBasicNote()
102+
viewModel.refreshData().join()
103+
assertIs<StudyOptionsState.StudyOptions>(awaitItem())
104+
}
105+
}
106+
107+
@Test
108+
fun `refreshData - multiple cards counted correctly`() =
109+
runTest {
110+
repeat(5) { addBasicNote("Front $it", "Back $it") }
111+
112+
viewModel.refreshData().join()
113+
114+
val state = assertIs<StudyOptionsState.StudyOptions>(viewModel.state.value)
115+
assertEquals(5, state.data.newCardsToday)
116+
assertEquals(5, state.data.numberOfCardsInDeck)
117+
}
118+
119+
@Test
120+
fun `refreshData - deck name is correct`() =
121+
runTest {
122+
addBasicNote()
123+
124+
viewModel.refreshData().join()
125+
126+
val state = assertIs<StudyOptionsState.StudyOptions>(viewModel.state.value)
127+
assertEquals("Default", state.deckName)
128+
}
129+
130+
@Test
131+
fun `rebuildCram - updates state`() =
132+
runTest {
133+
addBasicNote()
134+
addDynamicDeck("Filtered", "")
135+
136+
viewModel.rebuildCram()
137+
138+
val state = viewModel.state.value
139+
assertThat(state, instanceOf(StudyOptionsState::class.java))
140+
assertTrue(viewModel.isFilteredDeck.value)
141+
}
142+
143+
@Test
144+
fun `emptyCram - is undoable`() =
145+
runTest {
146+
addBasicNote()
147+
addDynamicDeck("Filtered", "")
148+
149+
ensureOpsExecuted(1) {
150+
viewModel.emptyCram()
151+
}
152+
}
153+
154+
@Test
155+
fun `rebuildCram - is undoable`() =
156+
runTest {
157+
addBasicNote()
158+
addDynamicDeck("Filtered", "")
159+
160+
ensureOpsExecuted(1) {
161+
viewModel.rebuildCram()
162+
}
163+
}
164+
165+
@Test
166+
fun `unbury - is undoable`() =
167+
runTest {
168+
addBasicNote()
169+
withCol {
170+
val card = sched.card!!
171+
sched.buryCards(listOf(card.id), true)
172+
}
173+
174+
ensureOpsExecuted(1) {
175+
viewModel.unbury().join()
176+
}
177+
}
178+
179+
@Test
180+
fun `haveBuried - false when no buried cards`() =
181+
runTest {
182+
addBasicNote()
183+
184+
viewModel.refreshData().join()
185+
186+
assertFalse(viewModel.haveBuried.value)
187+
}
188+
189+
@Test
190+
fun `refreshData - buried cards are counted`() =
191+
runTest {
192+
addBasicNote("Front1", "Back1")
193+
addBasicNote("Front2", "Back2")
194+
withCol {
195+
val card = sched.card!!
196+
sched.buryCards(listOf(card.id), true)
197+
}
198+
199+
viewModel.refreshData().join()
200+
201+
val state = assertIs<StudyOptionsState.StudyOptions>(viewModel.state.value)
202+
assertTrue(state.data.buriedNew > 0, "expected buried new cards")
203+
assertEquals(1, state.data.newCardsToday)
204+
}
205+
}

0 commit comments

Comments
 (0)