Rebrand entire project from Feels to Reflect

Complete rename across all bundle IDs, App Groups, CloudKit containers,
StoreKit product IDs, data store filenames, URL schemes, logger subsystems,
Swift identifiers, user-facing strings (7 languages), file names, directory
names, Xcode project, schemes, assets, and documentation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-02-26 11:47:16 -06:00
parent b1a54d2844
commit 0442eab1f8
380 changed files with 858 additions and 1077 deletions

View File

@@ -0,0 +1,366 @@
//
// DataControllerCRUDTests.swift
// ReflectTests
//
// Tests for DataController create, read, update, and delete operations.
//
import XCTest
import SwiftData
@testable import Reflect
@MainActor
final class DataControllerCreateTests: XCTestCase {
var sut: DataController!
override func setUp() {
super.setUp()
let schema = Schema([MoodEntryModel.self])
let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
sut = DataController(container: try! ModelContainer(for: schema, configurations: [config]))
}
override func tearDown() {
sut = nil
super.tearDown()
}
// MARK: - Phase 2: Create Tests
func test_add_insertsEntry() {
let date = makeDate(2024, 6, 15)
sut.add(mood: .great, forDate: date, entryType: .listView)
let entry = sut.getEntry(byDate: date)
XCTAssertNotNil(entry, "Entry should exist after add")
XCTAssertEqual(entry?.mood, .great)
XCTAssertEqual(entry?.entryType, EntryType.listView.rawValue)
}
func test_add_replacesExistingForSameDate() {
let date = makeDate(2024, 6, 15)
sut.add(mood: .great, forDate: date, entryType: .listView)
sut.add(mood: .bad, forDate: date, entryType: .widget)
let allEntries = sut.getAllEntries(byDate: date)
XCTAssertEqual(allEntries.count, 1, "Should have exactly 1 entry after 2 adds for same date")
XCTAssertEqual(allEntries.first?.mood, .bad, "Should keep the latest mood")
}
func test_add_setsCorrectWeekday() {
// June 15, 2024 is a Saturday (weekday = 7 in Gregorian)
let date = makeDate(2024, 6, 15)
sut.add(mood: .average, forDate: date, entryType: .listView)
let entry = sut.getEntry(byDate: date)
let expectedWeekday = Calendar.current.component(.weekday, from: date)
XCTAssertEqual(entry?.weekDay, expectedWeekday)
}
func test_add_allMoodValues() {
let moods: [Mood] = [.horrible, .bad, .average, .good, .great]
for (i, mood) in moods.enumerated() {
let date = makeDate(2024, 6, 10 + i)
sut.add(mood: mood, forDate: date, entryType: .listView)
let entry = sut.getEntry(byDate: date)
XCTAssertEqual(entry?.mood, mood, "Mood \(mood) should persist and read back correctly")
}
}
func test_addBatch_insertsMultiple() {
let entries: [(mood: Mood, date: Date, entryType: EntryType)] = [
(.great, makeDate(2024, 6, 10), .listView),
(.good, makeDate(2024, 6, 11), .widget),
(.average, makeDate(2024, 6, 12), .watch),
]
sut.addBatch(entries: entries)
for (mood, date, _) in entries {
let entry = sut.getEntry(byDate: date)
XCTAssertNotNil(entry, "Entry for \(date) should exist")
XCTAssertEqual(entry?.mood, mood)
}
}
func test_addBatch_replacesExisting() {
let date = makeDate(2024, 6, 10)
sut.add(mood: .horrible, forDate: date, entryType: .listView)
let batchEntries: [(mood: Mood, date: Date, entryType: EntryType)] = [
(.great, date, .widget),
]
sut.addBatch(entries: batchEntries)
let allEntries = sut.getAllEntries(byDate: date)
XCTAssertEqual(allEntries.count, 1)
XCTAssertEqual(allEntries.first?.mood, .great)
}
}
// MARK: - Phase 3: Read Tests
@MainActor
final class DataControllerReadTests: XCTestCase {
var sut: DataController!
override func setUp() {
super.setUp()
let schema = Schema([MoodEntryModel.self])
let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
sut = DataController(container: try! ModelContainer(for: schema, configurations: [config]))
}
override func tearDown() {
sut = nil
super.tearDown()
}
func test_getEntry_emptyDB_returnsNil() {
let entry = sut.getEntry(byDate: Date())
XCTAssertNil(entry)
}
func test_getEntry_boundaryExclusion() {
// Insert entry at exactly midnight of Jan 2 (start of Jan 2)
// Use hour=0 to place it precisely at the boundary
let jan2Midnight = makeDate(2024, 1, 2, hour: 0)
let entry = MoodEntryModel(forDate: jan2Midnight, mood: .great, entryType: .listView)
sut.modelContext.insert(entry)
sut.save()
// Query for Jan 1 should NOT find the Jan 2 entry
// getEntry builds range [Jan 1 00:00, Jan 2 00:00] with <= the midnight entry leaks in
let jan1 = makeDate(2024, 1, 1)
let result = sut.getEntry(byDate: jan1)
XCTAssertNil(result, "Entry at midnight Jan 2 should NOT be returned when querying Jan 1")
}
func test_getData_filtersWeekdays() {
// Add entries for a full week (Mon-Sun)
// Jan 1, 2024 is a Monday (weekday=2)
for i in 0..<7 {
let date = Calendar.current.date(byAdding: .day, value: i, to: makeDate(2024, 1, 1))!
sut.add(mood: .average, forDate: date, entryType: .listView)
}
// Filter to only weekdays 2 (Mon) and 6 (Fri)
let startDate = makeDate(2024, 1, 1)
let endDate = makeDate(2024, 1, 7)
let results = sut.getData(startDate: startDate, endDate: endDate, includedDays: [2, 6])
XCTAssertEqual(results.count, 2, "Should return only Monday and Friday")
}
func test_getData_emptyIncludedDays_returnsAll() {
for i in 0..<7 {
let date = Calendar.current.date(byAdding: .day, value: i, to: makeDate(2024, 1, 1))!
sut.add(mood: .average, forDate: date, entryType: .listView)
}
let startDate = makeDate(2024, 1, 1)
let endDate = makeDate(2024, 1, 7)
let results = sut.getData(startDate: startDate, endDate: endDate, includedDays: [])
XCTAssertEqual(results.count, 7, "Empty includedDays should return all 7 entries")
}
func test_splitIntoYearMonth_groupsCorrectly() {
// Add entries in different months
sut.add(mood: .great, forDate: makeDate(2024, 1, 15), entryType: .listView)
sut.add(mood: .good, forDate: makeDate(2024, 1, 20), entryType: .listView)
sut.add(mood: .bad, forDate: makeDate(2024, 3, 5), entryType: .listView)
let grouped = sut.splitIntoYearMonth(includedDays: [])
XCTAssertEqual(grouped[2024]?[1]?.count, 2, "Jan 2024 should have 2 entries")
XCTAssertEqual(grouped[2024]?[3]?.count, 1, "Mar 2024 should have 1 entry")
XCTAssertNil(grouped[2024]?[2], "Feb 2024 should have no entries")
}
func test_earliestEntry() {
sut.add(mood: .great, forDate: makeDate(2024, 6, 15), entryType: .listView)
sut.add(mood: .bad, forDate: makeDate(2024, 1, 1), entryType: .listView)
sut.add(mood: .good, forDate: makeDate(2024, 3, 10), entryType: .listView)
let earliest = sut.earliestEntry
XCTAssertNotNil(earliest)
XCTAssertTrue(Calendar.current.isDate(earliest!.forDate, inSameDayAs: makeDate(2024, 1, 1)))
}
func test_latestEntry() {
sut.add(mood: .great, forDate: makeDate(2024, 6, 15), entryType: .listView)
sut.add(mood: .bad, forDate: makeDate(2024, 1, 1), entryType: .listView)
sut.add(mood: .good, forDate: makeDate(2024, 3, 10), entryType: .listView)
let latest = sut.latestEntry
XCTAssertNotNil(latest)
XCTAssertTrue(Calendar.current.isDate(latest!.forDate, inSameDayAs: makeDate(2024, 6, 15)))
}
}
// MARK: - Phase 4: Update Tests
@MainActor
final class DataControllerUpdateTests: XCTestCase {
var sut: DataController!
override func setUp() {
super.setUp()
let schema = Schema([MoodEntryModel.self])
let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
sut = DataController(container: try! ModelContainer(for: schema, configurations: [config]))
}
override func tearDown() {
sut = nil
super.tearDown()
}
func test_update_changesMood() {
let date = makeDate(2024, 6, 15)
sut.add(mood: .bad, forDate: date, entryType: .listView)
let result = sut.update(entryDate: date, withMood: .great)
XCTAssertTrue(result)
let entry = sut.getEntry(byDate: date)
XCTAssertEqual(entry?.mood, .great)
}
func test_update_nonexistent_returnsFalse() {
let result = sut.update(entryDate: makeDate(2024, 6, 15), withMood: .great)
XCTAssertFalse(result)
}
func test_updateNotes_setsNotes() {
let date = makeDate(2024, 6, 15)
sut.add(mood: .good, forDate: date, entryType: .listView)
let result = sut.updateNotes(forDate: date, notes: "Had a great day")
XCTAssertTrue(result)
let entry = sut.getEntry(byDate: date)
XCTAssertEqual(entry?.notes, "Had a great day")
}
func test_updateNotes_nilClearsNotes() {
let date = makeDate(2024, 6, 15)
sut.add(mood: .good, forDate: date, entryType: .listView)
sut.updateNotes(forDate: date, notes: "Some notes")
let result = sut.updateNotes(forDate: date, notes: nil)
XCTAssertTrue(result)
let entry = sut.getEntry(byDate: date)
XCTAssertNil(entry?.notes)
}
func test_updatePhoto_setsPhotoID() {
let date = makeDate(2024, 6, 15)
sut.add(mood: .good, forDate: date, entryType: .listView)
let photoID = UUID()
let result = sut.updatePhoto(forDate: date, photoID: photoID)
XCTAssertTrue(result)
let entry = sut.getEntry(byDate: date)
XCTAssertEqual(entry?.photoID, photoID)
}
func test_update_refreshesTimestamp() {
let date = makeDate(2024, 6, 15)
sut.add(mood: .bad, forDate: date, entryType: .listView)
let originalTimestamp = sut.getEntry(byDate: date)!.timestamp
// Small delay so timestamp would differ
let result = sut.update(entryDate: date, withMood: .great)
XCTAssertTrue(result)
let updatedEntry = sut.getEntry(byDate: date)!
XCTAssertGreaterThanOrEqual(updatedEntry.timestamp, originalTimestamp,
"Timestamp should be updated on mood change so removeDuplicates picks the right entry")
}
}
// MARK: - Phase 5: Delete Tests
@MainActor
final class DataControllerDeleteTests: XCTestCase {
var sut: DataController!
override func setUp() {
super.setUp()
let schema = Schema([MoodEntryModel.self])
let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
sut = DataController(container: try! ModelContainer(for: schema, configurations: [config]))
}
override func tearDown() {
sut = nil
super.tearDown()
}
func test_clearDB_removesAll() {
sut.add(mood: .great, forDate: makeDate(2024, 6, 10), entryType: .listView)
sut.add(mood: .bad, forDate: makeDate(2024, 6, 11), entryType: .listView)
sut.add(mood: .good, forDate: makeDate(2024, 6, 12), entryType: .listView)
sut.clearDB()
let all = sut.getData(startDate: Date(timeIntervalSince1970: 0), endDate: Date(), includedDays: [])
XCTAssertTrue(all.isEmpty, "clearDB should remove all entries")
}
func test_deleteAllEntries_removesOnlyTargetDate() {
let date1 = makeDate(2024, 6, 10)
let date2 = makeDate(2024, 6, 11)
sut.add(mood: .great, forDate: date1, entryType: .listView)
sut.add(mood: .bad, forDate: date2, entryType: .listView)
sut.deleteAllEntries(forDate: date1)
XCTAssertNil(sut.getEntry(byDate: date1), "Deleted date should be gone")
XCTAssertNotNil(sut.getEntry(byDate: date2), "Other dates should be untouched")
}
func test_removeDuplicates_keepsBestEntry() {
let date = makeDate(2024, 6, 15)
// Manually insert 3 entries for the same date to simulate CloudKit conflict
let entry1 = MoodEntryModel(forDate: date, mood: .missing, entryType: .filledInMissing)
entry1.timestamp = makeDate(2024, 6, 15, hour: 8)
sut.modelContext.insert(entry1)
let entry2 = MoodEntryModel(forDate: date, mood: .great, entryType: .listView)
entry2.timestamp = makeDate(2024, 6, 15, hour: 10)
sut.modelContext.insert(entry2)
let entry3 = MoodEntryModel(forDate: date, mood: .good, entryType: .listView)
entry3.timestamp = makeDate(2024, 6, 15, hour: 9)
sut.modelContext.insert(entry3)
sut.save()
let removed = sut.removeDuplicates()
XCTAssertEqual(removed, 2, "Should remove 2 duplicates")
let remaining = sut.getAllEntries(byDate: date)
XCTAssertEqual(remaining.count, 1, "Should keep exactly 1 entry")
// Should keep non-missing with most recent timestamp (entry2, .great at 10am)
XCTAssertEqual(remaining.first?.mood, .great, "Should keep the best entry (non-missing, most recent)")
}
}
// MARK: - Test Helpers
func makeDate(_ year: Int, _ month: Int, _ day: Int, hour: Int = 12) -> Date {
var components = DateComponents()
components.year = year
components.month = month
components.day = day
components.hour = hour
components.minute = 0
components.second = 0
return Calendar.current.date(from: components)!
}

