From d70d0c78dabe9b04127d70ed96042fbd53409cf5 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 21 Oct 2026 19:57:19 +0200 Subject: [PATCH] test(worldclock): make the seconds tests mean what they say The no-re-emission check now waits for the write to be readable before asserting, and the seconds switch has its own settings test instead of riding in the clock-defaults one. WorldClockSource.state takes its refresh signal without a default, and ARCHITECTURE.md's slice count and alignedTicks guarantee match the code. --- .../clockula/ui/worldclock/WorldClockSource.kt | 3 +-- .../clockula/data/prefs/ClockPrefsTest.kt | 3 +++ .../ui/settings/SettingsViewModelTest.kt | 17 +++++++++++++++-- docs/ARCHITECTURE.md | 10 ++++++---- 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockSource.kt b/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockSource.kt index 4511f61..f250453 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockSource.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockSource.kt @@ -15,7 +15,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map @@ -64,7 +63,7 @@ internal class WorldClockSource @Inject constructor( * state and `distinctUntilChanged` only drops the odd early wake (D18). */ @OptIn(ExperimentalCoroutinesApi::class) - fun state(refreshes: Flow = emptyFlow()): Flow = + fun state(refreshes: Flow): Flow = combine( worldClocks.worldClocks(), storedHomeZoneId, diff --git a/app/src/test/java/de/jeanlucmakiola/clockula/data/prefs/ClockPrefsTest.kt b/app/src/test/java/de/jeanlucmakiola/clockula/data/prefs/ClockPrefsTest.kt index 43225db..c11a2c6 100644 --- a/app/src/test/java/de/jeanlucmakiola/clockula/data/prefs/ClockPrefsTest.kt +++ b/app/src/test/java/de/jeanlucmakiola/clockula/data/prefs/ClockPrefsTest.kt @@ -90,6 +90,9 @@ class ClockPrefsTest { assertThat(awaitItem()).isEqualTo(ClockDefaults()) prefs.setWorldClockSeconds(true) + // Wait until the write is visible to a reader, so a re-emission of + // the defaults would have had its chance to arrive. + prefs.worldClockSeconds.first { it } expectNoEvents() cancel() diff --git a/app/src/test/java/de/jeanlucmakiola/clockula/ui/settings/SettingsViewModelTest.kt b/app/src/test/java/de/jeanlucmakiola/clockula/ui/settings/SettingsViewModelTest.kt index 2dd8c52..4391896 100644 --- a/app/src/test/java/de/jeanlucmakiola/clockula/ui/settings/SettingsViewModelTest.kt +++ b/app/src/test/java/de/jeanlucmakiola/clockula/ui/settings/SettingsViewModelTest.kt @@ -119,7 +119,6 @@ class SettingsViewModelTest { viewModel.onSnoozeLimitChanged(2) viewModel.onVolumeRampChanged(30) viewModel.onDismissChallengeChanged(DismissChallenge.MATH) - viewModel.onWorldClockSecondsChanged(true) advanceUntilIdle() val defaults = (viewModel.state.value as SettingsUiState.Loaded).defaults @@ -128,9 +127,23 @@ class SettingsViewModelTest { assertThat(defaults.snoozeLimit).isEqualTo(2) assertThat(defaults.volumeRampSeconds).isEqualTo(30) assertThat(defaults.dismissChallenge).isEqualTo(DismissChallenge.MATH) - assertThat((viewModel.state.value as SettingsUiState.Loaded).worldClockSeconds).isTrue() } + @Test + fun `the world clock's seconds switch writes through, apart from the defaults`(@TempDir tempDir: Path) = + runTest(main.dispatcher) { + val viewModel = collecting(newViewModel(tempDir)) + advanceUntilIdle() + val defaultsBefore = (viewModel.state.value as SettingsUiState.Loaded).defaults + + viewModel.onWorldClockSecondsChanged(true) + advanceUntilIdle() + + val loaded = viewModel.state.value as SettingsUiState.Loaded + assertThat(loaded.worldClockSeconds).isTrue() + assertThat(loaded.defaults).isEqualTo(defaultsBefore) + } + @Test fun `choosing a default alarm ringtone writes through and closes the picker`(@TempDir tempDir: Path) = runTest(main.dispatcher) { diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 86f1f56..09a9585 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -592,7 +592,7 @@ one anywhere under `domain/` or `ui/`. ## 6. Preferences One DataStore file, `clockula_prefs`, behind floret-kit's typed `PrefStore`. -Four slices: +Six slices: | Slice | Keys | Owner | |---|---|---| @@ -1429,9 +1429,11 @@ nothing but `delay`, which makes it testable under `runTest`'s virtual clock. `alignedTicks(unit)` is the same seam for a readout of whole wall-clock units: after the immediate emission it waits `WallBoundaries.untilNext(now, unit)`, re-measured against `WallClock` after every wake, so it fires on the second or -minute boundary itself and a long `delay` or a set clock does not accumulate. -The world clock is its one caller (§16, "The cadence"), and it merges in -`ClockChanges`, the system's own word that the clock or zone moved, so a long +minute boundary itself and a `delay` that ran long does not accumulate. A +clock set mid-wait is only caught up once that wait ends, so a caller on a +long cadence restarts the flow on `ClockChanges`, the system's own word that +the clock or zone moved. The world clock is its one caller (§16, "The +cadence"), and does exactly that, so a long cadence never means a long wait for those. ### The dismiss challenge, and why it cannot strand anyone