167b736aad
La tarjeta Momentum & Streaks mostraba "Streak 0x" casi todo el mes aunque la racha estuviera intacta: stats() empezaba a contar en el mes en curso y cortaba en el primer mes sin check-in on-time. El check-in del mes en curso está dentro de plazo hasta fin de mes (y con graceDays = 20 se hace normalmente ya entrado el mes siguiente), así que tenerlo pendiente no debe leerse como racha rota. Ahora el recuento arranca en el mes en curso si ya está hecho y, si no, en el anterior. Un mes anterior sin check-in sí rompe la racha, porque su plazo ya venció. Tests: 3 casos nuevos en MonthlyCheckInStoreTests (mes en curso pendiente, mes en curso completado, mes anterior perdido). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YcDn5ccuRFV83q7xWWokBT
114 lines
5.2 KiB
Swift
114 lines
5.2 KiB
Swift
import XCTest
|
||
@testable import PortfolioJournal
|
||
|
||
final class MonthlyCheckInStoreTests: XCTestCase {
|
||
override func setUp() {
|
||
super.setUp()
|
||
MonthlyCheckInStore.clearAll()
|
||
}
|
||
|
||
override func tearDown() {
|
||
MonthlyCheckInStore.clearAll()
|
||
super.tearDown()
|
||
}
|
||
|
||
func testEffectiveMonthUsesPreviousMonthWithinGracePeriod() {
|
||
let calendar = Calendar(identifier: .gregorian)
|
||
let referenceDate = calendar.date(from: DateComponents(year: 2026, month: 2, day: 20))!
|
||
let effective = MonthlyCheckInStore.effectiveMonth(for: referenceDate, relativeTo: referenceDate, graceDays: 20)
|
||
|
||
let expected = calendar.date(from: DateComponents(year: 2026, month: 1, day: 1))!
|
||
XCTAssertEqual(effective, expected)
|
||
}
|
||
|
||
func testEffectiveMonthUsesCurrentMonthAfterGracePeriod() {
|
||
let calendar = Calendar(identifier: .gregorian)
|
||
let referenceDate = calendar.date(from: DateComponents(year: 2026, month: 2, day: 21))!
|
||
let effective = MonthlyCheckInStore.effectiveMonth(for: referenceDate, relativeTo: referenceDate, graceDays: 20)
|
||
|
||
let expected = calendar.date(from: DateComponents(year: 2026, month: 2, day: 1))!
|
||
XCTAssertEqual(effective, expected)
|
||
}
|
||
|
||
func testCompletionAfterGraceAutoFillsMissingMonths() {
|
||
let calendar = Calendar(identifier: .gregorian)
|
||
let decemberDate = calendar.date(from: DateComponents(year: 2025, month: 12, day: 15))!
|
||
let decemberCompletion = calendar.date(from: DateComponents(year: 2025, month: 12, day: 31))!
|
||
MonthlyCheckInStore.setNote("Carry", for: decemberDate)
|
||
MonthlyCheckInStore.setRating(4, for: decemberDate)
|
||
MonthlyCheckInStore.setMood(.balanced, for: decemberDate)
|
||
MonthlyCheckInStore.setCompletionDate(decemberCompletion, for: decemberDate)
|
||
|
||
let completionDate = calendar.date(from: DateComponents(year: 2026, month: 2, day: 21))!
|
||
MonthlyCheckInStore.setCompletionDate(completionDate, for: completionDate)
|
||
|
||
let januaryDate = calendar.date(from: DateComponents(year: 2026, month: 1, day: 5))!
|
||
let januaryEntry = MonthlyCheckInStore.entry(for: januaryDate)
|
||
XCTAssertEqual(januaryEntry?.note, "Carry")
|
||
XCTAssertEqual(januaryEntry?.rating, 4)
|
||
XCTAssertEqual(januaryEntry?.mood, .balanced)
|
||
XCTAssertNotNil(januaryEntry?.completionDate)
|
||
}
|
||
|
||
// MARK: - Current streak
|
||
|
||
/// Completing June–August and looking at the card on 12 September (the
|
||
/// September check-in is still within its deadline) must show a live streak,
|
||
/// not 0x.
|
||
func testCurrentStreakCountsWhenCurrentMonthIsStillPending() {
|
||
let calendar = Calendar(identifier: .gregorian)
|
||
for month in 6...8 {
|
||
let monthDate = calendar.date(from: DateComponents(year: 2026, month: month, day: 15))!
|
||
let completion = calendar.date(from: DateComponents(year: 2026, month: month, day: 28))!
|
||
MonthlyCheckInStore.setCompletionDate(completion, for: monthDate)
|
||
}
|
||
|
||
let referenceDate = calendar.date(from: DateComponents(year: 2026, month: 9, day: 12))!
|
||
let stats = MonthlyCheckInStore.stats(referenceDate: referenceDate)
|
||
|
||
XCTAssertEqual(stats.currentStreak, 3)
|
||
}
|
||
|
||
/// The current month being pending is forgiven; an earlier month whose
|
||
/// deadline already passed is not.
|
||
func testCurrentStreakBreaksWhenPreviousMonthWasMissed() {
|
||
let calendar = Calendar(identifier: .gregorian)
|
||
for month in 6...8 {
|
||
let monthDate = calendar.date(from: DateComponents(year: 2026, month: month, day: 15))!
|
||
let completion = calendar.date(from: DateComponents(year: 2026, month: month, day: 28))!
|
||
MonthlyCheckInStore.setCompletionDate(completion, for: monthDate)
|
||
}
|
||
|
||
// September was never checked in and October is under way.
|
||
let referenceDate = calendar.date(from: DateComponents(year: 2026, month: 10, day: 12))!
|
||
let stats = MonthlyCheckInStore.stats(referenceDate: referenceDate)
|
||
|
||
XCTAssertEqual(stats.currentStreak, 0)
|
||
}
|
||
|
||
func testCurrentStreakCountsCurrentMonthWhenAlreadyCompleted() {
|
||
let calendar = Calendar(identifier: .gregorian)
|
||
for month in 7...9 {
|
||
let monthDate = calendar.date(from: DateComponents(year: 2026, month: month, day: 15))!
|
||
let completion = calendar.date(from: DateComponents(year: 2026, month: month, day: 28))!
|
||
MonthlyCheckInStore.setCompletionDate(completion, for: monthDate)
|
||
}
|
||
|
||
let referenceDate = calendar.date(from: DateComponents(year: 2026, month: 9, day: 29))!
|
||
let stats = MonthlyCheckInStore.stats(referenceDate: referenceDate)
|
||
|
||
XCTAssertEqual(stats.currentStreak, 3)
|
||
}
|
||
|
||
func testSetCompletionDateForVeryOldMonthStillCompletesEntry() {
|
||
let calendar = Calendar(identifier: .gregorian)
|
||
let oldMonthDate = calendar.date(from: DateComponents(year: 2024, month: 1, day: 10))!
|
||
let completionDate = calendar.date(from: DateComponents(year: 2026, month: 2, day: 22))!
|
||
|
||
MonthlyCheckInStore.setCompletionDate(completionDate, for: oldMonthDate)
|
||
|
||
let entry = MonthlyCheckInStore.entry(for: oldMonthDate)
|
||
XCTAssertNotNil(entry?.completionDate)
|
||
}
|
||
}
|