View File

@@ -0,0 +1,42 @@
//
// DayViewViewModelTests.swift
// ReflectTests
//
// Unit tests for DayViewViewModel.countEntries verifies safe counting
// across various grouped dictionary shapes (TDD for force-unwrap fix).
//
import XCTest
import SwiftData
@testable import Reflect
@MainActor
final class DayViewViewModelTests: XCTestCase {
func testCountEntries_EmptyGrouped() {
let grouped: [Int: [Int: [MoodEntryModel]]] = [:]
XCTAssertEqual(DayViewViewModel.countEntries(in: grouped), 0)
}
func testCountEntries_SingleYearSingleMonth() {
let entry = MoodEntryModel(forDate: Date(), mood: .great, entryType: .listView)
let grouped: [Int: [Int: [MoodEntryModel]]] = [2026: [2: [entry]]]
XCTAssertEqual(DayViewViewModel.countEntries(in: grouped), 1)
}
func testCountEntries_MultipleYearsAndMonths() {
let e1 = MoodEntryModel(forDate: Date(), mood: .great, entryType: .listView)
let e2 = MoodEntryModel(forDate: Date(), mood: .good, entryType: .listView)
let e3 = MoodEntryModel(forDate: Date(), mood: .bad, entryType: .listView)
let grouped: [Int: [Int: [MoodEntryModel]]] = [
2025: [12: [e1, e2]],
2026: [1: [e3], 2: [e1, e2, e3]]
]
XCTAssertEqual(DayViewViewModel.countEntries(in: grouped), 6)
}
func testCountEntries_YearWithEmptyMonth() {
let grouped: [Int: [Int: [MoodEntryModel]]] = [2026: [1: []]]
XCTAssertEqual(DayViewViewModel.countEntries(in: grouped), 0)
}
}

