Skip to content

Commit e698786

Browse files
committed
test: AutoAdvance
1 parent 73893f8 commit e698786

File tree

1 file changed

+213
-0
lines changed

1 file changed

+213
-0
lines changed
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
/*
2+
* Copyright (c) 2025 Brayan Oliveira <[email protected]>
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.ui.windows.reviewer.autoadvance
17+
18+
import com.ichi2.anki.libanki.Card
19+
import io.mockk.MockKAnnotations
20+
import io.mockk.Runs
21+
import io.mockk.coEvery
22+
import io.mockk.coVerify
23+
import io.mockk.every
24+
import io.mockk.impl.annotations.MockK
25+
import io.mockk.just
26+
import io.mockk.mockk
27+
import io.mockk.mockkObject
28+
import io.mockk.unmockkAll
29+
import kotlinx.coroutines.CompletableDeferred
30+
import kotlinx.coroutines.test.StandardTestDispatcher
31+
import kotlinx.coroutines.test.TestScope
32+
import kotlinx.coroutines.test.advanceTimeBy
33+
import kotlinx.coroutines.test.runTest
34+
import org.junit.After
35+
import org.junit.Before
36+
import org.junit.Test
37+
import kotlin.time.Duration.Companion.seconds
38+
39+
class AutoAdvanceTest {
40+
private val testScope = TestScope(StandardTestDispatcher())
41+
42+
@MockK
43+
lateinit var listener: AutoAdvance.ActionListener
44+
45+
@MockK
46+
lateinit var card: Card
47+
48+
private lateinit var autoAdvance: AutoAdvance
49+
50+
private val defaultSettings get() =
51+
AutoAdvanceSettings(
52+
questionAction = QuestionAction.SHOW_ANSWER,
53+
answerAction = AnswerAction.ANSWER_GOOD,
54+
durationToShowQuestionFor = 5.seconds,
55+
durationToShowAnswerFor = 5.seconds,
56+
waitForAudio = false,
57+
)
58+
59+
@Before
60+
fun setUp() {
61+
MockKAnnotations.init(this)
62+
every { card.currentDeckId() } returns 1L
63+
coEvery { listener.onAutoAdvanceAction(any()) } just Runs
64+
mockkObject(AutoAdvanceSettings)
65+
coEvery { AutoAdvanceSettings.createInstance(any()) } returns defaultSettings
66+
}
67+
68+
@After
69+
fun tearDown() {
70+
unmockkAll()
71+
}
72+
73+
private fun createAutoAdvance(): AutoAdvance = AutoAdvance(testScope, listener, CompletableDeferred(card))
74+
75+
@Test
76+
fun `onShowQuestion triggers action after delay when enabled`() =
77+
testScope.runTest {
78+
autoAdvance = createAutoAdvance()
79+
autoAdvance.isEnabled = true
80+
testScheduler.advanceUntilIdle()
81+
82+
autoAdvance.onShowQuestion()
83+
84+
// Advance time less than duration (4s) - should NOT fire yet
85+
advanceTimeBy(4.seconds)
86+
coVerify(exactly = 0) { listener.onAutoAdvanceAction(any()) }
87+
88+
// Advance past duration (5s total) - SHOULD fire
89+
advanceTimeBy(2.seconds)
90+
coVerify(exactly = 1) { listener.onAutoAdvanceAction(QuestionAction.SHOW_ANSWER) }
91+
}
92+
93+
@Test
94+
fun `onShowAnswer triggers action after delay when enabled`() =
95+
testScope.runTest {
96+
autoAdvance = createAutoAdvance()
97+
autoAdvance.isEnabled = true
98+
testScheduler.advanceUntilIdle()
99+
100+
autoAdvance.onShowAnswer()
101+
102+
// Advance time less than duration (4s) - should NOT fire yet
103+
advanceTimeBy(4.seconds)
104+
coVerify(exactly = 0) { listener.onAutoAdvanceAction(any()) }
105+
106+
// Advance past duration (5s total) - SHOULD fire
107+
advanceTimeBy(2.seconds)
108+
coVerify(exactly = 1) { listener.onAutoAdvanceAction(AnswerAction.ANSWER_GOOD) }
109+
}
110+
111+
@Test
112+
fun `actions do not trigger if AutoAdvance is disabled`() =
113+
testScope.runTest {
114+
autoAdvance = createAutoAdvance()
115+
autoAdvance.isEnabled = false
116+
testScheduler.advanceUntilIdle()
117+
118+
autoAdvance.onShowQuestion()
119+
advanceTimeBy(6.seconds) // Past the 5s delay
120+
121+
coVerify(exactly = 0) { listener.onAutoAdvanceAction(any()) }
122+
}
123+
124+
@Test
125+
fun `setting isEnabled to false cancels pending jobs`() =
126+
testScope.runTest {
127+
autoAdvance = createAutoAdvance()
128+
autoAdvance.isEnabled = true
129+
testScheduler.advanceUntilIdle()
130+
131+
autoAdvance.onShowQuestion()
132+
133+
// Advance partially
134+
advanceTimeBy(2.seconds)
135+
136+
// Disable mid-wait
137+
autoAdvance.isEnabled = false
138+
139+
// Advance past the original trigger time
140+
advanceTimeBy(4.seconds)
141+
142+
// Should not have fired
143+
coVerify(exactly = 0) { listener.onAutoAdvanceAction(any()) }
144+
}
145+
146+
@Test
147+
fun `zero duration does not trigger action`() =
148+
testScope.runTest {
149+
coEvery { AutoAdvanceSettings.createInstance(any()) } returns
150+
defaultSettings.copy(durationToShowQuestionFor = 0.seconds)
151+
152+
autoAdvance = createAutoAdvance()
153+
autoAdvance.isEnabled = true
154+
testScheduler.advanceUntilIdle()
155+
156+
autoAdvance.onShowQuestion()
157+
advanceTimeBy(10.seconds)
158+
159+
coVerify(exactly = 0) { listener.onAutoAdvanceAction(any()) }
160+
}
161+
162+
@Test
163+
fun `onCardChange reloads settings`() =
164+
testScope.runTest {
165+
val initialSettings = defaultSettings
166+
val newSettings =
167+
initialSettings.copy(
168+
questionAction = QuestionAction.SHOW_REMINDER,
169+
durationToShowQuestionFor = 2.seconds,
170+
)
171+
172+
coEvery { AutoAdvanceSettings.createInstance(any()) } returns initialSettings andThen newSettings
173+
174+
autoAdvance = createAutoAdvance()
175+
autoAdvance.isEnabled = true
176+
testScheduler.advanceUntilIdle()
177+
178+
val newCard = mockk<Card>(relaxed = true)
179+
every { newCard.currentDeckId() } returns 2L
180+
autoAdvance.onCardChange(newCard)
181+
testScheduler.advanceUntilIdle()
182+
183+
autoAdvance.onShowQuestion()
184+
advanceTimeBy(3.seconds)
185+
186+
coVerify { listener.onAutoAdvanceAction(QuestionAction.SHOW_REMINDER) }
187+
}
188+
189+
@Test
190+
fun `switching from Question to Answer cancels Question job`() =
191+
testScope.runTest {
192+
autoAdvance = createAutoAdvance()
193+
autoAdvance.isEnabled = true
194+
testScheduler.advanceUntilIdle()
195+
196+
// Start question timer (5s)
197+
autoAdvance.onShowQuestion()
198+
advanceTimeBy(2.seconds)
199+
200+
autoAdvance.onShowAnswer()
201+
202+
// Advance time enough that Question timer WOULD have fired (total 6s)
203+
advanceTimeBy(4.seconds)
204+
205+
// Verify QuestionAction was NEVER fired
206+
coVerify(exactly = 0) { listener.onAutoAdvanceAction(QuestionAction.SHOW_ANSWER) }
207+
208+
// AnswerAction should be pending (needs 1 more second to reach 5s)
209+
coVerify(exactly = 0) { listener.onAutoAdvanceAction(any()) }
210+
advanceTimeBy(2.seconds)
211+
coVerify(exactly = 1) { listener.onAutoAdvanceAction(AnswerAction.ANSWER_GOOD) }
212+
}
213+
}

0 commit comments

Comments
 (0)