- buildSegments lays a Plan out as holds with Transitions (get ready, switch sides, next round, next exercise) or sets with count-up Rests; Supersets alternate members and rest per round unless asked otherwise - SessionRunner drives segments against an injected clock: auto-advance with a chime, pause, skip, back, rep sets confirmed with logged reps and kg, timed sets started on demand, early end - Player screen: countdown ring, accent pulse in the last 3 s, Flip Animation, next-up cards, Rest Screen, mute toggle, screen kept on - Sessions table (schema v2 migration) keeps a Plan snapshot and Set Logs; nothing is saved when no hold or set was done - Summary with time, exercises or sets logged, and every Set Log - Synthesised quiet cue sounds (tools/sounds), status bar per Mode Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
159 lines
5.4 KiB
Dart
159 lines
5.4 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:zenxercise/src/modes/mode.dart';
|
|
import 'package:zenxercise/src/plans/models/plan.dart';
|
|
import 'package:zenxercise/src/player/segments.dart';
|
|
import 'package:zenxercise/src/player/session_runner.dart';
|
|
|
|
Plan _plan(Mode mode, List<PlanItem> items, {int transition = 5}) => Plan(
|
|
id: 'p',
|
|
mode: mode,
|
|
name: 'Plan',
|
|
items: items,
|
|
transitionSeconds: transition,
|
|
createdAt: DateTime(2026),
|
|
updatedAt: DateTime(2026),
|
|
);
|
|
|
|
final t0 = DateTime(2026, 10, 29, 7);
|
|
DateTime at(int seconds) => t0.add(Duration(seconds: seconds));
|
|
|
|
void main() {
|
|
group('Stretching runs by itself', () {
|
|
late SessionRunner runner;
|
|
var cues = 0;
|
|
|
|
setUp(() {
|
|
cues = 0;
|
|
runner = SessionRunner(
|
|
_plan(Mode.stretching, const [StretchingStep(exerciseId: 'A', holdSeconds: 30, bothSides: true, rounds: 1)]),
|
|
startedAt: t0,
|
|
onCue: () => cues++,
|
|
);
|
|
});
|
|
|
|
test('counting down a get-ready, two holds and the switch between them', () {
|
|
expect(runner.current, isA<TransitionSegment>());
|
|
expect(runner.remaining(at(2)), const Duration(seconds: 3));
|
|
|
|
runner.tick(at(5));
|
|
expect(runner.current, isA<HoldSegment>().having((h) => h.side, 'side', Side.left));
|
|
|
|
runner.tick(at(35));
|
|
expect(runner.current, isA<TransitionSegment>().having((t) => t.kind, 'kind', TransitionKind.switchSides));
|
|
|
|
runner.tick(at(40));
|
|
runner.tick(at(70));
|
|
expect(runner.isFinished, isTrue);
|
|
expect(runner.isComplete, isTrue);
|
|
expect(cues, 4);
|
|
});
|
|
|
|
test('pausing stops the clock', () {
|
|
runner.tick(at(5));
|
|
runner.pause(at(10));
|
|
runner.tick(at(100));
|
|
expect(runner.current, isA<HoldSegment>());
|
|
expect(runner.remaining(at(100)), const Duration(seconds: 25));
|
|
|
|
runner.resume(at(100));
|
|
expect(runner.remaining(at(110)), const Duration(seconds: 15));
|
|
});
|
|
|
|
test('skipping and going back move between holds, passing over Transitions', () {
|
|
runner.skip(at(1));
|
|
expect(runner.current, isA<HoldSegment>().having((h) => h.side, 'side', Side.left));
|
|
runner.skip(at(2));
|
|
runner.skip(at(3));
|
|
expect(runner.current, isA<HoldSegment>().having((h) => h.side, 'side', Side.right));
|
|
|
|
runner.back(at(4));
|
|
expect(runner.current, isA<HoldSegment>().having((h) => h.side, 'side', Side.left));
|
|
expect(runner.remaining(at(4)), const Duration(seconds: 30));
|
|
});
|
|
|
|
test('there is progress worth saving only once a hold has ended', () {
|
|
expect(runner.hasProgress, isFalse);
|
|
runner.tick(at(5));
|
|
// Skipping the left side is not progress; the right side starts after its switch at 15 s.
|
|
runner.skip(at(10));
|
|
runner.tick(at(15));
|
|
expect(runner.hasProgress, isFalse);
|
|
runner.tick(at(45));
|
|
expect(runner.hasProgress, isTrue);
|
|
});
|
|
|
|
test('ending early finishes incomplete', () {
|
|
runner.end(at(12));
|
|
|
|
expect(runner.isFinished, isTrue);
|
|
expect(runner.isComplete, isFalse);
|
|
expect(runner.result(at(12)).duration, const Duration(seconds: 12));
|
|
});
|
|
});
|
|
|
|
group('Workout runs at the user\'s pace', () {
|
|
final plan = _plan(Mode.workout, const [
|
|
WorkoutStep(exerciseId: 'Squat', sets: 2, target: WorkoutTarget.reps(10), weightKg: 60),
|
|
WorkoutStep(exerciseId: 'Plank', sets: 1, target: WorkoutTarget.seconds(30)),
|
|
]);
|
|
|
|
test('a rep set waits for "Set done", then the Rest counts up until the next set starts', () {
|
|
final runner = SessionRunner(plan, startedAt: t0);
|
|
|
|
runner.tick(at(600));
|
|
expect(runner.current, isA<SetSegment>());
|
|
|
|
runner.completeSet(reps: 8, weightKg: 62.5, now: at(40));
|
|
expect(runner.hasProgress, isTrue);
|
|
expect(runner.current, isA<RestSegment>());
|
|
expect(runner.elapsed(at(112)), const Duration(seconds: 72));
|
|
|
|
runner.startNext(at(120));
|
|
expect(runner.current, isA<SetSegment>().having((s) => s.set, 'set', 2));
|
|
});
|
|
|
|
test('a timed set starts on demand, counts down and logs itself', () {
|
|
var cues = 0;
|
|
final runner = SessionRunner(plan, startedAt: t0, onCue: () => cues++);
|
|
runner.completeSet(reps: 10, weightKg: 60, now: at(30));
|
|
runner.startNext(at(90));
|
|
runner.completeSet(reps: 10, weightKg: 60, now: at(120));
|
|
runner.startNext(at(180));
|
|
|
|
expect(runner.isWaitingToStart, isTrue);
|
|
runner.tick(at(500));
|
|
expect(runner.current, isA<SetSegment>());
|
|
|
|
runner.startTimer(at(500));
|
|
runner.tick(at(530));
|
|
|
|
expect(runner.isFinished, isTrue);
|
|
expect(cues, 1);
|
|
expect(runner.result(at(530)).setLogs.map((l) => (l.exerciseId, l.set, l.reps, l.seconds, l.weightKg)), [
|
|
('Squat', 1, 10, null, 60.0),
|
|
('Squat', 2, 10, null, 60.0),
|
|
('Plank', 1, null, 30, null),
|
|
]);
|
|
});
|
|
|
|
test('redoing a set after going back replaces its log', () {
|
|
final runner = SessionRunner(plan, startedAt: t0);
|
|
runner.completeSet(reps: 6, weightKg: 60, now: at(30));
|
|
|
|
runner.back(at(40));
|
|
runner.completeSet(reps: 10, weightKg: 60, now: at(60));
|
|
|
|
expect(runner.result(at(60)).setLogs.single.reps, 10);
|
|
});
|
|
|
|
test('a skipped set is not logged', () {
|
|
final runner = SessionRunner(plan, startedAt: t0);
|
|
runner.skip(at(10));
|
|
runner.skip(at(11));
|
|
|
|
expect(runner.current, isA<SetSegment>().having((s) => s.set, 'set', 2));
|
|
expect(runner.result(at(11)).setLogs, isEmpty);
|
|
});
|
|
});
|
|
}
|