Phase 3: Kalender-Widget mit echten EventKit-Daten
Die Unterscheidung zwischen "keine Termine" und "keine Information" steckt von Anfang an in den Grundlagen, nicht als Nachtrag für Calendarr. Ein Kalender ohne Termine und ein Kalender, über den nichts bekannt ist, sehen in einer Liste identisch aus — nämlich leer. Ein leerer März ist überzeugend und schlicht falsch, wenn die Quelle nur 49 Tage abdeckt. EventWindow trägt deshalb eine coverage mit. Bei EventKit ist sie der angefragte Zeitraum, weil direkt abgefragt wird; bei Calendarr wird sie später aus dem Snapshot kommen. Tage außerhalb werden im Mini-Monat ausgegraut und tragen einen erklärenden Hinweis, statt als frei durchzugehen. Die Abdeckung gewinnt auch gegen widersprüchliche Eingaben: liefert eine Quelle einen Termin für einen Tag, den sie nach eigener Angabe nicht abdeckt, bleibt der Tag unbekannt. Sonst behauptet die Anzeige mehr zu wissen, als belegt ist. Als Test festgehalten. CalendarSourceState statt eines Optionals: denied, neverWritten, loggedOut, incompatible und unreadable bekommen jeweils eine eigene Antwort in der Oberfläche. "Nichts anzuzeigen" ist keine. Serientermine bekommen eine aus Kennung und Beginn zusammengesetzte ID — EKEvent.eventIdentifier ist für alle Vorkommen gleich, eine Terminliste mit einer täglichen Serie fiele sonst auf einen Eintrag zusammen. Monatsraster testgetrieben, inklusive Schaltjahr, Wochenstart Montag wie Sonntag und der Regel, dass das Ende der Abdeckung nicht mehr dazugehört. 93 Tests grün.
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
import Testing
|
||||
import Foundation
|
||||
@testable import CalendarProvider
|
||||
|
||||
@Suite("Monatsraster")
|
||||
struct MonthGridTests {
|
||||
|
||||
private var calendar: Calendar {
|
||||
var c = Calendar(identifier: .gregorian)
|
||||
c.timeZone = TimeZone(identifier: "Europe/Berlin")!
|
||||
c.firstWeekday = 2 // Montag
|
||||
return c
|
||||
}
|
||||
|
||||
private func date(_ string: String) -> Date {
|
||||
let f = DateFormatter()
|
||||
f.calendar = calendar
|
||||
f.timeZone = calendar.timeZone
|
||||
f.dateFormat = "yyyy-MM-dd"
|
||||
return f.date(from: string)!
|
||||
}
|
||||
|
||||
// MARK: - Aufbau
|
||||
|
||||
@Test("August 2026 beginnt an einem Samstag und braucht deshalb fünf Vortage")
|
||||
func leadingDaysForAugust2026() {
|
||||
let grid = MonthGrid(containing: date("2026-08-10"), calendar: calendar)
|
||||
|
||||
// 1. August 2026 ist ein Samstag; bei Wochenstart Montag stehen davor
|
||||
// Mo–Fr aus dem Juli.
|
||||
#expect(grid.days.first?.date == date("2026-07-27"))
|
||||
#expect(grid.days.prefix(5).allSatisfy { !$0.isInMonth })
|
||||
}
|
||||
|
||||
@Test("Das Raster besteht immer aus vollen Wochen")
|
||||
func gridIsWholeWeeks() {
|
||||
for month in ["2026-01-15", "2026-02-15", "2026-08-10", "2026-12-01"] {
|
||||
let grid = MonthGrid(containing: date(month), calendar: calendar)
|
||||
#expect(grid.days.count % 7 == 0, "Monat \(month) ergibt keine vollen Wochen")
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Februar 2027 hat 28 Tage im Monat, egal wie viele Zellen das Raster hat")
|
||||
func februaryHasCorrectDayCount() {
|
||||
let grid = MonthGrid(containing: date("2027-02-10"), calendar: calendar)
|
||||
#expect(grid.days.filter(\.isInMonth).count == 28)
|
||||
}
|
||||
|
||||
@Test("Ein Schaltjahr wird korrekt behandelt")
|
||||
func leapYear() {
|
||||
let grid = MonthGrid(containing: date("2028-02-10"), calendar: calendar)
|
||||
#expect(grid.days.filter(\.isInMonth).count == 29)
|
||||
}
|
||||
|
||||
@Test("Bei Wochenstart Sonntag verschiebt sich die erste Zeile")
|
||||
func sundayStart() {
|
||||
var sundayFirst = calendar
|
||||
sundayFirst.firstWeekday = 1
|
||||
let grid = MonthGrid(containing: date("2026-08-10"), calendar: sundayFirst)
|
||||
|
||||
#expect(grid.days.first?.date == date("2026-07-26"))
|
||||
}
|
||||
|
||||
@Test("Tage außerhalb des Monats sind als solche markiert")
|
||||
func outOfMonthDaysAreMarked() {
|
||||
let grid = MonthGrid(containing: date("2026-08-10"), calendar: calendar)
|
||||
let inMonth = grid.days.filter(\.isInMonth)
|
||||
|
||||
#expect(inMonth.count == 31)
|
||||
#expect(grid.days.count > inMonth.count)
|
||||
}
|
||||
|
||||
// MARK: - Abdeckung: der Unterschied zwischen „frei" und „unbekannt"
|
||||
|
||||
@Test("Ohne Abdeckungsgrenze gilt jeder Tag als bekannt")
|
||||
func unlimitedCoverageKnowsEverything() {
|
||||
let grid = MonthGrid(containing: date("2026-08-10"), calendar: calendar)
|
||||
let status = grid.status(for: date("2026-08-10"), coverage: nil, busyDays: [])
|
||||
|
||||
#expect(status == .free)
|
||||
}
|
||||
|
||||
@Test("Ein Tag innerhalb der Abdeckung ohne Termine ist frei")
|
||||
func dayInCoverageWithoutEventsIsFree() {
|
||||
let grid = MonthGrid(containing: date("2026-08-10"), calendar: calendar)
|
||||
let coverage = DateInterval(start: date("2026-08-01"), end: date("2026-09-01"))
|
||||
|
||||
#expect(grid.status(for: date("2026-08-10"), coverage: coverage, busyDays: []) == .free)
|
||||
}
|
||||
|
||||
@Test("Ein Tag AUSSERHALB der Abdeckung ist unbekannt, nicht frei")
|
||||
func dayOutsideCoverageIsUnknown() {
|
||||
// Der Kernpunkt. Ein leerer März sieht überzeugend echt aus und ist
|
||||
// falsch, wenn die Quelle dort gar nichts weiß.
|
||||
let grid = MonthGrid(containing: date("2026-08-10"), calendar: calendar)
|
||||
let coverage = DateInterval(start: date("2026-08-01"), end: date("2026-08-15"))
|
||||
|
||||
#expect(grid.status(for: date("2026-08-20"), coverage: coverage, busyDays: []) == .unknown)
|
||||
}
|
||||
|
||||
@Test("Der letzte Tag der Abdeckung gehört nicht mehr dazu")
|
||||
func coverageEndIsExclusive() {
|
||||
// [start, end) — so ist es in der Calendarr-Anleitung festgelegt.
|
||||
let grid = MonthGrid(containing: date("2026-08-10"), calendar: calendar)
|
||||
let coverage = DateInterval(start: date("2026-08-01"), end: date("2026-08-15"))
|
||||
|
||||
#expect(grid.status(for: date("2026-08-14"), coverage: coverage, busyDays: []) == .free)
|
||||
#expect(grid.status(for: date("2026-08-15"), coverage: coverage, busyDays: []) == .unknown)
|
||||
}
|
||||
|
||||
@Test("Ein Tag vor Beginn der Abdeckung ist ebenfalls unbekannt")
|
||||
func dayBeforeCoverageIsUnknown() {
|
||||
let grid = MonthGrid(containing: date("2026-08-10"), calendar: calendar)
|
||||
let coverage = DateInterval(start: date("2026-08-05"), end: date("2026-08-30"))
|
||||
|
||||
#expect(grid.status(for: date("2026-08-01"), coverage: coverage, busyDays: []) == .unknown)
|
||||
}
|
||||
|
||||
@Test("Ein Tag mit Terminen ist belegt")
|
||||
func dayWithEventsIsBusy() {
|
||||
let grid = MonthGrid(containing: date("2026-08-10"), calendar: calendar)
|
||||
let busy: Set<Date> = [calendar.startOfDay(for: date("2026-08-10"))]
|
||||
|
||||
#expect(grid.status(for: date("2026-08-10"), coverage: nil, busyDays: busy) == .busy)
|
||||
}
|
||||
|
||||
@Test("Termine außerhalb der Abdeckung ändern nichts — sie dürften gar nicht existieren")
|
||||
func busyOutsideCoverageStaysUnknown() {
|
||||
// Widersprüchliche Eingabe: die Quelle liefert einen Termin für einen
|
||||
// Tag, den sie nach eigener Angabe nicht abdeckt. Die Abdeckung gewinnt,
|
||||
// sonst behauptet die Anzeige mehr zu wissen, als belegt ist.
|
||||
let grid = MonthGrid(containing: date("2026-08-10"), calendar: calendar)
|
||||
let coverage = DateInterval(start: date("2026-08-01"), end: date("2026-08-15"))
|
||||
let busy: Set<Date> = [calendar.startOfDay(for: date("2026-08-20"))]
|
||||
|
||||
#expect(grid.status(for: date("2026-08-20"), coverage: coverage, busyDays: busy) == .unknown)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user