import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:gametime/application/application.dart'; import 'package:gametime/domain/domain.dart'; import 'package:gametime/presentation/presentation.dart'; void main() { testWidgets('le temps affiché est recalculé depuis les horodatages', ( tester, ) async { final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12, 0, 15)); final session = ActiveWorkoutSession( metadata: _metadata('session-1'), sourceWorkoutTemplateId: 'template-1', status: ActiveWorkoutStatus.running, startedAt: DateTime.utc(2026, 7, 17, 11, 59), lastPersistedAt: DateTime.utc(2026, 7, 17, 12), elapsedActiveMs: 30000, currentProgramIndex: 0, currentExerciseIndex: 0, currentSetIndex: 0, resolvedTemplateSnapshotJson: _sessionSnapshot(), ); await tester.pumpWidget( MaterialApp( home: WorkoutExecutionScreen( initialSession: session, activeUseCases: _activeUseCases( _FakeActiveSessionRepository(), clock, ), closeUseCase: _closeUseCase(_FakeActiveSessionRepository(), clock), historyUseCases: _historyUseCases(clock), workoutTemplateUseCases: _workoutTemplateUseCases(), ), ), ); expect(find.text('00:45'), findsOneWidget); }); testWidgets('les mesures cumulées respectent la hiérarchie visuelle', ( tester, ) async { final scoreController = TextEditingController(); addTearDown(scoreController.dispose); await tester.pumpWidget( MaterialApp( home: Scaffold( body: SetMeasureInput( exercise: const ExecutionExercise( id: 'exercise-1', name: 'Squat', setsCount: 3, timeEnabled: true, repsEnabled: true, scoreEnabled: true, restSeconds: 0, scoreUnit: 'kg', ), reps: 0, scoreController: scoreController, onRepsChanged: (_) {}, ), ), ), ); final timeY = tester.getTopLeft(find.text('Temps')).dy; final repsY = tester.getTopLeft(find.text('Répétitions')).dy; final scoreY = tester.getTopLeft(find.text('Score (kg)')).dy; expect(timeY, lessThan(repsY)); expect(repsY, lessThan(scoreY)); }); testWidgets('aucun repos ne s’affiche après la toute dernière série', ( tester, ) async { final plan = WorkoutExecutionPlan.fromSession( ActiveWorkoutSession( metadata: _metadata('session-1'), sourceWorkoutTemplateId: 'template-1', status: ActiveWorkoutStatus.running, startedAt: DateTime.utc(2026, 7, 17, 12), lastPersistedAt: DateTime.utc(2026, 7, 17, 12), elapsedActiveMs: 0, currentProgramIndex: 0, currentExerciseIndex: 0, currentSetIndex: 0, resolvedTemplateSnapshotJson: _sessionSnapshot( setsCount: 1, restSeconds: 60, ), ), ); const position = ExecutionPosition( programIndex: 0, exerciseIndex: 0, setIndex: 0, ); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Text( plan.shouldShowRestAfter(position) ? 'Repos' : 'Pas de repos', ), ), ); expect(find.text('Pas de repos'), findsOneWidget); }); testWidgets('le bouton médias est absent sans média', (tester) async { final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); final session = ActiveWorkoutSession( metadata: _metadata('session-1'), sourceWorkoutTemplateId: 'template-1', status: ActiveWorkoutStatus.running, startedAt: DateTime.utc(2026, 7, 17, 12), lastPersistedAt: DateTime.utc(2026, 7, 17, 12), elapsedActiveMs: 0, currentProgramIndex: 0, currentExerciseIndex: 0, currentSetIndex: 0, resolvedTemplateSnapshotJson: _sessionSnapshot(), ); await tester.pumpWidget( MaterialApp( home: WorkoutExecutionScreen( initialSession: session, activeUseCases: _activeUseCases( _FakeActiveSessionRepository(), clock, ), closeUseCase: _closeUseCase(_FakeActiveSessionRepository(), clock), historyUseCases: _historyUseCases(clock), workoutTemplateUseCases: _workoutTemplateUseCases(), ), ), ); expect(find.text('Voir médias'), findsNothing); expect(find.byTooltip('Voir les médias de l’exercice'), findsNothing); }); testWidgets('le bouton médias ouvre la sheet si médias présents', ( tester, ) async { final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); final session = ActiveWorkoutSession( metadata: _metadata('session-1'), sourceWorkoutTemplateId: 'template-1', status: ActiveWorkoutStatus.running, startedAt: DateTime.utc(2026, 7, 17, 12), lastPersistedAt: DateTime.utc(2026, 7, 17, 12), elapsedActiveMs: 0, currentProgramIndex: 0, currentExerciseIndex: 0, currentSetIndex: 0, resolvedTemplateSnapshotJson: _sessionSnapshot( imageMediaIds: const ['image-1'], videoMediaId: 'video-1', ), ); await tester.pumpWidget( MaterialApp( home: WorkoutExecutionScreen( initialSession: session, activeUseCases: _activeUseCases( _FakeActiveSessionRepository(), clock, ), closeUseCase: _closeUseCase(_FakeActiveSessionRepository(), clock), historyUseCases: _historyUseCases(clock), workoutTemplateUseCases: _workoutTemplateUseCases(), ), ), ); expect(find.byTooltip('Voir les médias de l’exercice'), findsOneWidget); await tester.tap(find.text('Voir médias')); await tester.pumpAndSettle(); expect(find.text('Médias de l’exercice'), findsOneWidget); expect(find.text('Images'), findsOneWidget); expect(find.text('Vidéo'), findsOneWidget); expect(find.text('1/1'), findsOneWidget); expect(find.text('Image 1'), findsOneWidget); }); testWidgets('la galerie médias navigue entre les images', (tester) async { final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); final session = ActiveWorkoutSession( metadata: _metadata('session-1'), sourceWorkoutTemplateId: 'template-1', status: ActiveWorkoutStatus.running, startedAt: DateTime.utc(2026, 7, 17, 12), lastPersistedAt: DateTime.utc(2026, 7, 17, 12), elapsedActiveMs: 0, currentProgramIndex: 0, currentExerciseIndex: 0, currentSetIndex: 0, resolvedTemplateSnapshotJson: _sessionSnapshot( imageMediaIds: const ['image-1', 'image-2', 'image-3'], ), ); await tester.pumpWidget( MaterialApp( home: WorkoutExecutionScreen( initialSession: session, activeUseCases: _activeUseCases( _FakeActiveSessionRepository(), clock, ), closeUseCase: _closeUseCase(_FakeActiveSessionRepository(), clock), historyUseCases: _historyUseCases(clock), workoutTemplateUseCases: _workoutTemplateUseCases(), ), ), ); await tester.tap(find.text('Voir médias')); await tester.pumpAndSettle(); expect(find.text('1/3'), findsOneWidget); await tester.drag(find.byType(PageView), const Offset(-400, 0)); await tester.pumpAndSettle(); expect(find.text('2/3'), findsOneWidget); expect(find.text('Image 2'), findsOneWidget); }); testWidgets('le chrono score démarre et s’arrête avec la bonne durée', ( tester, ) async { await tester.binding.setSurfaceSize(const Size(400, 1400)); addTearDown(() => tester.binding.setSurfaceSize(null)); final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); final repository = _FakeActiveSessionRepository(); final session = ActiveWorkoutSession( metadata: _metadata('session-1'), sourceWorkoutTemplateId: 'template-1', status: ActiveWorkoutStatus.running, startedAt: DateTime.utc(2026, 7, 17, 12), lastPersistedAt: DateTime.utc(2026, 7, 17, 12), elapsedActiveMs: 0, currentProgramIndex: 0, currentExerciseIndex: 0, currentSetIndex: 0, resolvedTemplateSnapshotJson: _sessionSnapshot( scoreInputMode: ScoreInputMode.stopwatch, ), ); repository.session = session; await tester.pumpWidget( MaterialApp( home: WorkoutExecutionScreen( initialSession: session, activeUseCases: _activeUseCases(repository, clock), closeUseCase: _closeUseCase(repository, clock), historyUseCases: _historyUseCases(clock), workoutTemplateUseCases: _workoutTemplateUseCases(), ), ), ); expect(find.text('Chrono score'), findsOneWidget); expect(find.text('00:00.0'), findsOneWidget); await tester.tap(find.text('Démarrer')); await tester.pump(); clock.value = DateTime.utc(2026, 7, 17, 12, 0, 1, 200); await tester.pump(const Duration(seconds: 1)); expect(find.text('00:01.2'), findsOneWidget); await tester.tap(find.text('Arrêter')); await tester.pump(); expect(find.text('00:01.2'), findsOneWidget); expect( repository.scoreStopwatchStates.single.status, ActiveScoreStopwatchStatus.stopped, ); }); testWidgets('terminer sans chrono démarré affiche une confirmation', ( tester, ) async { await tester.binding.setSurfaceSize(const Size(400, 1400)); addTearDown(() => tester.binding.setSurfaceSize(null)); final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); final repository = _FakeActiveSessionRepository(); final session = ActiveWorkoutSession( metadata: _metadata('session-1'), sourceWorkoutTemplateId: 'template-1', status: ActiveWorkoutStatus.running, startedAt: DateTime.utc(2026, 7, 17, 12), lastPersistedAt: DateTime.utc(2026, 7, 17, 12), elapsedActiveMs: 0, currentProgramIndex: 0, currentExerciseIndex: 0, currentSetIndex: 0, resolvedTemplateSnapshotJson: _sessionSnapshot( scoreInputMode: ScoreInputMode.stopwatch, ), ); repository.session = session; await tester.pumpWidget( MaterialApp( home: WorkoutExecutionScreen( initialSession: session, activeUseCases: _activeUseCases(repository, clock), closeUseCase: _closeUseCase(repository, clock), historyUseCases: _historyUseCases(clock), workoutTemplateUseCases: _workoutTemplateUseCases(), ), ), ); await tester.tap(find.text('Terminer la série')); await tester.pumpAndSettle(); expect(find.text('Aucun temps chronométré'), findsOneWidget); expect(find.text("Tu n'as pas démarré le chrono score."), findsOneWidget); expect(find.text('Démarrer le chrono'), findsOneWidget); expect(find.text('Terminer sans chrono'), findsOneWidget); expect(repository.results, isEmpty); }); testWidgets('terminer avec chrono en cours l’arrête et enregistre', ( tester, ) async { await tester.binding.setSurfaceSize(const Size(400, 1400)); addTearDown(() => tester.binding.setSurfaceSize(null)); final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); final repository = _FakeActiveSessionRepository(); final session = ActiveWorkoutSession( metadata: _metadata('session-1'), sourceWorkoutTemplateId: 'template-1', status: ActiveWorkoutStatus.running, startedAt: DateTime.utc(2026, 7, 17, 12), lastPersistedAt: DateTime.utc(2026, 7, 17, 12), elapsedActiveMs: 0, currentProgramIndex: 0, currentExerciseIndex: 0, currentSetIndex: 0, resolvedTemplateSnapshotJson: _sessionSnapshot( setsCount: 2, scoreInputMode: ScoreInputMode.stopwatch, ), ); repository.session = session; await tester.pumpWidget( MaterialApp( home: WorkoutExecutionScreen( initialSession: session, activeUseCases: _activeUseCases(repository, clock), closeUseCase: _closeUseCase(repository, clock), historyUseCases: _historyUseCases(clock), workoutTemplateUseCases: _workoutTemplateUseCases(), ), ), ); await tester.tap(find.text('Démarrer')); await tester.pump(); clock.value = DateTime.utc(2026, 7, 17, 12, 0, 2, 500); await tester.pump(const Duration(seconds: 1)); await tester.tap(find.text('Terminer la série')); await tester.pumpAndSettle(); expect(repository.results.single.actualScoreTimeMs, 2500); expect( repository.scoreStopwatchStates.single.status, ActiveScoreStopwatchStatus.stopped, ); expect(repository.session?.currentSetIndex, 1); }); testWidgets('un repos actif est repris avec le temps restant recalculé', ( tester, ) async { final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); final repository = _FakeActiveSessionRepository(); final session = ActiveWorkoutSession( metadata: _metadata('session-1'), sourceWorkoutTemplateId: 'template-1', status: ActiveWorkoutStatus.running, startedAt: DateTime.utc(2026, 7, 17, 12), lastPersistedAt: DateTime.utc(2026, 7, 17, 12), elapsedActiveMs: 0, currentProgramIndex: 0, currentExerciseIndex: 0, currentSetIndex: 0, resolvedTemplateSnapshotJson: _sessionSnapshot( setsCount: 2, restSeconds: 60, ), ); repository.session = session; repository.restStates.add( ActiveRestState( metadata: _metadata('rest-1'), activeWorkoutSessionId: 'session-1', afterProgramIndex: 0, afterExerciseIndex: 0, afterSetIndex: 0, plannedRestSeconds: 60, adjustedRestSeconds: 60, startedAt: DateTime.now().toUtc().subtract(const Duration(seconds: 15)), ), ); await tester.pumpWidget( MaterialApp( home: WorkoutExecutionScreen( initialSession: session, activeUseCases: _activeUseCases(repository, clock), closeUseCase: _closeUseCase(repository, clock), historyUseCases: _historyUseCases(clock), workoutTemplateUseCases: _workoutTemplateUseCases(), ), ), ); await tester.pump(); expect(find.text('Repos avant la prochaine série'), findsOneWidget); expect(find.text('00:45'), findsOneWidget); expect(find.text('Série 2 / 2'), findsOneWidget); await tester.tap(find.text('+15 s')); await tester.pump(); expect(find.text('01:00'), findsOneWidget); expect(repository.restStates.single.adjustedRestSeconds, 75); await tester.tap(find.text('Ignorer le repos')); await tester.pump(); expect(repository.restStates.single.skippedAt, isNotNull); }); testWidgets('la mesure temps enregistre une durée réelle de série', ( tester, ) async { await tester.binding.setSurfaceSize(const Size(400, 1400)); addTearDown(() => tester.binding.setSurfaceSize(null)); final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); final repository = _FakeActiveSessionRepository(); final session = ActiveWorkoutSession( metadata: _metadata('session-1'), sourceWorkoutTemplateId: 'template-1', status: ActiveWorkoutStatus.running, startedAt: DateTime.utc(2026, 7, 17, 12), lastPersistedAt: DateTime.utc(2026, 7, 17, 12), elapsedActiveMs: 0, currentProgramIndex: 0, currentExerciseIndex: 0, currentSetIndex: 0, resolvedTemplateSnapshotJson: _sessionSnapshot( setsCount: 2, restSeconds: 0, ), ); repository.session = session; await tester.pumpWidget( MaterialApp( home: WorkoutExecutionScreen( initialSession: session, activeUseCases: _activeUseCases(repository, clock), closeUseCase: _closeUseCase(repository, clock), historyUseCases: _historyUseCases(clock), workoutTemplateUseCases: _workoutTemplateUseCases(), ), ), ); await tester.pump(const Duration(seconds: 2)); await tester.tap(find.text('Terminer la série')); await tester.pump(); expect(repository.results.single.actualTimeMs, greaterThan(0)); }); testWidgets( 'terminer la série 2 avec temps répétitions et score manuel avance', (tester) async { await tester.binding.setSurfaceSize(const Size(400, 1400)); addTearDown(() => tester.binding.setSurfaceSize(null)); final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); final repository = _FakeActiveSessionRepository(); final session = ActiveWorkoutSession( metadata: _metadata('session-1'), sourceWorkoutTemplateId: 'template-1', status: ActiveWorkoutStatus.running, startedAt: DateTime.utc(2026, 7, 17, 12), lastPersistedAt: DateTime.utc(2026, 7, 17, 12), elapsedActiveMs: 0, currentProgramIndex: 0, currentExerciseIndex: 0, currentSetIndex: 1, resolvedTemplateSnapshotJson: _sessionSnapshot( setsCount: 3, restSeconds: 0, targetTimeSeconds: 30, targetReps: 5, targetScore: 10, scoreUnit: 'points', ), ); repository.session = session; await tester.pumpWidget( MaterialApp( home: WorkoutExecutionScreen( initialSession: session, activeUseCases: _activeUseCases(repository, clock), closeUseCase: _closeUseCase(repository, clock), historyUseCases: _historyUseCases(clock), workoutTemplateUseCases: _workoutTemplateUseCases(), ), ), ); expect(find.text('Programme 1/1 · Exercice 1/1'), findsOneWidget); expect(find.text('SÉRIE'), findsOneWidget); expect(find.text('2 / 3'), findsOneWidget); expect(find.text('30 s'), findsOneWidget); for (var i = 0; i < 3; i++) { await tester.tap(find.byIcon(Icons.add)); await tester.pump(); } await tester.enterText( find.widgetWithText(TextField, 'Score (points)'), '5', ); await tester.ensureVisible(find.text('Terminer la série')); await tester.pumpAndSettle(); await tester.tap(find.text('Terminer la série')); await tester.pumpAndSettle(); expect(repository.results.single.setIndex, 1); expect(repository.results.single.actualReps, 3); expect(repository.results.single.actualScore, 5); expect(repository.session?.currentSetIndex, 2); expect(find.text('Programme 1/1 · Exercice 1/1'), findsOneWidget); expect(find.text('3 / 3'), findsOneWidget); }, ); testWidgets('la flèche de retour met la séance active en pause', ( tester, ) async { final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); final repository = _FakeActiveSessionRepository(); final session = ActiveWorkoutSession( metadata: _metadata('session-1'), sourceWorkoutTemplateId: 'template-1', status: ActiveWorkoutStatus.running, startedAt: DateTime.utc(2026, 7, 17, 12), lastPersistedAt: DateTime.utc(2026, 7, 17, 12), elapsedActiveMs: 0, currentProgramIndex: 0, currentExerciseIndex: 0, currentSetIndex: 0, resolvedTemplateSnapshotJson: _sessionSnapshot( setsCount: 2, restSeconds: 0, ), ); repository.session = session; await tester.pumpWidget( MaterialApp( home: WorkoutExecutionScreen( initialSession: session, activeUseCases: _activeUseCases(repository, clock), closeUseCase: _closeUseCase(repository, clock), historyUseCases: _historyUseCases(clock), workoutTemplateUseCases: _workoutTemplateUseCases(), ), ), ); await tester.tap(find.byTooltip('Mettre en pause')); await tester.pumpAndSettle(); expect(find.text('Reprendre'), findsOneWidget); expect(repository.session?.status, ActiveWorkoutStatus.paused); }); testWidgets('le plan de séance affiche les états des séries', (tester) async { final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); final repository = _FakeActiveSessionRepository(); final session = ActiveWorkoutSession( metadata: _metadata('session-1'), sourceWorkoutTemplateId: 'template-1', status: ActiveWorkoutStatus.running, startedAt: DateTime.utc(2026, 7, 17, 12), lastPersistedAt: DateTime.utc(2026, 7, 17, 12), elapsedActiveMs: 0, currentProgramIndex: 0, currentExerciseIndex: 0, currentSetIndex: 2, resolvedTemplateSnapshotJson: _sessionSnapshot( setsCount: 4, restSeconds: 0, ), ); repository.session = session; repository.results.add( ActiveSetResult( metadata: _metadata('result-1'), activeWorkoutSessionId: 'session-1', programSnapshotId: 'template-program-1', exerciseSnapshotId: 'exercise-1', programIndex: 0, exerciseIndex: 0, setIndex: 0, actualReps: 10, actualScore: 7, scoreUnitSnapshot: 'paniers', ), ); await tester.pumpWidget( MaterialApp( home: WorkoutExecutionScreen( initialSession: session, activeUseCases: _activeUseCases(repository, clock), closeUseCase: _closeUseCase(repository, clock), historyUseCases: _historyUseCases(clock), workoutTemplateUseCases: _workoutTemplateUseCases(), ), ), ); await tester.tap(find.byTooltip('Voir le plan')); await tester.pumpAndSettle(); expect(find.text('Plan de séance'), findsOneWidget); expect(find.text('Terminée'), findsOneWidget); expect(find.text('10 reps · 7 paniers'), findsOneWidget); expect(find.text('Passée'), findsOneWidget); expect(find.text('Aucun résultat'), findsOneWidget); expect(find.text('En cours'), findsOneWidget); expect(find.text('À faire'), findsOneWidget); await tester.tap(find.text('En cours')); await tester.pumpAndSettle(); expect(find.text('Plan de séance'), findsNothing); }); testWidgets('le plan affiche le score chrono formaté avec objectif', ( tester, ) async { final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); final repository = _FakeActiveSessionRepository(); final session = ActiveWorkoutSession( metadata: _metadata('session-1'), sourceWorkoutTemplateId: 'template-1', status: ActiveWorkoutStatus.running, startedAt: DateTime.utc(2026, 7, 17, 12), lastPersistedAt: DateTime.utc(2026, 7, 17, 12), elapsedActiveMs: 0, currentProgramIndex: 0, currentExerciseIndex: 0, currentSetIndex: 1, resolvedTemplateSnapshotJson: _sessionSnapshot( setsCount: 2, restSeconds: 0, scoreInputMode: ScoreInputMode.stopwatch, targetScoreTimeMs: 45000, ), ); repository.session = session; repository.results.add( ActiveSetResult( metadata: _metadata('result-1'), activeWorkoutSessionId: 'session-1', programSnapshotId: 'template-program-1', exerciseSnapshotId: 'exercise-1', programIndex: 0, exerciseIndex: 0, setIndex: 0, actualScoreTimeMs: 38700, scoreInputModeSnapshot: ScoreInputMode.stopwatch, ), ); await tester.pumpWidget( MaterialApp( home: WorkoutExecutionScreen( initialSession: session, activeUseCases: _activeUseCases(repository, clock), closeUseCase: _closeUseCase(repository, clock), historyUseCases: _historyUseCases(clock), workoutTemplateUseCases: _workoutTemplateUseCases(), ), ), ); await tester.tap(find.byTooltip('Voir le plan')); await tester.pumpAndSettle(); expect(find.text('Score chrono 00:38.7 / objectif 00:45'), findsOneWidget); expect(find.textContaining('Temps réalisé 00:38.7'), findsNothing); }); testWidgets( 'modifier une série passée la termine sans changer la position courante', (tester) async { final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); final repository = _FakeActiveSessionRepository(); final session = ActiveWorkoutSession( metadata: _metadata('session-1'), sourceWorkoutTemplateId: 'template-1', status: ActiveWorkoutStatus.running, startedAt: DateTime.utc(2026, 7, 17, 12), lastPersistedAt: DateTime.utc(2026, 7, 17, 12), elapsedActiveMs: 0, currentProgramIndex: 0, currentExerciseIndex: 0, currentSetIndex: 2, resolvedTemplateSnapshotJson: _sessionSnapshot( setsCount: 4, restSeconds: 0, ), ); repository.session = session; await tester.pumpWidget( MaterialApp( home: WorkoutExecutionScreen( initialSession: session, activeUseCases: _activeUseCases(repository, clock), closeUseCase: _closeUseCase(repository, clock), historyUseCases: _historyUseCases(clock), workoutTemplateUseCases: _workoutTemplateUseCases(), ), ), ); await tester.tap(find.byTooltip('Voir le plan')); await tester.pumpAndSettle(); await tester.tap(find.widgetWithText(ListTile, 'Série 2')); await tester.pumpAndSettle(); expect(find.text('Modifier la série'), findsOneWidget); await tester.enterText(_textFieldWithLabel('Répétitions'), '12'); await tester.ensureVisible(find.text('Enregistrer le résultat')); await tester.tap(find.text('Enregistrer le résultat')); await tester.pumpAndSettle(); expect(repository.session?.currentSetIndex, 2); expect(repository.results.single.status, SetResultStatus.completed); expect(repository.results.single.actualReps, 12); expect(find.text('Série mise à jour'), findsWidgets); expect(find.text('12 reps'), findsOneWidget); await tester.tap(find.byTooltip('Fermer')); await tester.pumpAndSettle(); expect(find.text('Programme 1/1 · Exercice 1/1'), findsOneWidget); expect(find.text('3 / 4'), findsOneWidget); }, ); testWidgets( 'toucher la série courante dans le plan revient à l’écran principal', (tester) async { final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); final repository = _FakeActiveSessionRepository(); final session = ActiveWorkoutSession( metadata: _metadata('session-1'), sourceWorkoutTemplateId: 'template-1', status: ActiveWorkoutStatus.running, startedAt: DateTime.utc(2026, 7, 17, 12), lastPersistedAt: DateTime.utc(2026, 7, 17, 12), elapsedActiveMs: 0, currentProgramIndex: 0, currentExerciseIndex: 0, currentSetIndex: 1, resolvedTemplateSnapshotJson: _sessionSnapshot( setsCount: 3, restSeconds: 0, ), ); repository.session = session; await tester.pumpWidget( MaterialApp( home: WorkoutExecutionScreen( initialSession: session, activeUseCases: _activeUseCases(repository, clock), closeUseCase: _closeUseCase(repository, clock), historyUseCases: _historyUseCases(clock), workoutTemplateUseCases: _workoutTemplateUseCases(), ), ), ); await tester.tap(find.byTooltip('Voir le plan')); await tester.pumpAndSettle(); await tester.tap(find.text('En cours')); await tester.pumpAndSettle(); expect(find.text('Plan de séance'), findsNothing); expect(find.text('Modifier la série'), findsNothing); expect(find.text('Programme 1/1 · Exercice 1/1'), findsOneWidget); expect(find.text('2 / 3'), findsOneWidget); }, ); } Finder _textFieldWithLabel(String label) { return find.byWidgetPredicate( (widget) => widget is TextField && widget.decoration?.labelText == label, description: 'TextField with label "$label"', ); } ActiveWorkoutSessionUseCases _activeUseCases( _FakeActiveSessionRepository repository, _FakeClock clock, ) { return ActiveWorkoutSessionUseCases( sessionRepository: repository, templateRepository: _FakeWorkoutTemplateRepository(), clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ); } CloseWorkoutSessionUseCase _closeUseCase( _FakeActiveSessionRepository repository, _FakeClock clock, ) { return CloseWorkoutSessionUseCase( sessionRepository: repository, historyRepository: _FakeWorkoutHistoryRepository(), clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ); } WorkoutHistoryUseCases _historyUseCases(_FakeClock clock) { return WorkoutHistoryUseCases( repository: _FakeWorkoutHistoryRepository(), clock: clock, ); } WorkoutTemplateUseCases _workoutTemplateUseCases() { return WorkoutTemplateUseCases( templateRepository: _FakeWorkoutTemplateRepository(), programRepository: _FakeProgramRepository(), clock: _FakeClock(DateTime.utc(2026, 7, 17)), ids: _FakeIds(), originDeviceId: 'device-1', ); } String _sessionSnapshot({ int setsCount = 3, int restSeconds = 0, int targetTimeSeconds = 45, int targetReps = 10, double targetScore = 80, int? targetScoreTimeMs, String scoreUnit = 'kg', ScoreInputMode scoreInputMode = ScoreInputMode.manual, List imageMediaIds = const [], String? videoMediaId, }) { return jsonEncode({ 'name': 'Séance jambes', 'programs': [ { 'id': 'template-program-1', 'programNameSnapshot': 'Programme jambes', 'programSnapshotJson': jsonEncode({ 'exercises': [ { 'id': 'exercise-1', 'exerciseNameSnapshot': 'Squat', 'setsCount': setsCount, 'timeEnabled': true, 'repsEnabled': true, 'scoreEnabled': true, 'targetTimeSeconds': targetTimeSeconds, 'targetReps': targetReps, 'targetScore': targetScore, 'targetScoreTimeMs': targetScoreTimeMs, 'scoreInputModeSnapshot': scoreInputMode.name, 'scoreLabelSnapshot': 'Score', 'scoreUnitSnapshot': scoreUnit, 'restSecondsOverride': restSeconds, 'exerciseImageMediaIdsSnapshot': imageMediaIds, 'imageMediaIdsSnapshot': imageMediaIds, 'exerciseVideoMediaIdSnapshot': videoMediaId, 'videoMediaIdSnapshot': videoMediaId, }, ], }), }, ], 'overrides': const [], }); } EntityMetadata _metadata(String id) { return EntityMetadata( id: id, createdAt: DateTime.utc(2026, 7, 17), updatedAt: DateTime.utc(2026, 7, 17), originDeviceId: 'device-1', ); } final class _FakeClock implements Clock { _FakeClock(this.value); DateTime value; @override DateTime now() => value; } final class _FakeIds implements IdGenerator { var _next = 0; @override String newId() { _next += 1; return 'id-$_next'; } } final class _FakeActiveSessionRepository implements ActiveSessionRepository { ActiveWorkoutSession? session; final results = []; final restStates = []; final scoreStopwatchStates = []; @override Future findById(String id) async { return session?.metadata.id == id ? session : null; } @override Future findOpen() async => session; @override Future findRestStateById(String id) async { for (final restState in restStates) { if (restState.metadata.id == id) { return restState; } } return null; } @override Future findScoreStopwatchState({ required String sessionId, required int programIndex, required int exerciseIndex, required int setIndex, }) async { for (final state in scoreStopwatchStates) { if (state.activeWorkoutSessionId == sessionId && state.programIndex == programIndex && state.exerciseIndex == exerciseIndex && state.setIndex == setIndex && state.metadata.deletedAt == null) { return state; } } return null; } @override Future> listRestStates(String sessionId) async { return restStates .where((restState) => restState.activeWorkoutSessionId == sessionId) .toList(); } @override Future> listScoreStopwatchStates( String sessionId, ) async { return scoreStopwatchStates .where((state) => state.activeWorkoutSessionId == sessionId) .toList(); } @override Future> listSetResults(String sessionId) async { return results; } @override Future save(ActiveWorkoutSession session) async { this.session = session; } @override Future saveRestState(ActiveRestState restState) async { final index = restStates.indexWhere( (saved) => saved.metadata.id == restState.metadata.id, ); if (index == -1) { restStates.add(restState); } else { restStates[index] = restState; } } @override Future saveScoreStopwatchState(ActiveScoreStopwatchState state) async { final index = scoreStopwatchStates.indexWhere( (saved) => saved.metadata.id == state.metadata.id, ); if (index == -1) { scoreStopwatchStates.add(state); } else { scoreStopwatchStates[index] = state; } } @override Future deleteScoreStopwatchState({ required String sessionId, required int programIndex, required int exerciseIndex, required int setIndex, required DateTime deletedAt, }) async { scoreStopwatchStates.removeWhere( (state) => state.activeWorkoutSessionId == sessionId && state.programIndex == programIndex && state.exerciseIndex == exerciseIndex && state.setIndex == setIndex, ); } @override Future saveSetResult(ActiveSetResult result) async { final index = results.indexWhere( (saved) => saved.activeWorkoutSessionId == result.activeWorkoutSessionId && saved.programIndex == result.programIndex && saved.exerciseIndex == result.exerciseIndex && saved.setIndex == result.setIndex, ); if (index == -1) { results.add(result); } else { results[index] = result; } } } final class _FakeWorkoutTemplateRepository implements WorkoutTemplateRepository { @override Future findById(String id) async => null; @override Future> listActive() async => const []; @override Future replaceComposition( WorkoutTemplate template, DateTime deletedAt, ) async {} @override Future save(WorkoutTemplate template) async {} @override Future saveOverride(WorkoutTemplateExerciseOverride override) async {} @override Future saveProgram(WorkoutTemplateProgram program) async {} } final class _FakeWorkoutHistoryRepository implements WorkoutHistoryRepository { @override Future delete(String id, DateTime deletedAt) async {} @override Future findById(String id) async => null; @override Future> listActive() async => const []; @override Future save(WorkoutHistory history) async {} @override Future saveSetResult(WorkoutHistorySetResult result) async {} } final class _FakeProgramRepository implements ProgramRepository { @override Future replaceExercises(Program program, DateTime deletedAt) async {} @override Future findById(String id) async => null; @override Future> listActive() async => const []; @override Future save(Program program) async {} @override Future saveExercise(ProgramExercise exercise) async {} }