View File

@@ -0,0 +1,141 @@
//
// IntegrationTests.swift
// ReflectTests
//
// Integration tests verifying full lifecycle pipelines.
//
import XCTest
import SwiftData
@testable import Reflect
@MainActor
final class IntegrationTests: XCTestCase {
var sut: DataController!
override func setUp() {
super.setUp()
let schema = Schema([MoodEntryModel.self])
let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
sut = DataController(container: try! ModelContainer(for: schema, configurations: [config]))
}
override func tearDown() {
sut = nil
super.tearDown()
}
// MARK: - Phase 8: Integration Pipelines
func test_fullCRUD_lifecycle() {
let date = makeDate(2024, 6, 15)
// Create
sut.add(mood: .good, forDate: date, entryType: .listView)
XCTAssertNotNil(sut.getEntry(byDate: date))
// Read
let entry = sut.getEntry(byDate: date)
XCTAssertEqual(entry?.mood, .good)
// Update
let updated = sut.update(entryDate: date, withMood: .great)
XCTAssertTrue(updated)
XCTAssertEqual(sut.getEntry(byDate: date)?.mood, .great)
// Update notes
sut.updateNotes(forDate: date, notes: "Lifecycle test")
XCTAssertEqual(sut.getEntry(byDate: date)?.notes, "Lifecycle test")
// Delete
sut.deleteAllEntries(forDate: date)
XCTAssertNil(sut.getEntry(byDate: date))
}
func test_streak_throughLifecycle() {
let today = Calendar.current.startOfDay(for: Date())
let yesterday = Calendar.current.date(byAdding: .day, value: -1, to: today)!
// Add today streak=1
sut.add(mood: .good, forDate: today, entryType: .listView)
XCTAssertEqual(sut.calculateStreak(from: today).streak, 1)
// Add yesterday streak=2
sut.add(mood: .great, forDate: yesterday, entryType: .listView)
XCTAssertEqual(sut.calculateStreak(from: today).streak, 2)
// Update today's mood streak still 2
sut.update(entryDate: today, withMood: .average)
XCTAssertEqual(sut.calculateStreak(from: today).streak, 2)
// Delete today streak=1 (counting from yesterday)
sut.deleteAllEntries(forDate: today)
XCTAssertEqual(sut.calculateStreak(from: today).streak, 1)
}
func test_voteStatus_throughLifecycle() {
let onboarding = OnboardingData()
onboarding.inputDay = .Today
// Set unlock time to 00:01 so we're always "after" unlock
var components = Calendar.current.dateComponents([.year, .month, .day], from: Date())
components.hour = 0
components.minute = 1
onboarding.date = Calendar.current.date(from: components)!
let votingDate = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: onboarding)
// Helper: check vote status using testable DataController
func isVoteNeeded() -> Bool {
let entry = sut.getEntry(byDate: votingDate.startOfDay)
return entry == nil || entry?.mood == .missing
}
// No entry vote needed
XCTAssertTrue(isVoteNeeded())
// Add entry no vote needed
sut.add(mood: .great, forDate: votingDate, entryType: .listView)
XCTAssertFalse(isVoteNeeded())
// Delete entry vote needed again
sut.deleteAllEntries(forDate: votingDate)
XCTAssertTrue(isVoteNeeded())
}
func test_duplicateRemoval() {
let date = makeDate(2024, 6, 15)
// Manually insert 3 entries for same date (simulating CloudKit conflict)
let entry1 = MoodEntryModel(forDate: date, mood: .great, entryType: .listView)
entry1.timestamp = makeDate(2024, 6, 15, hour: 10)
sut.modelContext.insert(entry1)
let entry2 = MoodEntryModel(forDate: date, mood: .good, entryType: .listView)
entry2.timestamp = makeDate(2024, 6, 15, hour: 8)
sut.modelContext.insert(entry2)
let entry3 = MoodEntryModel(forDate: date, mood: .missing, entryType: .filledInMissing)
entry3.timestamp = makeDate(2024, 6, 15, hour: 12)
sut.modelContext.insert(entry3)
sut.save()
let removed = sut.removeDuplicates()
XCTAssertEqual(removed, 2)
XCTAssertEqual(sut.getAllEntries(byDate: date).count, 1)
}
func test_dataListeners_fireOnMutation() {
var listenerCallCount = 0
sut.addNewDataListener {
listenerCallCount += 1
}
// add() calls saveAndRunDataListeners internally
sut.add(mood: .good, forDate: makeDate(2024, 6, 15), entryType: .listView)
// add() does: delete-save (if existing) + insert + saveAndRunDataListeners
// For a fresh add with no existing entry, listener fires once from saveAndRunDataListeners
XCTAssertGreaterThanOrEqual(listenerCallCount, 1, "Listener should fire at least once on mutation")
}
}

