1.1.0 feature work: Monthly Check-in, Charts, Goals, Share, Reviews
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -51,4 +51,40 @@ final class ReviewPromptServiceTests: XCTestCase {
|
||||
|
||||
XCTAssertFalse(didRequest)
|
||||
}
|
||||
|
||||
func testAchievementPromptTriggersForNewUnlockWhenNotReviewed() {
|
||||
let service = ReviewPromptService(
|
||||
userDefaults: defaults,
|
||||
dateProvider: Date.init,
|
||||
reviewRequestHandler: {}
|
||||
)
|
||||
|
||||
let shouldAsk = service.shouldAskForAchievementSatisfaction(newlyUnlockedAchievementKeys: ["streak_3"])
|
||||
|
||||
XCTAssertTrue(shouldAsk)
|
||||
}
|
||||
|
||||
func testAchievementPromptDoesNotTriggerTwiceForSameAchievement() {
|
||||
let service = ReviewPromptService(
|
||||
userDefaults: defaults,
|
||||
dateProvider: Date.init,
|
||||
reviewRequestHandler: {}
|
||||
)
|
||||
|
||||
XCTAssertTrue(service.shouldAskForAchievementSatisfaction(newlyUnlockedAchievementKeys: ["streak_3"]))
|
||||
XCTAssertFalse(service.shouldAskForAchievementSatisfaction(newlyUnlockedAchievementKeys: ["streak_3"]))
|
||||
}
|
||||
|
||||
func testAchievementPromptDoesNotTriggerAfterStoreReviewCompleted() {
|
||||
let service = ReviewPromptService(
|
||||
userDefaults: defaults,
|
||||
dateProvider: Date.init,
|
||||
reviewRequestHandler: {}
|
||||
)
|
||||
service.markStoreReviewCompleted()
|
||||
|
||||
let shouldAsk = service.shouldAskForAchievementSatisfaction(newlyUnlockedAchievementKeys: ["streak_6"])
|
||||
|
||||
XCTAssertFalse(shouldAsk)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,4 +25,22 @@ final class ShareServiceTests: XCTestCase {
|
||||
XCTAssertTrue(text.contains("Net performance:"))
|
||||
XCTAssertTrue(text.contains("Shared from Portfolio Journal"))
|
||||
}
|
||||
|
||||
func testBuildPortfolioValueShareText() {
|
||||
let text = ShareService.buildPortfolioValueShareText(
|
||||
totalValue: "€120,000",
|
||||
changeText: "+€2,500 (+2.1%)",
|
||||
changeLabel: "since last check-in",
|
||||
yearChange: "+€8,000 (+7.1%)",
|
||||
sinceInceptionChange: "+€24,000 (+25.0%)",
|
||||
appName: "Portfolio Journal"
|
||||
)
|
||||
|
||||
XCTAssertTrue(text.contains("Total Portfolio Value"))
|
||||
XCTAssertTrue(text.contains("€120,000"))
|
||||
XCTAssertTrue(text.contains("+€2,500 (+2.1%) since last check-in"))
|
||||
XCTAssertTrue(text.contains("YoY: +€8,000 (+7.1%)"))
|
||||
XCTAssertTrue(text.contains("Since inception: +€24,000 (+25.0%)"))
|
||||
XCTAssertTrue(text.contains("Shared from Portfolio Journal"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import XCTest
|
||||
@testable import PortfolioJournal
|
||||
|
||||
@MainActor
|
||||
final class ChartsViewModelDisplayRulesTests: XCTestCase {
|
||||
func testAllocationTargetsSupportedOnlyForCategoryBreakdown() {
|
||||
XCTAssertTrue(ChartsViewModel.supportsAllocationTargets(for: .category))
|
||||
XCTAssertFalse(ChartsViewModel.supportsAllocationTargets(for: .source))
|
||||
}
|
||||
|
||||
func testEvolutionTimeRangesIncludeRequestedOptions() {
|
||||
let viewModel = ChartsViewModel(iapService: IAPService())
|
||||
let ranges = viewModel.availableTimeRanges(for: .evolution)
|
||||
|
||||
XCTAssertEqual(ranges, [.all, .yearToDate, .year, .quarter])
|
||||
}
|
||||
|
||||
func testYearToDateRangeStartsAtBeginningOfYear() {
|
||||
let calendar = Calendar(identifier: .gregorian)
|
||||
let referenceDate = calendar.date(from: DateComponents(year: 2026, month: 8, day: 15))!
|
||||
let startDate = ChartsViewModel.TimeRange.yearToDate.startDate(referenceDate: referenceDate)
|
||||
let expected = calendar.date(from: DateComponents(year: 2026, month: 1, day: 1))!
|
||||
|
||||
XCTAssertEqual(startDate, expected)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import XCTest
|
||||
@testable import PortfolioJournal
|
||||
|
||||
@MainActor
|
||||
final class GoalsDisplayRulesTests: XCTestCase {
|
||||
func testIsAchievedProgressThreshold() {
|
||||
XCTAssertFalse(GoalsViewModel.isAchieved(progress: 0.998))
|
||||
XCTAssertTrue(GoalsViewModel.isAchieved(progress: 0.999))
|
||||
XCTAssertTrue(GoalsViewModel.isAchieved(progress: 1.0))
|
||||
}
|
||||
|
||||
func testUrgencyLevelIsCriticalWhenBehindAndPastTargetDate() {
|
||||
let targetDate = Date(timeIntervalSince1970: 1_000_000)
|
||||
let referenceDate = Date(timeIntervalSince1970: 1_100_000)
|
||||
|
||||
let level = GoalsViewModel.urgencyLevel(
|
||||
targetDate: targetDate,
|
||||
isBehind: true,
|
||||
isAchieved: false,
|
||||
referenceDate: referenceDate
|
||||
)
|
||||
|
||||
XCTAssertEqual(level, .critical)
|
||||
}
|
||||
|
||||
func testUrgencyLevelIsWarningWhenBehindBeforeTargetDate() {
|
||||
let targetDate = Date(timeIntervalSince1970: 1_100_000)
|
||||
let referenceDate = Date(timeIntervalSince1970: 1_000_000)
|
||||
|
||||
let level = GoalsViewModel.urgencyLevel(
|
||||
targetDate: targetDate,
|
||||
isBehind: true,
|
||||
isAchieved: false,
|
||||
referenceDate: referenceDate
|
||||
)
|
||||
|
||||
XCTAssertEqual(level, .warning)
|
||||
}
|
||||
|
||||
func testUrgencyLevelIsNormalForAchievedGoals() {
|
||||
let level = GoalsViewModel.urgencyLevel(
|
||||
targetDate: Date(),
|
||||
isBehind: true,
|
||||
isAchieved: true,
|
||||
referenceDate: Date().addingTimeInterval(86_400)
|
||||
)
|
||||
|
||||
XCTAssertEqual(level, .normal)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import XCTest
|
||||
@testable import PortfolioJournal
|
||||
|
||||
final class MonthlyCheckInViewTests: XCTestCase {
|
||||
func testMonthLabelUsesMonthAndYear() {
|
||||
var components = DateComponents()
|
||||
components.year = 2026
|
||||
components.month = 2
|
||||
components.day = 1
|
||||
let date = Calendar(identifier: .gregorian).date(from: components)!
|
||||
|
||||
let label = MonthlyCheckInView.monthLabel(
|
||||
for: date,
|
||||
relativeTo: date,
|
||||
locale: Locale(identifier: "en_US_POSIX")
|
||||
)
|
||||
|
||||
XCTAssertEqual(label, "February 2026")
|
||||
}
|
||||
|
||||
func testMonthLabelUsesPreviousMonthDuringGracePeriod() {
|
||||
var components = DateComponents()
|
||||
components.year = 2026
|
||||
components.month = 2
|
||||
components.day = 20
|
||||
let date = Calendar(identifier: .gregorian).date(from: components)!
|
||||
|
||||
let label = MonthlyCheckInView.monthLabel(
|
||||
for: date,
|
||||
relativeTo: date,
|
||||
locale: Locale(identifier: "en_US_POSIX")
|
||||
)
|
||||
|
||||
XCTAssertEqual(label, "January 2026")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user