The UX redesign's first landing (spec in UX-REDESIGN.md): ContentView becomes a Today / Progress / Settings TabView, "Routine" replaces "Split" in every user-facing string and view name (code-level types keep their names), and workout starting moves to shared WorkoutStarter / StartedWorkoutNavigator plumbing. - New Progress tab: weekly goal streaks, workout trends, per-exercise weight progression, achievements, and the full history list (WorkoutLogsView -> WorkoutHistoryView). - Goals: stable categories workouts roll up to, managed from Settings. - New Meditation exercise + starter routine; timed sits record to Apple Health as Mind & Body sessions. Claude-Session: https://claude.ai/code/session_012qw2itfzKyEJ1HpsFt8Ex4
85 lines
4.1 KiB
Swift
85 lines
4.1 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import Workouts
|
|
|
|
/// Locks `GoalKind`'s persisted-order contract: the stored raw-value list drives the
|
|
/// display order, unknown raw values are dropped, and any case missing from the stored
|
|
/// list is appended in declaration order — so a newly shipped goal kind surfaces
|
|
/// automatically without a migration. A throwaway `UserDefaults` suite keeps each case
|
|
/// isolated from the real domain.
|
|
struct GoalKindTests {
|
|
|
|
/// A fresh suite scoped to one test; cleared on teardown so nothing leaks between runs.
|
|
private func makeDefaults(_ function: String = #function) -> UserDefaults {
|
|
let suite = "GoalKindTests.\(function).\(UUID().uuidString)"
|
|
let defaults = UserDefaults(suiteName: suite)!
|
|
defaults.removePersistentDomain(forName: suite)
|
|
return defaults
|
|
}
|
|
|
|
/// With nothing stored, the order is exactly declaration order (all cases present).
|
|
@Test func emptyDefaultsGivesDeclarationOrder() {
|
|
let defaults = makeDefaults()
|
|
#expect(GoalKind.orderedCases(defaults: defaults) == GoalKind.allCases)
|
|
#expect(GoalKind.orderedCases(defaults: defaults) == [.mobility, .cardio, .strength, .mindfulness])
|
|
}
|
|
|
|
/// A partial stored list keeps the stored ones first (in stored order), then appends
|
|
/// the missing cases in declaration order — the "new kind appears automatically" rule.
|
|
@Test func partialStoredListPutsStoredFirstThenMissing() {
|
|
let defaults = makeDefaults()
|
|
GoalKind.saveOrder([.mindfulness, .mobility], defaults: defaults)
|
|
|
|
let ordered = GoalKind.orderedCases(defaults: defaults)
|
|
#expect(ordered == [.mindfulness, .mobility, .cardio, .strength])
|
|
}
|
|
|
|
/// A full stored list is honored verbatim — a user-chosen reorder round-trips.
|
|
@Test func fullStoredListIsHonoredVerbatim() {
|
|
let defaults = makeDefaults()
|
|
let chosen: [GoalKind] = [.cardio, .strength, .mindfulness, .mobility]
|
|
GoalKind.saveOrder(chosen, defaults: defaults)
|
|
#expect(GoalKind.orderedCases(defaults: defaults) == chosen)
|
|
}
|
|
|
|
/// Unknown raw values in the stored list are dropped (e.g. a kind removed in a future
|
|
/// version), and the survivors still slot ahead of the missing declaration-order tail.
|
|
@Test func unknownRawValuesAreDropped() {
|
|
let defaults = makeDefaults()
|
|
defaults.set(["mindfulness", "flexibility", "strength"], forKey: GoalKind.orderDefaultsKey)
|
|
|
|
let ordered = GoalKind.orderedCases(defaults: defaults)
|
|
#expect(ordered == [.mindfulness, .strength, .mobility, .cardio])
|
|
}
|
|
|
|
/// Duplicate raw values collapse to a single entry (first wins), so a corrupt list
|
|
/// never yields a kind twice.
|
|
@Test func duplicateRawValuesCollapse() {
|
|
let defaults = makeDefaults()
|
|
defaults.set(["strength", "strength", "mobility"], forKey: GoalKind.orderDefaultsKey)
|
|
|
|
let ordered = GoalKind.orderedCases(defaults: defaults)
|
|
#expect(ordered == [.strength, .mobility, .cardio, .mindfulness])
|
|
#expect(Set(ordered).count == ordered.count) // no dupes
|
|
}
|
|
|
|
/// `saveOrder` persists `[String]` of raw values — the exact shape `orderedCases`
|
|
/// reads back.
|
|
@Test func saveOrderStoresRawValueStrings() {
|
|
let defaults = makeDefaults()
|
|
GoalKind.saveOrder([.mobility, .strength], defaults: defaults)
|
|
#expect(defaults.stringArray(forKey: GoalKind.orderDefaultsKey) == ["mobility", "strength"])
|
|
}
|
|
|
|
/// Every case carries a valid, distinct-hued color name `Color.color(from:)` maps
|
|
/// to a non-default color, and stable display metadata.
|
|
@Test func metadataIsPopulatedAndDistinct() {
|
|
#expect(Set(GoalKind.allCases.map(\.colorName)).count == GoalKind.allCases.count)
|
|
#expect(Set(GoalKind.allCases.map(\.systemImage)).count == GoalKind.allCases.count)
|
|
#expect(GoalKind.strength.displayName == "Strength")
|
|
#expect(GoalKind.mindfulness.displayName == "Mindfulness")
|
|
// Each color name is one the palette understands (falls back to indigo otherwise).
|
|
#expect(GoalKind.allCases.allSatisfy { availableColors.contains($0.colorName) })
|
|
}
|
|
}
|