View File

@@ -0,0 +1,125 @@
//
// StreakTests.swift
// ReflectTests
//
// Tests for DataController streak calculation.
//
import XCTest
import SwiftData
@testable import Reflect
@MainActor
final class StreakTests: XCTestCase {
var sut: DataController!
override func setUp() {
super.setUp()
let schema = Schema([MoodEntryModel.self])
let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
sut = DataController(container: try! ModelContainer(for: schema, configurations: [config]))
}
override func tearDown() {
sut = nil
super.tearDown()
}
// MARK: - Phase 6: Streak Calculation
func test_streak_emptyDB_zero() {
let result = sut.calculateStreak(from: Date())
XCTAssertEqual(result.streak, 0)
XCTAssertNil(result.todaysMood)
}
func test_streak_singleEntryToday_one() {
let today = Calendar.current.startOfDay(for: Date())
sut.add(mood: .great, forDate: today, entryType: .listView)
let result = sut.calculateStreak(from: today)
XCTAssertEqual(result.streak, 1)
XCTAssertEqual(result.todaysMood, .great)
}
func test_streak_consecutiveDays() {
let today = Calendar.current.startOfDay(for: Date())
for i in 0..<5 {
let date = Calendar.current.date(byAdding: .day, value: -i, to: today)!
sut.add(mood: .good, forDate: date, entryType: .listView)
}
let result = sut.calculateStreak(from: today)
XCTAssertEqual(result.streak, 5)
}
func test_streak_gapBreaks() {
let today = Calendar.current.startOfDay(for: Date())
// Today, yesterday, then skip a day, then 2 more
sut.add(mood: .good, forDate: today, entryType: .listView)
sut.add(mood: .good, forDate: Calendar.current.date(byAdding: .day, value: -1, to: today)!, entryType: .listView)
// Skip day -2
sut.add(mood: .good, forDate: Calendar.current.date(byAdding: .day, value: -3, to: today)!, entryType: .listView)
sut.add(mood: .good, forDate: Calendar.current.date(byAdding: .day, value: -4, to: today)!, entryType: .listView)
let result = sut.calculateStreak(from: today)
XCTAssertEqual(result.streak, 2, "Streak should stop at the gap")
}
func test_streak_missingDoesNotCount() {
let today = Calendar.current.startOfDay(for: Date())
sut.add(mood: .good, forDate: today, entryType: .listView)
// Yesterday is a .missing entry (should be ignored)
sut.add(mood: .missing, forDate: Calendar.current.date(byAdding: .day, value: -1, to: today)!, entryType: .filledInMissing)
sut.add(mood: .good, forDate: Calendar.current.date(byAdding: .day, value: -2, to: today)!, entryType: .listView)
let result = sut.calculateStreak(from: today)
XCTAssertEqual(result.streak, 1, ".missing entries should not count toward streak")
}
func test_streak_noEntryToday_countsFromYesterday() {
let today = Calendar.current.startOfDay(for: Date())
let yesterday = Calendar.current.date(byAdding: .day, value: -1, to: today)!
let twoDaysAgo = Calendar.current.date(byAdding: .day, value: -2, to: today)!
sut.add(mood: .good, forDate: yesterday, entryType: .listView)
sut.add(mood: .great, forDate: twoDaysAgo, entryType: .listView)
let result = sut.calculateStreak(from: today)
XCTAssertEqual(result.streak, 2, "Should count from yesterday when today has no entry")
XCTAssertNil(result.todaysMood, "Today's mood should be nil when no entry exists")
}
func test_streak_entryAfterVotingTime_stillCounts() {
// Bug: calculateStreak passes votingDate (with time) as endDate to getData.
// getData uses <=, so an entry logged AFTER votingDate's time is excluded.
// E.g. if votingDate is "today at 8am" and user logged at 10am, the 10am entry
// is excluded from the query, making streak miss today.
let today = Calendar.current.startOfDay(for: Date())
let morningVotingDate = Calendar.current.date(byAdding: .hour, value: 8, to: today)!
// Add entry at 10am (after the 8am voting date time)
let afternoonDate = Calendar.current.date(byAdding: .hour, value: 10, to: today)!
sut.add(mood: .great, forDate: afternoonDate, entryType: .listView)
let result = sut.calculateStreak(from: morningVotingDate)
XCTAssertEqual(result.streak, 1, "Entry logged after votingDate time should still count")
XCTAssertEqual(result.todaysMood, .great, "Today's mood should be found even if logged after votingDate time")
}
func test_streak_afterDelete_decreases() {
let today = Calendar.current.startOfDay(for: Date())
let yesterday = Calendar.current.date(byAdding: .day, value: -1, to: today)!
sut.add(mood: .good, forDate: today, entryType: .listView)
sut.add(mood: .great, forDate: yesterday, entryType: .listView)
let beforeDelete = sut.calculateStreak(from: today)
XCTAssertEqual(beforeDelete.streak, 2)
sut.deleteAllEntries(forDate: yesterday)
let afterDelete = sut.calculateStreak(from: today)
XCTAssertEqual(afterDelete.streak, 1, "Streak should decrease after deleting a day")
}
}

