137 lines
5.1 KiB
Dart
137 lines
5.1 KiB
Dart
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);
|
|||
|
|
});
|
|||
|
|
}
|