Restructure into a three-tab app with Progress, goals, and Meditation
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
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
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) })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user