@@ -6,16 +6,16 @@ import io.kotest.matchers.collections.shouldEndWith
66import io.kotest.matchers.collections.shouldNotBeEmpty
77import io.kotest.matchers.shouldBe
88import io.mockk.Runs
9- import io.mockk.coEvery
109import io.mockk.coVerify
1110import io.mockk.every
1211import io.mockk.just
1312import io.mockk.mockk
14- import kotlinx.coroutines.delay
15- import kotlinx.coroutines.suspendCancellableCoroutine
13+ import io.mockk.verify
14+ import kotlinx.coroutines.channels.Channel
1615import kotlinx.coroutines.test.StandardTestDispatcher
1716import kotlinx.coroutines.test.TestScope
1817import kotlinx.coroutines.test.advanceTimeBy
18+ import kotlinx.coroutines.test.runCurrent
1919import kotlinx.coroutines.test.runTest
2020import kotlinx.coroutines.yield
2121import org.junit.BeforeClass
@@ -33,24 +33,35 @@ import voice.core.sleeptimer.SleepTimerMode
3333import voice.core.sleeptimer.SleepTimerState
3434import kotlin.time.Duration.Companion.seconds
3535
36+ private class TestShakeDetector : ShakeDetector {
37+ private val shakes = Channel <Unit >(capacity = Channel .UNLIMITED )
38+ override suspend fun detect () {
39+ shakes.receive()
40+ }
41+
42+ fun shake () {
43+ shakes.trySend(Unit )
44+ }
45+ }
46+
3647class SleepTimerImplTest {
3748
3849 private val playStateManager = PlayStateManager ().apply {
3950 playState = PlayStateManager .PlayState .Playing
4051 }
41- private val shakeDetector = mockk<ShakeDetector > {
42- coEvery { detect() } coAnswers {
43- delay(30 .seconds)
44- }
45- }
46-
52+ private val shakeDetector = TestShakeDetector ()
4753 private val sleepTimerPreferenceStore = MemoryDataStore (SleepTimerPreference .Default )
4854 private val setVolumeSlots = mutableListOf<Float >()
4955 private val playerController = mockk<PlayerController > {
5056 every { setVolume(capture(setVolumeSlots)) } just Runs
5157 every { pauseWithRewind(any()) } answers {
5258 playStateManager.playState = PlayStateManager .PlayState .Paused
5359 }
60+ every {
61+ play()
62+ } answers {
63+ playStateManager.playState = PlayStateManager .PlayState .Playing
64+ }
5465 }
5566
5667 private val fadeOutStore = MemoryDataStore (2 .seconds)
@@ -78,10 +89,6 @@ class SleepTimerImplTest {
7889
7990 @Test
8091 fun `enable with fixed duration eventually disables and pauses playback` () = testScope.runTest {
81- coEvery { shakeDetector.detect() } coAnswers {
82- suspendCancellableCoroutine { } // never completes
83- }
84-
8592 sleepTimer.enable(SleepTimerMode .TimedWithDuration (1 .seconds))
8693
8794 advanceTimeBy(2 .seconds)
@@ -130,6 +137,34 @@ class SleepTimerImplTest {
130137 setVolumeSlots.shouldEndWith(1f )
131138 }
132139
140+ @Test
141+ fun shake_does_not_cancel_second_countdown_after_window () = testScope.runTest {
142+ // Use a LONG duration so we can observe behavior across the 30s window
143+ val longDuration = SleepTimerImpl .SHAKE_TO_RESET_TIME * 2
144+
145+ sleepTimer.enable(SleepTimerMode .TimedWithDuration (longDuration))
146+
147+ // 1) Let the first countdown finish and enter the shake window
148+ advanceTimeBy(longDuration + 1 .seconds)
149+ runCurrent()
150+ coVerify(exactly = 1 ) { playerController.pauseWithRewind(any()) }
151+ sleepTimer.state.value shouldBe SleepTimerState .Disabled
152+
153+ // 2) Trigger the shake → a new countdown should start independently of the old timeout
154+ shakeDetector.shake()
155+ runCurrent()
156+ verify(exactly = 1 ) { playerController.play() }
157+ sleepTimer.state.value shouldBe SleepTimerState .Enabled .WithDuration (longDuration)
158+
159+ // 3) Advance past the original 30s shake window and allow the second countdown to finish
160+ advanceTimeBy(SleepTimerImpl .SHAKE_TO_RESET_TIME + longDuration + 2 .seconds)
161+ runCurrent()
162+
163+ // The second countdown should complete normally
164+ coVerify(exactly = 2 ) { playerController.pauseWithRewind(any()) }
165+ sleepTimer.state.value shouldBe SleepTimerState .Disabled
166+ }
167+
133168 companion object {
134169
135170 @BeforeClass
0 commit comments