Add Warm-Up and Stretching activity types

Adds WorkoutActivityType.warmUp (HealthKit .preparationAndRecovery) and
.stretching (.flexibility), and retags the six starter splits that were all
mislabeled as Functional Strength:

- Warm-Up:    Upper Body Warm-Up, Lower Body Warm-Up, Morning Wake-Up
- Stretching: Morning Mobility, Full Body Stretch, Evening Stretch

The split editor's activity picker surfaces them automatically (CaseIterable).
Older app versions decode the new raw values as the default type — additive and
not schema-gated, so no quarantine.
This commit is contained in:
2026-07-09 15:45:31 -04:00
parent f01e149269
commit e12fe31152
308 changed files with 5594 additions and 990 deletions
File diff suppressed because one or more lines are too long
+44
View File
@@ -213,3 +213,47 @@ struct SetEntryTests {
#expect(re.weight == 42.5)
}
}
/// The Completed screen's one-line actuals summary (`SetEntryFormat.actualsLine`):
/// a uniform per-set list collapses to "N × v" for brevity, anything varying stays
/// a "· "-separated list.
struct SetEntryFormatTests {
private static let date = Date(timeIntervalSince1970: 1_700_000_000)
private func entry(_ reps: Int? = nil, w: Double? = nil, s: Int? = nil) -> SetEntry {
if let s { return SetEntry(seconds: s, completedAt: Self.date) }
if let w { return SetEntry(reps: reps, weight: w, completedAt: Self.date) }
return SetEntry(reps: reps, completedAt: Self.date)
}
private func line(_ entries: [SetEntry]) -> String {
SetEntryFormat.actualsLine(entries, weightUnit: .lb)
}
/// Four identical weighted sets read as "4 × 10", not "10 · 10 · 10 · 10".
@Test func uniformWeightedCollapses() {
let e = entry(10, w: 130)
#expect(line([e, e, e, e]) == "4 × 10 reps @ 130 lb")
}
/// Varying reps at one weight stay a list.
@Test func varyingRepsStayAList() {
#expect(line([entry(10, w: 100), entry(8, w: 100), entry(6, w: 100)]) == "10 · 8 · 6 reps @ 100 lb")
}
/// Varying weight falls back to the per-set "reps × weight" list.
@Test func varyingWeightIsPerSet() {
#expect(line([entry(10, w: 100), entry(8, w: 90)]) == "10 × 100 lb · 8 × 90 lb")
}
/// A single set is just its value never "1 × 10".
@Test func singleSetIsNotCollapsed() {
#expect(line([entry(10, w: 130)]) == "10 reps @ 130 lb")
}
/// Bodyweight (no weight) and duration lists collapse the same way.
@Test func bodyweightAndDurationCollapse() {
#expect(line([entry(12), entry(12), entry(12)]) == "3 × 12 reps")
#expect(line([entry(s: 45), entry(s: 45), entry(s: 45)]) == "3 × 45 sec")
#expect(line([entry(s: 45), entry(s: 45), entry(s: 30)]) == "45 · 45 · 30 sec")
}
}