2.18.0 (#126)
All checks were successful
Release — F-Droid repo + Gitea/Codeberg release + Play / detect (push) Successful in 6s
Release — F-Droid repo + Gitea/Codeberg release + Play / release (push) Successful in 13m15s
Release — F-Droid repo + Gitea/Codeberg release + Play / play (push) Successful in 1m34s

Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/126
This commit is contained in:
Jean-Luc Makiola
2026-07-31 22:22:10 +02:00
parent 9557ca73ed
commit a8a83a39d0
27 changed files with 889 additions and 125 deletions

View File

@@ -502,6 +502,56 @@ class SettingsPrefsTest {
assertThat(prefs.snoozeMinutes.first()).isEqualTo(1)
}
@Test
fun `event duration defaults to an hour and clamps to a sane span`(
@TempDir tempDir: Path,
) = runTest {
val prefs = SettingsPrefs(newDataStore(tempDir))
assertThat(prefs.defaultEventDurationMinutes.first()).isEqualTo(60)
prefs.setDefaultEventDurationMinutes(480)
assertThat(prefs.defaultEventDurationMinutes.first()).isEqualTo(480)
prefs.setDefaultEventDurationMinutes(0)
assertThat(prefs.defaultEventDurationMinutes.first()).isEqualTo(1)
prefs.setDefaultEventDurationMinutes(5_000)
assertThat(prefs.defaultEventDurationMinutes.first()).isEqualTo(1_440)
}
@Test
fun `per-calendar duration round-trips and clears back to inherit`(
@TempDir tempDir: Path,
) = runTest {
val prefs = SettingsPrefs(newDataStore(tempDir))
assertThat(prefs.perCalendarEventDuration.first()).isEmpty()
prefs.setCalendarEventDuration(7L, 480)
prefs.setCalendarEventDuration(9L, 30)
assertThat(prefs.perCalendarEventDuration.first()).containsExactly(7L, 480, 9L, 30)
prefs.setCalendarEventDuration(7L, null)
assertThat(prefs.perCalendarEventDuration.first()).containsExactly(9L, 30)
}
@Test
fun `garbage per-calendar duration entries are dropped, the rest survive`(
@TempDir tempDir: Path,
) = runTest {
val store = newDataStore(tempDir)
val prefs = SettingsPrefs(store)
store.updateData { p ->
val m = p.toMutablePreferences()
// A bad id, a non-numeric length, one out of range, and a good entry.
m[SettingsPrefs.CALENDAR_EVENT_DURATION_KEY] = "x=60;7=soon;8=99999;9=45"
m
}
assertThat(prefs.perCalendarEventDuration.first()).containsExactly(9L, 45)
}
@Test
fun `resolveDefaultEventDuration prefers the calendar's own length`() {
val overrides = mapOf(7L to 480)
assertThat(resolveDefaultEventDuration(60, overrides, calendarId = 7L)).isEqualTo(480)
assertThat(resolveDefaultEventDuration(60, overrides, calendarId = 9L)).isEqualTo(60)
assertThat(resolveDefaultEventDuration(60, overrides, calendarId = null)).isEqualTo(60)
}
@Test
fun `custom-font stamps default to zero and bump per role independently`(@TempDir tempDir: Path) = runTest {
val prefs = SettingsPrefs(newDataStore(tempDir))

View File

@@ -10,6 +10,7 @@ import de.jeanlucmakiola.calendula.data.prefs.CalendarPrefs
import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs
import de.jeanlucmakiola.calendula.domain.CalendarSource
import de.jeanlucmakiola.calendula.domain.EventDetail
import de.jeanlucmakiola.calendula.domain.EventForm
import de.jeanlucmakiola.calendula.domain.EventInstance
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
@@ -22,6 +23,9 @@ import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import kotlinx.coroutines.Dispatchers
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.LocalTime
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
@@ -85,9 +89,11 @@ class EventEditViewModelTest {
private fun viewModel(
tempDir: Path,
fake: FakeCalendarDataSource,
// Passed in by the tests that need to seed a setting first; a second
// DataStore on the same file would clash, so it is built only once.
s: SettingsPrefs = settings(tempDir),
): EventEditViewModel {
val p = prefs(tempDir)
val s = settings(tempDir)
val repo = CalendarRepositoryImpl(fake, p, s, dispatcher as CoroutineDispatcher)
return EventEditViewModel(repo, p, s, dispatcher)
}
@@ -200,6 +206,120 @@ class EventEditViewModelTest {
job.cancel()
}
@Test
fun `a new event takes its calendar's default duration, and follows a switch`(
@TempDir tempDir: Path,
) = runTest(dispatcher) {
val fake = FakeCalendarDataSource().apply { calendarsResult = listOf(cal(1L), cal(2L)) }
val s = settings(tempDir)
s.setDefaultEventDurationMinutes(30)
s.setCalendarEventDuration(2L, 480)
val vm = viewModel(tempDir, fake, s)
val job = activate(vm)
vm.openNew(LocalDate(2030, 1, 15), startMinutes = 9 * 60)
advanceUntilIdle()
// Calendar 1 has no length of its own, so it inherits the global 30 min.
assertThat(vm.state.value?.form?.end).isEqualTo(LocalDateTime(2030, 1, 15, 9, 30))
vm.setCalendar(2L)
advanceUntilIdle()
assertThat(vm.state.value?.form?.end).isEqualTo(LocalDateTime(2030, 1, 15, 17, 0))
job.cancel()
}
@Test
fun `an end time set by hand survives a calendar switch`(
@TempDir tempDir: Path,
) = runTest(dispatcher) {
val fake = FakeCalendarDataSource().apply { calendarsResult = listOf(cal(1L), cal(2L)) }
val s = settings(tempDir)
s.setCalendarEventDuration(2L, 480)
val vm = viewModel(tempDir, fake, s)
val job = activate(vm)
vm.openNew(LocalDate(2030, 1, 15), startMinutes = 9 * 60)
advanceUntilIdle()
vm.setEndTime(LocalTime(10, 15))
vm.setCalendar(2L)
advanceUntilIdle()
assertThat(vm.state.value?.form?.end).isEqualTo(LocalDateTime(2030, 1, 15, 10, 15))
job.cancel()
}
@Test
fun `an imported event keeps the span its source named`(
@TempDir tempDir: Path,
) = runTest(dispatcher) {
val fake = FakeCalendarDataSource().apply { calendarsResult = listOf(cal(1L), cal(2L)) }
val s = settings(tempDir)
s.setDefaultEventDurationMinutes(30)
s.setCalendarEventDuration(2L, 480)
val vm = viewModel(tempDir, fake, s)
val job = activate(vm)
vm.openImported(importedForm(end = LocalDateTime(2030, 1, 15, 11, 30)), ImportSource.File)
advanceUntilIdle()
vm.setCalendar(2L)
advanceUntilIdle()
// The file owns the length; a calendar switch must not restretch it.
assertThat(vm.state.value?.form?.end).isEqualTo(LocalDateTime(2030, 1, 15, 11, 30))
job.cancel()
}
@Test
fun `an insert intent with no end takes the default duration`(
@TempDir tempDir: Path,
) = runTest(dispatcher) {
val fake = FakeCalendarDataSource().apply { calendarsResult = listOf(cal(1L), cal(2L)) }
val s = settings(tempDir)
s.setDefaultEventDurationMinutes(30)
val vm = viewModel(tempDir, fake, s)
val job = activate(vm)
// The placeholder hour buildInsertEventForm fills in for a missing end.
vm.openImported(
importedForm(end = LocalDateTime(2030, 1, 15, 10, 0)),
ImportSource.InsertOpenEnded,
)
advanceUntilIdle()
assertThat(vm.state.value?.form?.end).isEqualTo(LocalDateTime(2030, 1, 15, 9, 30))
job.cancel()
}
private fun importedForm(end: LocalDateTime) = EventForm(
calendarId = null,
title = "Imported",
start = LocalDateTime(2030, 1, 15, 9, 0),
end = end,
)
@Test
fun `an all-day event ignores the default duration`(
@TempDir tempDir: Path,
) = runTest(dispatcher) {
val fake = FakeCalendarDataSource().apply { calendarsResult = listOf(cal(1L), cal(2L)) }
val s = settings(tempDir)
s.setCalendarEventDuration(2L, 480)
val vm = viewModel(tempDir, fake, s)
val job = activate(vm)
vm.openNew(LocalDate(2030, 1, 15), startMinutes = 9 * 60)
advanceUntilIdle()
vm.setAllDay(true)
vm.setCalendar(2L)
advanceUntilIdle()
// Dates carry no length: the times stay where the form put them.
assertThat(vm.state.value?.form?.end).isEqualTo(LocalDateTime(2030, 1, 15, 10, 0))
job.cancel()
}
@Test
fun `editing a recurring event without moving still asks for the scope`(
@TempDir tempDir: Path,