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.
This commit is contained in:
makiolaj committed 2026-10-21 19:57:19 +02:00
1 parent 78da94d775
commit d70d0c78da
4 files changed
+25 -8

No files matched your search

@@ -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<Unit> = emptyFlow()): Flow<WorldClockUiState.Loaded> =
fun state(refreshes: Flow<Unit>): Flow<WorldClockUiState.Loaded> =
combine(
worldClocks.worldClocks(),
storedHomeZoneId,
@@ -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()
@@ -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) {
+6 -4
View File
@@ -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