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 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()); expect(runner.remaining(at(2)), const Duration(seconds: 3)); runner.tick(at(5)); expect(runner.current, isA().having((h) => h.side, 'side', Side.left)); runner.tick(at(35)); expect(runner.current, isA().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()); 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().having((h) => h.side, 'side', Side.left)); runner.skip(at(2)); runner.skip(at(3)); expect(runner.current, isA().having((h) => h.side, 'side', Side.right)); runner.back(at(4)); expect(runner.current, isA().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()); runner.completeSet(reps: 8, weightKg: 62.5, now: at(40)); expect(runner.hasProgress, isTrue); expect(runner.current, isA()); expect(runner.elapsed(at(112)), const Duration(seconds: 72)); runner.startNext(at(120)); expect(runner.current, isA().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()); 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().having((s) => s.set, 'set', 2)); expect(runner.result(at(11)).setLogs, isEmpty); }); }); }