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( 'ajoute un exercice au programme avec sa configuration par défaut', (tester) async { final programRepository = _FakeProgramRepository(); final exerciseRepository = _FakeExerciseRepository() ..exercises.add( Exercise( metadata: _metadata('exercise-1'), name: 'Squat', hasTimeMeasure: true, hasRepsMeasure: true, hasScoreMeasure: false, defaultTargetTimeSeconds: 45, defaultTargetReps: 10, ), ); await tester.pumpWidget( MaterialApp( home: ProgramFormScreen( programUseCases: _programUseCases( programRepository, exerciseRepository, ), exerciseUseCases: _exerciseUseCases(exerciseRepository), ), ), ); await tester.enterText( find.widgetWithText(TextFormField, 'Nom'), 'Jambes', ); await tester.tap(find.text('Ajouter un exercice')); await tester.pumpAndSettle(); await tester.tap(find.text('Squat')); await tester.pumpAndSettle(); expect(find.text('Squat'), findsOneWidget); expect(find.text('Repos 60 s'), findsOneWidget); expect(find.text('Exercice ajouté'), findsOneWidget); expect(find.byTooltip("Personnaliser l'exercice"), findsOneWidget); expect(find.byType(CheckboxListTile), findsNothing); await tester.tap(find.text('Enregistrer')); await tester.pumpAndSettle(); final savedExercise = programRepository.programs.single.exercises.single; expect(savedExercise.setsCount, 3); expect(savedExercise.timeEnabled, isTrue); expect(savedExercise.repsEnabled, isTrue); expect(savedExercise.scoreEnabled, isFalse); expect(savedExercise.targetTimeSeconds, 45); expect(savedExercise.targetReps, 10); expect(savedExercise.restSecondsOverride, 60); }, ); testWidgets( 'limite les mesures aux mesures autorisées par l’exercice source', (tester) async { final exerciseRepository = _FakeExerciseRepository() ..exercises.add( Exercise( metadata: _metadata('exercise-1'), name: 'Pompes', hasTimeMeasure: false, hasRepsMeasure: true, hasScoreMeasure: false, defaultTargetReps: 12, ), ); await tester.pumpWidget( MaterialApp( home: ProgramFormScreen( programUseCases: _programUseCases( _FakeProgramRepository(), exerciseRepository, ), exerciseUseCases: _exerciseUseCases(exerciseRepository), ), ), ); await tester.tap(find.text('Ajouter un exercice')); await tester.pumpAndSettle(); await tester.tap(find.text('Pompes')); await tester.pumpAndSettle(); await tester.tap(find.byTooltip("Personnaliser l'exercice")); await tester.pumpAndSettle(); expect(find.text("Personnaliser l'exercice"), findsOneWidget); expect( find.widgetWithText(SwitchListTile, 'Répétitions'), findsOneWidget, ); expect(find.widgetWithText(SwitchListTile, 'Temps'), findsNothing); expect(find.widgetWithText(SwitchListTile, 'Score'), findsNothing); }, ); testWidgets( 'la popup exercice ajouté disparaît et se ferme à la personnalisation', (tester) async { final exerciseRepository = _FakeExerciseRepository() ..exercises.add( Exercise( metadata: _metadata('exercise-1'), name: 'Squat', hasTimeMeasure: true, hasRepsMeasure: false, hasScoreMeasure: false, defaultTargetTimeSeconds: 45, ), ); await tester.pumpWidget( MaterialApp( home: ProgramFormScreen( programUseCases: _programUseCases( _FakeProgramRepository(), exerciseRepository, ), exerciseUseCases: _exerciseUseCases(exerciseRepository), ), ), ); await tester.tap(find.text('Ajouter un exercice')); await tester.pumpAndSettle(); await tester.tap(find.text('Squat')); await tester.pumpAndSettle(); expect(find.text('Exercice ajouté'), findsOneWidget); for (var i = 0; i < 5; i++) { await tester.pump(const Duration(seconds: 1)); } await tester.pump(const Duration(milliseconds: 500)); expect(find.text('Exercice ajouté'), findsNothing); await tester.tap(find.text('Ajouter un exercice')); await tester.pumpAndSettle(); await tester.tap(find.text('Squat').first); await tester.pumpAndSettle(); expect(find.text('Exercice ajouté'), findsOneWidget); await tester.tap(find.text('Personnaliser')); await tester.pumpAndSettle(); expect(find.text('Exercice ajouté'), findsNothing); expect(find.text("Personnaliser l'exercice"), findsOneWidget); }, ); testWidgets('affiche l’objectif de chrono pour un score en mode chrono', ( tester, ) async { final exerciseRepository = _FakeExerciseRepository() ..exercises.add( Exercise( metadata: _metadata('exercise-1'), name: 'Suicides', hasTimeMeasure: true, hasRepsMeasure: true, hasScoreMeasure: true, scoreInputMode: ScoreInputMode.stopwatch, scoreLabel: 'Temps réalisé', defaultTargetTimeSeconds: 30, defaultTargetReps: 5, defaultTargetScoreTimeMs: 12000, ), ); await tester.pumpWidget( MaterialApp( home: ProgramFormScreen( programUseCases: _programUseCases( _FakeProgramRepository(), exerciseRepository, ), exerciseUseCases: _exerciseUseCases(exerciseRepository), ), ), ); await tester.tap(find.text('Ajouter un exercice')); await tester.pumpAndSettle(); await tester.tap(find.text('Suicides')); await tester.pumpAndSettle(); await tester.tap(find.byTooltip("Personnaliser l'exercice")); await tester.pumpAndSettle(); expect(find.text('Score chrono'), findsOneWidget); await tester.dragUntilVisible( find.text( "Temps sert d'objectif de durée ; Score chrono enregistre " 'le temps réalisé.', ), find.byType(ListView), const Offset(0, -200), ); expect( find.text( "Temps sert d'objectif de durée ; Score chrono enregistre " 'le temps réalisé.', ), findsOneWidget, ); await tester.dragUntilVisible( find.text('Objectif de chrono'), find.byType(ListView), const Offset(0, -200), ); expect( find.widgetWithText(TextFormField, 'Objectif de chrono'), findsOneWidget, ); expect(find.widgetWithText(TextFormField, 'Cible score'), findsNothing); expect( find.text('Le résultat réel sera mesuré pendant la série.'), findsOneWidget, ); }); testWidgets( 'supprimer un programme demande confirmation puis le retire de la liste', (tester) async { final programRepository = _FakeProgramRepository() ..programs.add( Program( metadata: _metadata('program-1'), name: 'Jambes', defaultRestSeconds: 60, ), ); final templateRepository = _FakeWorkoutTemplateRepository() ..templates.add( WorkoutTemplate( metadata: _metadata('template-1'), name: 'Séance jambes', programs: [ WorkoutTemplateProgram( metadata: _metadata('template-program-1'), workoutTemplateId: 'template-1', sourceProgramId: 'program-1', position: 0, programNameSnapshot: 'Jambes', defaultRestSecondsSnapshot: 60, programSnapshotJson: '{"exercises":[]}', ), ], ), ); final exerciseRepository = _FakeExerciseRepository(); await tester.pumpWidget( MaterialApp( home: ProgramListScreen( programUseCases: _programUseCases( programRepository, exerciseRepository, templateRepository: templateRepository, ), exerciseUseCases: _exerciseUseCases(exerciseRepository), ), ), ); await tester.pump(); await tester.pump(); await tester.tap(find.byTooltip('Supprimer le programme')); await tester.pumpAndSettle(); expect(find.text('Supprimer Jambes ?'), findsOneWidget); expect( find.textContaining('Le supprimer le retirera de ces séances.'), findsOneWidget, ); await tester.tap(find.text('Annuler')); await tester.pumpAndSettle(); expect(find.text('Jambes'), findsOneWidget); expect(programRepository.saved, isEmpty); await tester.tap(find.byTooltip('Supprimer le programme')); await tester.pumpAndSettle(); await tester.tap(find.widgetWithText(FilledButton, 'Supprimer')); await tester.pumpAndSettle(); expect(find.text('Jambes'), findsNothing); expect(programRepository.saved.single.metadata.deletedAt, isNotNull); }, ); } ExerciseUseCases _exerciseUseCases(_FakeExerciseRepository repository) { return ExerciseUseCases( repository: repository, programRepository: _FakeProgramRepository(), clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)), ids: _FakeIds(), originDeviceId: 'device-1', ); } ProgramUseCases _programUseCases( _FakeProgramRepository programRepository, _FakeExerciseRepository exerciseRepository, { _FakeWorkoutTemplateRepository? templateRepository, }) { return ProgramUseCases( programRepository: programRepository, exerciseRepository: exerciseRepository, templateRepository: templateRepository ?? _FakeWorkoutTemplateRepository(), clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)), ids: _FakeIds(), originDeviceId: 'device-1', ); } final class _FakeWorkoutTemplateRepository implements WorkoutTemplateRepository { final templates = []; @override Future findById(String id) async => null; @override Future> listActive() async { return templates .where((template) => template.metadata.deletedAt == null) .toList(); } @override Future replaceComposition( WorkoutTemplate template, DateTime deletedAt, ) async { templates.removeWhere((item) => item.metadata.id == template.metadata.id); templates.add(template); } @override Future save(WorkoutTemplate template) async { templates.removeWhere((item) => item.metadata.id == template.metadata.id); templates.add(template); } @override Future saveOverride(WorkoutTemplateExerciseOverride override) async {} @override Future saveProgram(WorkoutTemplateProgram program) async {} } 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 { const _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 _FakeExerciseRepository implements ExerciseRepository { final exercises = []; final saved = []; @override Future findById(String id) async { for (final exercise in exercises) { if (exercise.metadata.id == id) { return exercise; } } return null; } @override Future isReferencedByProgram(String id) async => false; @override Future> listActive() async => exercises; @override Future save(Exercise exercise) async { saved.add(exercise); } } final class _FakeProgramRepository implements ProgramRepository { final programs = []; final saved = []; @override Future findById(String id) async { for (final program in programs) { if (program.metadata.id == id) { return program; } } return null; } @override Future> listActive() async { return programs .where((program) => program.metadata.deletedAt == null) .toList(); } @override Future replaceExercises(Program program, DateTime deletedAt) async { programs.removeWhere((item) => item.metadata.id == program.metadata.id); programs.add(program); } @override Future save(Program program) async { saved.add(program); programs.removeWhere((item) => item.metadata.id == program.metadata.id); programs.add(program); } @override Future saveExercise(ProgramExercise exercise) async {} }