refactor(shell): the pill's stopwatch half goes through the engine
M6 left this as M7's: the pill wrote to the stopwatch repository directly because there was no engine to ask. There is one now, so the pill's buttons mean exactly what the tab's and the shade's mean, service transitions included. `LivePillSelector` is re-pointed at `StopwatchReadings` — the same extraction M6 did for the timers — and its existing cases pass unmodified, which is the proof that the reading moved rather than changed. A stopwatch notification now opens the Stopwatch tab.
This commit is contained in:
1 parent
7b727e0d40
commit
5e73e81dc3
7 files changed
+139
-17
No files matched your search
@@ -5,6 +5,7 @@ import de.jeanlucmakiola.clockula.domain.StopwatchRun
|
||||
import de.jeanlucmakiola.clockula.domain.StopwatchState
|
||||
import de.jeanlucmakiola.clockula.domain.Timer
|
||||
import de.jeanlucmakiola.clockula.domain.TimerState
|
||||
import de.jeanlucmakiola.clockula.domain.stopwatch.StopwatchReadings
|
||||
import de.jeanlucmakiola.clockula.testing.BERLIN
|
||||
import de.jeanlucmakiola.clockula.testing.instantAt
|
||||
import de.jeanlucmakiola.clockula.testing.runningTimer
|
||||
@@ -386,4 +387,30 @@ class LivePillSelectorTest {
|
||||
|
||||
assertThat(first).isEqualTo(second)
|
||||
}
|
||||
|
||||
|
||||
// --- M7 §5.10: the pill and the tab share one reading (D6) ---
|
||||
|
||||
/** §5.10 #1 */
|
||||
@Test
|
||||
fun `the pill's stopwatch reading is StopwatchReadings' own`() {
|
||||
val runs = listOf(
|
||||
StopwatchRun(StopwatchState.RUNNING, 10.seconds, now - 30.seconds),
|
||||
StopwatchRun(StopwatchState.PAUSED, 42.seconds, null),
|
||||
StopwatchRun(StopwatchState.RUNNING, 25.seconds, now + 500.seconds),
|
||||
idleStopwatch,
|
||||
)
|
||||
|
||||
val pills = runs.map { select(emptyList(), run = it) }
|
||||
|
||||
val expected = runs.map { run ->
|
||||
val reading = StopwatchReadings.of(run, now)
|
||||
when (reading.mode) {
|
||||
StopwatchState.IDLE -> null
|
||||
StopwatchState.RUNNING -> LivePillMode.RUNNING to reading.elapsed
|
||||
StopwatchState.PAUSED -> LivePillMode.PAUSED to reading.elapsed
|
||||
}
|
||||
}
|
||||
assertThat(pills.map { it?.let { pill -> pill.mode to pill.value } }).isEqualTo(expected)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package de.jeanlucmakiola.clockula.ui.shell
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import de.jeanlucmakiola.clockula.stopwatch.StopwatchIntents
|
||||
import de.jeanlucmakiola.clockula.timer.TimerIntents
|
||||
import de.jeanlucmakiola.clockula.ui.timers.TimerRoutes
|
||||
import org.junit.jupiter.api.Test
|
||||
@@ -172,4 +173,25 @@ class ShellNavigationTest {
|
||||
|
||||
assertThat(answers).containsExactly(null, null)
|
||||
}
|
||||
|
||||
// --- M7 §5.11: the stopwatch notification's deep link (D16) ---
|
||||
|
||||
/** §5.11 #1 */
|
||||
@Test
|
||||
fun `a stopwatch notification asks for the stopwatch tab`() {
|
||||
assertThat(ShellNavigation.tabForAction(StopwatchIntents.ACTION_SHOW_STOPWATCH))
|
||||
.isEqualTo(ClockulaDestination.STOPWATCH)
|
||||
}
|
||||
|
||||
/** §5.11 #2 */
|
||||
@Test
|
||||
fun `the third action changes none of the other answers`() {
|
||||
val answers = listOf(
|
||||
ShellNavigation.tabForAction(TimerIntents.ACTION_SHOW_TIMERS),
|
||||
ShellNavigation.tabForAction(null),
|
||||
ShellNavigation.tabForAction("android.intent.action.VIEW"),
|
||||
)
|
||||
|
||||
assertThat(answers).containsExactly(ClockulaDestination.TIMERS, null, null).inOrder()
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import de.jeanlucmakiola.clockula.testing.FakeStopwatchRepository
|
||||
import de.jeanlucmakiola.clockula.testing.FakeTicker
|
||||
import de.jeanlucmakiola.clockula.testing.FakeTimerRepository
|
||||
import de.jeanlucmakiola.clockula.testing.FakeWallClock
|
||||
import de.jeanlucmakiola.clockula.testing.StopwatchEngineSeams
|
||||
import de.jeanlucmakiola.clockula.testing.TimerEngineSeams
|
||||
import de.jeanlucmakiola.clockula.testing.MainDispatcherExtension
|
||||
import de.jeanlucmakiola.clockula.testing.instantAt
|
||||
@@ -57,6 +58,15 @@ class ShellViewModelTest {
|
||||
*/
|
||||
private val seams = TimerEngineSeams(timers = timers, now = wallInstant, uptime = now)
|
||||
|
||||
/**
|
||||
* And the stopwatch branch goes through the **real** [StopwatchEngine] over
|
||||
* the same repository (M7 D8): without it, pausing from the pill would
|
||||
* leave the shade showing a stopwatch that is no longer running. The stored
|
||||
* record — not a call log — is therefore what the stopwatch cases assert on.
|
||||
*/
|
||||
private val stopwatchSeams =
|
||||
StopwatchEngineSeams(stopwatch = stopwatch, now = wallInstant, uptime = now)
|
||||
|
||||
private fun viewModel() = ShellViewModel(
|
||||
source = LivePillSource(
|
||||
timers = timers,
|
||||
@@ -66,7 +76,7 @@ class ShellViewModelTest {
|
||||
ticker = ticker,
|
||||
),
|
||||
timers = timers,
|
||||
stopwatch = stopwatch,
|
||||
stopwatchEngine = stopwatchSeams.engine,
|
||||
timerEngine = seams.engine,
|
||||
)
|
||||
|
||||
@@ -77,6 +87,12 @@ class ShellViewModelTest {
|
||||
otherActiveTimers = 0,
|
||||
)
|
||||
|
||||
private fun runningStopwatch(elapsedSoFar: Duration = 4.minutes): StopwatchRun = StopwatchRun(
|
||||
state = StopwatchState.RUNNING,
|
||||
accumulated = elapsedSoFar,
|
||||
startedAtElapsedRealtime = now,
|
||||
)
|
||||
|
||||
private fun stopwatchState(mode: LivePillMode) = LivePillState(
|
||||
subject = LivePillSubject.StopwatchSubject,
|
||||
mode = mode,
|
||||
@@ -153,16 +169,20 @@ class ShellViewModelTest {
|
||||
|
||||
@Test
|
||||
fun `the primary action pauses a running stopwatch`() = runTest(main.dispatcher) {
|
||||
stopwatch.set(runningStopwatch())
|
||||
|
||||
viewModel().onPillPrimaryAction(stopwatchState(mode = LivePillMode.RUNNING))
|
||||
|
||||
assertThat(stopwatch.calls.last()).isEqualTo("pause()")
|
||||
assertThat(stopwatch.currentRun().state).isEqualTo(StopwatchState.PAUSED)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the primary action resumes a paused stopwatch`() = runTest(main.dispatcher) {
|
||||
stopwatch.set(StopwatchRun(state = StopwatchState.PAUSED, accumulated = 4.minutes))
|
||||
|
||||
viewModel().onPillPrimaryAction(stopwatchState(mode = LivePillMode.PAUSED))
|
||||
|
||||
assertThat(stopwatch.calls.last()).isEqualTo("start()")
|
||||
assertThat(stopwatch.currentRun().state).isEqualTo(StopwatchState.RUNNING)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -188,9 +208,11 @@ class ShellViewModelTest {
|
||||
|
||||
@Test
|
||||
fun `stop resets the stopwatch`() = runTest(main.dispatcher) {
|
||||
stopwatch.set(runningStopwatch())
|
||||
|
||||
viewModel().onPillStop(stopwatchState(mode = LivePillMode.RUNNING))
|
||||
|
||||
assertThat(stopwatch.calls.last()).isEqualTo("reset()")
|
||||
assertThat(stopwatch.currentRun()).isEqualTo(StopwatchRun())
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -258,4 +280,44 @@ class ShellViewModelTest {
|
||||
assertThat(seams.scheduler.scheduled).isEmpty()
|
||||
assertThat(seams.service.transitions).isEmpty()
|
||||
}
|
||||
|
||||
// --- M7 §5.9: the pill's stopwatch branch goes through the engine (D8) ---
|
||||
|
||||
/** §5.9 #1 */
|
||||
@Test
|
||||
fun `pausing from the pill leaves the stopwatch service up`() = runTest(main.dispatcher) {
|
||||
stopwatch.set(runningStopwatch())
|
||||
|
||||
viewModel().onPillPrimaryAction(stopwatchState(mode = LivePillMode.RUNNING))
|
||||
|
||||
assertThat(stopwatch.currentRun().state).isEqualTo(StopwatchState.PAUSED)
|
||||
assertThat(stopwatchSeams.service.active).isTrue()
|
||||
}
|
||||
|
||||
/** §5.9 #2 */
|
||||
@Test
|
||||
fun `stop resets the stopwatch and takes its service down`() = runTest(main.dispatcher) {
|
||||
stopwatch.set(runningStopwatch())
|
||||
val viewModel = viewModel()
|
||||
viewModel.onPillPrimaryAction(stopwatchState(mode = LivePillMode.RUNNING))
|
||||
|
||||
viewModel.onPillStop(stopwatchState(mode = LivePillMode.PAUSED))
|
||||
|
||||
assertThat(stopwatch.currentRun()).isEqualTo(StopwatchRun())
|
||||
assertThat(stopwatch.laps().first()).isEmpty()
|
||||
assertThat(stopwatchSeams.service.transitions.last()).isFalse()
|
||||
}
|
||||
|
||||
/** §5.9 #3 */
|
||||
@Test
|
||||
fun `the timer's half of the pill does not reach the stopwatch engine`() =
|
||||
runTest(main.dispatcher) {
|
||||
timers.set(listOf(runningTimerRow()))
|
||||
val viewModel = viewModel()
|
||||
|
||||
viewModel.onPillPrimaryAction(timerState(mode = LivePillMode.RUNNING))
|
||||
viewModel.onPillStop(timerState(mode = LivePillMode.RUNNING))
|
||||
|
||||
assertThat(stopwatchSeams.service.transitions).isEmpty()
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user