215 lines
7.3 KiB
Swift
215 lines
7.3 KiB
Swift
import Foundation
|
|
import SwiftData
|
|
import Testing
|
|
@testable import Rituals
|
|
|
|
struct RitualStoreTests {
|
|
@MainActor
|
|
@Test func quickRitualStartsIncomplete() throws {
|
|
let store = makeStore()
|
|
store.createQuickRitual()
|
|
|
|
#expect(store.activeRitual != nil)
|
|
#expect(abs(store.activeRitualProgress) < 0.0001)
|
|
}
|
|
|
|
@MainActor
|
|
@Test func toggleHabitCompletionMarksComplete() throws {
|
|
let store = makeStore()
|
|
store.createQuickRitual()
|
|
|
|
guard let habit = store.activeRitual?.habits.first else {
|
|
throw TestError.missingHabit
|
|
}
|
|
|
|
store.toggleHabitCompletion(habit)
|
|
#expect(store.isHabitCompletedToday(habit) == true)
|
|
}
|
|
|
|
@MainActor
|
|
@Test func toggleHabitCompletionForSpecificDate() throws {
|
|
let store = makeStore()
|
|
store.createQuickRitual()
|
|
|
|
guard let habit = store.activeRitual?.habits.first else {
|
|
throw TestError.missingHabit
|
|
}
|
|
|
|
let yesterday = Calendar.current.date(byAdding: .day, value: -1, to: Date())!
|
|
store.toggleHabitCompletion(habit, date: yesterday)
|
|
|
|
let completions = store.habitCompletions(for: yesterday)
|
|
let completion = completions.first { $0.habit.id == habit.id }
|
|
#expect(completion?.isCompleted == true)
|
|
#expect(store.isHabitCompletedToday(habit) == false)
|
|
}
|
|
|
|
@MainActor
|
|
@Test func arcRenewalCreatesNewArc() throws {
|
|
let store = makeStore()
|
|
store.createQuickRitual()
|
|
|
|
guard let ritual = store.activeRitual else {
|
|
throw TestError.missingHabit
|
|
}
|
|
|
|
#expect((ritual.arcs?.count ?? 0) == 1)
|
|
#expect(ritual.currentArc?.arcNumber == 1)
|
|
|
|
// End the current arc
|
|
store.endArc(for: ritual)
|
|
|
|
#expect(ritual.currentArc == nil)
|
|
|
|
// Renew the arc
|
|
store.renewArc(for: ritual, durationDays: 30, copyHabits: true)
|
|
|
|
#expect((ritual.arcs?.count ?? 0) == 2)
|
|
#expect(ritual.currentArc?.arcNumber == 2)
|
|
#expect((ritual.currentArc?.habits?.count ?? 0) == 3)
|
|
}
|
|
|
|
@MainActor
|
|
@Test func currentStreakCountsOnlyPerfectDays() throws {
|
|
let store = makeStore()
|
|
store.createQuickRitual()
|
|
|
|
guard let ritual = store.activeRitual else {
|
|
throw TestError.missingRitual
|
|
}
|
|
|
|
let habits = ritual.habits
|
|
guard habits.count >= 2 else {
|
|
throw TestError.missingHabit
|
|
}
|
|
|
|
let calendar = Calendar.current
|
|
let today = Date()
|
|
let yesterday = calendar.date(byAdding: .day, value: -1, to: today) ?? today
|
|
|
|
// Yesterday is perfect: complete all habits.
|
|
for habit in habits {
|
|
store.toggleHabitCompletion(habit, date: yesterday)
|
|
}
|
|
|
|
// Today is partial: complete only one habit.
|
|
store.toggleHabitCompletion(habits[0], date: today)
|
|
|
|
#expect(store.currentStreak() == 1)
|
|
}
|
|
|
|
@MainActor
|
|
@Test func allRitualCompletionRateIncludesEndedArcsForHistoricalDates() throws {
|
|
let store = makeStore()
|
|
store.createQuickRitual()
|
|
|
|
guard let ritual = store.activeRitual,
|
|
let habit = ritual.habits.first,
|
|
let arcStart = ritual.currentArc?.startDate else {
|
|
throw TestError.missingHabit
|
|
}
|
|
|
|
store.toggleHabitCompletion(habit, date: arcStart)
|
|
store.endArc(for: ritual)
|
|
|
|
let rate = store.completionRate(for: arcStart, ritual: nil)
|
|
#expect(rate > 0.0)
|
|
}
|
|
|
|
@MainActor
|
|
@Test func completedActiveArcStillTriggersRenewalPrompt() throws {
|
|
let store = makeStore()
|
|
store.createQuickRitual()
|
|
|
|
guard let ritual = store.activeRitual,
|
|
let arc = ritual.currentArc else {
|
|
throw TestError.missingRitual
|
|
}
|
|
|
|
let yesterday = Calendar.current.date(byAdding: .day, value: -1, to: Date()) ?? Date()
|
|
arc.endDate = yesterday
|
|
|
|
#expect(ritual.hasActiveArc == false)
|
|
|
|
store.checkForCompletedArcs()
|
|
#expect(store.ritualNeedingRenewal?.id == ritual.id)
|
|
}
|
|
|
|
@MainActor
|
|
@Test func currentArcUsesMostRecentActiveArc() throws {
|
|
let calendar = Calendar.current
|
|
let today = calendar.startOfDay(for: Date())
|
|
let oldStart = calendar.date(byAdding: .day, value: -20, to: today) ?? today
|
|
let oldEnd = calendar.date(byAdding: .day, value: -5, to: today) ?? today
|
|
let currentStart = calendar.date(byAdding: .day, value: -2, to: today) ?? today
|
|
let currentEnd = calendar.date(byAdding: .day, value: 10, to: today) ?? today
|
|
|
|
let oldArc = RitualArc(startDate: oldStart, endDate: oldEnd, arcNumber: 1, isActive: true)
|
|
let currentArc = RitualArc(startDate: currentStart, endDate: currentEnd, arcNumber: 2, isActive: true)
|
|
let ritual = Ritual(title: "Test", theme: "Theme", arcs: [oldArc, currentArc])
|
|
|
|
#expect(ritual.currentArc?.arcNumber == 2)
|
|
#expect(ritual.hasActiveArc == true)
|
|
}
|
|
|
|
@MainActor
|
|
@Test func completedArcCountIncludesExpiredActiveArcs() throws {
|
|
let calendar = Calendar.current
|
|
let today = calendar.startOfDay(for: Date())
|
|
let start = calendar.date(byAdding: .day, value: -10, to: today) ?? today
|
|
let end = calendar.date(byAdding: .day, value: -1, to: today) ?? today
|
|
|
|
let expiredButActiveArc = RitualArc(startDate: start, endDate: end, arcNumber: 1, isActive: true)
|
|
let ritual = Ritual(title: "Expired", theme: "Theme", arcs: [expiredButActiveArc])
|
|
|
|
#expect(ritual.completedArcCount == 1)
|
|
#expect(Calendar.current.isDate(ritual.lastCompletedDate ?? .distantPast, inSameDayAs: end))
|
|
}
|
|
|
|
@MainActor
|
|
@Test func nextUpcomingRitualContextIdentifiesTomorrowForFutureArc() throws {
|
|
let calendar = Calendar.current
|
|
let today = calendar.startOfDay(for: Date())
|
|
let tomorrow = calendar.date(byAdding: .day, value: 1, to: today) ?? today
|
|
let nextWeek = calendar.date(byAdding: .day, value: 7, to: tomorrow) ?? tomorrow
|
|
let middayToday = calendar.date(byAdding: .hour, value: 12, to: today) ?? today
|
|
|
|
let tomorrowArc = RitualArc(startDate: tomorrow, endDate: nextWeek, arcNumber: 1, isActive: true)
|
|
let eveningRitual = Ritual(
|
|
title: "Evening Wind Down",
|
|
theme: "Test",
|
|
timeOfDay: .evening,
|
|
arcs: [tomorrowArc]
|
|
)
|
|
|
|
let context = RitualAnalytics.nextUpcomingRitualContext(from: [eveningRitual], currentDate: middayToday)
|
|
#expect(context?.ritual.id == eveningRitual.id)
|
|
#expect(context?.isTomorrow == true)
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
private func makeStore() -> RitualStore {
|
|
let schema = Schema([Ritual.self, RitualArc.self, ArcHabit.self])
|
|
let configuration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
|
|
let container: ModelContainer
|
|
do {
|
|
container = try ModelContainer(for: schema, configurations: [configuration])
|
|
} catch {
|
|
fatalError("Test container failed: \(error)")
|
|
}
|
|
|
|
return RitualStore(modelContext: container.mainContext, seedService: EmptySeedService(), settingsStore: SettingsStore())
|
|
}
|
|
|
|
private struct EmptySeedService: RitualSeedProviding {
|
|
func makeSeedRituals(startDate: Date) -> [Ritual] {
|
|
[]
|
|
}
|
|
}
|
|
|
|
private enum TestError: Error {
|
|
case missingHabit
|
|
case missingRitual
|
|
}
|