Add 57 tests covering all data mutation paths

Refactor ShowBasedOnVoteLogics to accept injectable `now: Date` parameter
across all methods, enabling deterministic testing of voting date logic
without simulator clock manipulation.

Test coverage (55 new tests across 3 files):
- Pipeline 1: Streak calculation (7 tests) — consecutive, gaps, missing/placeholder exclusion
- Pipeline 2: Duplicate prevention (5 tests) — add replaces, removeDuplicates keeps best
- Pipeline 3: Fill missing dates (4 tests) — gap fill, idempotent, no overwrite
- Pipeline 4: Delete flows (5 tests) — clearDB, deleteLast, deleteAllEntries
- Pipeline 5: Update flows (5 tests) — mood, notes, photo set/clear
- Pipeline 6: Batch import (4 tests) — bulk insert, replace, dedup in batch
- Pipeline 7: Data listeners (3 tests) — fire on save, multiple listeners, refreshFromDisk
- Pipeline 8: Boundary edge cases (5 tests) — midnight, 23:59, day boundary leak
- Pipeline 9: Voting date logic (5 tests) — Today/Previous x before/after voting time
- Pipeline 10: Side effects orchestration (7 tests) — logMood, updateMood, deleteMood
- Pipeline 11: Full integration (5 tests) — streak grows/breaks/rebuilds, voting lifecycle

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-02-14 23:25:14 -06:00
parent f1cd81c395
commit 4125c93dfe
6 changed files with 978 additions and 24 deletions

View File

@@ -31,15 +31,15 @@ import SwiftData
*/
@MainActor
class ShowBasedOnVoteLogics {
static private func returnCurrentVoteStatus(onboardingData: OnboardingData) -> (passedTimeToVote: Bool, inputDay: DayOptions) {
let passedTimeToVote = ShowBasedOnVoteLogics.passedTodaysVotingUnlock(voteDate: onboardingData.date)
static private func returnCurrentVoteStatus(onboardingData: OnboardingData, now: Date = Date()) -> (passedTimeToVote: Bool, inputDay: DayOptions) {
let passedTimeToVote = ShowBasedOnVoteLogics.passedTodaysVotingUnlock(voteDate: onboardingData.date, now: now)
let inputDay: DayOptions = onboardingData.inputDay
return (passedTimeToVote, inputDay)
}
static public func passedTodaysVotingUnlock(voteDate: Date) -> Bool {
let currentDateComp = Calendar.current.dateComponents([.hour, .minute], from: Date())
static public func passedTodaysVotingUnlock(voteDate: Date, now: Date = Date()) -> Bool {
let currentDateComp = Calendar.current.dateComponents([.hour, .minute], from: now)
let savedDateComp = Calendar.current.dateComponents([.hour, .minute], from: voteDate)
if let currentHour = currentDateComp.hour,
@@ -59,8 +59,8 @@ class ShowBasedOnVoteLogics {
return false
}
static public func isMissingCurrentVote(onboardingData: OnboardingData) -> Bool {
let startDate = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: onboardingData).startOfDay
static public func isMissingCurrentVote(onboardingData: OnboardingData, now: Date = Date()) -> Bool {
let startDate = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: onboardingData, now: now).startOfDay
#if WIDGET_EXTENSION
let entry = WidgetDataProvider.shared.getEntry(byDate: startDate)
#else
@@ -68,45 +68,45 @@ class ShowBasedOnVoteLogics {
#endif
return entry == nil || entry?.mood == .missing
}
static public func getCurrentVotingDate(onboardingData: OnboardingData) -> Date {
static public func getCurrentVotingDate(onboardingData: OnboardingData, now: Date = Date()) -> Date {
var date: Date?
let currentVoteStatus = ShowBasedOnVoteLogics.returnCurrentVoteStatus(onboardingData: onboardingData)
let currentVoteStatus = ShowBasedOnVoteLogics.returnCurrentVoteStatus(onboardingData: onboardingData, now: now)
// note to future self, this should account for midnight until next voting unlock
// the vote at 12:03 am will not be passed time, and will be for yesterday
switch (currentVoteStatus.passedTimeToVote, currentVoteStatus.inputDay) {
case (false, .Today):
// if we're passed time to vote and the voting type is previous - last vote should be -1
date = Calendar.current.date(byAdding: .day, value: -1, to: Date())
date = Calendar.current.date(byAdding: .day, value: -1, to: now)
case (true, .Today):
// if we're passed time to vote and the voting type is previous - last vote should be today
date = Date()
date = now
case (false, .Previous):
// if we're passed time to vote and the voting type is previous - last vote should be -2
date = Calendar.current.date(byAdding: .day, value: -2, to: Date())
date = Calendar.current.date(byAdding: .day, value: -2, to: now)
case (true, .Previous):
// if we're passed time to vote and the voting type is previous - last vote should be -1
date = Calendar.current.date(byAdding: .day, value: -1, to: Date())
date = Calendar.current.date(byAdding: .day, value: -1, to: now)
}
guard let date = date else {
fatalError("missing getCurrentVotingDate")
}
return date
}
static public func getVotingTitle(onboardingData: OnboardingData) -> String {
let currentVoteStatus = ShowBasedOnVoteLogics.returnCurrentVoteStatus(onboardingData: onboardingData)
static public func getVotingTitle(onboardingData: OnboardingData, now: Date = Date()) -> String {
let currentVoteStatus = ShowBasedOnVoteLogics.returnCurrentVoteStatus(onboardingData: onboardingData, now: now)
switch (currentVoteStatus.passedTimeToVote, currentVoteStatus.inputDay) {
case (false, .Today):
return String(localized: "add_mood_header_view_title_yesterday")
case (true, .Today):
return String(localized: "add_mood_header_view_title_today")
case (false, .Previous):
let date = Calendar.current.date(byAdding: .day, value: -2, to: Date())!
let date = Calendar.current.date(byAdding: .day, value: -2, to: now)!
return String(format: String(localized: "add_mood_header_view_title"), Random.weekdayName(fromDate: date))
case (true, .Previous):
return String(localized: "add_mood_header_view_title_yesterday")