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('Progression affiche la période par défaut et l’état vide global', ( tester, ) async { final repository = _FakeProgressionStatsRepository(); await tester.pumpWidget(_app(repository: repository)); await tester.pumpAndSettle(); expect(find.text('Progression'), findsOneWidget); expect(find.text('4 semaines'), findsOneWidget); expect(find.text('3 mois'), findsOneWidget); expect(find.text('Tout'), findsOneWidget); expect(find.text('Aucune progression à afficher'), findsOneWidget); expect( find.text( 'Termine une séance pour voir ton volume, ton rythme et tes résultats évoluer ici.', ), findsOneWidget, ); expect(find.text('Voir les séances'), findsOneWidget); expect(repository.periods, [ProgressionPeriod.fourWeeks]); }); testWidgets('Progression affiche Volume et Régularité', (tester) async { final repository = _FakeProgressionStatsRepository() ..overview = ProgressionGlobalStatsData( completedSessionCount: 8, totalActiveMs: 20400000, activeWeekStarts: [ DateTime.utc(2026, 7, 6), DateTime.utc(2026, 7, 13), DateTime.utc(2026, 7, 20), ], hasAnyCompletedHistory: true, firstCompletedSessionAt: DateTime.utc(2026, 7, 1), ); await tester.pumpWidget(_app(repository: repository)); await tester.pumpAndSettle(); expect(find.text('Volume'), findsOneWidget); expect(find.text('Séances'), findsOneWidget); expect(find.text('8'), findsOneWidget); expect(find.text('Temps total'), findsOneWidget); expect(find.text('5 h 40'), findsOneWidget); expect(find.text('Régularité'), findsOneWidget); expect(find.text('Rythme récent'), findsOneWidget); expect(find.text('3 / 4 semaines actives'), findsOneWidget); expect( find.text('Une semaine active contient au moins une séance terminée.'), findsOneWidget, ); }); testWidgets('Par exercice affiche un exercice archivé et un point unique', ( tester, ) async { final repository = _FakeProgressionStatsRepository() ..overview = ProgressionGlobalStatsData( completedSessionCount: 1, totalActiveMs: 1800000, activeWeekStarts: [DateTime.utc(2026, 7, 13)], hasAnyCompletedHistory: true, ) ..exerciseOptions = [ ProgressionExerciseOptionData( exerciseKey: 'shoot', nameSnapshot: 'Shoot ligne', isArchived: true, lastPerformedAt: DateTime.utc(2026, 7, 17), ), ] ..measuresByExercise = const { 'shoot': [ ProgressionMeasureOptionData( measure: ProgressionMeasure.reps, label: 'Répétitions', lowerIsBetter: false, ), ], } ..seriesByExerciseMeasure = { 'shoot:${ProgressionMeasure.reps.name}': ProgressionExerciseSeriesData( exerciseKey: 'shoot', exerciseNameSnapshot: 'Shoot ligne', measure: ProgressionMeasureOptionData( measure: ProgressionMeasure.reps, label: 'Répétitions', lowerIsBetter: false, ), points: [ ProgressionPointData( workoutHistoryId: 'history-1', workoutNameSnapshot: 'Prépa match', startedAt: DateTime.utc(2026, 7, 17), value: 45, rawValue: 45, ), ], hasAnyAllTimeData: true, hasAnyCompletedExerciseResult: true, ), }; await tester.pumpWidget(_app(repository: repository)); await tester.pumpAndSettle(); expect(find.text('Par exercice'), findsOneWidget); expect(find.text('Choisir un exercice'), findsOneWidget); expect(find.text('Shoot ligne'), findsOneWidget); expect(find.text('Exercice archivé'), findsOneWidget); expect(find.text('Encore un point de départ'), findsOneWidget); expect( find.text( 'Termine cet exercice dans une autre séance pour voir son évolution.', ), findsOneWidget, ); expect(find.text('Dernière séance : 45 répétitions'), findsOneWidget); }); testWidgets('Par exercice affiche Temps réalisé et Plus bas = mieux', ( tester, ) async { final repository = _progressionRepositoryWithSeries( measure: const ProgressionMeasureOptionData( measure: ProgressionMeasure.stopwatchScore, label: 'Temps réalisé', lowerIsBetter: true, ), rawValues: [45100, 42800], values: [45100, 42800], ); await tester.pumpWidget(_app(repository: repository)); await tester.pumpAndSettle(); expect(find.text('Temps réalisé'), findsOneWidget); expect(find.text('Plus bas = mieux'), findsOneWidget); expect(find.text('Meilleur : 00:42.8'), findsOneWidget); expect(find.text('Dernier : 00:42.8'), findsOneWidget); }); testWidgets('tap sur un point affiche le détail et ouvre la séance', ( tester, ) async { final historyRepository = _FakeWorkoutHistoryRepository() ..histories['history-2'] = _history(id: 'history-2'); final repository = _progressionRepositoryWithSeries( measure: const ProgressionMeasureOptionData( measure: ProgressionMeasure.manualScore, label: 'Score', scoreLabel: 'Réussites', scoreUnit: 'paniers', lowerIsBetter: false, ), rawValues: [12.0, 14.0], values: [12, 14], ); await tester.pumpWidget( _app(repository: repository, historyRepository: historyRepository), ); await tester.pumpAndSettle(); final pointFinder = find.byKey(const ValueKey('progression-point-1')); await tester.ensureVisible(pointFinder); await tester.pumpAndSettle(); await tester.tap(pointFinder); await tester.pumpAndSettle(); expect(find.text('12 juillet'), findsOneWidget); expect(find.text('Séance : Prépa match'), findsOneWidget); expect(find.text('Meilleur score : 14 paniers'), findsOneWidget); await tester.tap(find.text('Voir la séance')); await tester.pumpAndSettle(); expect(historyRepository.requestedIds, ['history-2']); expect(find.text('Séance jambes'), findsWidgets); }); } Widget _app({ required _FakeProgressionStatsRepository repository, _FakeWorkoutHistoryRepository? historyRepository, }) { final clock = _FakeClock(DateTime.utc(2026, 7, 22, 12)); final activeRepository = _FakeActiveSessionRepository(); final resolvedHistoryRepository = historyRepository ?? _FakeWorkoutHistoryRepository(); return MaterialApp( theme: courtBlazerLightTheme(), home: ProgressionScreen( progressionStatsUseCase: ProgressionStatsUseCase( repository: repository, clock: clock, ), historyUseCases: WorkoutHistoryUseCases( repository: resolvedHistoryRepository, clock: clock, ), workoutTemplateUseCases: WorkoutTemplateUseCases( templateRepository: _FakeWorkoutTemplateRepository(), programRepository: _FakeProgramRepository(), clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ), activeUseCases: ActiveWorkoutSessionUseCases( sessionRepository: activeRepository, templateRepository: _FakeWorkoutTemplateRepository(), clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ), closeUseCase: CloseWorkoutSessionUseCase( sessionRepository: activeRepository, historyRepository: resolvedHistoryRepository, clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ), ), ); } _FakeProgressionStatsRepository _progressionRepositoryWithSeries({ required ProgressionMeasureOptionData measure, required List rawValues, required List values, }) { return _FakeProgressionStatsRepository() ..overview = ProgressionGlobalStatsData( completedSessionCount: 2, totalActiveMs: 3600000, activeWeekStarts: [DateTime.utc(2026, 7, 6), DateTime.utc(2026, 7, 13)], hasAnyCompletedHistory: true, ) ..exerciseOptions = [ ProgressionExerciseOptionData( exerciseKey: 'shoot', nameSnapshot: 'Shoot ligne', isArchived: false, lastPerformedAt: DateTime.utc(2026, 7, 17), ), ] ..measuresByExercise = { 'shoot': [measure], } ..seriesByExerciseMeasure = { 'shoot:${measure.measure.name}': ProgressionExerciseSeriesData( exerciseKey: 'shoot', exerciseNameSnapshot: 'Shoot ligne', measure: measure, points: [ ProgressionPointData( workoutHistoryId: 'history-1', workoutNameSnapshot: 'Prépa match', startedAt: DateTime.utc(2026, 7, 5), value: values[0], rawValue: rawValues[0], ), ProgressionPointData( workoutHistoryId: 'history-2', workoutNameSnapshot: 'Prépa match', startedAt: DateTime.utc(2026, 7, 12), value: values[1], rawValue: rawValues[1], ), ], hasAnyAllTimeData: true, hasAnyCompletedExerciseResult: true, ), }; } final class _FakeProgressionStatsRepository implements ProgressionStatsRepository { ProgressionGlobalStatsData overview = const ProgressionGlobalStatsData( completedSessionCount: 0, totalActiveMs: 0, activeWeekStarts: [], hasAnyCompletedHistory: false, ); List exerciseOptions = const []; Map> measuresByExercise = const {}; Map seriesByExerciseMeasure = const {}; final periods = []; @override Future readGlobalStats( ProgressionDateRange range, ) async { periods.add( range.startedAt == null ? ProgressionPeriod.all : range.startedAt!.isAfter(DateTime.utc(2026, 5)) ? ProgressionPeriod.fourWeeks : ProgressionPeriod.threeMonths, ); return overview; } @override Future> listExerciseOptions( ProgressionDateRange range, ) async { return exerciseOptions; } @override Future> listMeasureOptions({ required ProgressionDateRange range, required String exerciseKey, }) async { return measuresByExercise[exerciseKey] ?? const []; } @override Future readExerciseSeries({ required ProgressionDateRange range, required String exerciseKey, required ProgressionMeasure measure, }) async { return seriesByExerciseMeasure['$exerciseKey:${measure.name}'] ?? ProgressionExerciseSeriesData( exerciseKey: exerciseKey, exerciseNameSnapshot: exerciseKey, measure: ProgressionMeasureOptionData( measure: measure, label: 'Score', lowerIsBetter: false, ), points: const [], hasAnyAllTimeData: false, hasAnyCompletedExerciseResult: false, ); } } WorkoutHistory _history({required String id}) { final start = DateTime.utc(2026, 7, 12, 10); return WorkoutHistory( metadata: _metadata(id), sourceWorkoutTemplateId: 'template-1', sourceActiveWorkoutSessionId: 'session-1', nameSnapshot: 'Séance jambes', startedAt: start, endedAt: start.add(const Duration(minutes: 30)), totalActiveMs: 1800000, completed: true, historySnapshotJson: jsonEncode({ 'sessionId': 'session-1', 'resolvedTemplateSnapshotJson': jsonEncode({ 'name': 'Séance jambes', 'programs': [ { 'id': 'program-snapshot-1', 'programNameSnapshot': 'Programme jambes', 'programSnapshotJson': jsonEncode({ 'exercises': [ { 'id': 'exercise-snapshot-1', 'exerciseNameSnapshot': 'Squat', 'setsCount': 1, 'timeEnabled': true, 'repsEnabled': true, 'scoreEnabled': true, 'targetTimeSeconds': 45, 'targetReps': 10, 'targetScore': 80, 'scoreInputModeSnapshot': ScoreInputMode.manual.name, 'scoreLabelSnapshot': 'Charge', 'scoreUnitSnapshot': 'kg', 'restSecondsOverride': 0, }, ], }), }, ], 'overrides': const [], }), 'results': [ { 'programSnapshotId': 'program-snapshot-1', 'exerciseSnapshotId': 'exercise-snapshot-1', 'programIndex': 0, 'exerciseIndex': 0, 'setIndex': 0, 'actualTimeMs': 45000, 'actualReps': 10, 'actualScore': 80, 'scoreLabelSnapshot': 'Charge', 'scoreInputModeSnapshot': ScoreInputMode.manual.name, 'scoreUnitSnapshot': 'kg', }, ], }), ); } 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); final DateTime value; @override DateTime now() => value; } final class _FakeIds implements IdGenerator { var _next = 0; @override String newId() { _next += 1; return 'id-$_next'; } } final class _FakeWorkoutHistoryRepository implements WorkoutHistoryRepository { final histories = {}; final requestedIds = []; @override Future delete(String id, DateTime deletedAt) async {} @override Future findById(String id) async { requestedIds.add(id); return histories[id]; } @override Future> listActive() async => histories.values.toList(); @override Future save(WorkoutHistory history) async {} @override Future saveSetResult(WorkoutHistorySetResult result) async {} @override Future saveStepResult(WorkoutHistoryStepResult result) async {} } 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 _FakeProgramRepository implements ProgramRepository { @override Future findById(String id) async => null; @override Future> listActive() async => const []; @override Future replaceExercises(Program program, DateTime deletedAt) async {} @override Future save(Program program) async {} @override Future saveExercise(ProgramExercise exercise) async {} } final class _FakeActiveSessionRepository implements ActiveSessionRepository { @override Future deleteScoreStopwatchState({ required String sessionId, required int programIndex, required int exerciseIndex, required int setIndex, required DateTime deletedAt, }) async {} @override Future findById(String id) async => null; @override Future findExerciseStepProgressState({ required String sessionId, required int programIndex, required int exerciseIndex, required int setIndex, }) async => null; @override Future findOpen() async => null; @override Future findRestStateById(String id) async => null; @override Future findScoreStopwatchState({ required String sessionId, required int programIndex, required int exerciseIndex, required int setIndex, }) async => null; @override Future findSetTimerState({ required String sessionId, required int programIndex, required int exerciseIndex, required int setIndex, }) async => null; @override Future> listExerciseStepProgressStates( String sessionId, ) async => const []; @override Future> listExerciseStepResults( String sessionId, ) async => const []; @override Future> listRestStates(String sessionId) async { return const []; } @override Future> listScoreStopwatchStates( String sessionId, ) async => const []; @override Future> listSetResults(String sessionId) async { return const []; } @override Future> listSetTimerStates(String sessionId) async { return const []; } @override Future save(ActiveWorkoutSession session) async {} @override Future saveExerciseStepProgressState( ActiveExerciseStepProgressState state, ) async {} @override Future saveExerciseStepResult(ActiveExerciseStepResult result) async {} @override Future saveRestState(ActiveRestState restState) async {} @override Future saveScoreStopwatchState(ActiveScoreStopwatchState state) async {} @override Future saveSetResult(ActiveSetResult result) async {} @override Future saveSetTimerState(ActiveSetTimerState state) async {} }