View File

@@ -0,0 +1,157 @@
//
// VoteLogicsTests.swift
// ReflectTests
//
// Tests for ShowBasedOnVoteLogics vote status and timing.
//
import XCTest
import SwiftData
@testable import Reflect
@MainActor
final class VoteLogicsTests: XCTestCase {
// MARK: - Phase 7: passedTodaysVotingUnlock Tests
func test_passedVotingUnlock_beforeTime() {
// Voting unlock set to 6:00 PM, current time is 2:00 PM
let voteDate = makeDateWithTime(hour: 18, minute: 0)
let now = makeDateWithTime(hour: 14, minute: 0)
let result = ShowBasedOnVoteLogics.passedTodaysVotingUnlock(voteDate: voteDate, now: now)
XCTAssertFalse(result, "Should not have passed unlock when current time is before vote time")
}
func test_passedVotingUnlock_afterTime() {
// Voting unlock set to 6:00 PM, current time is 8:00 PM
let voteDate = makeDateWithTime(hour: 18, minute: 0)
let now = makeDateWithTime(hour: 20, minute: 0)
let result = ShowBasedOnVoteLogics.passedTodaysVotingUnlock(voteDate: voteDate, now: now)
XCTAssertTrue(result, "Should have passed unlock when current time is after vote time")
}
func test_passedVotingUnlock_exactTime() {
// Voting unlock set to 6:00 PM, current time is exactly 6:00 PM
let voteDate = makeDateWithTime(hour: 18, minute: 0)
let now = makeDateWithTime(hour: 18, minute: 0)
let result = ShowBasedOnVoteLogics.passedTodaysVotingUnlock(voteDate: voteDate, now: now)
XCTAssertTrue(result, "Should have passed unlock at exact vote time (uses >=)")
}
// MARK: - Phase 7: getCurrentVotingDate Tests
func test_votingDate_today_beforeTime() {
let onboarding = OnboardingData()
onboarding.inputDay = .Today
onboarding.date = makeDateWithTime(hour: 18, minute: 0)
let now = makeDateWithTime(hour: 14, minute: 0) // before 6pm
let result = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: onboarding, now: now)
let yesterday = Calendar.current.date(byAdding: .day, value: -1, to: now)!
XCTAssertTrue(Calendar.current.isDate(result, inSameDayAs: yesterday),
"Today mode, before unlock: should return yesterday")
}
func test_votingDate_today_afterTime() {
let onboarding = OnboardingData()
onboarding.inputDay = .Today
onboarding.date = makeDateWithTime(hour: 18, minute: 0)
let now = makeDateWithTime(hour: 20, minute: 0) // after 6pm
let result = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: onboarding, now: now)
XCTAssertTrue(Calendar.current.isDate(result, inSameDayAs: now),
"Today mode, after unlock: should return today")
}
func test_votingDate_previous_beforeTime() {
let onboarding = OnboardingData()
onboarding.inputDay = .Previous
onboarding.date = makeDateWithTime(hour: 18, minute: 0)
let now = makeDateWithTime(hour: 14, minute: 0) // before 6pm
let result = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: onboarding, now: now)
let twoDaysAgo = Calendar.current.date(byAdding: .day, value: -2, to: now)!
XCTAssertTrue(Calendar.current.isDate(result, inSameDayAs: twoDaysAgo),
"Previous mode, before unlock: should return 2 days ago")
}
func test_votingDate_previous_afterTime() {
let onboarding = OnboardingData()
onboarding.inputDay = .Previous
onboarding.date = makeDateWithTime(hour: 18, minute: 0)
let now = makeDateWithTime(hour: 20, minute: 0) // after 6pm
let result = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: onboarding, now: now)
let yesterday = Calendar.current.date(byAdding: .day, value: -1, to: now)!
XCTAssertTrue(Calendar.current.isDate(result, inSameDayAs: yesterday),
"Previous mode, after unlock: should return yesterday")
}
// MARK: - Phase 7: Vote Needed Tests (exercising the same logic as isMissingCurrentVote)
// Note: isMissingCurrentVote uses DataController.shared (singleton) internally,
// so we test the equivalent logic using a testable DataController instance.
func test_voteNeeded_noEntry() {
let sut = makeTestDataController()
let onboarding = OnboardingData()
onboarding.inputDay = .Today
onboarding.date = makeDateWithTime(hour: 0, minute: 1)
let votingDate = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: onboarding)
let entry = sut.getEntry(byDate: votingDate.startOfDay)
let isMissing = entry == nil || entry?.mood == .missing
XCTAssertTrue(isMissing, "Should need a vote when no entry exists")
}
func test_voteNeeded_validEntry() {
let sut = makeTestDataController()
let onboarding = OnboardingData()
onboarding.inputDay = .Today
onboarding.date = makeDateWithTime(hour: 0, minute: 1)
let votingDate = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: onboarding)
sut.add(mood: .great, forDate: votingDate, entryType: .listView)
let entry = sut.getEntry(byDate: votingDate.startOfDay)
let isMissing = entry == nil || entry?.mood == .missing
XCTAssertFalse(isMissing, "Should not need a vote when valid entry exists")
}
func test_voteNeeded_missingEntry() {
let sut = makeTestDataController()
let onboarding = OnboardingData()
onboarding.inputDay = .Today
onboarding.date = makeDateWithTime(hour: 0, minute: 1)
let votingDate = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: onboarding)
sut.add(mood: .missing, forDate: votingDate, entryType: .filledInMissing)
let entry = sut.getEntry(byDate: votingDate.startOfDay)
let isMissing = entry == nil || entry?.mood == .missing
XCTAssertTrue(isMissing, "Should need a vote when entry is .missing")
}
// MARK: - Helpers
private func makeTestDataController() -> DataController {
let schema = Schema([MoodEntryModel.self])
let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
return DataController(container: try! ModelContainer(for: schema, configurations: [config]))
}
private func makeDateWithTime(hour: Int, minute: Int) -> Date {
let calendar = Calendar.current
var components = calendar.dateComponents([.year, .month, .day], from: Date())
components.hour = hour
components.minute = minute
components.second = 0
return calendar.date(from: components)!
}
}