Files
ZenXercise/test/plans/plan_test.dart
T
CedricandClaude Opus 5.5 bbdca95541 Stage 3: Plan editor with Steps, Supersets and reordering
- Plan model with immutable edits: add with Mode defaults, move, remove,
  Superset with next, take a member out, ungroup; Stretching length
  estimate from holds, sides, rounds and Transitions
- drift database (plans table, items as JSON, ADR 0005) and
  PlanRepository; Plans list per Mode with rename/duplicate/delete
- Editor: drag handles, step editor sheet (sets, reps or time, optional
  kg; hold, both sides, rounds; Superset sets and rest between),
  pause between holds for Stretching Plans
- Exercise picker reusing the Library browser; Add to plan from the
  Exercise detail page
- Theme: Mode-styled snackbar, FAB, menus, segmented buttons, switches

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 23:48:07 +01:00

138 lines
5.1 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter_test/flutter_test.dart';
import 'package:zenxercise/src/modes/mode.dart';
import 'package:zenxercise/src/plans/models/plan.dart';
Plan _plan(Mode mode, List<PlanItem> items) => Plan(
id: 'p1',
mode: mode,
name: 'Push day',
items: items,
createdAt: DateTime(2026, 10, 1),
updatedAt: DateTime(2026, 10, 1),
);
WorkoutStep _w(String id, {int sets = 3, WorkoutTarget target = const WorkoutTarget.reps(10), double? kg}) =>
WorkoutStep(exerciseId: id, sets: sets, target: target, weightKg: kg);
void main() {
group('adding Exercises', () {
test('a Workout Plan adds a Workout Step of 3 × 10 reps without weight', () {
final plan = _plan(Mode.workout, const []).withExerciseAdded('Bench_Press');
expect(plan.items.single, _w('Bench_Press'));
});
test('a Stretching Plan adds a single 30 s hold on one side', () {
final plan = _plan(Mode.stretching, const []).withExerciseAdded('Calf_Stretch');
expect(
plan.items.single,
const StretchingStep(exerciseId: 'Calf_Stretch', holdSeconds: 30, bothSides: false, rounds: 1),
);
});
});
test('moving follows the list convention of an index past the removed item', () {
final plan = _plan(Mode.workout, [_w('A'), _w('B'), _w('C')]);
expect(plan.withItemMoved(0, 3).items, [_w('B'), _w('C'), _w('A')]);
expect(plan.withItemMoved(2, 0).items, [_w('C'), _w('A'), _w('B')]);
expect(plan.withItemRemoved(1).items, [_w('A'), _w('C')]);
});
group('Supersets', () {
test('a Step grouped with the next keeps each target and takes the first set count', () {
final plan = _plan(Mode.workout, [
_w('A', sets: 4, kg: 20),
_w('B', sets: 2, target: const WorkoutTarget.seconds(45)),
_w('C'),
]).withSupersetWithNext(0);
expect(plan.items, [
const Superset(
sets: 4,
restBetween: false,
members: [
SupersetMember(exerciseId: 'A', target: WorkoutTarget.reps(10), weightKg: 20),
SupersetMember(exerciseId: 'B', target: WorkoutTarget.seconds(45)),
],
),
_w('C'),
]);
expect(plan.exerciseCount, 3);
});
test('grouping again with the next item grows the Superset, also when the next is a Superset', () {
final grown = _plan(Mode.workout, [_w('A'), _w('B'), _w('C')]).withSupersetWithNext(0).withSupersetWithNext(0);
expect((grown.items.single as Superset).members.map((m) => m.exerciseId), ['A', 'B', 'C']);
final merged = _plan(Mode.workout, [_w('A'), _w('B'), _w('C')]).withSupersetWithNext(1).withSupersetWithNext(0);
expect((merged.items.single as Superset).members.map((m) => m.exerciseId), ['A', 'B', 'C']);
});
test('only an item with a next item can be grouped', () {
final plan = _plan(Mode.workout, [_w('A'), _w('B')]);
expect(plan.canSupersetWithNext(0), isTrue);
expect(plan.canSupersetWithNext(1), isFalse);
expect(_plan(Mode.stretching, const []).withExerciseAdded('X').canSupersetWithNext(0), isFalse);
});
test('a member taken out lands right after its Superset with the Superset set count', () {
final plan = _plan(Mode.workout, [
_w('A'),
_w('B'),
_w('C'),
_w('D'),
]).withSupersetWithNext(0).withSupersetWithNext(0).withMemberRemovedFromSuperset(0, 0);
expect((plan.items[0] as Superset).members.map((m) => m.exerciseId), ['B', 'C']);
expect(plan.items[1], _w('A'));
expect(plan.items[2], _w('D'));
});
test('a Superset left with one member turns back into a Step', () {
final plan = _plan(Mode.workout, [
_w('A', sets: 5),
_w('B'),
]).withSupersetWithNext(0).withMemberRemovedFromSuperset(0, 1);
expect(plan.items, [_w('A', sets: 5), _w('B', sets: 5)]);
});
test('ungrouping gives every member its own Step with the Superset set count', () {
final plan = _plan(Mode.workout, [_w('A', sets: 4), _w('B')]).withSupersetWithNext(0).withUngrouped(0);
expect(plan.items, [_w('A', sets: 4), _w('B', sets: 4)]);
});
});
test('a Stretching Plan estimates its length from holds, sides, rounds and Transitions', () {
final plan = _plan(Mode.stretching, const [
StretchingStep(exerciseId: 'A', holdSeconds: 30, bothSides: true, rounds: 2),
StretchingStep(exerciseId: 'B', holdSeconds: 60, bothSides: false, rounds: 1),
]);
// Holds: 4 × 30 s + 60 s = 180 s; 5 holds have 4 Transitions of 5 s between them.
expect(plan.estimatedDuration, const Duration(seconds: 200));
expect(_plan(Mode.workout, [_w('A')]).estimatedDuration, isNull);
});
test('items survive a JSON round trip', () {
final items = [
_w('A', kg: 12.5),
const StretchingStep(exerciseId: 'B', holdSeconds: 45, bothSides: true, rounds: 3),
const Superset(
sets: 3,
restBetween: true,
members: [
SupersetMember(exerciseId: 'C', target: WorkoutTarget.seconds(30)),
SupersetMember(exerciseId: 'D', target: WorkoutTarget.reps(8), weightKg: 40),
],
),
];
expect(PlanItem.listFromJson(PlanItem.listToJson(items)), items);
});
}