Tests: recurring-task occurrence expansion (date mapping)

8 tests covering daily / weekly / every-weekday / monthly / yearly expansion,
repeat-until cutoff, time-of-day preservation on the mapped local day, and
non-recurring single-day placement. Uses Calendar.current so it holds in any
timezone. Closes the recurrence-coverage gap from the calendar fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
kutesir
2026-06-24 20:59:43 +03:00
parent eb7d563bbd
commit af2996876c
2 changed files with 104 additions and 0 deletions

View File

@@ -22,6 +22,7 @@
3E9DE1CF20BAC479805DF940 /* MyTasksWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 208F82348DEBD9FF7B0DCF17 /* MyTasksWidget.swift */; }; 3E9DE1CF20BAC479805DF940 /* MyTasksWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 208F82348DEBD9FF7B0DCF17 /* MyTasksWidget.swift */; };
45AA93D76970B39DB8BA6A5B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C5EF0E7944C7F0763B83BB0F /* LaunchScreen.storyboard */; }; 45AA93D76970B39DB8BA6A5B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C5EF0E7944C7F0763B83BB0F /* LaunchScreen.storyboard */; };
497732557745AE9BDA44FB2F /* ShortcutHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326B77A7B317A7DB44E13EA5 /* ShortcutHandler.swift */; }; 497732557745AE9BDA44FB2F /* ShortcutHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326B77A7B317A7DB44E13EA5 /* ShortcutHandler.swift */; };
4FB79BBC1EE0F6C7EF844B02 /* RecurrenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72D757FA29923B6C150B5EE /* RecurrenceTests.swift */; };
552E2F85B01C9CC437D383B5 /* EventCountdownWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC17C7BA15BD2E84AE5F77CF /* EventCountdownWidget.swift */; }; 552E2F85B01C9CC437D383B5 /* EventCountdownWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC17C7BA15BD2E84AE5F77CF /* EventCountdownWidget.swift */; };
55F00C433F853F7B54F136B3 /* WidgetViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 429806CE1021C8DE2EB770CE /* WidgetViews.swift */; }; 55F00C433F853F7B54F136B3 /* WidgetViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 429806CE1021C8DE2EB770CE /* WidgetViews.swift */; };
5EF4A5B6CE91ADA0CCF72D0D /* ProfileSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C96EC00F6B021DBA3CC1A79 /* ProfileSetupView.swift */; }; 5EF4A5B6CE91ADA0CCF72D0D /* ProfileSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C96EC00F6B021DBA3CC1A79 /* ProfileSetupView.swift */; };
@@ -132,6 +133,7 @@
C5EF0E7944C7F0763B83BB0F /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; }; C5EF0E7944C7F0763B83BB0F /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
C786EBC7DF879D64EB28165E /* TodayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodayView.swift; sourceTree = "<group>"; }; C786EBC7DF879D64EB28165E /* TodayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodayView.swift; sourceTree = "<group>"; };
D44530A77DF12A17E52AAF34 /* MatrixView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixView.swift; sourceTree = "<group>"; }; D44530A77DF12A17E52AAF34 /* MatrixView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixView.swift; sourceTree = "<group>"; };
D72D757FA29923B6C150B5EE /* RecurrenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecurrenceTests.swift; sourceTree = "<group>"; };
DCC2AFB4FA765383740767CB /* TaskItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskItem.swift; sourceTree = "<group>"; }; DCC2AFB4FA765383740767CB /* TaskItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskItem.swift; sourceTree = "<group>"; };
ED5DB5340BEB3EF7A78CA153 /* TodayMenuFeatures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodayMenuFeatures.swift; sourceTree = "<group>"; }; ED5DB5340BEB3EF7A78CA153 /* TodayMenuFeatures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodayMenuFeatures.swift; sourceTree = "<group>"; };
FA3D5289C5F93484E22DEB63 /* TutorialView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TutorialView.swift; sourceTree = "<group>"; }; FA3D5289C5F93484E22DEB63 /* TutorialView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TutorialView.swift; sourceTree = "<group>"; };
@@ -146,6 +148,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
74FAA2B41FCEBC7E3F156F0F /* CalendarGridTests.swift */, 74FAA2B41FCEBC7E3F156F0F /* CalendarGridTests.swift */,
D72D757FA29923B6C150B5EE /* RecurrenceTests.swift */,
); );
path = KisaniCalTests; path = KisaniCalTests;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -431,6 +434,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
6BCBF3902E38FBAA7348840D /* CalendarGridTests.swift in Sources */, 6BCBF3902E38FBAA7348840D /* CalendarGridTests.swift in Sources */,
4FB79BBC1EE0F6C7EF844B02 /* RecurrenceTests.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };

View File

@@ -0,0 +1,100 @@
import XCTest
@testable import KisaniCal
/// Verifies recurring-task occurrence expansion maps to the correct real calendar
/// days (the calendar's event-date mapping for tasks). Uses Calendar.current the
/// same calendar the production code uses so assertions hold in any timezone.
@MainActor
final class RecurrenceTests: XCTestCase {
private let cal = Calendar.current
private func day(_ y: Int, _ m: Int, _ d: Int, h: Int = 0, min: Int = 0) -> Date {
cal.date(from: DateComponents(year: y, month: m, day: d, hour: h, minute: min))!
}
private func vm(with tasks: [TaskItem]) -> TaskViewModel {
let v = TaskViewModel()
v.tasks = tasks
return v
}
private func task(_ label: String, start: Date, hasTime: Bool = false, end: Date? = nil) -> TaskItem {
var t = TaskItem(title: "T-\(label)", dueDate: start)
t.isRecurring = true
t.recurrenceLabel = label
t.hasTime = hasTime
t.recurrenceEnd = end
return t
}
private func hasOccurrence(_ v: TaskViewModel, _ id: UUID, on date: Date) -> Bool {
v.occurrences(on: date).contains { $0.id == id }
}
func testDailyExpandsEveryDayFromStart() {
let t = task("Daily", start: day(2026, 6, 1))
let v = vm(with: [t])
XCTAssertFalse(hasOccurrence(v, t.id, on: day(2026, 5, 31)), "no occurrence before start")
XCTAssertTrue(hasOccurrence(v, t.id, on: day(2026, 6, 1)))
XCTAssertTrue(hasOccurrence(v, t.id, on: day(2026, 6, 24)))
}
func testWeeklyHitsEverySevenDaysOnly() {
let t = task("Weekly", start: day(2026, 6, 1)) // Monday
let v = vm(with: [t])
XCTAssertTrue(hasOccurrence(v, t.id, on: day(2026, 6, 8)))
XCTAssertTrue(hasOccurrence(v, t.id, on: day(2026, 6, 22)))
XCTAssertFalse(hasOccurrence(v, t.id, on: day(2026, 6, 9)), "off-cadence day must not match")
XCTAssertFalse(hasOccurrence(v, t.id, on: day(2026, 6, 24)))
}
func testEveryWeekdaySkipsWeekend() {
let t = task("Every Weekday", start: day(2026, 6, 1))
let v = vm(with: [t])
XCTAssertTrue(hasOccurrence(v, t.id, on: day(2026, 6, 24)), "Wed is a weekday")
XCTAssertFalse(hasOccurrence(v, t.id, on: day(2026, 6, 27)), "Sat excluded")
XCTAssertFalse(hasOccurrence(v, t.id, on: day(2026, 6, 28)), "Sun excluded")
}
func testMonthlyMatchesDayOfMonth() {
let t = task("Monthly", start: day(2026, 1, 15))
let v = vm(with: [t])
XCTAssertTrue(hasOccurrence(v, t.id, on: day(2026, 6, 15)))
XCTAssertFalse(hasOccurrence(v, t.id, on: day(2026, 6, 16)))
}
func testYearlyMatchesMonthAndDay() {
let t = task("Yearly", start: day(2025, 6, 24))
let v = vm(with: [t])
XCTAssertTrue(hasOccurrence(v, t.id, on: day(2026, 6, 24)))
XCTAssertFalse(hasOccurrence(v, t.id, on: day(2026, 6, 23)))
}
func testRecurrenceEndStopsExpansion() {
let t = task("Daily", start: day(2026, 6, 1), end: day(2026, 6, 10))
let v = vm(with: [t])
XCTAssertTrue(hasOccurrence(v, t.id, on: day(2026, 6, 10)), "inclusive of end day")
XCTAssertFalse(hasOccurrence(v, t.id, on: day(2026, 6, 11)), "past repeat-until")
XCTAssertFalse(hasOccurrence(v, t.id, on: day(2026, 6, 24)))
}
func testOccurrenceCopyPreservesTimeOnTargetLocalDay() {
let t = task("Daily", start: day(2026, 6, 1, h: 9, min: 30), hasTime: true)
let v = vm(with: [t])
let occ = v.occurrences(on: day(2026, 6, 24)).first { $0.id == t.id }
XCTAssertNotNil(occ)
XCTAssertTrue(cal.isDate(occ!.dueDate!, inSameDayAs: day(2026, 6, 24)), "mapped to June 24")
XCTAssertEqual(cal.component(.hour, from: occ!.dueDate!), 9)
XCTAssertEqual(cal.component(.minute, from: occ!.dueDate!), 30)
}
func testNonRecurringTaskAppearsOnlyOnItsDay() {
var t = TaskItem(title: "Dinner with Jay", dueDate: day(2026, 6, 24))
t.isRecurring = false
let v = vm(with: [t])
XCTAssertTrue(hasOccurrence(v, t.id, on: day(2026, 6, 24)))
XCTAssertFalse(hasOccurrence(v, t.id, on: day(2026, 6, 23)))
XCTAssertFalse(hasOccurrence(v, t.id, on: day(2026, 6, 25)))
}
}