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:
@@ -22,6 +22,7 @@
|
||||
3E9DE1CF20BAC479805DF940 /* MyTasksWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 208F82348DEBD9FF7B0DCF17 /* MyTasksWidget.swift */; };
|
||||
45AA93D76970B39DB8BA6A5B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C5EF0E7944C7F0763B83BB0F /* LaunchScreen.storyboard */; };
|
||||
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 */; };
|
||||
55F00C433F853F7B54F136B3 /* WidgetViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 429806CE1021C8DE2EB770CE /* WidgetViews.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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -146,6 +148,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
74FAA2B41FCEBC7E3F156F0F /* CalendarGridTests.swift */,
|
||||
D72D757FA29923B6C150B5EE /* RecurrenceTests.swift */,
|
||||
);
|
||||
path = KisaniCalTests;
|
||||
sourceTree = "<group>";
|
||||
@@ -431,6 +434,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
6BCBF3902E38FBAA7348840D /* CalendarGridTests.swift in Sources */,
|
||||
4FB79BBC1EE0F6C7EF844B02 /* RecurrenceTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
100
KisaniCalTests/RecurrenceTests.swift
Normal file
100
KisaniCalTests/RecurrenceTests.swift
Normal 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)))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user